Commit Graph

1711 Commits

Author SHA1 Message Date
hsiegeln
7837272a46 docs(handoff): SaaS-side post_logout_redirect_uri requirement
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>
2026-04-27 12:00:54 +02:00
hsiegeln
2535741474 docs(rules): document POST /auth/logout on UiAuthController
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>
2026-04-27 12:00:05 +02:00
hsiegeln
32c8786d06 feat(ui): signed-out splash + prompt=login on OIDC redirect
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>
2026-04-27 11:59:04 +02:00
hsiegeln
82e2593332 fix(ui): proper OIDC logout — server revoke + top-level redirect
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>
2026-04-27 11:57:04 +02:00
hsiegeln
da3895c31d chore(ui): regenerate OpenAPI schema for /auth/logout
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>
2026-04-27 11:53:40 +02:00
hsiegeln
83a10de497 fix(auth): close same-ms revocation race + tidy audit cleanup
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>
2026-04-27 09:26:05 +02:00
hsiegeln
9031533077 feat(auth): add POST /auth/logout that revokes all user tokens
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>
2026-04-27 09:21:47 +02:00
hsiegeln
b4c6e45d35 test(auth): JwtRevocationIT cleanup + unrevoked-token coverage
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>
2026-04-27 09:18:10 +02:00
hsiegeln
7066795c3c fix(auth): strip user: prefix before token-revocation lookup
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>
2026-04-27 09:11:55 +02:00
hsiegeln
6e4977ea3b docs(plan): logout hardening implementation plan
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>
2026-04-27 09:01:52 +02:00
hsiegeln
1809574fe6 ci: include cameleer-license-api in maven deploy project list
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 3m31s
CI / docker (push) Successful in 2m44s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 48s
SonarQube / sonarqube (push) Successful in 8m32s
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>
2026-04-26 20:41:26 +02:00
hsiegeln
858975f03f refactor(license): extract cameleer-license-api module from server-core
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 2m57s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
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).

Closes cameleer/cameleer-server#156

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:06:52 +02:00
hsiegeln
30db609aff Merge feature/auth-harmonization: capability-driven login UX
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 3m6s
CI / docker (push) Successful in 2m49s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 53s
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.
2026-04-26 19:52:31 +02:00
hsiegeln
45b5f473c9 refactor(auth): post-review tidy — drop @NotNull, refresh e2e comment, use oidc.primary
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 19:48:20 +02:00
hsiegeln
71688dea16 docs(auth): document AuthCapabilitiesController + login routing 2026-04-26 19:41:20 +02:00
hsiegeln
b63b9aa4bb fix(ui): drop OidcCallback ?local trap on login_required 2026-04-26 19:38:15 +02:00
hsiegeln
7565cdcf2f fix(ui): try/finally in handleOidcLogin; logout redirects to /login (not ?local)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 19:36:23 +02:00
hsiegeln
b7d390adf4 feat(ui): capability-driven LoginPage; drop prompt=none silent SSO
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 19:29:59 +02:00
hsiegeln
29769480be feat(ui): useAuthCapabilities hook 2026-04-26 19:23:39 +02:00
hsiegeln
657281461d chore(api): regenerate OpenAPI types for /auth/capabilities
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 19:22:14 +02:00
hsiegeln
af53eca7f6 test(auth): tighten AuthCapabilitiesControllerIT — drop redundant stub, add coverage gaps 2026-04-26 19:17:05 +02:00
hsiegeln
4f6e7ea4dc feat(auth): AuthCapabilitiesController — GET /api/v1/auth/capabilities
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 19:10:17 +02:00
96fc55b932 Merge pull request 'feature/auth-harmonization' (#155) from feature/auth-harmonization into main
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 3m58s
CI / docker (push) Successful in 37s
CI / deploy (push) Successful in 38s
CI / deploy-feature (push) Has been skipped
Reviewed-on: #155
2026-04-26 19:01:00 +02:00
hsiegeln
cd92036f91 ci(minter): deploy license-minter JARs to Gitea Maven registry on push
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 4m57s
CI / cleanup-branch (pull_request) Has been skipped
CI / build (pull_request) Successful in 4m35s
CI / docker (pull_request) Has been skipped
CI / docker (push) Successful in 3m44s
CI / deploy (pull_request) Has been skipped
CI / deploy-feature (pull_request) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Failing after 2m29s
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>
2026-04-26 18:59:38 +02:00
hsiegeln
2f7c6aa005 fix(auth): @NotNull on AuthCapabilitiesResponse.Oidc.providerName 2026-04-26 18:59:20 +02:00
hsiegeln
f945d10d48 feat(auth): AuthCapabilitiesResponse DTO 2026-04-26 18:57:09 +02:00
hsiegeln
ddb18c4f17 feat(auth): OidcProviderNameDeriver — issuer URI → display label
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 18:53:31 +02:00
hsiegeln
f1aa1ea19f docs(auth): implementation plan for login routing harmonization
9 tasks, TDD throughout. Backend: OidcProviderNameDeriver utility,
AuthCapabilitiesResponse DTO, AuthCapabilitiesController. Frontend:
useAuthCapabilities hook, capability-driven LoginPage rewrite,
OidcCallback ?local trap removal. Plus docs and manual smoke for
the original SaaS-provisioned tenant bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 18:46:55 +02:00
hsiegeln
a3c0e9aa7f docs(auth): harmonization design — login routing capability model
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 2m37s
CI / docker (push) Successful in 2m32s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 53s
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>
2026-04-26 18:37:00 +02:00
hsiegeln
5216dab043 Merge feature/runtime-hardening: license enforcement (36 tasks)
Delivers a signed-token license tier system with 8 enforced cap
surfaces (envs/apps/agents/users/outbound/alert-rules/compute/jar-
retention), per-tenant validation, daily revalidation, ClickHouse
TTL recompute on license change, audit trail, and Prometheus
metrics. Plus runtime container-hardening pre-work and a stand-
alone cameleer-license-minter Maven module (test-scope only on
the server).

40 license commits + 2 prior runtime-hardening commits +
3 design/spec/plan + 3 docs (minter README, operator guide,
SaaS handoff).

Range: ec51aef8..5864553f
Tasks: 1-36 of docs/superpowers/plans/2026-04-25-license-enforcement.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:33:44 +02:00
hsiegeln
5864553fed docs(license): minter README + operator guide + SaaS handoff
cameleer-license-minter/README.md — vendor-side guide: build, public
LicenseMinter API, CLI usage with all flags, token format (standard
base64, not url-safe), LicenseInfo schema, Ed25519 key generation,
worked example, security guidance, runtime-separation verification.

docs/license-enforcement.md — operator guide: install paths and
priority (env > file > DB > none), public-key config, REST API,
state machine (ABSENT/ACTIVE/GRACE/EXPIRED/INVALID), default tier
caps, 403 envelope semantics, retention TTL recompute, daily
revalidation, audit + Prometheus surfaces, troubleshooting.

docs/handoff/2026-04-26-license-saas-handoff.md — SaaS playbook:
trust model, onboarding/renewal/revocation runbooks, key management,
cap matrix per plan tier, telemetry, failure modes, testing guidance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:33:12 +02:00
hsiegeln
140ea88460 docs(rules): document license enforcement classes + endpoints
Final consolidation pass after the 36-task license-enforcement work.

core-classes.md:
- New license/ section: LicenseInfo, LicenseLimits, DefaultTierLimits,
  LicenseValidator, LicenseGate, LicenseStateMachine, LicenseState.
- runtime/: added CreateGuard (functional interface for license-cap
  hooks consulted by EnvironmentService/AppService/AgentRegistryService).
- admin/: AuditCategory.LICENSE added to the documented enum value list.

app-classes.md:
- New license/ section: LicenseService, LicenseRepository, LicenseRecord,
  PostgresLicenseRepository, LicenseChangedEvent, LicenseEnforcer,
  LicenseUsageReader, LicenseCapExceededException, LicenseExceptionAdvice,
  LicenseMessageRenderer, RetentionPolicyApplier, LicenseRevalidationJob,
  LicenseMetrics.
- LicenseAdminController entry expanded to document the GET response
  shape and the LicenseService.install delegation pattern.
- config/: RuntimeBeanConfig note about CreateGuard wiring; new
  LicenseBeanConfig entry covering the four-bean topology and the
  always-failing-validator fallback.

Note: LicenseChangedEvent, LicenseRepository, LicenseRecord, and
PostgresLicenseRepository live in cameleer-server-app, not -core; the
plan's section assignments were corrected against the actual code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:17:31 +02:00
hsiegeln
581dc1ad13 test(license): SchemaBootstrapIT — assert V5 license + retention columns
Two new assertions: license table has tenant_id/license_id/token/
installed_at/installed_by/expires_at/last_validated_at columns with
expected types + NOT NULL constraints, PK on tenant_id; environments
has execution_retention_days/log_retention_days/metric_retention_days
all integer NOT NULL DEFAULT 1.

Note: V5 migration does not include an installed_via column; the
plan's spec was aspirational. Test asserts what the migration
actually creates (and what PostgresLicenseRepository reads/writes).

OpenAPI regen (Step 35.2) deferred to session end — requires running
backend + UI dev server.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:13:50 +02:00
hsiegeln
e198c13e8a test(license): RetentionRuntimeRecomputeIT — TTL recompute on license change
Install license with max_log_retention_days=30, env.configured=60 →
effective=30; verify ClickHouse logs table reflects toIntervalDay(30).
Replace with max=7 → effective=7; verify TTL recomputed. Polls
system.tables.create_table_query up to 5s for the @Async listener
to apply.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:08:28 +02:00
hsiegeln
1e78439ddd test(license): LicenseEnforcementIT — cross-cap smoke regression net
Five @Nested cap surfaces (envs, apps, outbound, alert rules, users)
share a single synthetic license with cap=1 each. Each test pushes
just past the cap and verifies the standard 403 envelope plus a
cap_exceeded audit row. Per-limit ITs cover full per-cap behavior;
this IT catches accidental wire-rip regressions across all caps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:00:50 +02:00
hsiegeln
1a307da6b2 test(license): LicenseLifecycleIT — install/persist/revalidate/reject
End-to-end IT covering the full lifecycle: mint a token via
cameleer-license-minter (test-scope), POST it via /api/v1/admin/license,
verify state=ACTIVE, clear gate, revalidate from PG, verify state restored.
Plus: tampered signature -> 400 + LICENSE/FAILURE audit row, gate not
mutated to ACTIVE.

Adds cameleer-license-minter as a test-scope dep on cameleer-server-app
(verified absent from runtime/compile classpaths). Also disables the
default spring-boot:repackage execution on the minter pom so the main
artifact stays as a plain library JAR consumable as a Maven dependency
(the cli classifier still produces the executable jar).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:56:01 +02:00
hsiegeln
885f2be16b feat(license): Prometheus gauges for state + days remaining
cameleer_license_state{state=...} (one-hot per LicenseState),
cameleer_license_days_remaining (negative when ABSENT/INVALID),
cameleer_license_last_validated_age_seconds. Refreshed on
LicenseChangedEvent and every 60s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:43:54 +02:00
hsiegeln
945ecd78cf feat(license): LicenseUsageController GET /api/v1/admin/license/usage
Returns state, expiresAt/daysRemaining, lastValidatedAt, message
(LicenseMessageRenderer.forState), and a limits[] array where each
entry carries key/current/cap/source ("license" vs "default"). Adds
public AgentRegistryService.liveCount() so max_agents can be reported
from the in-memory registry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:42:39 +02:00
hsiegeln
3f69e546e4 refactor(license): LicenseAdminController delegates to LicenseService
GET returns {state, invalidReason, envelope, lastValidatedAt}. POST
delegates to licenseService.install(token, userId, "api") so install
goes through audit + persistence + event publish. Removes the inline
LicenseValidator construction from the controller.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:34:07 +02:00
hsiegeln
340d954fed feat(license): LicenseRevalidationJob — daily cron + 60s post-startup
@Scheduled(cron = "0 0 3 * * *") triggers svc.revalidate() daily.
@EventListener(ApplicationReadyEvent.class) @Async fires once 60s
after boot to catch ABSENT->ACTIVE transitions if the license was
written to PG between server starts. Exceptions are logged but never
propagate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:32:33 +02:00
hsiegeln
484a55f4f4 feat(license): RetentionPolicyApplier listens on LicenseChangedEvent
@EventListener fires on every license install/replace/expire. For each
environment, computes effective TTL = min(licenseCap, env.configured)
and emits one ALTER TABLE ... MODIFY TTL ... per (table, env). Tables
covered: executions, processor_executions, logs, agent_metrics,
agent_events. ClickHouse failures are logged but do not propagate
(listener is async-tolerant).

route_diagrams is intentionally excluded -- it has no TTL clause in
init.sql (ReplacingMergeTree keyed on content_hash, not time-series).
server_metrics is also excluded -- it has no environment column
(server straddles environments).

Per-environment TTL via WHERE requires ClickHouse 22.3+; the project's
current image (clickhouse/clickhouse-server:24.12) is well above that
floor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:28:42 +02:00
hsiegeln
cc5d88d708 feat(license): surface execution/log/metric retention days on Environment
Adds three int fields to the Environment record + repository row mapper,
matching the columns added in V5. Default value is 1 per the V5 NOT NULL
DEFAULT 1. Read DTO surfaces the fields via Jackson record serialization;
setter endpoint deferred to a follow-up that wires the corresponding
license cap checks.

The canonical constructor enforces >= 1 for each retention field — V5
guarantees this at the DB level, but the runtime guard catches in-memory
construction errors (e.g., test sites that pass 0).

Test sites updated to the 12-arg signature with retention defaults of 1.
EnvironmentAdminControllerIT gains a regression test asserting the wire
shape exposes all three fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:22:40 +02:00
hsiegeln
046f08fe87 feat(license): enforce max_jar_retention_count at PUT jar-retention
Returns 422 UNPROCESSABLE_ENTITY when jarRetentionCount exceeds
license cap. Default tier cap = 3. The other three retention caps
(execution/log/metric retention days) are deferred to T26+ where
the corresponding fields are added to Environment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:16:04 +02:00
hsiegeln
56bddcc747 feat(license): enforce compute caps at DeploymentExecutor PRE_FLIGHT
Adds ComputeUsage record + computeUsage() helper to LicenseUsageReader
that aggregates from PG. DeploymentExecutor.executeAsync runs three
assertWithinCap checks (max_total_cpu_millis, max_total_memory_mb,
max_total_replicas) right after config resolution. The existing
executor try/catch turns a LicenseCapExceededException into a FAILED
deployment with the cap message in the failure reason.

Adds ComputeCapEnforcementIT (HTTP-driven; @MockBean RuntimeOrchestrator,
since cap rejection short-circuits before any orchestrator call) plus
defensive license lifts in BlueGreenStrategyIT, RollingStrategyIT,
DeploymentSnapshotIT, and DeploymentControllerAuditIT so sequential
deploys under testcontainer reuse don't trip the new caps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:09:39 +02:00
hsiegeln
71f3b70b86 feat(license): enforce max_alert_rules at AlertRuleController.create
Adds AlertRuleRepository.count() and a LicenseEnforcer.assertWithinCap
call at the top of the POST handler. Default cap = 2; the 3rd rule
gets the standard 403 envelope. Sibling alert ITs that legitimately
need more than 2 rules get the cap lifted via the test-license helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 14:50:59 +02:00
hsiegeln
5a579415a1 feat(license): enforce max_outbound_connections at OutboundConnectionServiceImpl.create
Adds LicenseEnforcer.assertWithinCap call at the top of create() using
repo.listByTenant(tenantId).size() as the current count. Lifts the cap
in OutboundConnectionAdminControllerIT (duplicateNameReturns409 needs
2 creates in one test). LicenseExceptionAdvice maps the rejection to
the standard 403 envelope; cap_exceeded audit row emitted via the
LicenseEnforcer 3-arg ctor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 14:40:12 +02:00
hsiegeln
1ff30905f7 feat(license): enforce max_users at user creation paths
Wires LicenseEnforcer into UserAdminController.createUser and
OidcAuthController auto-signup. Cap fires before any validation so
over-cap creates short-circuit cheaply. Audit emission already
present (LicenseEnforcer 3-arg ctor from T16 emits cap_exceeded
under AuditCategory.LICENSE).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 14:29:54 +02:00
hsiegeln
afdaee628b feat(license): enforce max_agents at AgentRegistryService.register
Adds a CreateGuard to AgentRegistryService that fires only on NEW
registrations: re-registers of an existing agent bypass the cap (they
don't grow the registry, and rejecting them would orphan an agent that
already counts against the cap). Live-only count for cap enforcement —
STALE/DEAD/SHUTDOWN agents are excluded so the cap reflects the working
fleet, not historical residue.

Reuses the CreateGuard pattern from T18-T19. The global
LicenseExceptionAdvice maps the resulting LicenseCapExceededException to
403 with the structured envelope — no AgentRegistrationController
changes needed.

AgentCapEnforcementIT exercises the HTTP path end-to-end: two registers
succeed at cap=2, a third returns 403 with the expected envelope, and a
re-register of an already-registered agent succeeds at-cap.

Sibling agent-registering ITs (Agent*ControllerIT, Diagram*IT,
Execution*IT, Search*IT, Protocol*IT, Backpressure*IT, JwtRefresh*IT,
Registration*IT, Security*IT, SseSigning*IT, IngestionSchemaIT) lift
max_agents in @BeforeEach and clear the synthetic license in @AfterEach
— the in-memory registry is shared across @SpringBootTest reuse
boundaries, so without the lift the default-tier max_agents=5 would be
exhausted by accumulated test residue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 14:19:08 +02:00
hsiegeln
80dafe685b feat(license): enforce max_apps at AppService.createApp
Adds CreateGuard hook to AppService.createApp using the same pattern
as T18 (EnvironmentService). AppRepository.count() added; the bean
wires LicenseEnforcer.assertWithinCap("max_apps", current, 1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 13:36:34 +02:00
hsiegeln
198811b752 refactor(license-test): rename installTestLicenseWithCaps -> installSyntheticUnsignedLicense
Makes the signature-bypass loud at every call site since T19-T25 will
copy this pattern 5+ more times. The helper still loads via
LicenseGate.load() directly (no signature check) — the new name
ensures any future caller has to acknowledge that.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 13:24:58 +02:00