# theta-suite — unified SSO Manager + Proxy.
#
# Brings up the two all-in-one images on one bridge network so the proxy can
# reach the SSO internally (http://sso-manager:3001 for token/userinfo,
# ldaps://sso-manager:636 for LDAP) without exposing the SSO's HTTP port to the
# internet. The proxy is the public front (80/443); the SSO sits behind it.
#
# Each project builds from its git submodule:
#   ./sso-manager-node  -> Dockerfile.openldap (app + OpenLDAP + Redis)
#   ./proxy              -> Dockerfile          (OpenResty + app + Redis)
# So `git clone --recursive` is required to get the submodules first.
#
# Config + secrets live in bind-mounted ./config/ (gitignored):
#   ./config/sso-secrets.js   — SSO app + orchestrator config
#   ./config/proxy-secrets.js — proxy OIDC/LDAP/auth config
# Each app's entrypoint points CONF_SECRETS at its file so @simpleworkjs/conf
# (>= 1.2.0) reads it directly -- no app_* env is passed (app_* env would
# override secrets.js), and no write access to /app/conf is needed. The
# sso-manager mounts ./config read-write so the bootstrap can write the
# generated OAuth client creds back into proxy-secrets.js; the proxy mounts
# it read-only.
#
# Compose only interpolates the port defaults below — there is no .env file.
# First-run wiring (LDAP service account, first admin, OAuth client) is
# automated by ./setup.sh, which runs bootstrap/bootstrap.js inside the
# sso-manager container.

services:
  sso-manager:
    build:
      context: ./sso-manager-node
      dockerfile: Dockerfile.openldap
      args:
        # A submodule's .git is a pointer file, not a real repo — the image
        # can't resolve its own commit hash from inside the build context.
        # setup.sh sets this from the host, where the submodule resolves
        # correctly (git -C sso-manager-node rev-parse --short HEAD).
        GIT_COMMIT: ${SSO_GIT_COMMIT:-}
        # Optional upstream HTTP(S) proxy for npm/apt during the build (NOT
        # the theta42 "proxy" app). Set CFG_HTTP_PROXY in setup.env; empty by
        # default, so this is a no-op unless configured.
        HTTP_PROXY: ${CFG_HTTP_PROXY:-}
        HTTPS_PROXY: ${CFG_HTTPS_PROXY:-}
        NO_PROXY: ${CFG_NO_PROXY:-}
    container_name: sso-manager
    restart: unless-stopped
    depends_on:
      - openbao
    networks: [theta-net]
    ports:
      # SSO web UI. Bind address is configurable via SSO_BIND (default 0.0.0.0 so
      # the UI is reachable on the LAN during setup). Set SSO_BIND=127.0.0.1 to
      # lock it to localhost once the proxy fronts it at https://<SSO_HOST>.
      - "${SSO_BIND:-0.0.0.0}:${SSO_PORT:-3001}:3001"
      # LDAPS for EXTERNAL direct-LDAP clients (legacy apps). The proxy itself
      # reaches LDAPS over theta-net (sso-manager:636) without this host mapping.
      # Prefer an internal-only hostname (set CFG_LDAPS_HOST in setup.env / ldapsHost
      # in sso-secrets.js) and do NOT forward 636 to the public internet.
      - "${LDAPS_PORT:-636}:636"
      # Plain LDAP (389) is NOT mapped — direct-LDAP clients should use LDAPS.
    environment:
      # Config (LDAP, OAuth, SMTP, ...) is loaded by @simpleworkjs/conf from
      # ./config/sso-secrets.js (see volumes), then @simpleworkjs/bao-conf
      # deep-merges secret/sso-manager/conf from OpenBao over it at boot
      # (VAULT_ADDR/VAULT_TOKEN below). NODE_ENV/NODE_PORT are the only other
      # env the app reads. VAULT_TOKEN is the scoped SSO_VAULT_TOKEN minted by
      # setup.sh (policy sso-broker) — NOT the root token.
      - NODE_ENV=production
      - NODE_PORT=3001
      - LDAP_SERVER_ID=${LDAP_SERVER_ID:-}
      - LDAP_REPLICATION_HOSTS=${LDAP_REPLICATION_HOSTS:-}
      - VAULT_ADDR=http://openbao:8200
      - VAULT_TOKEN=${SSO_VAULT_TOKEN:-}
      # Optional upstream HTTP(S) proxy for outbound calls (SMTP, etc.) at
      # runtime. See the build args above for the same setting during build.
      - HTTP_PROXY=${CFG_HTTP_PROXY:-}
      - HTTPS_PROXY=${CFG_HTTPS_PROXY:-}
      - NO_PROXY=${CFG_NO_PROXY:-}
    volumes:
      # Operator-edited SSO secrets (sso-secrets.js). Read-WRITE so the bootstrap
      # can write the generated OAuth client creds into proxy-secrets.js. The
      # entrypoint points CONF_SECRETS at /config/sso-secrets.js.
      - ./config:/config
      # Persist the LDAP database across container recreation.
      - ldap-data:/var/lib/ldap
      # Persist the auto-generated self-signed TLS cert so clients don't have to
      # re-trust it on every rebuild.
      - ldap-certs:/etc/openldap/certs
      # Persist Redis (AOF + RDB) so OAuth clients, tokens, and other Redis state
      # survive container recreation.
      - sso-data:/data
      # Bind-mount the bootstrap script so `docker compose exec sso-manager node
      # /bootstrap/bootstrap.js` can run it (read-only).
      - ./bootstrap:/bootstrap:ro
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3001/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 30s

  proxy:
    build:
      context: ./proxy
      dockerfile: Dockerfile
      args:
        # A submodule's .git is a pointer file, not a real repo — the image
        # can't resolve its own commit hash from inside the build context.
        # setup.sh sets this from the host, where the submodule resolves
        # correctly (git -C proxy rev-parse --short HEAD).
        GIT_COMMIT: ${PROXY_GIT_COMMIT:-}
        # Optional upstream HTTP(S) proxy for npm/apt during the build. See
        # the sso-manager service above for details.
        HTTP_PROXY: ${CFG_HTTP_PROXY:-}
        HTTPS_PROXY: ${CFG_HTTPS_PROXY:-}
        NO_PROXY: ${CFG_NO_PROXY:-}
    container_name: proxy
    restart: unless-stopped
    networks: [theta-net]
    depends_on:
      sso-manager:
        condition: service_healthy
      openbao:
        condition: service_started
    ports:
      - "${HTTP_PORT:-80}:80"
      - "${HTTPS_PORT:-443}:443"
      - "${HTTPS_ALT_PORT:-4443}:4443"
      # Management UI/API. Bind address is configurable via MGMT_BIND (default
      # 0.0.0.0 so it's reachable on the LAN during setup). Set MGMT_BIND=127.0.0.1
      # to lock it to localhost once the proxy fronts it under TLS.
      - "${MGMT_BIND:-0.0.0.0}:${MGMT_PORT:-3000}:3000"
    environment:
      # oidc/ldap/auth config is loaded by @simpleworkjs/conf from
      # ./config/proxy-secrets.js (see volumes), then @simpleworkjs/bao-conf
      # deep-merges secret/proxy/conf from OpenBao over it at boot. The OAuth
      # clientSecret is consumed at require time, so bao-conf.init() runs
      # BEFORE require('../app') in bin/www. NODE_ENV/NODE_PORT are process env
      # the app reads directly. VAULT_TOKEN is the scoped PROXY_VAULT_TOKEN
      # (policy proxy — read only secret/proxy/conf).
      - NODE_ENV=production
      - NODE_PORT=3000
      - VAULT_ADDR=http://openbao:8200
      - VAULT_TOKEN=${PROXY_VAULT_TOKEN:-}
      # Optional upstream HTTP(S) proxy for outbound calls (ACME/Let's
      # Encrypt, DNS providers) at runtime.
      - HTTP_PROXY=${CFG_HTTP_PROXY:-}
      - HTTPS_PROXY=${CFG_HTTPS_PROXY:-}
      - NO_PROXY=${CFG_NO_PROXY:-}
    volumes:
      # Operator-edited proxy secrets (proxy-secrets.js). READ-ONLY — the proxy
      # only reads it; the sso-manager bootstrap writes the OAuth creds. The
      # entrypoint points CONF_SECRETS at /config/proxy-secrets.js. Kept as a
      # fail-soft fallback: bao-conf.init() is fail-soft, so if OpenBao is
      # unreachable the app boots from this file instead.
      - ./config:/config:ro
      # Persist Redis (AOF + RDB) so Host records, permissions, DNS creds, local
      # users, AND the auto-ssl Let's Encrypt certs survive container recreation.
      - proxy-data:/data
      - proxy-cache:/var/cache/nginx/proxy
      - proxy-logs:/var/log/nginx
      # OPTIONAL, for strict LDAPS trust (see README "Security notes"): mount
      # the SSO's self-signed cert into the proxy read-only, then set
      # ldap.tlsOptions.ca=<path-below> in ./config/proxy-secrets.js.
      # - ldap-certs:/etc/ssl/sso-ldap-certs:ro
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 30s

  # SSH jump host — a core component, always built + started alongside the
  # SSO and proxy. Authenticates users against the SSO's OpenLDAP, resolves
  # reachable hosts from the directory API, and bridges SSH through.
  jump-host:
    build:
      context: ./jump-host
      dockerfile: Dockerfile
      args:
        GIT_COMMIT: ${JUMP_GIT_COMMIT:-}
        # Optional upstream HTTP(S) proxy for npm/apt during the build. See
        # the sso-manager service above for details.
        HTTP_PROXY: ${CFG_HTTP_PROXY:-}
        HTTPS_PROXY: ${CFG_HTTPS_PROXY:-}
        NO_PROXY: ${CFG_NO_PROXY:-}
    container_name: jump-host
    restart: unless-stopped
    networks: [theta-net]
    depends_on:
      sso-manager:
        condition: service_healthy
      openbao:
        condition: service_started
    ports:
      - "${JUMP_SSH_PORT:-2222}:2222"   # SSH front door
      - "${JUMP_WEB_BIND:-0.0.0.0}:${JUMP_WEB_PORT:-3002}:3002"  # web UI/API
    environment:
      - NODE_ENV=production
      # Secrets are loaded by @simpleworkjs/conf from ./config/jump-secrets.js,
      # then @simpleworkjs/bao-conf deep-merges secret/jump-host/conf from
      # OpenBao over it at boot. VAULT_TOKEN is the scoped JUMP_VAULT_TOKEN
      # (policy jump-host — read only secret/jump-host/conf).
      - VAULT_ADDR=http://openbao:8200
      - VAULT_TOKEN=${JUMP_VAULT_TOKEN:-}
      # Optional upstream HTTP(S) proxy for outbound calls (the directory API
      # client) at runtime.
      - HTTP_PROXY=${CFG_HTTP_PROXY:-}
      - HTTPS_PROXY=${CFG_HTTPS_PROXY:-}
      - NO_PROXY=${CFG_NO_PROXY:-}
    volumes:
      - ./config:/config:ro         # jump-secrets.js (written by ensure_config/bootstrap)
      - jump-data:/var/lib/jump-host  # generated host keys persist here
      - jump-redis-data:/data         # Redis (sessions, OAuth state, API tokens) persists here

  # A real, LDAP-joined (SSSD + AuthorizedKeysCommand) downstream host for
  # testing jump-host's actual key-injection -> upstream-connect flow --
  # a container with a manually-dropped public key in authorized_keys never
  # exercises the LDAP-key-serving path a real production host does. Built
  # from the theta42/ldap-client submodule -- see ./config/ldap-test-host.vars
  # for setup notes. Opt-in test fixture: bring it up explicitly with
  # `docker compose --profile ldap-test up` (jump-host itself now starts
  # unconditionally, so this only adds a downstream host for it to reach).
  ldap-test-host:
    profiles: ["ldap-test"]
    build:
      context: ./ldap-client
      dockerfile: Dockerfile
    container_name: ldap-test-host
    hostname: ldap-test-host
    restart: unless-stopped
    networks: [theta-net]
    depends_on:
      sso-manager:
        condition: service_healthy
    privileged: false
    volumes:
      - ./config/ldap-test-host.vars:/config/ldap.vars:ro
      - ./config/ldap-ca.crt:/config/ldap-ca.crt:ro

  # Renews the three periodic service tokens (theta-svc role, 768h period)
  # every 12h. Periodic tokens live forever ONLY while something renews them —
  # this sidecar is that something, so the stack survives arbitrarily long
  # uptimes and the tokens in .env never silently expire. If a token is missing
  # or already dead it just logs and moves on (setup.sh re-mints on next run).
  bao-renewer:
    image: quay.io/openbao/openbao:latest
    container_name: bao-renewer
    restart: unless-stopped
    depends_on:
      - openbao
    environment:
      - BAO_ADDR=http://openbao:8200
      - SSO_VAULT_TOKEN=${SSO_VAULT_TOKEN:-}
      - PROXY_VAULT_TOKEN=${PROXY_VAULT_TOKEN:-}
      - JUMP_VAULT_TOKEN=${JUMP_VAULT_TOKEN:-}
    entrypoint: ["/bin/sh", "-c"]
    command:
      - |
        renew() {
          if [ -z "$$2" ]; then return 0; fi
          if BAO_TOKEN="$$2" bao token renew > /dev/null 2>&1; then
            echo "[bao-renewer] renewed $$1"
          else
            echo "[bao-renewer] FAILED to renew $$1 (expired/revoked? re-run setup.sh to re-mint)"
          fi
        }
        while true; do
          renew SSO_VAULT_TOKEN "$$SSO_VAULT_TOKEN"
          renew PROXY_VAULT_TOKEN "$$PROXY_VAULT_TOKEN"
          renew JUMP_VAULT_TOKEN "$$JUMP_VAULT_TOKEN"
          sleep 43200
        done
    networks:
      - theta-net

  openbao:
    image: quay.io/openbao/openbao:latest
    container_name: openbao
    restart: unless-stopped
    cap_add:
      - IPC_LOCK
    command: server -config=/vault/config/openbao.hcl
    environment:
      - BAO_ADDR=http://127.0.0.1:8200
    ports:
      - "8080:8200"
    volumes:
      - ./config/openbao.hcl:/vault/config/openbao.hcl:ro
      - openbao-data:/vault/data
    networks:
      - theta-net
networks:
  theta-net:
    driver: bridge

volumes:
  ldap-data:
  ldap-certs:
  sso-data:
  proxy-data:
  proxy-cache:
  proxy-logs:
  jump-data:
  jump-redis-data:
  openbao-data: