cosec-integration ยท diff

git:20261028.6a0a26d to git:20261028.1990d1e

189 added, 60 removed. Audit A to A.

---
name: cosec-integration
- description: Integrate CoSec into Spring Boot by selecting WebFlux, WebMVC, or Gateway support and configuring JWT, local policies, Redis, auditing, or optional integrations. Do not use for policy-only authoring or diagnosis.
+ description: Integrate CoSec 8.0 into a Spring Boot service - choose the starter's feature variants (WebFlux, WebMVC, Gateway, Redis cache, control plane, ...), configure JWT and revocation, Redis with noeviction, route maps, the first platform document, checks in code with AuthorizationChecker, and hosting the control plane. Do not use for policy-only authoring or diagnosis.
---
# CoSec Spring Boot Integration
- Make the smallest integration that fits the target application. Preserve its build tool, dependency-management style, HTTP stack, configuration format, and existing security boundaries.
+ Make the smallest integration that fits the target application. Preserve its build tool, dependency-management style,
+ HTTP stack, configuration format, and existing security boundaries. Guide: https://cosec.ahoo.me/guide/quick-start.
## Inspect first
Determine:
- - Spring WebFlux, Spring MVC, or Spring Cloud Gateway; add exactly the matching CoSec transport.
+ - Spring WebFlux, Spring MVC, or Spring Cloud Gateway; add exactly the matching transport.
+ - For a service behind a CoSec gateway: whether it decides for itself (default) or trusts the gateway's decision
+ (gateway mode, below), and whether only the gateway can reach it.
- The CoSec version already managed by a BOM, version catalog, parent, or lockfile. Never copy a version from this skill.
- - Whether policies are local-only or must initialize a shared repository.
- - Whether Redis, OAuth, IP enrichment, tracing, OpenAPI, Kafka, or token revocation is actually required.
+ - Whether the service has Redis (needed for shared policy storage, token revocation, cluster-wide rate limits and the
+ control plane), and whether this service should also host the control plane.
+ - Whether OAuth login, IP regions, tracing, metrics, OpenAPI or Kafka audit are actually required.
- The starter already brings `cosec-core` and `cosec-jwt`. Prefer its Gradle feature capabilities because they include the integration and its runtime dependencies:
+ The starter brings `cosec-core`, `cosec-jwt` and `cosec-runtime`. Each integration is a Gradle feature variant
+ (capability) of the starter:
- | Need | Starter capability | CoSec module |
+ | Need | Capability | Module |
|---|---|---|
| Spring MVC | `webmvc-support` | `cosec-webmvc` |
| Spring WebFlux | `webflux-support` | `cosec-webflux` |
| Spring Cloud Gateway | `gateway-support` | `cosec-gateway` |
- | Social OAuth | `oauth-support` | `cosec-social` |
- | Redis cache, revocation, cluster-wide rate limits | `cache-support` | `cosec-cocache` |
- | IP region | `ip2region-support` | `cosec-ip2region` |
- | OpenTelemetry | `opentelemetry-support` | `cosec-opentelemetry` |
+ | Redis: policy snapshot, revocation, API keys, cluster-wide rate limits, shadow | `cache-support` | `cosec-cocache` |
+ | Control plane (change, simulate, explain, publish) | `control-support` | `cosec-control` |
+ | Social OAuth login | `oauth-support` | `cosec-social` |
+ | IP region attributes (`ext.ip2region.*`) | `ip2region-support` | `cosec-ip2region` |
+ | OpenTelemetry spans | `opentelemetry-support` | `cosec-opentelemetry` |
| Micrometer metrics | `micrometer-support` | `cosec-micrometer` |
- | OpenAPI | `openapi-support` | `cosec-openapi` |
- | Kafka audit sink | `kafka-support` | `cosec-kafka` |
-
- For Gradle, select a capability on the starter dependency:
+ | OpenAPI route map and app document generation | `openapi-support` | `cosec-openapi` |
+ | Kafka audit | `kafka-support` | `cosec-kafka` |
```kotlin
implementation(platform(libs.cosec.bom))
implementation("me.ahoo.cosec:cosec-spring-boot-starter") {
- capabilities {
- requireCapability("me.ahoo.cosec:webflux-support")
- }
+ capabilities { requireCapability("me.ahoo.cosec:webflux-support") }
}
+ implementation("me.ahoo.cosec:cosec-spring-boot-starter") {
+ capabilities { requireCapability("me.ahoo.cosec:cache-support") }
+ }
```
- Declare the starter again for each additional capability. The module column is a mapping, not a complete dependency set. With Maven or a build that cannot select Gradle variants, use the BOM, starter, one transport module, and mirror the selected feature's runtime dependencies. In the current build, cache support also needs `org.springframework.boot:spring-boot-data-redis` and `me.ahoo.cocache:cocache-spring-boot-starter`, OpenAPI support needs `org.springframework.boot:spring-boot-starter-actuator` and `org.springdoc:springdoc-openapi-starter-common`, and Kafka support needs `org.springframework.boot:spring-boot-starter-kafka`.
+ Declare the starter once per capability. With Maven, or a build that cannot select variants, add the module and mirror
+ the feature's dependencies from `host/spring-boot/build.gradle.kts`: for example `cache-support` also needs
+ `org.springframework.boot:spring-boot-data-redis` and `me.ahoo.cocache:cocache-spring-boot-starter`; WebFlux needs
+ `kotlinx-coroutines-reactor`; OpenAPI needs the actuator and `springdoc-openapi-starter-common`; Kafka needs
+ `spring-boot-starter-kafka`. `control-support` requires `cache-support`.
## Minimal configuration
```yaml
cosec:
jwt:
- algorithm: hmac256
- secret: ${COSEC_JWT_SECRET}
+ algorithm: hmac256 # hmac256 | hmac384 | hmac512
+ secret: ${COSEC_JWT_SECRET} # blank fails startup while JWT is enabled
authorization:
- local-policy:
- enabled: true
- init-repository: true
- locations:
- - classpath:cosec-policy/*-policy.json
+ scope:
+ app-id: order-service # the app this host serves; omit when requests declare it (CoSec-App-Id)
+ remote-ip:
+ max-trusted-index: 1 # trusted X-Forwarded-For hops; 0 ignores the header
+ routes:
+ location: classpath:cosec-routes.json # the default; set it empty to run without a route map
```
- Keep the JWT secret outside source control. A blank secret fails startup while JWT is enabled. Supported algorithms are `hmac256`, `hmac384`, and `hmac512`; issuer and verifier must agree.
+ Startup fails, on purpose, when:
- `LocalPolicyLoader` only parses files; authorization reads `PolicyRepository`. This bootstrap configuration therefore initializes the repository at startup and requires `PolicyRepository` and `AppRolePermissionRepository` beans. The cache capability supplies Redis-backed implementations; otherwise provide them explicitly.
+ - **JWT is on without revocation storage.** Revocation is always on. With `cache-support` the Redis token store
+ supplies it; without it, register a `TokenStore` and a `RevocationReader` bean yourself, or set
+ `cosec.jwt.enabled=false`.
+ - **The route map location does not exist.** The default `classpath:cosec-routes.json` must exist on WebFlux and
+ WebMvc hosts unless `cosec.authorization.routes.location` is set empty. A gateway host loads no route map.
+ - **Redis may evict the snapshot.** With `cache-support`, every Redis node must have `maxmemory-policy noeviction`. A
+ different policy, or one that cannot be read (`CONFIG GET` denied on a managed Redis, a wrapped CoCache backend),
+ fails startup. Only when the operator has set `noeviction` and CoCache cannot read it, set
+ `cosec.authorization.snapshot.eviction-check=warn` (it starts and logs a `!!!` warning).
+ - **API keys with `store=redis` and no `ReactiveStringRedisTemplate`.**
- Create policies under `src/main/resources/cosec-policy/` using the configured pattern. A safe complete skeleton is:
+ Every `cosec.*` property: https://cosec.ahoo.me/operations/configuration. In a CoSec checkout the source of truth is
+ `host/spring-boot/src/main/kotlin/.../pipeline/PipelineProperties.kt`, `control/ControlProperties.kt` and the
+ auto-configurations listed in `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`.
+ ## Route maps
+
+ On WebFlux and WebMvc hosts, `cosec-routes.json` gives requests their `service:verb` action and typed resource:
+
```json
- {
- "id": "service-access",
- "name": "Service Access",
- "category": "access",
- "description": "Service endpoint access",
- "type": "global",
- "tenantId": "(platform)",
- "statements": [
- {
- "name": "Health",
- "action": "/actuator/health"
- }
- ]
- }
+ { "service": "order",
+ "routes": [
+ { "method": "GET", "path": "/orders/{orderId}", "action": "order:read", "resource": "order/{orderId}" },
+ { "method": "POST", "path": "/orders/{orderId}/cancel", "action": "order:cancel", "resource": "order/{orderId}" } ] }
```
- Use `$cosec-policy-author` when the requested policy is more than this bootstrap rule.
+ It loads all or nothing: overlapping routes for one method, a malformed route, or a resource variable the path does not
+ capture fails startup. Requests no route matches stay `http:<METHOD>` on `http:path:<path>`. With `openapi-support`,
+ `GET /actuator/cosecServiceGenerator/routes` generates the route map and `/app` the app document. Policies on `http:*`
+ never cover mapped requests: tell the policy author which namespace each endpoint is in.
+ ## Publishing the first documents
+
+ The service decides on a snapshot of documents (platform, app, tenant), read from Redis with `cache-support` or from an
+ in-memory store without it. **Until a platform document exists, every request is 503.**
+
+ - With Redis: write the documents into a directory (`platform.json`, `apps/<appId>.json`, `tenants/<tenantId>.json`)
+ and run `cosec-migrate publish --to-redis redis://... [--key-prefix cosec] --in <dir>`. It publishes first
+ revisions only, never overwrites, and reads a `report.json` with each file's SHA-256 (`convert` writes it; for
+ hand-written files write `{"accepted": true, "outputSha256": {"platform.json": "<sha256>"}}`). Migrating 7.x data:
+ `cosec-migrate convert`, then `publish`; see https://cosec.ahoo.me/operations/upgrading.
+ - Later changes go through the control plane (below). `cosec-migrate bootstrap` writes only the platform tenant's
+ document; it does not create the platform document.
+ - While no platform document is installed, the instance also reports itself not ready (Spring readiness
+ `REFUSING_TRAFFIC`; liveness unaffected). With actuator, add the `cosecSnapshot` indicator to the readiness group
+ (`management.endpoint.health.group.readiness.include=readinessState,cosecSnapshot`); it is `DOWN` until the platform
+ document and every `prewarm-apps`/`prewarm-tenants` document are installed, so `/actuator/health` is `DOWN` too.
+ - Without Redis: the `SnapshotDocumentStore` bean is an `InMemorySnapshotDocumentStore`; put documents into it
+ (`put(SnapshotKey.Platform, SnapshotDocument(2, revision, publishedAt, bytes))`), for example from files at startup.
+ Each instance holds its own copy.
+
+ Use `$cosec-policy-author` to write the documents. Storage details: https://cosec.ahoo.me/operations/storage.
+
+ ## Services behind a CoSec gateway
+
+ The gateway strips any client `CoSec-Identity` and, on a permit for an authenticated principal, forwards the identity
+ it verified (tenant, principal, app, space, the roles there, credential id and expiry, and the claims named in
+ `cosec.gateway.identity.claims`). It no longer forwards `Authorization` unless
+ `cosec.gateway.identity.forward-authorization=true`. Route metadata `cosec-identity: false` opts a route out. A
+ credential without expiry (an API key without `expires-at`) makes the gateway answer 500.
+
+ A service behind it either keeps deciding itself (default `cosec.pipeline.mode=enforce`: same documents, keys and Redis
+ as the gateway, and `forward-authorization=true` so it gets the token), or trusts the gateway:
+
+ ```yaml
+ cosec:
+ pipeline:
+ mode: gateway # read the caller from CoSec-Identity; no snapshot, engine, route map, listeners or Redis
+ jwt:
+ enabled: false # it verifies no tokens, so it needs no secret
+ ```
+
+ - Add only the web capability (`webflux-support` or `webmvc-support`); no `cache-support`. No `AuthorizationChecker`
+ is wired in this mode.
+ - **Only the gateway may reach it** (NetworkPolicy, private network or mTLS): it believes any `CoSec-Identity`, and
+ logs a WARN at startup saying so. Never expose such a service directly.
+ - Answers: no header is anonymous; unreadable, repeated or oversize (`cosec.pipeline.gateway.max-size`, 8KB) is 400;
+ older than `cosec.pipeline.gateway.max-age` (60s) plus `cosec.authentication.revocation.skew`, or past the
+ credential's expiry, is 401 `token-expired`; unknown version, no expiry or an identity from the future is 401
+ `token-invalid`; a scope that does not resolve here (for example a different `scope.app-id`) is 400.
+ - Startup fails for an unknown `cosec.pipeline.mode`, and for `mode=gateway` on a Spring Cloud Gateway host.
+
+ Details: https://cosec.ahoo.me/integration/web#services-behind-the-gateway and
+ https://cosec.ahoo.me/guide/security-model#trust-boundaries.
+
+ ## Tokens, memberships and API keys
+
+ - Tokens are read from the `Authorization` header, then the `authorization` cookie; the query parameter only with
+ `cosec.pipeline.allow-query-token=true`. Issue them with the `TokenConverter` bean.
+ - A token's `roles` claim (`role` in the default space, `role@space` elsewhere) and `policies` claim (attached policy
+ ids, which must be `attachable`) are the principal's memberships. A token with the claim `cosec.membership:
+ "provider"` and no roles or policies takes them from a `MembershipProvider` bean instead; a `JwtTokenConverter` bean
+ of your own with `membershipClaimLimit` set issues such tokens when the claims would exceed that many bytes (the
+ starter's converter never does). Register the provider before issuing them, or those requests are 503.
+ - Logout: `TokenRevoker.revoke(accessToken)`. Emergency: `TokenRevoker.revokeSubject(tenant, principal)` revokes every
+ token of a principal issued until now (needs the Redis token store).
+ - API keys: `cosec.api-key.enabled=true`, keys in `cosec.api-key.keys[n]` (`id`, `hash` = SHA-256 hex of the key,
+ `principal-id`, `tenant-id`, `roles`, `policies`, `attributes`, `expires-at`) or `cosec.api-key.store=redis`. They
+ arrive in `X-API-Key` (`cosec.pipeline.api-key-header`).
+ - Clients send `CoSec-App-Id` (unless the host sets `scope.app-id`) and `CoSec-Space-Id` for a non-default space; a
+ space needs a role there, or the request is denied (403 `scope-rejected`; 401 for an anonymous caller). Details:
+ https://cosec.ahoo.me/integration/authentication.
+
+ ## Adapters and checks in code
+
+ The starter registers one filter: `PipelineServletFilter` (MVC), `PipelineWebFilter` (WebFlux) or
+ `PipelineGatewayFilter` (Gateway). Adapters only translate; status codes and problem+json bodies are decided once, in
+ the pipeline: 401/403 deny, 429 rate limited, 503 undecidable, 400 ambiguous request. A permitted request carries a
+ `SecurityContext`: `ServerWebExchange.getSecurityContext()` / `HttpServletRequest.getSecurityContext()` (extension
+ functions in `ServerWebExchanges` / `ServletRequests`). Web details: https://cosec.ahoo.me/integration/web.
+
+ To decide in code (UI buttons, message consumers, object-level checks), inject `AuthorizationChecker`:
+
+ ```kotlin
+ checker.check(securityContext, "order:ship", "order/42") // suspend
+ checker.checkMono(securityContext, "order:ship", "order/42") // cosec-webflux
+ checker.checkBlocking(securityContext, "order:ship", "order/42") // cosec-webmvc
+ ```
+
+ The resource is `type/id`, or a path (`/orders/42`) for an `http:` action. It uses the principal and scope of the
+ permitted request, runs no rate limits and notifies no listener, and is `false` on any deny, error or outage. Keep
+ enforcing on the real request. See https://cosec.ahoo.me/integration/checks.
+
+ ## Hosting the control plane
+
+ With `control-support` (and `cache-support`), the host serves the control API under
+ `{cosec.control.http.base-path}/v1` (default `/cosec/control/v1`): documents, changes, shadow windows, approval,
+ break-glass, `simulate`, `explain`, `analyze`. Each call first passes the host pipeline (so the host's documents must
+ permit the authenticated caller on that path), then is authorized again in app `cosec.control.app-id`
+ (`cosec-control`) against the built-in `cosec:platform-admin`, `cosec:tenant-admin` and `cosec:tenant-viewer`
+ policies. Bind the first administrator with `cosec-migrate bootstrap --to-redis ... --platform-admin <principal>`
+ (a `(platform)`-tenant principal), or with a principal binding of the platform document to `cosec:platform-admin`
+ in app `cosec-control`. On WebFlux, raise `spring.codec.max-in-memory-size` for large documents; on WebMvc the limit is
+ `cosec.control.http.max-body-size` (16MB). Workflow: https://cosec.ahoo.me/policies/testing.
+
## Optional behavior
- - `local-policy.init-repository` is `false` by default. The bootstrap above enables it because the repository is not otherwise populated from local files. Disable it only when policies are already stored elsewhere; use `force-refresh` only when overwriting repository state is intended.
- - Revocation is always on with JWT (8.0 has no `cosec.jwt.token-revocation.*`): it needs a real `TokenStore` and revocation reader, which the cache capability supplies on Redis. Without them, or a `TokenStore` and `RevocationReader` bean of your own, startup fails. `TokenRevoker.revoke(accessToken)` revokes one token (logout); `TokenRevoker.revokeSubject(TenantId, PrincipalId)` revokes every token of a principal issued until now, for an emergency or when roles carried in tokens must stop applying at once (it throws without a Redis-backed store).
- - Token `roles` and `policies` claims keep their format in 8.0 (they are the principal's memberships). Do not set `JwtTokenConverter`'s `membershipClaimLimit` or issue tokens with a `cosec.membership` claim yet: until 8.0 switches authorization to memberships, such a token authorizes as a principal with no roles and no policies.
- - Rate limit rules load from `cosec.limiter.locations` (default `classpath:cosec-rate-limit/*-rate-limit.json`); an unreadable rule, a duplicate rule id, or a configured location that matches no file (only the default may match nothing) fails startup. Shadow candidate locations (`cosec.shadow.*`) load just as strictly. The starter wraps its auto-configured `Authorization` with `RateLimitingAuthorization`. With the cache capability and a `StringRedisTemplate` the counters are cluster-wide in Redis (prefix `cosec.limiter.key-prefix`), otherwise per JVM. Use per-rule `strictFailure` when Redis outage behavior must fail closed; `cosec.limiter.enabled=false` turns rate limiting off. The `rateLimiter`/`redisRateLimiter` policy conditions were removed in 6.1.0; migrate them to rules.
- - With the starter's auto-configured `Authorization`, audit logging is enabled by default. The logging sink records denies at WARN and allows at DEBUG under `me.ahoo.cosec.audit`; the Kafka capability replaces it when a unique Kafka template is available. A custom `Authorization` bean is not wrapped automatically, so compose it with `AuditingAuthorization` explicitly.
- - To decide in code (UI buttons, message consumers), inject `AuthorizationChecker` and call `check(context, path, method)` or `checkAll(context, mapOf(key to CheckRequest(...)))`. Checks reuse the policies, are not audited or rate limited, inherit the current request's app, space and IP, and deny on errors; keep enforcing on the real request.
- - For machine clients, enable `cosec.api-key.enabled=true` and configure `cosec.api-key.keys[n]` with `id`, `hash` (SHA-256 hex from `ApiKeys.hash(key)`; never the key itself), `principal-id`, `roles`, `policies`, optional `tenant-id` and `expires-at`. Keys arrive in `X-API-Key` (configurable) and take precedence over JWTs; invalid keys deny with `401 Token Invalid`. With `cache-support`, `cosec.api-key.store=redis` shares keys across instances; issue and revoke them through the `RedisApiKeyStore` bean (`issue`, `save`, `revoke`). A custom `ApiKeyStore` that does I/O needs `cosec.api-key.store-blocking=true`.
- - Set `cosec.authorization.remote-ip.max-trusted-index` to the real trusted-proxy depth. `0` ignores `X-Forwarded-For`.
- - For role-based access (CoSec 7+), grants are scoped to an app and a space. Write them through `RolePermissionCache` with `AppSpacedRoleId(appId, roleId, spaceId)` (Redis key `<prefix>:app:role:permission:<appId>:<roleId>[@<spaceId>]`; app ids must not contain `:`, role ids not `@`). Issue token or API key roles as `role` for the default space and `role@spaceId` for a space, only after checking the principal's membership: `CoSec-Space-Id` only selects among the spaces the credential states. Clients send `CoSec-App-Id` (and `CoSec-Space-Id` outside the default space). When upgrading from 6.x, migrate the old app-less grants with `RolePermissionKeyMigration.migrate(appId)` (dry run first) before 7.0 serves traffic, and `deleteLegacy()` once verified.
+ - Audit: decisions are logged under `me.ahoo.cosec.audit` (denies WARN, allows DEBUG) unless
+ `cosec.audit.enabled=false`; `kafka-support` adds a Kafka listener (`cosec.audit.kafka.*`). Existing
+ `AuditEventSink` beans are picked up as BEST_EFFORT listeners. Queued events drain on shutdown for up to
+ `cosec.observability.listener.drain-timeout` (5s). Metrics and tracing
+ listeners come with their features. https://cosec.ahoo.me/operations/observability.
+ - Rate limits are rules in the documents. The limiter is Redis when a `ReactiveStringRedisTemplate` is present,
+ otherwise per JVM.
+ - Shadow windows: with `cache-support`, the instance replays a sample (`cosec.authorization.shadow.sample-rate`,
+ default 0.1) of requests on staged candidates and reports differences to the control plane.
+ - Blacklist: declare a `BlacklistSource` bean and the starter registers the blacklist provider; without one, startup
+ logs an INFO line and nothing is blocked.
+ - Extension beans (`AttributeProvider`, `MembershipProvider`, `DecisionListener`, `RateLimiter`, `Authenticator`): use
+ `$cosec-custom-matcher`. Register the same attribute providers on every instance and on the control-plane host.
+ - Policy tests: `cosec-testkit` (test scope) runs scenarios against your documents; use `$cosec-policy-author`.
## Verification
Start the application or run its context test, then verify:
- - policy resources are loaded with no skipped-file errors;
- - an allowed anonymous request succeeds;
- - an unmatched anonymous request returns 401;
- - an authenticated but denied request returns 403;
+ - startup succeeds (no eviction-check, route map or revocation failure);
+ - before the platform document is published, requests answer 503 and readiness is `REFUSING_TRAFFIC`; after, readiness
+ turns `ACCEPTING_TRAFFIC` and an allowed anonymous request succeeds;
+ - in gateway mode: a request without `CoSec-Identity` is anonymous, and the service is unreachable except from the
+ gateway;
+ - an unmatched anonymous request returns 401 `application/problem+json`, an authenticated denied one 403;
- a request over a rate limit rule returns 429 with `Retry-After` and `RateLimit-*` headers;
- - Redis-backed features work from more than one instance when cluster-wide behavior is required.
-
- In a CoSec checkout, treat `cosec-spring-boot-starter/build.gradle.kts` and the `*Properties.kt` / auto-configuration classes under `cosec-spring-boot-starter/src/main/kotlin` as the source of truth for capabilities, defaults, and activation conditions.
+ - a revoked token returns 401 `token-revoked`;
+ - with Redis, a change published on one instance reaches the others (they revalidate every
+ `cosec.authorization.snapshot.revalidate-interval`, default 10s).