docs: update CLAUDE.md with provisioning fixes and OIDC role flow
Documents traefik.docker.network label requirement, JAR volume mount, CAMELEER_API_URL env var, additionalScopes for org roles, and the OIDC role fallback priority (claim mapping > token roles > defaults). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
29
CLAUDE.md
29
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 + `<base>` 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
|
||||
|
||||
Reference in New Issue
Block a user