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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ everything is reachable on plain HTTP:
| <http://localhost/> | Web client |
| <http://localhost/docs> | Swagger UI |
| <http://localhost/api/v1/&lt;service&gt;/…> | APIs (organization, members, events, feedback, finance, letters, helper) |
| <http://localhost/auth> | Keycloak (via Traefik) |
| <http://localhost:8081/auth> | Keycloak (direct, for admin console) |
| <http://localhost:8080> | Traefik dashboard |

> **Do not run** `docker compose -f infra/docker-compose.yml up` locally — that
Expand Down Expand Up @@ -231,6 +233,27 @@ ghcr (tagged with the commit SHA), then `deploy-k8s` runs `helm upgrade
See [`infra/helm/README.md`](infra/helm/README.md) for the chart layout, required
one-time secrets (`genai-env`, `ghcr-pull`), and manual deploy instructions.

## Database

All five Spring services share a single **PostgreSQL 15** instance (`app_db`) but each owns a dedicated schema and a least-privilege user:

| Service | Schema | User |
|---|---|---|
| Organization | `organization` | `organization_user` |
| Member | `member` | `member_user` |
| Event | `event` | `event_user` |
| Feedback | `feedback` | `feedback_user` |
| Finance | `finance` | `finance_user` |

Schemas and users are created at DB init time by [`infra/postgres/init-db.sh`](infra/postgres/init-db.sh). Each service runs its own **Flyway** migrations on startup:

- `V1__create_tables.sql` — creates all tables for that schema
- `V2__add_foreign_keys.sql` — adds cross-schema foreign keys (e.g. `event.events.creator_id → member.members.id`)

Cross-schema `REFERENCES` privileges are granted via `ALTER DEFAULT PRIVILEGES` in `init-db.sh`, so foreign-key constraints across schemas work on fresh deploys without manual intervention.

The letter service has no database (`spring.flyway.enabled=false`). The GenAI service uses file-based storage for RAG documents.

## Authentication (Keycloak)

All services are protected by [Keycloak 26](https://www.keycloak.org) via OIDC/JWT. Keycloak is included in both the Docker Compose stack and the Helm chart — no separate installation is needed.
Expand Down
1 change: 1 addition & 0 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1296,6 +1296,7 @@ components:
required:
- first_name
- last_name
- email
description: Data transfer object for creating a new Member.
Event:
type: object
Expand Down
11 changes: 9 additions & 2 deletions infra/docker-compose.override.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@
# * Removes TLS (no Let's Encrypt, no HTTPS, no 443) -- local has no public DNS
# * Strips the Host(...) requirement from every router so localhost works
# * Disables the HTTP -> HTTPS redirect so plain http://localhost works
# * Exposes the app database on localhost:5432 for direct DB client access
#
# Access locally:
# http://localhost/ web client
# http://localhost/docs Swagger UI
# http://localhost/api/v1/<service>/... APIs
# http://localhost:8080 Traefik dashboard
# localhost:5432 (app_admin/app_admin_password, db: app_db) PostgreSQL

services:
traefik:
Expand Down Expand Up @@ -126,15 +128,20 @@ services:
- "traefik.http.services.web-client.loadbalancer.server.port=8080"

keycloak:
environment:
KC_HOSTNAME: "http://localhost:8081/auth"
labels: !override
- "traefik.enable=true"
- "traefik.http.routers.keycloak.entrypoints=web"
- "traefik.http.routers.keycloak.rule=PathPrefix(`/auth`)"
- "traefik.http.services.keycloak.loadbalancer.server.port=8080"
environment:
KC_HOSTNAME: "http://localhost:8081/auth"

app-database:
ports:
- "5432:5432"

traefik-forward-auth:
restart: "no"
extra_hosts:
- "localhost:host-gateway"
labels: !override
Expand Down
73 changes: 37 additions & 36 deletions infra/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,12 @@ services:
expose:
- 8080
depends_on:
member-database:
app-database:
condition: service_healthy
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://member-database:5432/member_db
- SPRING_DATASOURCE_USERNAME=member_user
- SPRING_DATASOURCE_PASSWORD=member_password
- SPRING_JPA_HIBERNATE_DDL_AUTO=update
- SPRING_DATASOURCE_URL=jdbc:postgresql://app-database:5432/app_db
- SPRING_DATASOURCE_USERNAME=organization_user
- SPRING_DATASOURCE_PASSWORD=organization_password
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=https://team-devoops.uaenorth.cloudapp.azure.com/auth/realms/devops
labels:
- "traefik.enable=true"
Expand All @@ -53,13 +52,12 @@ services:
expose:
- 8080
depends_on:
member-database:
app-database:
condition: service_healthy
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://member-database:5432/member_db
- SPRING_DATASOURCE_URL=jdbc:postgresql://app-database:5432/app_db
- SPRING_DATASOURCE_USERNAME=member_user
- SPRING_DATASOURCE_PASSWORD=member_password
- SPRING_JPA_HIBERNATE_DDL_AUTO=update
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=https://team-devoops.uaenorth.cloudapp.azure.com/auth/realms/devops
labels:
- "traefik.enable=true"
Expand All @@ -80,13 +78,12 @@ services:
expose:
- 8080
depends_on:
member-database:
app-database:
condition: service_healthy
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://member-database:5432/member_db
- SPRING_DATASOURCE_USERNAME=member_user
- SPRING_DATASOURCE_PASSWORD=member_password
- SPRING_JPA_HIBERNATE_DDL_AUTO=update
- SPRING_DATASOURCE_URL=jdbc:postgresql://app-database:5432/app_db
- SPRING_DATASOURCE_USERNAME=event_user
- SPRING_DATASOURCE_PASSWORD=event_password
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=https://team-devoops.uaenorth.cloudapp.azure.com/auth/realms/devops
labels:
- "traefik.enable=true"
Expand All @@ -107,13 +104,12 @@ services:
expose:
- 8080
depends_on:
member-database:
app-database:
condition: service_healthy
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://member-database:5432/member_db
- SPRING_DATASOURCE_USERNAME=member_user
- SPRING_DATASOURCE_PASSWORD=member_password
- SPRING_JPA_HIBERNATE_DDL_AUTO=update
- SPRING_DATASOURCE_URL=jdbc:postgresql://app-database:5432/app_db
- SPRING_DATASOURCE_USERNAME=feedback_user
- SPRING_DATASOURCE_PASSWORD=feedback_password
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=https://team-devoops.uaenorth.cloudapp.azure.com/auth/realms/devops
labels:
- "traefik.enable=true"
Expand All @@ -134,13 +130,12 @@ services:
expose:
- 8080
depends_on:
member-database:
app-database:
condition: service_healthy
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://member-database:5432/member_db
- SPRING_DATASOURCE_USERNAME=member_user
- SPRING_DATASOURCE_PASSWORD=member_password
- SPRING_JPA_HIBERNATE_DDL_AUTO=update
- SPRING_DATASOURCE_URL=jdbc:postgresql://app-database:5432/app_db
- SPRING_DATASOURCE_USERNAME=finance_user
- SPRING_DATASOURCE_PASSWORD=finance_password
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=https://team-devoops.uaenorth.cloudapp.azure.com/auth/realms/devops
labels:
- "traefik.enable=true"
Expand All @@ -161,13 +156,12 @@ services:
expose:
- 8080
depends_on:
member-database:
app-database:
condition: service_healthy
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://member-database:5432/member_db
- SPRING_DATASOURCE_USERNAME=member_user
- SPRING_DATASOURCE_PASSWORD=member_password
- SPRING_JPA_HIBERNATE_DDL_AUTO=update
- SPRING_DATASOURCE_URL=jdbc:postgresql://app-database:5432/app_db
- SPRING_DATASOURCE_USERNAME=letter_user
- SPRING_DATASOURCE_PASSWORD=letter_password
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=https://team-devoops.uaenorth.cloudapp.azure.com/auth/realms/devops
labels:
- "traefik.enable=true"
Expand Down Expand Up @@ -345,19 +339,26 @@ services:
networks:
- keycloak

member-database:
app-database:
image: postgres:15.6-alpine
container_name: member-db
container_name: app-db
expose:
- 5432
environment:
POSTGRES_USER: member_user
POSTGRES_PASSWORD: member_password
POSTGRES_DB: member_db
POSTGRES_USER: app_admin
POSTGRES_PASSWORD: app_admin_password
POSTGRES_DB: app_db
ORGANIZATION_USER_PASSWORD: organization_password
MEMBER_USER_PASSWORD: member_password
EVENT_USER_PASSWORD: event_password
FEEDBACK_USER_PASSWORD: feedback_password
FINANCE_USER_PASSWORD: finance_password
LETTER_USER_PASSWORD: letter_password
volumes:
- member_db_data:/var/lib/postgresql/data
- app_db_data:/var/lib/postgresql/data
- ./postgres/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U member_user -d member_db"]
test: ["CMD-SHELL", "pg_isready -U app_admin -d app_db"]
interval: 10s
timeout: 5s
retries: 5
Expand All @@ -366,7 +367,7 @@ services:
- data

volumes:
member_db_data:
app_db_data:
keycloak_db_data:
letsencrypt:

Expand Down
16 changes: 11 additions & 5 deletions infra/helm/team-devoops/templates/configmap-db.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
{{- if .Values.database.enabled }}
{{- range $name, $svc := .Values.services }}
{{- if $svc.db }}
{{- $dbKey := $svc.dbUser }}
{{- $dbUser := index $.Values.database.users $dbKey }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: db-config
name: db-config-{{ $name }}
labels:
{{- include "team-devoops.labels" (dict "name" "db-config" "root" $) | nindent 4 }}
{{- include "team-devoops.labels" (dict "name" (printf "db-config-%s" $name) "root" $) | nindent 4 }}
data:
SPRING_DATASOURCE_URL: jdbc:postgresql://{{ .Values.database.host }}:{{ .Values.database.port }}/{{ .Values.database.name }}
SPRING_DATASOURCE_USERNAME: {{ .Values.database.user | quote }}
SPRING_JPA_HIBERNATE_DDL_AUTO: update
SPRING_DATASOURCE_URL: jdbc:postgresql://{{ $.Values.database.host }}:{{ $.Values.database.port }}/{{ $.Values.database.name }}
SPRING_DATASOURCE_USERNAME: {{ $dbUser.username | quote }}
{{- end }}
{{- end }}
{{- end }}
4 changes: 2 additions & 2 deletions infra/helm/team-devoops/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ spec:
envFrom:
{{- if $svc.db }}
- configMapRef:
name: db-config
name: db-config-{{ $name }}
- secretRef:
name: db-credentials
name: db-credentials-{{ $svc.dbUser }}-service
{{- end }}
{{- if $svc.envFromSecret }}
- secretRef:
Expand Down
115 changes: 115 additions & 0 deletions infra/helm/team-devoops/templates/postgres-init-configmap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
{{- if .Values.database.enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: db-init-scripts
labels:
{{- include "team-devoops.labels" (dict "name" "db-init-scripts" "root" $) | nindent 4 }}
data:
init-db.sh: |
#!/usr/bin/env bash
# init-db.sh — runs once when the Postgres container is first initialised.
# Creates per-service users, all application schemas, and grants:
# - each service user: WRITE on its own schemas, READ (via reader role) on all
# - letter_user: READ only (no write schemas)
#
# Passwords are injected via environment variables so nothing is hardcoded.
set -euo pipefail

DB="${POSTGRES_DB}"
ADMIN="${POSTGRES_USER}"

psql -v ON_ERROR_STOP=1 --username "$ADMIN" --dbname "$DB" <<-EOSQL

-- Reader role: granted to every service user so each can SELECT anywhere
CREATE ROLE reader NOLOGIN;

-- Per-service users
CREATE USER organization_user WITH PASSWORD '${ORGANIZATION_USER_PASSWORD}';
CREATE USER member_user WITH PASSWORD '${MEMBER_USER_PASSWORD}';
CREATE USER event_user WITH PASSWORD '${EVENT_USER_PASSWORD}';
CREATE USER feedback_user WITH PASSWORD '${FEEDBACK_USER_PASSWORD}';
CREATE USER finance_user WITH PASSWORD '${FINANCE_USER_PASSWORD}';
CREATE USER letter_user WITH PASSWORD '${LETTER_USER_PASSWORD}';

-- Allow all users to connect to the application database
GRANT CONNECT ON DATABASE ${DB} TO
organization_user, member_user, event_user,
feedback_user, finance_user, letter_user;

-- All users inherit the reader role
GRANT reader TO
organization_user, member_user, event_user,
feedback_user, finance_user, letter_user;

-- Schemas: organization
CREATE SCHEMA organization;

-- Schemas: member
CREATE SCHEMA member;

-- Schemas: event
CREATE SCHEMA event;

-- Schemas: feedback
CREATE SCHEMA feedback;

-- Schemas: finance
CREATE SCHEMA finance;

-- Ownership
ALTER SCHEMA organization OWNER TO organization_user;
ALTER SCHEMA member OWNER TO member_user;
ALTER SCHEMA event OWNER TO event_user;
ALTER SCHEMA feedback OWNER TO feedback_user;
ALTER SCHEMA finance OWNER TO finance_user;

-- Reader USAGE on all schemas
GRANT USAGE ON SCHEMA
organization, member, event, feedback, finance
TO reader;

-- Reader SELECT on existing tables (defensive)
GRANT SELECT ON ALL TABLES IN SCHEMA
organization, member, event, feedback, finance
TO reader;

-- Default privileges for future tables
ALTER DEFAULT PRIVILEGES FOR ROLE organization_user
IN SCHEMA organization
GRANT SELECT ON TABLES TO reader;

ALTER DEFAULT PRIVILEGES FOR ROLE member_user
IN SCHEMA member
GRANT SELECT ON TABLES TO reader;

ALTER DEFAULT PRIVILEGES FOR ROLE event_user
IN SCHEMA event
GRANT SELECT ON TABLES TO reader;

ALTER DEFAULT PRIVILEGES FOR ROLE feedback_user
IN SCHEMA feedback
GRANT SELECT ON TABLES TO reader;

ALTER DEFAULT PRIVILEGES FOR ROLE finance_user
IN SCHEMA finance
GRANT SELECT ON TABLES TO reader;

-- -----------------------------------------------------------------------
-- Cross-schema REFERENCES: required for FK constraints across schemas.
-- Granted per-user on the schemas they reference.
-- -----------------------------------------------------------------------
ALTER DEFAULT PRIVILEGES FOR ROLE member_user
IN SCHEMA member
GRANT REFERENCES ON TABLES TO organization_user, event_user, feedback_user, finance_user;

ALTER DEFAULT PRIVILEGES FOR ROLE organization_user
IN SCHEMA organization
GRANT REFERENCES ON TABLES TO event_user;

ALTER DEFAULT PRIVILEGES FOR ROLE event_user
IN SCHEMA event
GRANT REFERENCES ON TABLES TO feedback_user;

EOSQL
{{- end }}
Loading
Loading