Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copy this file to ".env" and set strong, unique values before running
# `docker compose up`. The ".env" file is git-ignored and must never be
# committed. Do NOT reuse these placeholder values.

# Oracle SYS/SYSTEM administrator password (required)
ORACLE_PASSWORD=replace-with-a-strong-password

# Application database schema user (the user name is not a secret)
APP_USER=photoalbum

# Application database schema password (required)
APP_USER_PASSWORD=replace-with-a-strong-password

# Admin account used to authenticate photo upload/delete (state-changing endpoints).
# The user name is not a secret; the password is required and must be strong.
APP_ADMIN_USERNAME=admin
APP_ADMIN_PASSWORD=replace-with-a-strong-password
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,11 @@ __pycache__/
# tools/**
# !tools/packages.config

# Local environment / secrets (never commit real credentials)
.env
.env.*
!.env.example

# Tabs Studio
*.tss

Expand Down
37 changes: 22 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,12 @@ A photo gallery application built with Spring Boot and Oracle Database, featurin
cd PhotoAlbum-Java
```

2. **Start the application**:
2. **Configure credentials and start the application**:
```bash
# Use docker-compose directly
# Copy the template and set strong, unique passwords in .env (git-ignored)
cp .env.example .env

# Use docker-compose directly (credentials are read from .env)
docker-compose up --build -d
```

Expand All @@ -66,7 +69,7 @@ A photo gallery application built with Spring Boot and Oracle Database, featurin
- `5500` (Enterprise Manager) - mapped to host port 5500
- **Database**: `XE` (Express Edition)
- **Schema**: `photoalbum`
- **Username/Password**: `photoalbum/photoalbum`
- **Username/Password**: provided via the `APP_USER` / `APP_USER_PASSWORD` variables in `.env` (never hard-coded)

## Photo Album Java Application
- **Port**: `8080` (mapped to host port 8080)
Expand Down Expand Up @@ -124,17 +127,21 @@ The application creates the following table structure in Oracle:
### Running Locally (without Docker)

1. **Install Oracle Database** (or use Oracle XE)
2. **Create database user**:
2. **Create database user** (choose your own strong password; grant least
privilege only — do NOT grant DBA):
```sql
CREATE USER photoalbum IDENTIFIED BY photoalbum;
GRANT CONNECT, RESOURCE, DBA TO photoalbum;
CREATE USER photoalbum IDENTIFIED BY "<your-strong-password>";
GRANT CREATE SESSION, CREATE TABLE, CREATE SEQUENCE, CREATE VIEW,
CREATE PROCEDURE, CREATE TRIGGER, CREATE TYPE, CREATE SYNONYM
TO photoalbum;
ALTER USER photoalbum QUOTA UNLIMITED ON USERS;
```
3. **Update application.properties**:
```properties
spring.datasource.url=jdbc:oracle:thin:@localhost:1521:XE
spring.datasource.username=photoalbum
spring.datasource.password=photoalbum
spring.jpa.hibernate.ddl-auto=create
3. **Provide credentials via environment variables** (do not hard-code them
in `application.properties`):
```bash
export SPRING_DATASOURCE_URL="jdbc:oracle:thin:@localhost:1521:XE"
export SPRING_DATASOURCE_USERNAME="photoalbum"
export SPRING_DATASOURCE_PASSWORD="<your-strong-password>"
```
4. **Run the application**:
```bash
Expand Down Expand Up @@ -165,8 +172,8 @@ java -jar target/photo-album-1.0.0.jar

2. **Database connection errors**:
```bash
# Verify Oracle is ready
docker exec -it photoalbum-oracle sqlplus photoalbum/photoalbum@//localhost:1521/XE
# Verify Oracle is ready (credentials come from your .env values)
docker exec -it photoalbum-oracle sh -c 'sqlplus "$APP_USER/$APP_USER_PASSWORD@//localhost:1521/FREEPDB1"'
```

3. **Permission errors**:
Expand Down Expand Up @@ -207,7 +214,7 @@ docker-compose down -v

Oracle Enterprise Manager is available at `http://localhost:5500/em` for database administration:
- **Username**: `system`
- **Password**: `photoalbum`
- **Password**: the `ORACLE_PASSWORD` value you set in `.env`
- **Container**: `XE`

## Performance Notes
Expand Down
31 changes: 24 additions & 7 deletions azure-setup.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ $NC = "`e[0m" # No Color
Write-Host "${GREEN}=== Azure Photo Album Resources Setup ===${NC}" -NoNewline
Write-Host ""

# Generates a strong random password using a cryptographic RNG.
# Excludes quote/backslash characters so the value is safe to embed in SQL
# and shell/env contexts.
function New-StrongPassword {
param([int]$Length = 24)
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#%*+-_=.'
$bytes = New-Object 'System.Byte[]' $Length
$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
try { $rng.GetBytes($bytes) } finally { $rng.Dispose() }
$chars = foreach ($b in $bytes) { $alphabet[$b % $alphabet.Length] }
-join $chars
}

# Variables
$RANDOM_SUFFIX = -join ((1..3) | ForEach-Object { '{0:x}' -f (Get-Random -Maximum 256) })
$RESOURCE_GROUP = "photo-album-resources-${RANDOM_SUFFIX}"
Expand All @@ -17,11 +30,15 @@ $ACR_NAME = "photoalbumacr$(Get-Random -Maximum 99999)"
$AKS_NODE_VM_SIZE = "Standard_D8ds_v5"
$POSTGRES_SERVER_NAME = "$RESOURCE_GROUP-postgresql"
$PostgreSQL_SKU = "Standard_D4ads_v5"
$POSTGRES_ADMIN_USER="photoalbum_admin"
$POSTGRES_ADMIN_PASSWORD="P@ssw0rd123!"
$POSTGRES_DATABASE_NAME="photoalbum"
$POSTGRES_APP_USER="photoalbum"
$POSTGRES_APP_PASSWORD="photoalbum"

# Credentials are NOT hard-coded. They are taken from environment variables
# when provided, otherwise a strong random password is generated per run.
# The generated values are written only to the git-ignored .env file below.
$POSTGRES_ADMIN_USER = if ($env:POSTGRES_ADMIN_USER) { $env:POSTGRES_ADMIN_USER } else { "photoalbum_admin" }
$POSTGRES_ADMIN_PASSWORD = if ($env:POSTGRES_ADMIN_PASSWORD) { $env:POSTGRES_ADMIN_PASSWORD } else { New-StrongPassword }
$POSTGRES_APP_USER = if ($env:POSTGRES_APP_USER) { $env:POSTGRES_APP_USER } else { "photoalbum" }
$POSTGRES_APP_PASSWORD = if ($env:POSTGRES_APP_PASSWORD) { $env:POSTGRES_APP_PASSWORD } else { New-StrongPassword }

Write-Host "${YELLOW}Using default subscription...${NC}" -NoNewline
Write-Host ""
Expand Down Expand Up @@ -192,7 +209,7 @@ try {
--admin-user "$POSTGRES_ADMIN_USER" `
--admin-password "$POSTGRES_ADMIN_PASSWORD" `
--database-name "postgres" `
--querytext "CREATE USER photoalbum WITH PASSWORD 'photoalbum';"
--querytext "CREATE USER $POSTGRES_APP_USER WITH PASSWORD '$POSTGRES_APP_PASSWORD';"
} catch {
Write-Host "${YELLOW}User may already exist, continuing...${NC}" -NoNewline
Write-Host ""
Expand Down Expand Up @@ -295,11 +312,11 @@ Write-Host "AKS Cluster: $RESOURCE_GROUP-aks"
Write-Host "PostgreSQL Server: $POSTGRES_SERVER_NAME"
Write-Host "Location: $LOCATION"
Write-Host ""
Write-Host "${GREEN}PostgreSQL Connection Details (stored in environment variables and .env file):${NC}" -NoNewline
Write-Host "${GREEN}PostgreSQL Connection Details (stored in the git-ignored .env file):${NC}" -NoNewline
Write-Host ""
Write-Host "POSTGRES_SERVER: $env:POSTGRES_SERVER"
Write-Host "POSTGRES_USER: $env:POSTGRES_USER"
Write-Host "POSTGRES_PASSWORD: $env:POSTGRES_PASSWORD"
Write-Host "POSTGRES_PASSWORD: ******** (see .env file)"
Write-Host "POSTGRES_CONNECTION_STRING: $env:POSTGRES_CONNECTION_STRING"
Write-Host ""
Write-Host "${GREEN}All configuration has been saved to .env file in the project root.${NC}" -NoNewline
Expand Down
12 changes: 7 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ services:
image: gvenzl/oracle-free:latest
container_name: photoalbum-oracle
environment:
- ORACLE_PASSWORD=photoalbum
- APP_USER=photoalbum
- APP_USER_PASSWORD=photoalbum
- ORACLE_PASSWORD=${ORACLE_PASSWORD:?ORACLE_PASSWORD must be set (copy .env.example to .env)}
- APP_USER=${APP_USER:-photoalbum}
- APP_USER_PASSWORD=${APP_USER_PASSWORD:?APP_USER_PASSWORD must be set (copy .env.example to .env)}
ports:
- "1521:1521"
volumes:
Expand All @@ -30,8 +30,10 @@ services:
environment:
- SPRING_PROFILES_ACTIVE=docker
- SPRING_DATASOURCE_URL=jdbc:oracle:thin:@oracle-db:1521/FREEPDB1
- SPRING_DATASOURCE_USERNAME=photoalbum
- SPRING_DATASOURCE_PASSWORD=photoalbum
- SPRING_DATASOURCE_USERNAME=${APP_USER:-photoalbum}
- SPRING_DATASOURCE_PASSWORD=${APP_USER_PASSWORD:?APP_USER_PASSWORD must be set (copy .env.example to .env)}
- APP_ADMIN_USERNAME=${APP_ADMIN_USERNAME:-admin}
- APP_ADMIN_PASSWORD=${APP_ADMIN_PASSWORD:?APP_ADMIN_PASSWORD must be set (copy .env.example to .env)}
ports:
- "8080:8080"
depends_on:
Expand Down
29 changes: 14 additions & 15 deletions oracle-init/01-create-user.sql
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
-- This script runs automatically when Oracle XE container starts
-- It creates the photoalbum user and grants necessary privileges
-- This script runs automatically when the Oracle container starts.
--
-- The application schema user is created by the container itself from the
-- APP_USER / APP_USER_PASSWORD environment variables (see docker-compose.yml
-- and .env). Credentials are intentionally NOT hard-coded here.
--
-- This script only grants the least set of schema-scoped privileges the
-- application (Hibernate with ddl-auto=create) needs to manage its own
-- objects. It deliberately does NOT grant DBA or any system-wide "ANY"
-- privileges.

ALTER SESSION SET "_ORACLE_SCRIPT"=true;

-- Create photoalbum user
CREATE USER photoalbum IDENTIFIED BY photoalbum;

-- Grant system privileges
GRANT CONNECT TO photoalbum;
GRANT RESOURCE TO photoalbum;
GRANT DBA TO photoalbum;
-- Least-privilege grants for the application schema user.
-- NOTE: keep this user name in sync with APP_USER in docker-compose.yml / .env.
GRANT CREATE SESSION TO photoalbum;
GRANT CREATE TABLE TO photoalbum;
GRANT CREATE SEQUENCE TO photoalbum;
Expand All @@ -18,13 +21,9 @@ GRANT CREATE PROCEDURE TO photoalbum;
GRANT CREATE TRIGGER TO photoalbum;
GRANT CREATE TYPE TO photoalbum;
GRANT CREATE SYNONYM TO photoalbum;
GRANT UNLIMITED TABLESPACE TO photoalbum;

-- Grant object privileges needed by Hibernate
GRANT SELECT ANY DICTIONARY TO photoalbum;
GRANT CREATE ANY INDEX TO photoalbum;
GRANT ALTER ANY INDEX TO photoalbum;
GRANT DROP ANY INDEX TO photoalbum;
-- Allow the user to store objects in its default tablespace.
ALTER USER photoalbum QUOTA UNLIMITED ON USERS;

-- Set default and temporary tablespace
ALTER USER photoalbum DEFAULT TABLESPACE USERS;
Expand Down
44 changes: 28 additions & 16 deletions oracle-init/create-user.sh
Original file line number Diff line number Diff line change
@@ -1,30 +1,42 @@
#!/bin/bash
# This script ensures the photoalbum user is created in Oracle XE
# Ensures the application schema user exists in Oracle.
#
# Credentials are read from environment variables (provided by the container
# from docker-compose.yml / .env) and are NOT hard-coded in this file.
# ORACLE_PASSWORD - SYS/SYSTEM administrator password
# APP_USER - application schema user name (default: photoalbum)
# APP_USER_PASSWORD - application schema user password
set -euo pipefail

: "${ORACLE_PASSWORD:?ORACLE_PASSWORD must be set}"
: "${APP_USER:=photoalbum}"
: "${APP_USER_PASSWORD:?APP_USER_PASSWORD must be set}"

# Wait for Oracle to be fully ready
echo "Waiting for Oracle to be ready..."
sleep 30

# Connect to Oracle as SYSTEM and create the photoalbum user
sqlplus -s system/photoalbum@//localhost:1521/XE <<EOF
-- Create photoalbum user if it doesn't exist
APP_USER_UPPER=$(printf '%s' "$APP_USER" | tr '[:lower:]' '[:upper:]')

# Connect to Oracle as SYSTEM and create the application user if missing.
sqlplus -s "system/${ORACLE_PASSWORD}@//localhost:1521/XE" <<EOF
SET SERVEROUTPUT ON
DECLARE
user_exists NUMBER;
BEGIN
SELECT COUNT(*) INTO user_exists FROM dba_users WHERE username = 'PHOTOALBUM';
SELECT COUNT(*) INTO user_exists FROM dba_users WHERE username = '${APP_USER_UPPER}';

IF user_exists = 0 THEN
EXECUTE IMMEDIATE 'CREATE USER photoalbum IDENTIFIED BY photoalbum';
EXECUTE IMMEDIATE 'GRANT CONNECT, RESOURCE TO photoalbum';
EXECUTE IMMEDIATE 'GRANT CREATE SESSION TO photoalbum';
EXECUTE IMMEDIATE 'GRANT CREATE TABLE TO photoalbum';
EXECUTE IMMEDIATE 'GRANT CREATE SEQUENCE TO photoalbum';
EXECUTE IMMEDIATE 'GRANT UNLIMITED TABLESPACE TO photoalbum';
EXECUTE IMMEDIATE 'ALTER USER photoalbum DEFAULT TABLESPACE USERS';

DBMS_OUTPUT.PUT_LINE('User photoalbum created successfully');
EXECUTE IMMEDIATE 'CREATE USER "${APP_USER_UPPER}" IDENTIFIED BY "${APP_USER_PASSWORD}"';
EXECUTE IMMEDIATE 'GRANT CREATE SESSION TO "${APP_USER_UPPER}"';
EXECUTE IMMEDIATE 'GRANT CREATE TABLE TO "${APP_USER_UPPER}"';
EXECUTE IMMEDIATE 'GRANT CREATE SEQUENCE TO "${APP_USER_UPPER}"';
EXECUTE IMMEDIATE 'ALTER USER "${APP_USER_UPPER}" QUOTA UNLIMITED ON USERS';
EXECUTE IMMEDIATE 'ALTER USER "${APP_USER_UPPER}" DEFAULT TABLESPACE USERS';

DBMS_OUTPUT.PUT_LINE('Application user created successfully');
ELSE
DBMS_OUTPUT.PUT_LINE('User photoalbum already exists');
DBMS_OUTPUT.PUT_LINE('Application user already exists');
END IF;
END;
/
Expand Down
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<!-- Spring Boot Security (protects state-changing endpoints) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- Commons IO for file operations -->
<dependency>
<groupId>commons-io</groupId>
Expand Down
63 changes: 63 additions & 0 deletions src/main/java/com/photoalbum/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.photoalbum.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

/**
* Security configuration.
*
* The photo gallery is publicly readable, but the state-changing endpoints
* (photo upload and delete) require authentication so anonymous callers can no
* longer create or destroy data (CWE-862 / CWE-306). The admin credentials are
* supplied via environment variables (app.admin.username / app.admin.password),
* never hard-coded.
*/
@Configuration
public class SecurityConfig {

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public InMemoryUserDetailsManager userDetailsService(
PasswordEncoder passwordEncoder,
@Value("${app.admin.username:admin}") String adminUsername,
@Value("${app.admin.password}") String adminPassword) {
UserDetails admin = User.withUsername(adminUsername)
.password(passwordEncoder.encode(adminPassword))
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(admin);
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// Stateless HTTP Basic auth: credentials are sent per request, so a
// session-based CSRF token is not applicable. CSRF is disabled to
// keep this security fix minimal without breaking the JSON upload API.
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// Default deny for state-changing operations (CWE-862 / CWE-306).
.antMatchers(HttpMethod.POST, "/upload", "/detail/*/delete").authenticated()
// Public, read-only photo gallery.
.anyRequest().permitAll()
.and()
.httpBasic();
return http.build();
}
}
Loading
Loading