From c407b879edd0e6de2056036b55c3f81f9b91c04c Mon Sep 17 00:00:00 2001 From: gabdevele Date: Wed, 17 Jun 2026 21:26:31 +0200 Subject: [PATCH] feat: add federation authority, provider, and relying party directories to Dockerfile; enhance entrypoint script for database normalization --- Dockerfile | 4 + docker-compose.yml | 3 - entrypoint.sh | 23 ++++- scripts/normalize_db_urls.py | 175 +++++++++++++++++++++++++++++++++++ scripts/rewrite_dumps.py | 7 ++ 5 files changed, 204 insertions(+), 8 deletions(-) create mode 100644 scripts/normalize_db_urls.py diff --git a/Dockerfile b/Dockerfile index b0a04da..baf84dc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,10 @@ COPY vendor/spid-cie-oidc-django/setup.py /opt/spid_cie_oidc/setup.py COPY vendor/spid-cie-oidc-django/README.md /opt/spid_cie_oidc/README.md RUN pip install --no-cache-dir /opt/spid_cie_oidc +COPY federation_authority /app/federation_authority +COPY provider /app/provider +COPY relying_party /app/relying_party + COPY entrypoint.sh /entrypoint.sh COPY scripts /app/scripts RUN chmod +x /entrypoint.sh diff --git a/docker-compose.yml b/docker-compose.yml index 8fb4b41..f66293e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,6 @@ services: expose: - "8000" volumes: - - ./federation_authority:/app/federation_authority - federation-authority-data:/app/federation_authority/db restart: unless-stopped @@ -34,7 +33,6 @@ services: expose: - "8002" volumes: - - ./provider:/app/provider - provider-data:/app/provider/db depends_on: - federation-authority @@ -60,7 +58,6 @@ services: expose: - "8001" volumes: - - ./relying_party:/app/relying_party - relying-party-data:/app/relying_party/db depends_on: - federation-authority diff --git a/entrypoint.sh b/entrypoint.sh index 53d6687..fb94869 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -4,26 +4,39 @@ set -eu APP_DIR="$1" PORT="$2" DB_PATH="${DJANGO_DB_PATH:-$APP_DIR/db/db.sqlite3}" +DUMP_SRC="$APP_DIR/dumps/example.json" +LEGACY_DB_PATH="$APP_DIR/db.sqlite3" cd "$APP_DIR" mkdir -p logs static "$(dirname "$DB_PATH")" +if [ ! -f "$DB_PATH" ] && [ -f "$LEGACY_DB_PATH" ]; then + mkdir -p "$(dirname "$DB_PATH")" + cp "$LEGACY_DB_PATH" "$DB_PATH" +fi + if [ ! -f "$DB_PATH" ] && [ "${LOAD_FIXTURE_ON_EMPTY_DB:-true}" = "true" ]; then - TMP_DUMP="$(mktemp)" - python /app/scripts/rewrite_dumps.py dumps/example.json "$TMP_DUMP" python manage.py migrate - python manage.py loaddata "$TMP_DUMP" - rm -f "$TMP_DUMP" + if [ -f "$DUMP_SRC" ]; then + TMP_DUMP="$(mktemp)" + python /app/scripts/rewrite_dumps.py "$DUMP_SRC" "$TMP_DUMP" + python manage.py loaddata "$TMP_DUMP" + rm -f "$TMP_DUMP" + fi else python manage.py migrate fi +python /app/scripts/normalize_db_urls.py "$DB_PATH" + # With DEBUG disabled Django no longer serves app static assets implicitly. # Collect them into STATIC_ROOT so the explicit /static/ URL can serve them. python manage.py collectstatic --noinput -if grep -aEq '127\.0\.0\.1|provider:8002' "$DB_PATH"; then +if grep -aEq '127\.0\.0\.1|provider:8002' "$DB_PATH" \ + && [ -n "${DJANGO_ALLOWED_HOSTS:-}" ] \ + && [ "${DJANGO_ALLOWED_HOSTS:-*}" != "*" ]; then echo "Refusing to start: bootstrap data still contains internal/local URLs" >&2 exit 1 fi diff --git a/scripts/normalize_db_urls.py b/scripts/normalize_db_urls.py new file mode 100644 index 0000000..31a7a20 --- /dev/null +++ b/scripts/normalize_db_urls.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +import json +import os +import sqlite3 +import sys +from typing import Iterable + + +def env(name: str, default: str = "") -> str: + return os.getenv(name, default).strip().rstrip("/") + + +def infer_base_url(kind: str) -> str: + hosts = env("DJANGO_ALLOWED_HOSTS") or env("CIE_AUTH_ALLOWED_HOSTS") + items = [host.strip() for host in hosts.split(",") if host.strip() and host.strip() != "*"] + for host in items: + if kind in host: + return f"https://{host}" + defaults = { + "fa": "http://federation-authority:8000", + "provider": "http://provider:8002", + "rp": "http://relying-party:8001", + } + return defaults[kind] + + +TA = env("DJANGO_TRUST_ANCHOR") or infer_base_url("fa") +PROVIDER_ISSUER = env("DJANGO_PROVIDER_ISSUER") or f"{infer_base_url('provider')}/oidc/op" +RP_BASE = env("DJANGO_RP_BASE_URL") or infer_base_url("rp") +RP_ISSUER = f"{RP_BASE}/oidc/rp" +PROVIDER_BASE = PROVIDER_ISSUER.removesuffix("/oidc/op") + + +def rewrite_string(value: str) -> str: + return ( + value.replace("http://127.0.0.1:8000/oidc/op", TA + "/oidc/op") + .replace("http://127.0.0.1:8001/oidc/rp", RP_ISSUER) + .replace("http://127.0.0.1:8002/oidc/op", PROVIDER_ISSUER) + .replace("http://127.0.0.1:8000", TA) + .replace("http://127.0.0.1:8001", RP_BASE) + .replace("http://127.0.0.1:8002", PROVIDER_BASE) + .replace("http://provider:8002/oidc/op", PROVIDER_ISSUER) + .replace("http://provider:8002", PROVIDER_BASE) + .replace("http://relying-party:8001/oidc/rp", RP_ISSUER) + .replace("http://relying-party:8001", RP_BASE) + .replace("http://federation-authority:8000/oidc/op", TA + "/oidc/op") + .replace("http://federation-authority:8000", TA) + ) + + +REPLACEMENTS = [ + ("http://127.0.0.1:8000/oidc/op", TA + "/oidc/op"), + ("http://127.0.0.1:8001/oidc/rp", RP_ISSUER), + ("http://127.0.0.1:8002/oidc/op", PROVIDER_ISSUER), + ("http://127.0.0.1:8000", TA), + ("http://127.0.0.1:8001", RP_BASE), + ("http://127.0.0.1:8002", PROVIDER_BASE), + ("http://provider:8002/oidc/op", PROVIDER_ISSUER), + ("http://provider:8002", PROVIDER_BASE), + ("http://relying-party:8001/oidc/rp", RP_ISSUER), + ("http://relying-party:8001", RP_BASE), + ("http://federation-authority:8000/oidc/op", TA + "/oidc/op"), + ("http://federation-authority:8000", TA), +] + + +def deep_replace(value): + if isinstance(value, dict): + return {k: deep_replace(v) for k, v in value.items()} + if isinstance(value, list): + return [deep_replace(v) for v in value] + if isinstance(value, str): + return rewrite_string(value) + return value + + +def replace_in_all_text_columns(cur, table: str): + cols = cur.execute(f"PRAGMA table_info({table})").fetchall() + text_cols = [c[1] for c in cols if c[2] and c[2].upper() in {"TEXT", "VARCHAR(255)", "VARCHAR(2048)", "VARCHAR(64)", "VARCHAR(33)", "JSON"}] + for column in text_cols: + for old, new in REPLACEMENTS: + cur.execute( + f"UPDATE {table} SET {column} = REPLACE({column}, ?, ?) WHERE {column} LIKE ?", + (old, new, f"%{old}%"), + ) + + +def update_json_column(cur, table, pk, column): + rows = cur.execute(f"SELECT id, {column} FROM {table}").fetchall() + for row_id, raw in rows: + if not raw: + continue + try: + data = json.loads(raw) + except Exception: + continue + updated = deep_replace(data) + if updated != data: + cur.execute( + f"UPDATE {table} SET {column} = ? WHERE {pk} = ?", + (json.dumps(updated, ensure_ascii=False), row_id), + ) + + +def update_text_url_column(cur, table, pk, column): + rows = cur.execute(f"SELECT {pk}, {column} FROM {table}").fetchall() + for row_id, raw in rows: + if not raw: + continue + updated = rewrite_string(raw) + if updated != raw: + cur.execute( + f"UPDATE {table} SET {column} = ? WHERE {pk} = ?", + (updated, row_id), + ) + + +def main(): + if len(sys.argv) != 2: + raise SystemExit("usage: normalize_db_urls.py ") + + db_path = sys.argv[1] + if not os.path.exists(db_path): + raise SystemExit(0) + + conn = sqlite3.connect(db_path) + try: + cur = conn.cursor() + + tables = [ + row[0] + for row in cur.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'" + ).fetchall() + ] + + for table in tables: + replace_in_all_text_columns(cur, table) + + for table in ( + "spid_cie_oidc_entity_federationentityconfiguration", + "spid_cie_oidc_entity_fetchedentitystatement", + "spid_cie_oidc_entity_trustchain", + ): + if table not in tables: + continue + + if table == "spid_cie_oidc_entity_federationentityconfiguration": + update_text_url_column(cur, table, "id", "sub") + update_json_column(cur, table, "id", "metadata") + update_json_column(cur, table, "id", "authority_hints") + update_json_column(cur, table, "id", "trust_marks") + update_json_column(cur, table, "id", "trust_mark_issuers") + update_json_column(cur, table, "id", "jwks_fed") + update_json_column(cur, table, "id", "jwks_core") + elif table == "spid_cie_oidc_entity_fetchedentitystatement": + update_text_url_column(cur, table, "id", "sub") + update_text_url_column(cur, table, "id", "iss") + update_json_column(cur, table, "id", "statement") + update_text_url_column(cur, table, "id", "jwt") + elif table == "spid_cie_oidc_entity_trustchain": + update_text_url_column(cur, table, "id", "sub") + update_json_column(cur, table, "id", "chain") + update_json_column(cur, table, "id", "metadata") + update_json_column(cur, table, "id", "jwks") + update_json_column(cur, table, "id", "parties_involved") + update_json_column(cur, table, "id", "trust_marks") + + conn.commit() + finally: + conn.close() + + +if __name__ == "__main__": + main() diff --git a/scripts/rewrite_dumps.py b/scripts/rewrite_dumps.py index 314175d..f13bfa5 100644 --- a/scripts/rewrite_dumps.py +++ b/scripts/rewrite_dumps.py @@ -17,6 +17,13 @@ def infer_base_url(kind: str) -> str: for host in items: if kind in host: return f"https://{host}" + defaults = { + "fa": "http://federation-authority:8000", + "provider": "http://provider:8002", + "rp": "http://relying-party:8001", + } + if kind in defaults: + return defaults[kind] raise SystemExit(f"missing env for {kind}: set explicit base URL or include hostname in allowed hosts")