diff --git a/CLAUDE.md b/CLAUDE.md index 5fed7a8..af94528 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -169,9 +169,19 @@ These env vars are injected into provisioned per-tenant server containers: | `CAMELEER_SERVER_URL` | `http://cameleer3-server-{slug}:8081` | Per-tenant server URL (DNS alias on tenant network) | | `CAMELEER_ROUTING_DOMAIN` | `${PUBLIC_HOST}` | Domain for Traefik routing labels | | `CAMELEER_ROUTING_MODE` | `path` | `path` or `subdomain` routing | +| `CAMELEER_JAR_STORAGE_PATH` | `/data/jars` | Directory for uploaded JARs | +| `CAMELEER_DOCKER_NETWORK` | `cameleer-tenant-{slug}` | Primary network for deployed app containers | +| `CAMELEER_JAR_DOCKER_VOLUME` | `cameleer-jars-{slug}` | Docker volume name for JAR sharing between server and deployed containers | | `BASE_PATH` (server-ui) | `/t/{slug}` | React Router basename + `` tag | | `CAMELEER_API_URL` (server-ui) | `http://cameleer-server-{slug}:8081` | Nginx upstream proxy target (NOT `API_URL` — image uses `${CAMELEER_API_URL}`) | +### Per-tenant volume mounts (set by DockerTenantProvisioner) + +| Mount | Container path | Purpose | +|-------|---------------|---------| +| `/var/run/docker.sock` | `/var/run/docker.sock` | Docker socket for app deployment orchestration | +| `cameleer-jars-{slug}` (volume) | `/data/jars` | Shared JAR storage — server writes, deployed app containers read | + ### Server OIDC role extraction (two paths) | Path | Token type | Role source | How it works | @@ -179,7 +189,9 @@ These env vars are injected into provisioned per-tenant server containers: | SaaS platform -> server API | Logto org-scoped access token | `scope` claim | `JwtAuthenticationFilter.extractRolesFromScopes()` reads `server:admin` from scope | | Server-ui SSO login | Logto JWT access token (via Traditional Web App) | `roles` claim | `OidcTokenExchanger` decodes access_token, reads `roles` injected by Custom JWT | -The server's OIDC config (`OidcConfig`) includes `audience` (RFC 8707 resource indicator) and `additionalScopes`. The `audience` is sent as `resource` in both the authorization request and token exchange, which makes Logto return a JWT access token instead of opaque. The Custom JWT script maps org roles to `roles: ["server:admin"]`. If OIDC returns no roles and the user already exists, `syncOidcRoles` preserves existing local roles. +The server's OIDC config (`OidcConfig`) includes `audience` (RFC 8707 resource indicator) and `additionalScopes`. The `audience` is sent as `resource` in both the authorization request and token exchange, which makes Logto return a JWT access token instead of opaque. The Custom JWT script maps org roles to `roles: ["server:admin"]`. + +**CRITICAL:** `additionalScopes` MUST include `urn:logto:scope:organizations` and `urn:logto:scope:organization_roles` — without these, Logto doesn't populate `context.user.organizationRoles` in the Custom JWT script, so the `roles` claim is empty and all users get `defaultRoles` (VIEWER). The server's `OidcAuthController.applyClaimMappings()` uses OIDC token roles (from Custom JWT) as fallback when no DB claim mapping rules exist: claim mapping rules > OIDC token roles > defaultRoles. ### Deployment pipeline @@ -209,12 +221,14 @@ Idempotent script run via `logto-bootstrap` init container. **Clean slate** — 3b. Create API resource scopes (10 platform + 3 server scopes) 4. Create org roles (owner, operator, viewer with API resource scope assignments) + M2M server role (`cameleer-m2m-server` with `server:admin` scope) 5. Create admin user (platform owner with Logto console access) -7b. Configure Logto Custom JWT for access tokens (maps org roles -> `roles` claim: admin->server:admin, member->server:viewer) +7b. Configure Logto Custom JWT for access tokens (maps org roles -> `roles` claim: owner->server:admin, operator->server:operator, viewer->server:viewer; saas-vendor global role -> server:admin) 8. Configure Logto sign-in branding (Cameleer colors `#C6820E`/`#D4941E`, logo from `/platform/logo.svg`) 9. Cleanup seeded Logto apps 10. Write bootstrap results to `/data/logto-bootstrap.json` -Vendor user is seeded separately via `docker/vendor-seed.sh` (`VENDOR_SEED_ENABLED=true` in dev). The compose stack is: Traefik + PostgreSQL + ClickHouse + Logto + logto-bootstrap + cameleer-saas. No `cameleer3-server` or `cameleer3-server-ui` in compose — those are provisioned per-tenant by `DockerTenantProvisioner`. +12. (Optional) Vendor seed: create `saas-vendor` global role, vendor user, grant Logto console access (`VENDOR_SEED_ENABLED=true` in dev). + +The compose stack is: Traefik + PostgreSQL + ClickHouse + Logto + logto-bootstrap + cameleer-saas. The compose stack is: Traefik + PostgreSQL + ClickHouse + Logto + logto-bootstrap + cameleer-saas. No `cameleer3-server` or `cameleer3-server-ui` in compose — those are provisioned per-tenant by `DockerTenantProvisioner`. ### Tenant Provisioning Flow @@ -225,10 +239,11 @@ When vendor creates a tenant via `VendorTenantService`: 4. Register OIDC redirect URIs for `/t/{slug}/oidc/callback` on Logto Traditional Web App 5. Generate license (tier-appropriate, 365 days) 6. Create tenant-isolated Docker network (`cameleer-tenant-{slug}`) -7. Create server + UI containers with correct env vars, Traefik labels, health check -8. Wait for health check (`/api/v1/health`, not `/actuator/health` which requires auth) -9. Push license token to server via M2M API -10. Push OIDC config (Logto Traditional Web App credentials) to server for SSO +7. Create server container with env vars, Traefik labels (`traefik.docker.network`), health check, Docker socket bind, JAR volume (`cameleer-jars-{slug}:/data/jars`) +8. Create UI container with `CAMELEER_API_URL`, `BASE_PATH`, Traefik strip-prefix labels +9. Wait for health check (`/api/v1/health`, not `/actuator/health` which requires auth) +10. Push license token to server via M2M API +11. Push OIDC config (Traditional Web App credentials + `additionalScopes: [urn:logto:scope:organizations, urn:logto:scope:organization_roles]`) to server for SSO 11. Update tenant status -> ACTIVE ## Database Migrations