Init container that fetches the deployable JAR from a signed URL into the
shared /app/jars/ volume before the main runtime container starts. Pairs
with the controller (Task 7) and DockerRuntimeOrchestrator (Task 10).
- Dockerfile: busybox:1.37-musl, non-root USER (UID 1000)
- entrypoint.sh: POSIX sh, set -eu, required env vars (ARTIFACT_URL,
ARTIFACT_EXPECTED_SIZE), wget with retries/timeout, size verification
- README: build instructions and runtime contract
Smoke-tested locally (docker build + happy-path fetch + size-mismatch).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New permitAll endpoint GET /api/v1/artifacts/{appVersionId}?exp&sig that
the cameleer-runtime-loader init container hits to stream the deployed
JAR. Auth is the HMAC-signed URL (sig + exp) — no JWT, no bootstrap
token — so SecurityConfig permits the path and the controller does the
verification itself.
Also hardens ArtifactDownloadTokenSigner to reject null/blank jwtSecret
at construction (Task 6 review feedback I-3).
Wires the ArtifactDownloadTokenSigner bean in SecurityBeanConfig from
${cameleer.server.security.jwtsecret}, the same property the rest of
the security stack uses.
Test coverage: 200/401/404 paths via standalone-MockMvc unit test
(avoids dragging in WebConfig's audit + usage interceptors that pull
the full bean graph) plus the existing signer suite extended with a
null/blank-secret guard test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Existing rejectsTamperedSignature uses len+1 sig — short-circuits in
MessageDigest.isEqual on length mismatch. Same-length tamper test
forces the byte-by-byte compare so the constant-time branch is
exercised.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tactical filesystem-path read of the AppVersion locator survives until the
loader init-container lands — flagged inline so future readers don't read
the staging step as steady state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 4 of the init-container JAR fetch plan: migrate AppService.uploadJar
off direct filesystem writes onto the ArtifactStore abstraction so future
backends (OCI/Zot, S3) can swap in without touching service or controller
code.
- AppService constructor now takes (AppRepository, AppVersionRepository,
ArtifactStore, tenantId[, CreateGuard]). The store owns layout and the
locator string written into app_versions.jar_path.
- uploadJar buffers the request body once for hashing + storage, then
writes a scratch temp file solely for RuntimeDetector (which still
takes a Path); scratch is unconditionally deleted in finally.
- Add coordinatesFor(AppVersion) helper so downstream callers (Task 5+)
can derive ArtifactCoordinates without knowing the tenant binding.
- Remove resolveJarPath. DeploymentExecutor now reads jarPath directly
off the AppVersion record; the clean cut to download-URL delivery
lands in Task 11.
- RuntimeBeanConfig wires a FilesystemArtifactStore bean rooted at
cameleer.server.runtime.jarstoragepath and threads tenantId into the
AppService bean.
14-task TDD plan to replace bind-mount JAR delivery with init-container
download from Cameleer over HTTP, sitting behind a new ArtifactStore
abstraction. Lands withUsernsMode hardening (last open gap from #152) and
gives storage a clean migration path to OCI (Zot) tracked separately in
issue #158.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drove the full revocation flow against a running cameleer-server-app jar
(temp postgres+clickhouse, env-var admin):
GET /auth/me with fresh token -> 200
POST /auth/logout -> 204
GET /auth/me with same revoked token -> 401
POST /auth/logout (unauthenticated) -> 204
users.token_revoked_before -> non-null
audit_log (action=logout, category=AUTH) -> 1 row, SUCCESS
Proves the full chain end-to-end: controller revokes, audit lands, and
the JwtAuthenticationFilter prefix-strip fix actually enforces revocation
against the bare users.user_id (the original bug).
Browser-driven SPA smoke is still owed — Playwright MCP allowlist in
this env blocks 8081, so the SPA flow was verified by code-inspection
during Tasks 4+5. OIDC-user smoke against Logto remains owed pending
post_logout_redirect_uri registration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes a silent token-revocation bug (JwtAuthenticationFilter was looking
up users by prefixed JWT subject instead of the bare user_id), adds
POST /api/v1/auth/logout that bumps token_revoked_before, and replaces
the broken cross-origin fetch logout in the SPA with a proper top-level
RP-Initiated Logout redirect (id_token_hint + post_logout_redirect_uri
+ client_id). Adds a signed-out splash and prompt=login defence.
Operational follow-up: SaaS team must register
<base-url>/login as a post_logout_redirect_uri on each Logto tenant
client. See docs/handoff/2026-04-27-logout-hardening.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Records the automated outcomes (4/4 ITs pass, typecheck + build green)
and lists the three manual smoke tests still required from the SaaS
team — local-user, OIDC-user against Logto, stolen-token. The OIDC test
depends on Logto-side post_logout_redirect_uri registration; the others
can be exercised against any cameleer-server deployment.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Operational note for the cameleer-saas / Logto admin team. Covers what
changed in cameleer-server (RP-Initiated Logout via top-level redirect
+ POST /auth/logout server-side revocation + signed-out splash +
prompt=login defence), what they need to register in Logto per tenant,
how to verify, and a failure-mode runbook table.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates both UiAuthController listings (Auth flat + security/) so future
sessions know /logout exists, that it bumps token_revoked_before with a
+1ms race-safety bump, and that it audits under AuditCategory.AUTH.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two defensive layers complementing the RP-Initiated Logout in 82e25933:
1. cameleer:signed_out sessionStorage flag (set in auth-store.logout,
read+cleared in LoginPage on mount) renders a 'You have been signed
out successfully' card with an explicit 'Sign in again' button.
Mirrors the cameleer-saas pattern.
2. prompt=login on the OIDC authorization redirect forces the IdP to
re-prompt for credentials even if its session cookie somehow
survived RP-Initiated Logout (proxy, race, misconfigured
post_logout_redirect_uri). OIDC Core 1.0 §3.1.2.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous logout fired fetch(end_session, {mode:'no-cors'}), which is a
no-op for OIDC: cross-origin fetch never clears the IdP's session cookie.
Result: subsequent SSO clicks silently re-authenticated the prior user.
New flow:
1. Best-effort POST /auth/logout to bump token_revoked_before.
2. Clear localStorage + Zustand state.
3. Set sessionStorage 'cameleer:signed_out=1' so /login renders a
confirmation splash (mirrors cameleer-saas pattern).
4. window.location.replace(end_session_endpoint?id_token_hint=...
&post_logout_redirect_uri=...&client_id=...) — top-level navigation,
the only form that actually clears the IdP session cookie.
client_id is now persisted at OIDC initiation alongside
end_session_endpoint and id_token, so logout has all three params
without an extra round-trip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picks up the new POST /api/v1/auth/logout endpoint introduced in
90315330. Generated against a locally-running build (not the remote
generate-api:live URL, which lags behind this branch).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps token_revoked_before by 1ms so a JWT issued in the same millisecond
as a logout call (Date.from(Instant.now()) quantises iat to ms) does not
survive the filter's strict isBefore check.
Also extends LogoutControllerIT @AfterEach to delete the audit_log row,
keeping reused Postgres containers clean for downstream ITs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps users.token_revoked_before = now() for the calling user, audited
under AuditCategory.AUTH. Best-effort: returns 204 even when the request
is unauthenticated, so the SPA can call it on every logout regardless of
token state. Token-rejection is enforced by the existing
JwtAuthenticationFilter revocation check (fixed in 7066795c).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds @AfterEach to delete the test users so Testcontainers reuse does
not leak an authenticated user with a future token_revoked_before into
the shared schema (visible to LicenseUsageReader.snapshot, user-admin
listing tests, etc.). Adds unrevokedUserTokenIsAccepted to pin the
revoked == null no-op branch as a first-class assertion.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JwtAuthenticationFilter compared the JWT subject (user:alice) against
users.user_id (bare alice), so token_revoked_before was never read for
any user. Strips the prefix to match the convention documented in
CLAUDE.md. Adds JwtRevocationIT as a regression.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tracks the work to (a) fix the silently-inert token-revocation lookup in
JwtAuthenticationFilter, (b) add POST /api/v1/auth/logout that bumps
users.token_revoked_before, and (c) replace the broken cross-origin
fetch logout in the SPA with proper RP-Initiated Logout (top-level
redirect) plus a signed-out splash and prompt=login defence.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The license-api module was added in 858975f0 but the CI deploy step's
`-pl` list still only built parent + server-core + minter. server-core
now depends on cameleer-license-api, which wasn't in the registry yet,
so the deploy job failed with:
Could not find artifact com.cameleer:cameleer-license-api:jar:1.0-SNAPSHOT
in gitea (https://gitea.siegeln.net/api/packages/cameleer/maven)
Add cameleer-license-api to the project list so it builds and publishes
before its consumers in the same reactor invocation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Splits the pure license contract types (LicenseInfo, LicenseValidator,
LicenseState, LicenseStateMachine, LicenseLimits, DefaultTierLimits) into a
new cameleer-license-api module under package com.cameleer.license.
Why: cameleer-license-minter previously depended on cameleer-server-core for
these types, dragging cameleer-server-core + cameleer-common onto the
classpath of every minter consumer (notably cameleer-saas). The SaaS
management plane has no business carrying server-runtime types — it only
needs the license contract to mint and verify tokens.
After:
cameleer-license-minter -> cameleer-license-api (no server internals)
cameleer-server-core -> cameleer-license-api
cameleer-saas -> cameleer-license-minter -> cameleer-license-api
Verified: mvn -pl cameleer-license-minter dependency:tree shows the minter
no longer pulls cameleer-server-core or cameleer-common. Full reactor
verify (-DskipITs) green: 371 tests pass.
LicenseGate stays in server-core (server-runtime state holder, not contract).
Closescameleer/cameleer-server#156
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the prompt=none → /login?local trap with a deterministic
capability endpoint (GET /api/v1/auth/capabilities). LoginPage renders
SSO-primary or local form based on caps; ?local is the explicit
admin-recovery escape hatch. Drops prompt=none from the SSO authorize
URL per RFC 9700 §4.4. Adds Vitest + IT coverage and docs.
MFA enrollment / enforcement deferred to issue #154.
Adds <distributionManagement> at the parent POM and a push-only deploy
step in the build job. Selects the parent + core + minter via -pl so
both the plain library JAR and the Spring Boot fat CLI JAR are pushed
with their full dep tree resolvable; server-app is excluded as a
fat-jar runtime, not a library.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the decision to gate login UX on capabilities (no SaaS-mode
flag), drop prompt=none from the primary OIDC flow per RFC 9700 §4.4,
and keep ?local as the explicit admin-recovery escape hatch.
MFA enrollment / enforcement and password reset for local accounts are
explicitly deferred and tracked in issue #154.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>