Replace TenantResolutionFilter + TenantOwnershipValidator (15 manual
calls across 5 controllers) with a single TenantIsolationInterceptor
that uses Spring HandlerMapping path variables for fail-closed tenant
isolation. New endpoints with {tenantId}, {environmentId}, or {appId}
path variables are automatically isolated without manual code.
Simplify OrgResolver from dual-token fetch to single token — Logto
merges all scopes into either token type.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add @PreAuthorize annotations to all API controllers (14 endpoints
across 6 controllers) enforcing OAuth2 scopes: apps:manage, apps:deploy,
billing:manage, observe:read, platform:admin.
Enforce tenant isolation: TenantResolutionFilter now rejects cross-tenant
access on /api/tenants/{id}/* paths. New TenantOwnershipValidator checks
environment/app ownership for paths without tenantId. Platform admins
bypass both layers.
Fix frontend: OrgResolver split into two useEffect hooks so scopes
refresh on org switch. Scopes now served from /api/config (single source
of truth). Bootstrap cleaned — standalone org permissions removed.
Update docs/architecture.md, docs/user-manual.md, and CLAUDE.md to
reflect all auth hardening changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add @Primary + @ConditionalOnMissingBean so TestSecurityConfig.jwtDecoder()
wins over SecurityConfig.jwtDecoder() without needing a real OIDC endpoint
- Add spring.main.allow-bean-definition-overriding=true and
cameleer.clickhouse.enabled=false to src/test/resources/application-test.yml
so Testcontainers @ServiceConnection can supply the datasource
- Disable ClickHouse in test profile (src/main/resources/application-test.yml)
so the explicit ClickHouseConfig DataSource bean is not created, allowing
@ServiceConnection to wire the Testcontainers Postgres datasource
- Fix TenantControllerTest and LicenseControllerTest to explicitly grant
ROLE_platform-admin authority via .authorities() on the test JWT, since
spring-security-test does not run the custom JwtAuthenticationConverter
- Fix EnvironmentService.createDefaultForTenant() to use an internal
bootstrap path that skips license enforcement (chicken-and-egg: no license
exists at tenant creation time yet)
- Remove now-unnecessary license stub from EnvironmentServiceTest
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove bootstrapToken field/getter/setter from EnvironmentEntity and drop
the RuntimeConfig dependency from EnvironmentService. DeploymentService and
AgentStatusService now use a TODO-api-key placeholder until the ApiKeyService
wiring is complete. All test references to setBootstrapToken removed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove LogtoManagementClient dependency from TenantController; gate
listAll and create with @PreAuthorize("hasRole('platform-admin')"),
relying on the JWT roles claim already mapped by JwtAuthenticationConverter.
Update TenantControllerTest to supply the platform-admin role via jwt()
on all POST requests that expect 201/409.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drop JwtConfig dependency from LicenseService; generate license tokens as
random UUIDs instead. Add findByToken to LicenseRepository and update
verifyLicenseToken to do a DB lookup. Update LicenseServiceTest to match.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The bootstrap dynamically creates the M2M app and writes credentials
to the JSON file. LogtoConfig now falls back to the bootstrap file
when LOGTO_M2M_CLIENT_ID/SECRET env vars are not set.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Logto issues access tokens with typ "at+jwt" (RFC 9068) but Spring
Security's default NimbusJwtDecoder only allows "JWT". Custom decoder
accepts any type. Also removed hard 401 redirect from API client.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The bootstrap script runs before cameleer-saas (Flyway), so tenant
tables don't exist yet. Moved DB seeding to BootstrapDataSeeder
ApplicationRunner which runs after Flyway migrations complete.
Reads bootstrap JSON and creates tenant/environment/license if missing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bootstrap script now creates:
- SaaS Owner (admin/admin) with platform-admin role
- Tenant Admin (camel/camel) in Example Tenant org
- Traditional Web App for cameleer3-server OIDC
- DB records: tenant, default environment, license
- Configures cameleer3-server OIDC via its admin API
All credentials configurable via env vars.
Backend:
- Fix LogtoManagementClient resource URL (https://default.logto.app/api)
- Add getUserRoles/getUserOrganizations to LogtoManagementClient
- Add GET /api/me endpoint (user info, platform admin status, tenants)
- Add GET /api/tenants list-all for platform admins
- Remove insecure X-header forwarding from Traefik
Frontend:
- Org-scoped tokens: getAccessToken(resource, orgId) for tenant context
- OrgResolver component populates org store from /api/me
- useOrganization Zustand store (currentOrgId + currentTenantId)
- Platform admin sidebar section + AdminTenantsPage
- View Dashboard link points to cameleer3-server on port 8081
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Logto returns opaque access tokens when no resource is specified.
Added API resource creation to bootstrap, included resource indicator
in /api/config, and SPA now passes resource parameter in auth request.
Also fixed issuer-uri to match Logto's public endpoint.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Separate LOGTO_PUBLIC_ENDPOINT (browser-facing, defaults to
http://localhost:3001) from LOGTO_ENDPOINT (Docker-internal).
Also fix bootstrap M2M verification by using correct Host header
for default tenant token endpoint.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- logto-bootstrap.sh: API-driven init script that creates SPA app,
M2M app, and default user (camel/camel) via Logto Management API.
Reads m-default secret from DB, then removes seeded apps with
known secrets (security hardening). Idempotent.
- PublicConfigController: /api/config public endpoint serves Logto
client ID from bootstrap output file (runtime, not build-time)
- Frontend: LoginPage + CallbackPage fetch config from /api/config
instead of import.meta.env (fixes Vite build-time baking issue)
- Docker Compose: logto-bootstrap init service with health-gated
dependency chain, shared volume for bootstrap config
- SecurityConfig: permit /api/config without auth
Flow: docker compose up → bootstrap creates apps/user → SPA fetches
config → login page shows → sign in with Logto → camel/camel
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The SPA (index.html, /login, /callback, /assets/*) must be accessible
without authentication. API routes remain protected.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add ClickHouseProperties with @ConfigurationProperties
- @ConditionalOnProperty to toggle ClickHouse
- @Primary DataSource + JdbcTemplate for PostgreSQL (prevents Spring
Boot from routing JPA/Flyway to ClickHouse)
- HikariDataSource for ClickHouse with explicit credentials
- Remove separate DataSourceConfig.java (merged into ClickHouseConfig)
- Remove database-platform override (no longer needed with @Primary)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements AgentStatusService (TDD) that proxies cameleer3-server agent
registry API and queries ClickHouse for trace counts. Gracefully degrades
to UNKNOWN state when server is unreachable or DataSource is absent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds domain config to RuntimeConfig/application.yml, expands AppResponse
with exposedPort and computed routeUrl, adds updateRouting to AppService,
and adds PATCH /{appId}/routing endpoint to AppController.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add DeploymentResponse DTO, DeploymentController at /api/apps/{appId} with POST /deploy (202), GET /deployments, GET /deployments/{id}, POST /stop, POST /restart (202), and integration tests covering empty list, 404, and 401 cases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements DeploymentService with TDD: builds Docker images, starts containers with Cameleer env vars, polls for health, and handles stop/restart lifecycle. All 3 unit tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds AppController at /api/environments/{environmentId}/apps with POST (multipart
metadata+JAR), GET list, GET by ID, PUT jar reupload, and DELETE endpoints.
Also adds CreateAppRequest and AppResponse DTOs, integration tests (AppControllerTest),
and fixes ClickHouseConfig to be excluded in test profile via @Profile("!test").
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements AppService with JAR file storage, SHA-256 checksum computation,
tier-based app limit enforcement via LicenseDefaults, and audit logging.
Four TDD tests all pass covering creation, JAR validation, duplicate slug
rejection, and JAR re-upload.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements POST/GET/PATCH/DELETE endpoints at /api/tenants/{tenantId}/environments
with DTOs, mapping helpers, and a Spring Boot integration test (TestContainers).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements EnvironmentService with full CRUD, duplicate slug rejection,
tier-based environment count limits, and audit logging for create/update/delete.
Adds ENVIRONMENT_CREATE, ENVIRONMENT_UPDATE, ENVIRONMENT_DELETE to AuditAction.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add docker-java and ClickHouse JDBC dependencies, RuntimeConfig and
ClickHouseConfig Spring components, AsyncConfig with deployment thread
pool, and runtime/clickhouse config sections in application.yml.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Auth is now handled by Logto. Removed AuthController, AuthService,
and related DTOs. Integration tests use Spring Security JWT mocks.
Ed25519 JwtService retained for machine token signing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Creates Logto organizations when tenants are created. Authenticates
via M2M client credentials. Gracefully skips when Logto is not
configured (dev/test mode).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GET /auth/verify validates JWT and returns X-User-Id, X-User-Email
headers for downstream service routing via Traefik middleware.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TenantResolutionFilter extracts organization_id from Logto JWT and
resolves to local tenant via TenantService. ThreadLocal TenantContext
available throughout request lifecycle.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dual auth: machine endpoints use Ed25519 JWT filter, all other API
endpoints use Spring Security OAuth2 Resource Server with Logto OIDC.
Mock JwtDecoder provided for test isolation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /api/tenants/{id}/license generates Ed25519-signed license JWT.
GET /api/tenants/{id}/license returns active license.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Generates tier-aware license tokens with features/limits per tier.
Verifies signature and expiry. Audit logged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>