diff --git a/.env b/.env index 448bb8d..cdac0a7 100644 --- a/.env +++ b/.env @@ -7,3 +7,18 @@ KEYCLOAK_URL=https://kc.kellsupport.com # Custom project name (avoids long folder names in ~/.local/share/containers) COMPOSE_PROJECT_NAME=cmmc-platform + +# Keycloak OIDC config for Kong +KEYCLOAK_URL=https://kc.kellsupport.com +KEYCLOAK_REALM=cmmc +# KEYCLOAK_CLIENT_ID=kong-gateway +# KEYCLOAK_CLIENT_SECRET=3FefJAfN7Rox2x7EW7JzZ38vLI04cXMB +KEYCLOAK_CLIENT_ID=cmmc-oauth2-proxy +KEYCLOAK_CLIENT_SECRET=OOPXQpXhFeG57lC7G1AktnpPyodtDkib +OIDC_SESSION_SECRET=3b4b1b8c8366b1d7c50c49742f879bdd20b85d5b95adaaf4af38d89a36c372ab + +OAUTH2_PROXY_CLIENT_ID=${KEYCLOAK_CLIENT_ID} +OAUTH2_PROXY_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET} +OAUTH2_PROXY_COOKIE_SECRET=P1eo4uZRzZM1QNrXIlzlep5dsmbnJr1K19CETok0BhU= +OAUTH2_PROXY_PROVIDER_URL=https://kc.kellsupport.com/realms/cmmc + diff --git a/Dockerfile.kong-oidc b/Dockerfile.kong-oidc new file mode 100644 index 0000000..55b119d --- /dev/null +++ b/Dockerfile.kong-oidc @@ -0,0 +1,13 @@ +FROM kong:0.14-centos + +USER root + +RUN yum install -y git unzip openssl-devel gcc && yum clean all + +# Install kong-oidc version 1.0.1 +RUN luarocks install kong-oidc 1.0.1 + +# Enable the plugin +ENV KONG_PLUGINS=bundled,oidc + +USER kong diff --git a/Makefile b/Makefile index f4fdfa3..046a67f 100644 --- a/Makefile +++ b/Makefile @@ -27,6 +27,8 @@ check-idp: echo "🛠 No external Keycloak found."; echo 0 >.idp_flag ; \ fi +# kong-template: +# envsubst < kong/kong.template.yml > kong/kong.yml # ---------------------------------------------------------- # Lifecycle targets diff --git a/dev-compose.yaml b/dev-compose.yaml index a6f8304..4ba5028 100644 --- a/dev-compose.yaml +++ b/dev-compose.yaml @@ -1,4 +1,4 @@ -version: "3.9" +# version: "3.9" x-common-env: &common-env TZ: "UTC" @@ -8,21 +8,84 @@ x-common-env: &common-env ############################################################ services: # ────────────────────────────── - kong: - image: docker.io/library/kong:3.7 - container_name: kong - restart: unless-stopped + # kong: + # image: kong/kong:3.6 + # container_name: kong + # restart: unless-stopped + # environment: + # KONG_DATABASE: "off" + # KONG_DECLARATIVE_CONFIG: /config/kong.yml + # KONG_LOG_LEVEL: info + # KONG_PLUGINS: bundled + # KONG_PROXY_ACCESS_LOG: /dev/stdout + # KONG_ADMIN_ACCESS_LOG: /dev/stdout + # KONG_PROXY_ERROR_LOG: /dev/stderr + # KONG_ADMIN_ERROR_LOG: /dev/stderr + # volumes: + # - ./kong:/config + # networks: + # - internal + # - nginx-proxy + # depends_on: + # - fastapi + + + oauth2-proxy: + image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0 + container_name: oauth2-proxy + restart: always + networks: + - internal + - nginx-proxy environment: - <<: *common-env - KONG_DATABASE: "off" - KONG_DECLARATIVE_CONFIG: /config/kong.yml - KONG_LOG_LEVEL: info - volumes: - - ./kong/kong.yml:/config/kong.yml:ro - ports: - - "8000:8000" # proxy (handy for localhost curl) - - "8001:8001" # admin - networks: [internal, nginx-proxy] + OAUTH2_PROXY_PROVIDER: oidc + OAUTH2_PROXY_OIDC_ISSUER_URL: https://kc.kellsupport.com/realms/cmmc + OAUTH2_PROXY_CLIENT_ID: ${OAUTH2_PROXY_CLIENT_ID} + OAUTH2_PROXY_CLIENT_SECRET: ${OAUTH2_PROXY_CLIENT_SECRET} + OAUTH2_PROXY_REDIRECT_URL: https://api.kellsupport.com/oauth2/callback + OAUTH2_PROXY_COOKIE_SECRET: ${OAUTH2_PROXY_COOKIE_SECRET} + + # Forward all traffic to FastAPI (which handles /api/* etc) + OAUTH2_PROXY_UPSTREAMS: http://fastapi:8000 + + # Secure cookies and session behavior + OAUTH2_PROXY_COOKIE_DOMAIN: api.kellsupport.com + OAUTH2_PROXY_COOKIE_NAME: _oauth2_proxy + OAUTH2_PROXY_COOKIE_SECURE: "true" + OAUTH2_PROXY_COOKIE_HTTPONLY: "false" + OAUTH2_PROXY_COOKIE_SAMESITE: Lax + OAUTH2_PROXY_COOKIE_EXPIRE: 168h + OAUTH2_PROXY_COOKIE_REFRESH: 60m + + + # Auth config + OAUTH2_PROXY_EMAIL_DOMAINS: "*" + OAUTH2_PROXY_SET_XAUTHREQUEST: "true" + OAUTH2_PROXY_SET_AUTHORIZATION_HEADER: "true" + OAUTH2_PROXY_PASS_ACCESS_TOKEN: "true" + OAUTH2_PROXY_PASS_USER_HEADERS: "true" + OAUTH2_PROXY_PREFER_EMAIL_TO_USER: "true" + OAUTH2_PROXY_COOKIE_CSRF_PER_REQUEST: "true" + OAUTH2_PROXY_CSRF_COOKIE_NAME: _oauth2_proxy_csrf + OAUTH2_PROXY_SHOW_DEBUG_ON_ERROR: "true" + + + # Optional: PKCE + OAUTH2_PROXY_CODE_CHALLENGE_METHOD: S256 + + # Health routes allowed anonymously + OAUTH2_PROXY_SKIP_AUTH_ROUTES: GET=^/gateway-health$,GET=^/healthz$,GET=^/favicon\.ico$ + OAUTH2_PROXY_SKIP_PROVIDER_BUTTON: "true" + + # Networking + OAUTH2_PROXY_HTTP_ADDRESS: 0.0.0.0:4180 + OAUTH2_PROXY_REVERSE_PROXY: "true" + + expose: + - "4180" + + + # ────────────────────────────── fastapi: @@ -33,7 +96,7 @@ services: <<: *common-env # Default to local container; overridden by external URL in Makefile/CI KEYCLOAK_URL: "${KEYCLOAK_URL:-http://keycloak:8080}" - KEYCLOAK_REALM: "cmmc-platform-dev" + KEYCLOAK_REALM: "cmmc" KEYCLOAK_CLIENT_ID: "frontend" # ports: # # keep reachable only from localhost, not LAN diff --git a/kong/kong.template.yml b/kong/kong.template.yml new file mode 100644 index 0000000..760567a --- /dev/null +++ b/kong/kong.template.yml @@ -0,0 +1,77 @@ +_format_version: "3.0" +_transform: true + + + +services: + - name: fastapi-svc + host: fastapi + port: 8000 + protocol: http + +routes: + - name: api-root + paths: ["/api/"] + strip_path: true + methods: ["GET", "POST", "PUT", "PATCH", "DELETE"] + plugins: + - name: request-transformer + config: + add: + headers: + - "X-Forwarded-User: $http_x_auth_request_user" + - "X-Forwarded-Email: $http_x_auth_request_email" + remove: + headers: + - "Authorization" + + - name: debug-api + url: http://fastapi:8000 + + + # - name: openid + # config: + # issuer: "${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}" + # client_id: "${KEYCLOAK_CLIENT_ID}" + # client_secret: "${KEYCLOAK_CLIENT_SECRET}" + # redirect_uri: "https://api.kellsupport.com/api/" + # scopes: ["openid", "profile", "email"] + # bearer_only: false + # response_type: "code" + # session_secret: "${OIDC_SESSION_SECRET}" + # ssl_verify: false + # timeout: 10000 + + - name: cors + config: + origins: ["*"] + methods: ["GET", "POST", "PUT", "PATCH", "DELETE"] + headers: ["Accept", "Content-Type", "Authorization"] + credentials: true + max_age: 3600 + + - name: api-debug-auth + paths: ["/api/debug-auth"] + strip_path: true + methods: ["GET"] + + - name: gateway-health + paths: ["/gateway-health"] + strip_path: true + methods: ["GET"] + + - name: healthz + paths: ["/healthz"] + strip_path: true + methods: ["GET"] + + - name: debug-auth + paths: ["/api/debug-auth"] + strip_path: true + service: ["debug-api"] + +plugins: + - name: rate-limiting + config: + second: 25 + policy: local diff --git a/kong/kong.yml b/kong/kong.yml index c2f1dda..173d86f 100644 --- a/kong/kong.yml +++ b/kong/kong.yml @@ -1,46 +1,74 @@ _format_version: "3.0" _transform: true + + services: - name: fastapi-svc host: fastapi port: 8000 protocol: http - routes: - - name: fastapi-api - paths: ["/api/"] - strip_path: true - methods: ["GET", "POST", "PUT", "PATCH", "DELETE"] - plugins: - - name: cors - config: - origins: ["*"] - methods: ["GET", "POST", "PUT", "PATCH", "DELETE"] - headers: ["Accept", "Content-Type", "Authorization"] - credentials: false - max_age: 3600 - - - name: fastapi-health - paths: ["/gateway-health", "/healthz"] - strip_path: true - methods: ["GET"] - - - name: kong-meta - url: http://localhost:8001 - routes: - - name: root-status - paths: ["/"] - strip_path: true - methods: ["GET"] +routes: + - name: api-root + paths: ["/api/"] + strip_path: true + methods: ["GET", "POST", "PUT", "PATCH", "DELETE"] plugins: - - name: request-termination + - name: request-transformer config: - status_code: 200 - content_type: application/json - body: '{"status":"Kong Gateway OK"}' + add: + headers: + - "X-Forwarded-User: " + - "X-Forwarded-Email: " + remove: + headers: + - "Authorization" + + - name: debug-api + url: http://fastapi:8000 + # - name: openid + # config: + # issuer: "https://kc.kellsupport.com/realms/cmmc" + # client_id: "kong-gateway" + # client_secret: "3FefJAfN7Rox2x7EW7JzZ38vLI04cXMB" + # redirect_uri: "https://api.kellsupport.com/api/" + # scopes: ["openid", "profile", "email"] + # bearer_only: false + # response_type: "code" + # session_secret: "3b4b1b8c8366b1d7c50c49742f879bdd20b85d5b95adaaf4af38d89a36c372ab" + # ssl_verify: false + # timeout: 10000 + + - name: cors + config: + origins: ["*"] + methods: ["GET", "POST", "PUT", "PATCH", "DELETE"] + headers: ["Accept", "Content-Type", "Authorization"] + credentials: true + max_age: 3600 + + - name: api-debug-auth + paths: ["/api/debug-auth"] + strip_path: true + methods: ["GET"] + + - name: gateway-health + paths: ["/gateway-health"] + strip_path: true + methods: ["GET"] + + - name: healthz + paths: ["/healthz"] + strip_path: true + methods: ["GET"] + + - name: debug-auth + paths: ["/api/debug-auth"] + strip_path: true + service: ["debug-api"] plugins: - name: rate-limiting diff --git a/src/services/app/main.py b/src/services/app/main.py index de47086..2073d9e 100644 --- a/src/services/app/main.py +++ b/src/services/app/main.py @@ -1,4 +1,5 @@ -from fastapi import FastAPI +from fastapi import FastAPI, Request, Depends, HTTPException +from fastapi.responses import JSONResponse app = FastAPI(title="CMMC Platform API MVP", version="0.1.0") @@ -8,6 +9,31 @@ async def healthz() -> dict[str, str]: return {"status": "pong"} +@app.get("/get-health", tags=["meta"]) +async def get_health() -> dict[str, str]: + return {"status": "pong"} + + @app.get("/", tags=["meta"]) async def root() -> dict[str, str]: return {"message": "CMMC Platform – it works!"} + + +def get_current_user(request: Request) -> dict: + user = request.headers.get("X-Auth-Request-User") + email = request.headers.get("X-Auth-Request-Email") + preferred_username = request.headers.get("X-Auth-Request-Preferred-Username") + + if not user: + raise HTTPException(status_code=401, detail="Unauthorized") + + return { + "user": user, + "email": email, + "id": preferred_username + } + + +@app.get("/api/debug-auth", tags=["auth"]) +def debug_auth(user=Depends(get_current_user)): + return JSONResponse(user)