diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8719d2..38f3e97 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -149,13 +149,16 @@ jobs: uses: actions/checkout@v4 - name: Bring up stack - run: docker compose up -d --build --wait --wait-timeout 90 + run: docker compose up -d --build --wait --wait-timeout 180 - name: Smoke (readiness < 30s, public surface served) run: | bash scripts/demo.sh http://localhost:8080 bash scripts/demo.sh http://localhost:8081 + - name: Auth chain proof (token -> protected endpoint -> 200) + run: bash scripts/demo-auth.sh + - name: Dump container logs on failure if: failure() run: docker compose logs --no-color diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..1e7483b --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,10 @@ +# Extends the default gitleaks ruleset; only adds a path-scoped allowlist. +[extend] +useDefault = true + +[allowlist] +description = "Sandbox demo Keycloak realm + token-proof script: a documented, intentionally public demo client secret, not a real credential" +paths = [ + '''^deploy/keycloak/fincore-realm\.json$''', + '''^scripts/demo-auth\.sh$''', +] diff --git a/deploy/keycloak/fincore-realm.json b/deploy/keycloak/fincore-realm.json new file mode 100644 index 0000000..87b761d --- /dev/null +++ b/deploy/keycloak/fincore-realm.json @@ -0,0 +1,71 @@ +{ + "realm": "fincore", + "enabled": true, + "accessTokenLifespan": 300, + "sslRequired": "none", + "clientScopes": [ + { + "name": "ledger:read", + "protocol": "openid-connect", + "attributes": { "include.in.token.scope": "true", "display.on.consent.screen": "false" } + }, + { + "name": "ledger:write", + "protocol": "openid-connect", + "attributes": { "include.in.token.scope": "true", "display.on.consent.screen": "false" } + }, + { + "name": "payments:read", + "protocol": "openid-connect", + "attributes": { "include.in.token.scope": "true", "display.on.consent.screen": "false" } + }, + { + "name": "payments:write", + "protocol": "openid-connect", + "attributes": { "include.in.token.scope": "true", "display.on.consent.screen": "false" } + }, + { + "name": "decision:read", + "protocol": "openid-connect", + "attributes": { "include.in.token.scope": "true", "display.on.consent.screen": "false" } + }, + { + "name": "decision:write", + "protocol": "openid-connect", + "attributes": { "include.in.token.scope": "true", "display.on.consent.screen": "false" } + }, + { + "name": "compliance:read", + "protocol": "openid-connect", + "attributes": { "include.in.token.scope": "true", "display.on.consent.screen": "false" } + }, + { + "name": "compliance:write", + "protocol": "openid-connect", + "attributes": { "include.in.token.scope": "true", "display.on.consent.screen": "false" } + } + ], + "clients": [ + { + "clientId": "fincore-demo", + "enabled": true, + "protocol": "openid-connect", + "publicClient": false, + "secret": "fincore-demo-secret", + "serviceAccountsEnabled": true, + "standardFlowEnabled": false, + "directAccessGrantsEnabled": false, + "defaultClientScopes": [ + "ledger:read", + "ledger:write", + "payments:read", + "payments:write", + "decision:read", + "decision:write", + "compliance:read", + "compliance:write" + ], + "optionalClientScopes": [] + } + ] +} diff --git a/docker-compose.yml b/docker-compose.yml index f75264b..234a8ab 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,18 +14,44 @@ services: retries: 10 start_period: 5s + keycloak: + image: quay.io/keycloak/keycloak:26.6.1 + command: ["start-dev", "--import-realm"] + environment: + KC_BOOTSTRAP_ADMIN_USERNAME: admin + KC_BOOTSTRAP_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-admin} + KC_HOSTNAME: http://keycloak:8080 + KC_HTTP_ENABLED: "true" + KC_HEALTH_ENABLED: "true" + ports: + - "8085:8080" + volumes: + - ./deploy/keycloak:/opt/keycloak/data/import:ro + healthcheck: + test: + [ + "CMD-SHELL", + "exec 3<>/dev/tcp/localhost/9000; echo -e 'GET /health/ready HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3; cat <&3 | grep -q 'HTTP/1.1 200'", + ] + interval: 5s + timeout: 5s + retries: 30 + start_period: 30s + ledger: build: . depends_on: postgres: condition: service_healthy + keycloak: + condition: service_started ports: - "8080:8080" environment: SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/ledger SPRING_DATASOURCE_USERNAME: ledger SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD:-ledger} - KEYCLOAK_ISSUER_URI: http://localhost/realms/fincore + KEYCLOAK_ISSUER_URI: http://keycloak:8080/realms/fincore SPRING_LIQUIBASE_CONTEXTS: production,demo OTLP_TRACING_ENDPOINT: http://otel-collector:4318/v1/traces @@ -37,13 +63,15 @@ services: depends_on: postgres: condition: service_healthy + keycloak: + condition: service_started ports: - "8081:8080" environment: SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/payments SPRING_DATASOURCE_USERNAME: ledger SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD:-ledger} - KEYCLOAK_ISSUER_URI: http://localhost/realms/fincore + KEYCLOAK_ISSUER_URI: http://keycloak:8080/realms/fincore FINCORE_PAYMENTS_BANK_SANDBOX_ENABLED: "true" OTLP_TRACING_ENDPOINT: http://otel-collector:4318/v1/traces diff --git a/scripts/demo-auth.sh b/scripts/demo-auth.sh new file mode 100644 index 0000000..1ce07bf --- /dev/null +++ b/scripts/demo-auth.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: BUSL-1.1 +# SPDX-FileCopyrightText: 2026 FinCore Engine Authors +set -euo pipefail + +KEYCLOAK_URL="${KEYCLOAK_URL:-http://localhost:8085}" +LEDGER_URL="${LEDGER_URL:-http://localhost:8080}" +PAYMENTS_URL="${PAYMENTS_URL:-http://localhost:8081}" +CLIENT_ID="${FINCORE_DEMO_CLIENT_ID:-fincore-demo}" +CLIENT_SECRET="${FINCORE_DEMO_CLIENT_SECRET:-fincore-demo-secret}" +TOKEN_ENDPOINT="$KEYCLOAK_URL/realms/fincore/protocol/openid-connect/token" + +fetch_token() { + curl -fsS -X POST "$TOKEN_ENDPOINT" \ + -d grant_type=client_credentials \ + -d "client_id=$CLIENT_ID" \ + -d "client_secret=$CLIENT_SECRET" | + sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p' +} + +token="" +for attempt in $(seq 1 5); do + token=$(fetch_token || true) + [ -n "$token" ] && break + echo "token fetch attempt $attempt failed; retrying in 2s" + sleep 2 +done + +if [ -z "$token" ]; then + echo "FAIL: could not obtain a client_credentials token from $TOKEN_ENDPOINT" + exit 1 +fi + +ledger_code=$(curl -sS -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $token" "$LEDGER_URL/v1/accounts") +if [ "$ledger_code" != "200" ]; then + echo "FAIL: GET /v1/accounts with a bearer returned $ledger_code (expected 200)" + exit 1 +fi + +payments_code=$(curl -sS -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $token" "$PAYMENTS_URL/v1/payments") +if [ "$payments_code" != "200" ]; then + echo "FAIL: GET /v1/payments with a bearer returned $payments_code (expected 200)" + exit 1 +fi + +echo "auth chain OK: client_credentials token accepted by ledger and payments"