From ec91eb3af436ac7fa5e682d31d78cd5b01782d01 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 04:15:49 -0700 Subject: [PATCH 01/17] docs: define MCP-compatible OAuth authorization boundary (#115) * docs: define MCP OAuth authorization boundary * docs: clarify MCP ADR evidence and references * docs: clarify MCP token and metadata claims * docs: make OAuth scope metadata example valid * docs: cite OAuth registration and token profile RFCs --- docs/TRACEABILITY.md | 3 +- .../0013-mcp-oauth-client-authorization.md | 212 +++++++++++++++++ docs/adr/README.md | 8 + docs/doctoring/mcp-oauth-authorization.md | 221 ++++++++++++++++++ 4 files changed, 443 insertions(+), 1 deletion(-) create mode 100644 docs/adr/0013-mcp-oauth-client-authorization.md create mode 100644 docs/doctoring/mcp-oauth-authorization.md diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 4713bf5..c62728c 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -1,7 +1,7 @@ # Keyverse Requirements and Evidence Traceability **Status:** Accepted cross-cutting baseline -**Last reviewed:** 2026-08-12 +**Last reviewed:** 2026-08-21 | Requirement / decision | Standards / authoritative basis | Source/evidence boundary | Maturity | |---|---|---|---| @@ -21,6 +21,7 @@ | secrets from KV/DB, env bootstrap only | architecture/security decision | config/bootstrap/template validation | implemented-main | | work-conserving fail-closed hourly API gate | automation safety decision | PR #74 protected-main workflow tests/exact-head evidence; scheduled/manual run remains required | implemented-main | | non-fork RP Keyverse authorization boundary | ADR-0008; OIDC/JWT recipient validation and least-privilege policy | six-app audit, per-RP issuer/audience/tenant/ABAC/RBAC evidence required | accepted-contract | +| MCP-compatible OAuth client authorization | ADR-0013; MCP Authorization 2025-11-25, RFC 8414, RFC 8707, RFC 9728, RFC 9700 | design-only contract for Keycloak discovery, public-client PKCE, exact resource binding, LineageWeave protected-resource metadata, centralized revocation, and negative evidence; no protected-main runtime evidence yet | gap-not-claimed | | naruon Keyverse OIDC acceptance boundary | ADR-0008; exact issuer/audience/JWKS validation and required OIDC NumericDate claims | naruon PR #1321 `ca6ccba` names the Keyverse issuer and `naruon-web` audience, requires verified `iat`, tests explicit org/workspace/role acceptance plus missing-`iat` denial, strips orphaned HTML comment terminators, and resolves the `develop` CHANGELOG conflict; protected-branch Checks/review remain required | active-PR | | semantic-data-portal Keyverse claim boundary | ADR-0008; bounded claim mapping and fail-closed tenant/role/JWT-header validation | semantic-data-portal PR #58 `47e2215` aliases `org`/`role`, validates every present tenant alias, rejects malformed/conflicting aliases before `ActorContext`, explicitly rejects unsupported JWT `crit` headers, and keeps the cryptography floor; protected-branch approval remains required | active-PR | | pg-erd-cloud Keyverse organization boundary | ADR-0008; verified tenant binding before project authorization | pg-erd-cloud PR #855 `e4b4771` exact `org`/audience/`iat` checks, single-tenant profile, API-key bypass denial; shared multi-tenant persistence remains unimplemented | active-PR | diff --git a/docs/adr/0013-mcp-oauth-client-authorization.md b/docs/adr/0013-mcp-oauth-client-authorization.md new file mode 100644 index 0000000..c1e9cee --- /dev/null +++ b/docs/adr/0013-mcp-oauth-client-authorization.md @@ -0,0 +1,212 @@ +# ADR-0013: Provide MCP-compatible OAuth client authorization through Keyverse + +**Status:** Proposed +**Date:** 2026-08-21 +**Issue:** ContextualWisdomLab/keyverse#114 +**Depends on:** ADR-0008 and the current Keyverse authorization-plane hardening +**Figma file ID:** N/A — this is a protocol and trust-boundary change with no user-interface slice. + +## Context + +LineageWeave MCP clients need a supported passwordless path to a protected +resource. A long-lived MCP API key would create a second issuance, rotation, +deprovisioning, and audit system outside Keyverse. A client ID, repository +relationship, email address, or Keycloak mapper is not authorization evidence +by itself. + +Keyverse already uses Keycloak as the ecosystem identity and protocol engine. +The account-unification service owns deterministic desired state and operator +boundaries; it is not a second token issuer. The existing relying-party profile +is intentionally secret-free, authorization-code based, and PKCE protected. + +The MCP authorization specification requires protected-resource metadata, +authorization-server discovery, OAuth 2.1 security measures for clients, and a +canonical `resource` parameter. RFC 9700 requires exact redirect handling and +PKCE for public clients. RFC 8707 binds an authorization request to an +absolute resource URI. RFC 9728 makes the protected resource the owner of its +metadata. These protocol roles must remain separate from Keyverse's private +operator API and from LineageWeave's resource authorization policy. + +## Decision + +### 1. Keycloak remains the authorization server + +Keyverse will not introduce a bespoke token issuer, password grant, static MCP +bearer key, or LLM-based authorization decision. Keycloak remains the issuer +and signing-key authority for the configured public issuer. + +The deployment contract will provide one exact HTTPS issuer, for example: + +```text +https://keyverse.example/realms/cwl +``` + +The issuer string is compared exactly, including its path and trailing-slash +policy. The implementation must publish and test both discovery forms needed +by MCP clients: + +```text +{issuer}/.well-known/openid-configuration +https://{host}/.well-known/oauth-authorization-server/{issuer-path} +``` + +The second URL uses the RFC 8414 path-aware well-known construction. The +metadata documents must agree on the exact `issuer`, authorization endpoint, +token endpoint, JWKS URI, supported response type (`code`), and +`S256` code-challenge method. An endpoint that reports a different issuer or +an unapproved host is invalid. OIDC discovery remains the vendor-backed +source of truth; an RFC 8414 projection may be served by the public edge only +when it is byte-for-byte consistent on security-relevant fields. + +The public discovery surface must not expose operator configuration, private +Keycloak Admin REST, secret references, tenant inventories, or user data. + +### 2. The MCP resource owns protected-resource metadata + +LineageWeave, as the protected MCP resource, owns: + +```text +GET {resource-origin}/.well-known/oauth-protected-resource +``` + +and any path-aware equivalent required by RFC 9728. Its metadata must contain +the one canonical MCP `resource` URI and the exact Keyverse issuer in +`authorization_servers`. A protected `401` response must advertise the same +metadata URL through `WWW-Authenticate` when the client needs discovery. + +Keyverse does not proxy or silently rewrite LineageWeave metadata. Keyverse's +integration tests verify that the resource metadata points to the configured +issuer, while LineageWeave tests verify that it accepts only the advertised +resource and authorization server. + +### 3. Public clients use authorization code plus PKCE + +The first MCP client profile is a public OAuth client using authorization code +and mandatory `S256` PKCE: + +- no password, implicit, or direct-access grant; +- exact registered HTTPS redirect URI, with no wildcard or string expansion; +- `redirect_uri` is identical in the authorization and token requests; +- a fresh high-entropy `state` and PKCE verifier for every authorization; +- the authorization request and token request contain exactly one canonical + `resource` URI; +- scopes are an allowlisted subset of the resource's registered least- + privilege scopes; +- access tokens are accepted only after checking the configured issuer, subject, + expiry, and required scopes; the JWT `aud` claim is the canonical MCP + resource URI, while RFC 9068 `client_id` is checked separately against the + registered public client ID. If `azp` is present, it is validated under the + same client profile. A Keycloak audience-mapper client ID must never be + reused as the MCP resource audience. + +Keyverse reuses the existing closed secret-free relying-party lifecycle for +pre-registration. The MCP client representation is a separately named, +reviewed profile because resource-bound audience semantics are a new trust +boundary. It may not broaden the existing mapper allowlist by configuration +alone. No client secret is accepted, generated, returned, or persisted in this +profile. + +There is no open dynamic-registration endpoint in this decision. A deployment +may pre-register an approved public client and exact redirect through the +Keyverse desired-state boundary. If a real MCP client cannot operate with +pre-registration and requires a Client ID Metadata Document or RFC 7591 +registration, that mechanism requires a follow-up ADR with SSRF, metadata +freshness, redirect ownership, registration abuse, and audit controls before +implementation. + +### 4. Resource indicators and least privilege are enforced together + +Each MCP protected resource has one deployment-owned canonical absolute URI. +The URI has no fragment; its query policy is explicit; and its trailing-slash +spelling is stable. Keyverse rejects a missing, duplicated, differently +spelled, or unregistered `resource` parameter. + +The token audience and resource authorization decision must bind to that exact +URI. A valid Keyverse login or client registration cannot authorize a token +for another LineageWeave instance. The resource owns the initial scope list; +Keyverse stores and enforces only the reviewed, least-privilege list. Wildcard, +unregistered, role-like, or administrative scopes are rejected. + +The first implementation must record the exact resource-to-client-to-scope +binding and prove: + +```text +resource URI -> one public client -> bounded scopes -> one RP verifier +``` + +No name-only `Partner`, `Supplier`, person, or organization attribution is +introduced by this protocol contract. + +### 5. Revocation and audit remain centralized + +Keycloak remains the authority for user disablement, session termination, +token revocation, signing-key rotation, and authentication audit events. +Keyverse records authorization intent and non-secret outcome/audit references; +it never records bearer tokens, authorization codes, PKCE verifiers, or client +secrets. + +LineageWeave must prove revoked/deprovisioned access is denied. Local JWT +signature validation alone is insufficient for that acceptance claim while a +token remains unexpired. The implementation must use an approved active-token +or revocation check, or a separately accepted short-lived-token contract with +measured revocation bounds. This is a resource-server integration requirement, +not a reason to create a second user identity or API-key issuer. + +### 6. Device authorization is explicitly deferred + +RFC 8628 is not implemented speculatively. It may be evaluated only when a +real MCP client has no usable browser callback and supplies a concrete device +flow requirement, abuse model, polling/backoff limits, user-code lifecycle, +and revocation tests. Until then, browser-assisted authorization code plus +PKCE is the only supported client path. + +## Required negative evidence + +The implementation PR must test and retain evidence for denial of: + +| Input | Required result | +|---|---| +| wrong issuer or discovery host | deny before resource authorization | +| wrong or missing audience/resource | deny | +| unregistered redirect or redirect mismatch | deny | +| missing, duplicated, or unregistered scope | deny | +| missing/invalid PKCE or state | deny | +| expired, revoked, disabled-user, or malformed token | deny | +| token for another tenant/workspace/resource | deny | +| password/direct-access/device flow before its own ADR | deny | +| static MCP API key | unsupported and deny | + +## Consequences + +Positive consequences: + +- MCP clients use the existing passwordless Keyverse identity authority; +- LineageWeave remains a protected resource with its own ABAC/RBAC boundary; +- resource indicators prevent a token issued for one MCP resource being reused + at another resource; +- operator credentials, user tokens, and deployment secrets remain separate; +- discovery, revocation, and audit have one explicit ownership model. + +Costs and limitations: + +- a public issuer and WAF routing contract must be deployed and tested; +- resource-bound audience support requires a distinct Keyverse MCP profile and + downstream verifier changes; +- pre-registration is an operational dependency until a separately reviewed + registration mechanism is justified; +- revocation evidence may require protected-resource introspection or a + measured bounded-lifetime contract; +- this ADR does not claim a live Keycloak, browser, or LineageWeave acceptance + result. + +## Implementation and evidence gates + +Before implementation, update the issue-linked specification and doctoring +record. The implementation PR must reconcile PRD, TRD, Architecture, UML, ERD, +Threat Model, Test Strategy, Operability, Traceability, onboarding, and +`CHANGELOG.md` only when the runtime contract actually changes. It must use +real browser/client integration evidence, a non-secret test resource, exact +current-head coverage/docstring gates, and protected review/checks. + +No version, release, Figma artifact, Storybook artifact, dependency, realm +secret, or dynamic-registration endpoint is created by this design record. diff --git a/docs/adr/README.md b/docs/adr/README.md index e53fc6d..1a087a4 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -12,6 +12,14 @@ | [0006](0006-user-operation-lock.md) | Merge and SCIM full replacement share one user-operation lock boundary | Accepted | | [0007](0007-automation-authority.md) | Autonomous development remains separate from review/merge/release authority | Accepted | | [0008](0008-keyverse-rp-authorization-boundary.md) | Every non-fork RP explicitly validates Keyverse identity and manages ABAC/RBAC at its own boundary | Accepted | +| [0013](0013-mcp-oauth-client-authorization.md) | Use Keycloak-backed authorization code plus PKCE and exact resource binding for MCP clients | Proposed | + +ADR numbering note: protected `main` currently ends at ADR-0008. ADR-0009 is +proposed in the open LineageWeave claim-profile PR, and ADR-0010 through +ADR-0012 are proposed in the open authorization-plane PR. ADR-0013 preserves +the next intended number without renumbering parallel work; it must be +reconciled after those PRs land, and none of the absent records is accepted +architecture on protected `main` yet. ## ADR triggers diff --git a/docs/doctoring/mcp-oauth-authorization.md b/docs/doctoring/mcp-oauth-authorization.md new file mode 100644 index 0000000..4e284eb --- /dev/null +++ b/docs/doctoring/mcp-oauth-authorization.md @@ -0,0 +1,221 @@ +# MCP-compatible OAuth client authorization — doctoring record + +## Scope and current evidence + +This record interprets the current MCP authorization contract for Keyverse, +Keycloak, and a LineageWeave protected MCP resource. It is a design record for +issue #114, not proof that the runtime feature is deployed. + +Observed on the protected-main base used for this record: + +- Keycloak is the existing OIDC/OAuth protocol engine and signing authority. +- Keyverse already has a closed, secret-free authorization-code plus PKCE + relying-party lifecycle. +- The account-unification service has a private operator bearer boundary; it + must not become the public OAuth token endpoint or discovery authority. +- No protected-main browser/client MCP flow, resource metadata endpoint, + resource-bound token, revocation check, or LineageWeave end-to-end result was + observed in this design pass. + +Therefore all runtime behavior below is a target contract. `implemented-main` +must not be reported until exact-head tests and controlled integration evidence +exist. + +## Interpretation categories + +### Standards requirements + +- MCP clients discover the authorization server through protected-resource + metadata and support OAuth authorization-server metadata or OIDC discovery. +- Public clients use OAuth 2.1 security measures and PKCE. +- Authorization requests and token requests carry one canonical RFC 8707 + resource indicator. +- The protected resource publishes RFC 9728 metadata and advertises it through + a bearer challenge when required. + +### Vendor behavior + +Keycloak owns the realm issuer, authorization endpoint, token endpoint, JWKS, +sessions, user state, and protocol execution. Keycloak's native OIDC discovery +is authoritative for vendor endpoints. Any RFC 8414 metadata projection must +be checked against that discovery document and the configured public issuer. + +### Keyverse product policy + +- Keyverse remains the identity authority; no static MCP API key or second user + issuance system is allowed. +- Public MCP clients are pre-registered through the existing secret-free + desired-state boundary until a separate registration ADR is accepted. +- Exact HTTPS redirects, authorization code, `S256` PKCE, exact issuer, exact + resource, bounded scopes, and deny-first authorization are mandatory. +- Existing RP preflight remains side-effect-free: it must not fetch discovery, + protected-resource metadata, DNS, or remote Keycloak state. Runtime discovery + during an actual client/resource flow is a separate acceptance boundary. +- Discovery and metadata are public; operator tokens, client secrets, bearer + tokens, authorization codes, PKCE verifiers, and protected user data are not. + +### Measured evidence + +No MCP runtime evidence exists in this record. The required evidence is listed +below and must be attached to the implementation PR at the exact current head. + +### Assumptions and limitations + +- The deployment supplies one stable public HTTPS issuer and WAF route. +- LineageWeave supplies the canonical protected-resource URI and scope list. +- A JWT signature check without an active revocation check cannot prove + immediate revocation denial before token expiry. +- A successful discovery document or Keycloak client receipt cannot prove + downstream tenant/resource authorization. + +## Target discovery contract + +For a configured issuer `https://keyverse.example/realms/cwl`, publish and test: + +```text +https://keyverse.example/realms/cwl/.well-known/openid-configuration +https://keyverse.example/.well-known/oauth-authorization-server/realms/cwl +``` + +Both documents must contain the same exact `issuer`, authorization endpoint, +token endpoint, and JWKS URI. Each authorization-server metadata document must +publish these as separate RFC 8414 members: `response_types_supported: ["code"]`, +`grant_types_supported: ["authorization_code"]`, +`code_challenge_methods_supported: ["S256"]`, and +`scopes_supported: [""]`. +The documents must not expose client secrets, registration tokens, private +Keycloak URLs, tenant inventories, or user data. + +The tests must reject a document whose issuer, host, endpoint origin, or JWKS +origin is changed to an attacker-controlled value. They must also reject +discovery that is returned from an admin-only route or that causes an operator +credential to be sent. + +## Target protected-resource contract + +LineageWeave must publish RFC 9728 metadata for each MCP resource containing: + +```json +{ + "resource": "https://lineageweave.example/mcp", + "authorization_servers": ["https://keyverse.example/realms/cwl"], + "scopes_supported": [""] +} +``` + +The concrete scope value is deployment/product data and must be reviewed in +the LineageWeave integration. It must not contain credentials or PII. The +resource must send a 401 bearer challenge with its metadata URL when a client +has no usable access token. The resource must reject a token whose issuer, +audience/resource, subject, expiry, scope, tenant, workspace, or revocation +state does not match its own policy. + +## Target authorization sequence + +```text +MCP client -> LineageWeave: request without token +LineageWeave -> client: 401 + RFC9728 resource_metadata challenge +client -> resource metadata: discover exact resource and Keyverse issuer +client -> Keyverse discovery: obtain code/token/JWKS endpoints +client -> Keyverse authorization: code + exact redirect + S256 + resource + scopes +user -> Keyverse: passwordless browser/passkey authentication +Keyverse -> client: authorization code +client -> Keyverse token: code + verifier + same redirect + same resource +Keyverse -> client: resource-bound access token +client -> LineageWeave: Bearer access token +LineageWeave -> Keyverse/resource policy: verify issuer, signature, expiry, + audience/resource, scopes, tenant/workspace, and active/revocation state +``` + +The sequence must never send a password, static MCP API key, operator bearer, +authorization code, or PKCE verifier to LineageWeave. The resource must not +infer tenant or privilege from a client ID, email, UUID, or unverified header. + +## Resource and token binding + +The canonical resource URI is an absolute URI without a fragment. It is stored +with one deliberate trailing-slash spelling and compared as an exact value; +Keyverse must not silently normalize two resource identities into one. The +authorization request and token request each contain exactly that one value. + +The implementation must prove that the resulting access token's audience (or +equivalent resource authorization evidence) is bound to the same URI. It must +also prove that a token issued for resource A is rejected by resource B, even +when the same user, client, role, or scope name appears in both deployments. + +## Registration and headless boundary + +The first profile uses Keyverse pre-registration with an exact public client ID +and exact redirect list. It has no client secret and no general dynamic +registration endpoint. A real client that requires Client ID Metadata Documents +or RFC 7591 must trigger a new security review covering URL fetch/SSRF, +redirect ownership, registration abuse, cache freshness, and audit. + +RFC 8628 is a documented follow-up only. It must not appear in metadata or be +accepted by the token endpoint until a real callback-less client requirement, +bounded polling contract, user-code lifecycle, abuse limits, and revocation +tests are accepted. + +## Revocation and audit contract + +Keycloak remains authoritative for disabled identities, sessions, signing-key +rotation, and token revocation. The resource must use an active-token or +equivalent revocation check when claiming immediate revoked-token denial; a +local JWT check alone is insufficient. Audit records contain only non-secret +correlation identifiers, client/resource/scope decisions, actor class, outcome, +and timestamps. No bearer material is logged or persisted. + +## Required implementation evidence + +- discovery and RFC 8414/OIDC document agreement on exact issuer/endpoints; +- resource metadata and 401 challenge agreement; +- real browser-assisted passkey authorization-code/PKCE flow; +- exact redirect, state, verifier, resource, and scope checks; +- wrong issuer/audience/resource/scope/redirect/PKCE/expiry/revocation denial; +- cross-tenant and cross-workspace denial at the LineageWeave boundary; +- disabled-user/session and key-rotation behavior; +- no password grant, static API key, or premature device flow; +- no discovery/network side effect in existing preflight tests; +- secret/log/response/artifact scans; +- full 100% statement/branch/docstring gates and current-head protected Checks. + +## References — APA 7th + +Internet Engineering Task Force. (2018). *OAuth 2.0 authorization server +metadata* (RFC 8414). https://doi.org/10.17487/RFC8414 + +Richer, J., Jones, M., Bradley, J., Machulak, M., & Hunt, P. (2015). *OAuth +2.0 dynamic client registration protocol* (RFC 7591). Internet Engineering +Task Force. https://doi.org/10.17487/RFC7591 + +Internet Engineering Task Force. (2015). *Proof key for code exchange by OAuth +public clients* (RFC 7636). https://doi.org/10.17487/RFC7636 + +Internet Engineering Task Force. (2025). *OAuth 2.0 protected resource +metadata* (RFC 9728). https://doi.org/10.17487/RFC9728 + +Internet Engineering Task Force. (2020). *Resource indicators for OAuth 2.0* +(RFC 8707). https://doi.org/10.17487/RFC8707 + +Internet Engineering Task Force. (2021). *JSON Web Token (JWT) profile for +OAuth 2.0 access tokens* (RFC 9068). https://doi.org/10.17487/RFC9068 + +Lodderstedt, T., Bradley, J., Labunets, A., & Fett, D. (2025). *Best current +practice for OAuth 2.0 security* (BCP 240, RFC 9700). Internet Engineering Task +Force. https://doi.org/10.17487/RFC9700 + +Model Context Protocol. (2025, November 25). *Authorization*. +https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization + +OpenID Foundation. (2014). *OpenID Connect discovery 1.0 incorporating errata +set 2*. https://openid.net/specs/openid-connect-discovery-1_0.html + +Internet Engineering Task Force. (2019). *OAuth 2.0 device authorization grant* (RFC 8628). +https://doi.org/10.17487/RFC8628 + +## Source limitations + +These references establish standards and protocol interpretation only. They do +not prove Keycloak vendor conformance, LineageWeave authorization correctness, +browser success, token revocation latency, or production readiness. Those are +measured in the implementation and deployment evidence described above. From d2f48232fff1505c3274fe4296ca21cece5db102 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 20:35:12 +0900 Subject: [PATCH 02/17] docs: harden MCP OAuth issuer contract --- CHANGELOG.md | 4 ++ docs/TRACEABILITY.md | 2 +- .../0013-mcp-oauth-client-authorization.md | 36 ++++++++++++---- docs/doctoring/mcp-oauth-authorization.md | 41 ++++++++++++++++--- docs/operations/oidc-rp-reconciliation.md | 11 +++++ tests/test_documentation_contract.py | 23 ++++++++++- 6 files changed, 103 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5462dfb..db2e3ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,10 @@ Keep a Changelog, and releases use semantic versioning. ### Changed +- Updated the design-only MCP authorization contract to MCP Authorization + 2026-07-28, RFC 9207 callback-issuer validation, and RFC 9068 JWT + access-token header, claim, signature, and algorithm rejection evidence; + runtime acceptance remains unimplemented. - Relying-party deployment controllers now send validated, secret-free metadata to Keyverse desired-state PUT instead of applying client representations directly to Keycloak; confidential credential placement remains a separate diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index c62728c..77d1dee 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -21,7 +21,7 @@ | secrets from KV/DB, env bootstrap only | architecture/security decision | config/bootstrap/template validation | implemented-main | | work-conserving fail-closed hourly API gate | automation safety decision | PR #74 protected-main workflow tests/exact-head evidence; scheduled/manual run remains required | implemented-main | | non-fork RP Keyverse authorization boundary | ADR-0008; OIDC/JWT recipient validation and least-privilege policy | six-app audit, per-RP issuer/audience/tenant/ABAC/RBAC evidence required | accepted-contract | -| MCP-compatible OAuth client authorization | ADR-0013; MCP Authorization 2025-11-25, RFC 8414, RFC 8707, RFC 9728, RFC 9700 | design-only contract for Keycloak discovery, public-client PKCE, exact resource binding, LineageWeave protected-resource metadata, centralized revocation, and negative evidence; no protected-main runtime evidence yet | gap-not-claimed | +| MCP-compatible OAuth client authorization | ADR-0013; MCP Authorization 2026-07-28, RFC 8414, RFC 8707, RFC 8725, RFC 9068, RFC 9207, RFC 9728, RFC 9700 | design-only contract for Keycloak discovery, recorded-issuer versus callback-`iss` exact comparison, public-client PKCE, RFC 9068 JWT validation, exact resource binding, LineageWeave protected-resource metadata, centralized revocation, and negative evidence; a mismatch rejects the authorization code; no protected-main runtime evidence yet | gap-not-claimed | | naruon Keyverse OIDC acceptance boundary | ADR-0008; exact issuer/audience/JWKS validation and required OIDC NumericDate claims | naruon PR #1321 `ca6ccba` names the Keyverse issuer and `naruon-web` audience, requires verified `iat`, tests explicit org/workspace/role acceptance plus missing-`iat` denial, strips orphaned HTML comment terminators, and resolves the `develop` CHANGELOG conflict; protected-branch Checks/review remain required | active-PR | | semantic-data-portal Keyverse claim boundary | ADR-0008; bounded claim mapping and fail-closed tenant/role/JWT-header validation | semantic-data-portal PR #58 `47e2215` aliases `org`/`role`, validates every present tenant alias, rejects malformed/conflicting aliases before `ActorContext`, explicitly rejects unsupported JWT `crit` headers, and keeps the cryptography floor; protected-branch approval remains required | active-PR | | pg-erd-cloud Keyverse organization boundary | ADR-0008; verified tenant binding before project authorization | pg-erd-cloud PR #855 `e4b4771` exact `org`/audience/`iat` checks, single-tenant profile, API-key bypass denial; shared multi-tenant persistence remains unimplemented | active-PR | diff --git a/docs/adr/0013-mcp-oauth-client-authorization.md b/docs/adr/0013-mcp-oauth-client-authorization.md index c1e9cee..108ab26 100644 --- a/docs/adr/0013-mcp-oauth-client-authorization.md +++ b/docs/adr/0013-mcp-oauth-client-authorization.md @@ -19,13 +19,15 @@ The account-unification service owns deterministic desired state and operator boundaries; it is not a second token issuer. The existing relying-party profile is intentionally secret-free, authorization-code based, and PKCE protected. -The MCP authorization specification requires protected-resource metadata, -authorization-server discovery, OAuth 2.1 security measures for clients, and a -canonical `resource` parameter. RFC 9700 requires exact redirect handling and -PKCE for public clients. RFC 8707 binds an authorization request to an -absolute resource URI. RFC 9728 makes the protected resource the owner of its -metadata. These protocol roles must remain separate from Keyverse's private -operator API and from LineageWeave's resource authorization policy. +The MCP Authorization specification (2026-07-28) requires protected-resource +metadata, authorization-server discovery, OAuth 2.1 security measures for +clients, and a canonical `resource` parameter. RFC 9700 requires exact +redirect handling and PKCE for public clients. RFC 8707 binds an authorization +request to an absolute resource URI. RFC 9207 defines an issuer parameter for +authorization responses, including errors, to prevent mix-up attacks. RFC +9728 makes the protected resource the owner of its metadata. These protocol +roles must remain separate from Keyverse's private operator API and from +LineageWeave's resource authorization policy. ## Decision @@ -99,6 +101,24 @@ and mandatory `S256` PKCE: same client profile. A Keycloak audience-mapper client ID must never be reused as the MCP resource audience. +Before redirecting the user-agent, the client records the `issuer` from the +selected authorization server's validated metadata in the same per-request +state as the PKCE verifier and `state`. When +`authorization_response_iss_parameter_supported=true`, a missing authorization +response `iss` is rejected before token exchange. Whenever `iss` is present, +including in an error response, the client form-decodes it and compares it to +the recorded issuer with RFC 3986 simple string comparison. It does not apply +case folding, default-port elision, trailing-slash changes, or +percent-encoding normalization. A mismatch rejects the response before the +authorization code or error fields are used. + +The protected-resource token contract follows RFC 9068: the JWT `typ` header +is exactly `at+jwt` or `application/at+jwt`; `iss`, `exp`, `aud`, `sub`, +`client_id`, `iat`, and `jti` are required; the signature is verified against +the issuer's keys; only an explicitly allowlisted signing algorithm is +accepted; unsupported algorithms and `alg=none` are rejected. Any failed +check produces `invalid_token` and no resource authorization decision. + Keyverse reuses the existing closed secret-free relying-party lifecycle for pre-registration. The MCP client representation is a separately named, reviewed profile because resource-bound audience semantics are a new trust @@ -171,7 +191,9 @@ The implementation PR must test and retain evidence for denial of: | unregistered redirect or redirect mismatch | deny | | missing, duplicated, or unregistered scope | deny | | missing/invalid PKCE or state | deny | +| issuer mismatch, required-but-missing response `iss`, or issuer mismatch in an error response | deny before token exchange or error handling | | expired, revoked, disabled-user, or malformed token | deny | +| invalid JWT `typ`, missing `iat`/`jti`, invalid signature, unsupported `alg`, or `alg=none` | deny with `invalid_token` | | token for another tenant/workspace/resource | deny | | password/direct-access/device flow before its own ADR | deny | | static MCP API key | unsupported and deny | diff --git a/docs/doctoring/mcp-oauth-authorization.md b/docs/doctoring/mcp-oauth-authorization.md index 4e284eb..0060931 100644 --- a/docs/doctoring/mcp-oauth-authorization.md +++ b/docs/doctoring/mcp-oauth-authorization.md @@ -26,12 +26,15 @@ exist. ### Standards requirements - MCP clients discover the authorization server through protected-resource - metadata and support OAuth authorization-server metadata or OIDC discovery. + metadata and support OAuth authorization-server metadata or OIDC discovery + under the MCP Authorization 2026-07-28 contract. - Public clients use OAuth 2.1 security measures and PKCE. - Authorization requests and token requests carry one canonical RFC 8707 resource indicator. - The protected resource publishes RFC 9728 metadata and advertises it through a bearer challenge when required. +- RFC 9207 issuer validation binds a callback response to the authorization + server that was discovered for the request; a mismatch must stop the grant. ### Vendor behavior @@ -110,6 +113,24 @@ has no usable access token. The resource must reject a token whose issuer, audience/resource, subject, expiry, scope, tenant, workspace, or revocation state does not match its own policy. +For an RFC 9068 JWT access token, the resource must also require `typ` equal to +`at+jwt` or `application/at+jwt`, required `iss`, `exp`, `aud`, `sub`, +`client_id`, `iat`, and `jti` claims, a valid signature, and an explicitly +allowlisted signing algorithm. `alg=none`, unsupported algorithms, invalid +signatures, invalid `typ`, or missing `iat`/`jti` are rejected with +`invalid_token` before resource authorization. + +## Target authorization-response issuer contract + +Before redirecting, the client records the `issuer` from validated discovery in +the per-request state that also contains the PKCE verifier and `state`. For +`authorization_response_iss_parameter_supported=true`, a missing `iss` is an +error. If `iss` is present, including in an error response, the client decodes +the form value and compares it with the recorded issuer using RFC 3986 simple +string comparison. It must not normalize case, default ports, trailing slashes, +or percent encoding. A mismatch rejects the response before token exchange and +the client must not act on or display the response's error fields. + ## Target authorization sequence ```text @@ -171,6 +192,11 @@ and timestamps. No bearer material is logged or persisted. - resource metadata and 401 challenge agreement; - real browser-assisted passkey authorization-code/PKCE flow; - exact redirect, state, verifier, resource, and scope checks; +- successful and error authorization responses with matching `iss`; +- issuer mismatch, required-but-missing `iss`, and non-normalized string + comparison denial before token exchange; +- RFC 9068 denial for invalid `typ`, missing `iat`/`jti`, invalid signatures, + unsupported algorithms, and `alg=none`; - wrong issuer/audience/resource/scope/redirect/PKCE/expiry/revocation denial; - cross-tenant and cross-workspace denial at the LineageWeave boundary; - disabled-user/session and key-rotation behavior; @@ -197,15 +223,20 @@ metadata* (RFC 9728). https://doi.org/10.17487/RFC9728 Internet Engineering Task Force. (2020). *Resource indicators for OAuth 2.0* (RFC 8707). https://doi.org/10.17487/RFC8707 -Internet Engineering Task Force. (2021). *JSON Web Token (JWT) profile for -OAuth 2.0 access tokens* (RFC 9068). https://doi.org/10.17487/RFC9068 +Bertocci, V. (2021). *JSON Web Token (JWT) profile for OAuth 2.0 access tokens* +(RFC 9068). Internet Engineering Task Force. +https://doi.org/10.17487/RFC9068 + +Meyer zu Selhausen, K., & Fett, D. (2022). *OAuth 2.0 authorization server +issuer identification* (RFC 9207). Internet Engineering Task Force. +https://doi.org/10.17487/RFC9207 Lodderstedt, T., Bradley, J., Labunets, A., & Fett, D. (2025). *Best current practice for OAuth 2.0 security* (BCP 240, RFC 9700). Internet Engineering Task Force. https://doi.org/10.17487/RFC9700 -Model Context Protocol. (2025, November 25). *Authorization*. -https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization +Model Context Protocol. (2026, July 28). *Authorization*. +https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization OpenID Foundation. (2014). *OpenID Connect discovery 1.0 incorporating errata set 2*. https://openid.net/specs/openid-connect-discovery-1_0.html diff --git a/docs/operations/oidc-rp-reconciliation.md b/docs/operations/oidc-rp-reconciliation.md index 629f031..816970a 100644 --- a/docs/operations/oidc-rp-reconciliation.md +++ b/docs/operations/oidc-rp-reconciliation.md @@ -29,6 +29,17 @@ authorization-code/JWT acceptance. payload digest, apply receipt, versions, operator, controlled acceptance result, and rollback reference. +## MCP authorization handoff + +This RP reconciliation runbook does not claim an MCP runtime implementation or +protected-resource acceptance. When a downstream MCP client uses the separate +ADR-0013 profile, its controlled acceptance must record the validated discovery +issuer before redirect, compare every present callback `iss` with that issuer +using exact string comparison, reject a required-but-missing or mismatched +value before token exchange, and validate RFC 9068 `typ`, required claims, +signature, and algorithm policy at the protected resource. A successful +Keycloak client receipt or mapper read-back is not evidence of those checks. + ## Naruon mapper contract The Naruon runtime artifact is a public `naruon-web` client with exactly four diff --git a/tests/test_documentation_contract.py b/tests/test_documentation_contract.py index 47ef4cd..c5c0657 100644 --- a/tests/test_documentation_contract.py +++ b/tests/test_documentation_contract.py @@ -4,8 +4,8 @@ from pathlib import Path - ROOT = Path(__file__).resolve().parents[1] + REQUIRED_DOCUMENTS = ( "DOCUMENTATION.md", "docs/PRD.md", @@ -90,6 +90,27 @@ def test_erd_keeps_keycloak_internal_schema_external() -> None: assert "does not duplicate or directly edit unsupported Keycloak internal tables" in erd +def test_mcp_authorization_contract_tracks_current_issuer_and_token_rules() -> None: + """Keep the design-only MCP security contract aligned across its records.""" + + adr = _read("docs/adr/0013-mcp-oauth-client-authorization.md") + doctoring = _read("docs/doctoring/mcp-oauth-authorization.md") + traceability = _read("docs/TRACEABILITY.md") + changelog = _read("CHANGELOG.md") + for text in (adr, doctoring): + normalized = " ".join(text.split()) + assert "authorization_response_iss_parameter_supported" in text + assert "simple string comparison" in normalized + assert "at+jwt" in text + assert "application/at+jwt" in text + assert "alg=none" in text + assert "missing `iat`/`jti`" in text + assert "MCP Authorization 2026-07-28" in traceability + assert "RFC 9207" in traceability + assert "mismatch rejects the authorization code" in traceability + assert "MCP Authorization\n 2026-07-28" in changelog + + def test_adr_index_contains_governing_identity_decisions() -> None: """Keep every indexed architecture decision present and reviewable.""" From c55f6568d9339f2ad546e79ebfdccf5331aea172 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 06:59:57 -0700 Subject: [PATCH 03/17] build(deps): bump step-security/harden-runner from 2.20.1 to 2.21.0 (#105) * build(deps): bump step-security/harden-runner from 2.20.1 to 2.21.0 Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.20.1 to 2.21.0. - [Release notes](https://github.com/step-security/harden-runner/releases) - [Commits](https://github.com/step-security/harden-runner/compare/b09bb98e06d4d774595224525879c09bc6e98c40...05e31511f85b41b11d1cf0ef85d0992719546e2c) --- updated-dependencies: - dependency-name: step-security/harden-runner dependency-version: 2.21.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * build(account-unification): resync uv lockfile --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Seongho Bae --- .github/workflows/hourly-product-development.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/hourly-product-development.yml b/.github/workflows/hourly-product-development.yml index a612bdc..da144c5 100644 --- a/.github/workflows/hourly-product-development.yml +++ b/.github/workflows/hourly-product-development.yml @@ -49,7 +49,7 @@ jobs: patch_sha256: ${{ steps.package.outputs.patch_sha256 }} steps: - name: Harden runner and block undeclared egress - uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 + uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 with: egress-policy: block disable-telemetry: true @@ -639,7 +639,7 @@ jobs: publish: ${{ steps.seal.outputs.publish }} steps: - name: Harden independent verification - uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 + uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 with: egress-policy: block disable-telemetry: true @@ -780,7 +780,7 @@ jobs: pull-requests: read steps: - name: Harden publication - uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 + uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 with: egress-policy: block disable-telemetry: true From 9150e7c3e04f6a2ea089e542c48644f7de5a7a4d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 07:01:35 -0700 Subject: [PATCH 04/17] build(deps): bump astral-sh/setup-uv from 9.0.0 to 10.0.1 (#106) * build(deps): bump astral-sh/setup-uv from 9.0.0 to 10.0.1 Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 9.0.0 to 10.0.1. - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/c771a70e6277c0a99b617c7a806ffedaca235ff9...20cfd1bf945f4377ade1205e4dbc17946fc9a30d) --- updated-dependencies: - dependency-name: astral-sh/setup-uv dependency-version: 10.0.1 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * build(account-unification): resync uv lockfile --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Seongho Bae --- .github/workflows/ci.yml | 2 +- .github/workflows/hourly-product-development.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a1eca6..9396458 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: # v9 changed this default to false; retain bounded cache usage. prune-cache: true diff --git a/.github/workflows/hourly-product-development.yml b/.github/workflows/hourly-product-development.yml index da144c5..0a15d1d 100644 --- a/.github/workflows/hourly-product-development.yml +++ b/.github/workflows/hourly-product-development.yml @@ -319,7 +319,7 @@ jobs: - name: Set up the locked package runner if: steps.gate.outputs.develop == 'true' - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: prune-cache: true @@ -670,7 +670,7 @@ jobs: python-version: "3.12" - name: Set up the independent locked package runner - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: prune-cache: true From c167e4c48b85e5b4d6026e3a9203e23040ac7172 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 07:02:36 -0700 Subject: [PATCH 05/17] build(deps): bump uvicorn from 0.52.1 to 0.52.3 in /services/account_unification (#107) * build(deps): bump uvicorn in /services/account_unification Bumps [uvicorn](https://github.com/Kludex/uvicorn) from 0.52.1 to 0.52.3. - [Release notes](https://github.com/Kludex/uvicorn/releases) - [Changelog](https://github.com/Kludex/uvicorn/blob/main/docs/release-notes.md) - [Commits](https://github.com/Kludex/uvicorn/compare/0.52.1...0.52.3) --- updated-dependencies: - dependency-name: uvicorn dependency-version: 0.52.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * build(deps): refresh uv lock for uvicorn update * build(account-unification): regenerate exports for uvicorn update --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Seongho Bae --- services/account_unification/pyproject.toml | 2 +- .../account_unification/requirements-dev.txt | 32 ++++++------------- .../account_unification/requirements.lock | 6 ++-- services/account_unification/uv.lock | 8 ++--- 4 files changed, 17 insertions(+), 31 deletions(-) diff --git a/services/account_unification/pyproject.toml b/services/account_unification/pyproject.toml index 3afc27f..09ee712 100644 --- a/services/account_unification/pyproject.toml +++ b/services/account_unification/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ "pydantic==2.13.4", "httpx==0.28.1", "pyyaml==6.0.3", - "uvicorn==0.52.1", + "uvicorn==0.52.3", ] [project.optional-dependencies] diff --git a/services/account_unification/requirements-dev.txt b/services/account_unification/requirements-dev.txt index a1f2d38..bf9a29b 100644 --- a/services/account_unification/requirements-dev.txt +++ b/services/account_unification/requirements-dev.txt @@ -150,7 +150,6 @@ coverage==7.15.4 \ --hash=sha256:2c9872e4d9dc5d3cf616bf4b382f5a00359305a5be666a3dd0b5cdb4e49597f9 \ --hash=sha256:2e179f19bfe1d31f8eeeaa12990194d761c4f62f0759661000bca6cd8729f40b \ --hash=sha256:2ff8f5e9b8f7a94f0c11c45631eee103dbcb7d63274edd12c56efe1be690b3b4 \ - --hash=sha256:317db01a2cb02552fd67e2b1cca77a4b528a2a277176c5e0bf2cecbb639d3f54 \ --hash=sha256:337399ad2c93b3acd2a937627dae8b3e86b66707cd3d3e856347999aadf1ef8d \ --hash=sha256:349062d66f00b40fa2c1c222438bad25fabf755631b5d82937fe985c8008615c \ --hash=sha256:357a173465c7ce028d07a95cc2b63b5bf59f50ecdd5ad75c5cbb78ada984048e \ @@ -159,17 +158,14 @@ coverage==7.15.4 \ --hash=sha256:3a54f5a0d85050c73a38f6793090ee83974531e67fe5e57a1da9bee11398aa5e \ --hash=sha256:3d6f4955b73b5445271379a59e3792b0d978f42d4a01e0cf7a67d9c33a3bb0a5 \ --hash=sha256:3da9e92d1c551fd7563833e9ade686efb0c4b7363ab7681a94283958c950bf5e \ - --hash=sha256:3fc2130bf37df31852a8384f12601563a45a0024bccc6624f38355cba7a8d360 \ --hash=sha256:3fc9e047706fb4a9abb54f719d3aa643e80e5bb3818182c40aee01ac0f0247ba \ --hash=sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e \ --hash=sha256:4256ced708e598e05209bc1a8ab4074e04a51dba4c62fb45926a229af675ace7 \ - --hash=sha256:425920379052ff1fe465268f3361d35804a241bbdd5a1b592c8cb60df4c52325 \ --hash=sha256:43619d04c3671792d2c4706ae8bf45e265dc87bbd4078189ef8b847ea1e74be2 \ --hash=sha256:460115e32ee40566476db5048f9bec1e842c127ad8e6f8be745aad3ac9cbc839 \ --hash=sha256:46822e9b6ff1c6a72b518c162c44a8f45a61a1d609c51084bf5b16c023c5037b \ --hash=sha256:474223409d88eb20d2d6a0d37ea60e8647a65a90cc008dc1f0410af5f64f1e0d \ --hash=sha256:4a4bf917c9953f57c957be31c1cd504e3bd2f34d4a352b9d391a3025336f6768 \ - --hash=sha256:4dff9daa47d83120c3ec38ce921214242944a832aa04e903e50b5b7ebac8972d \ --hash=sha256:4e6f6f632b7b2f714bf7a1346e8f97b650ee71f3c298aaad42a2ab60f0f07645 \ --hash=sha256:5172326e861a38b48b48befca15e0f477a26b283337a33a739c8fed229934e36 \ --hash=sha256:5669c8378ebde86f5def7a25d29586631b58acc27ffde04399f678f3dfc6e082 \ @@ -180,7 +176,6 @@ coverage==7.15.4 \ --hash=sha256:6879ded16a27f3eeca19b900c147e81616e7054db451471a611b2755ee5249f7 \ --hash=sha256:68be5e1de60ff13c9095bbec0e5a7fa45b33b101752215b91345ea1f61c4a278 \ --hash=sha256:69484d1aca26e322e1c3ce03f09341e84524ababad2d7202161738d83cc9f82e \ - --hash=sha256:69bb2400abef928e365ea7d4d9925169ada78ed2295546780002d4b65de3df88 \ --hash=sha256:6befeab5fb2b51c958ca4ac6c5d141a1e8240f4f76e46350f1911963deda49cd \ --hash=sha256:6e0a8a5083b096487d6cfced94cdd514d8f5db6f113610fb36c0620edb1028cf \ --hash=sha256:770e9325ab5ea6d56f77e59b29ecfe0ac20b57a82a601876f90494a4dda0386f \ @@ -190,22 +185,17 @@ coverage==7.15.4 \ --hash=sha256:7d1abebdb047729e852b9c77a00497dfbeb11eb3a117e037d7dbc3ac8e5f5c54 \ --hash=sha256:7f2f62ae3cd189dd2e13aece758c57b3eecbd27be070dbd4cbd10936049e5dbf \ --hash=sha256:81294c7e6ab30c5f74c0353b11b2fd6320e72d9bee6ac73b357caa8b916323a5 \ - --hash=sha256:81661f82d302484e3119e7c80c519c02fa9bcc2a6b339baf67d67bc89c580f04 \ - --hash=sha256:83cf06cdd687677742caff1a9134833b7a8b75f111519d2cb0e0ba1b9a851e15 \ --hash=sha256:899b9da30f3c6c336566e3707495bb23e8302d39d862f01fa78c48b99b9437e2 \ --hash=sha256:8af623e5cd92080acddd02b38f2f406a2c3a0893c38950b211890361448fbf26 \ --hash=sha256:8b4f1c3a69ca580f3fbd6b2046915f536d7f586874f25c1bb23add2a3c88d50f \ --hash=sha256:8bc16bb47b7679670eceff71d78bfb7d6e5b143f6c2cd117487ec7c75e0d4b78 \ --hash=sha256:8df457da2249d3c75ca2e5e835d59c725abfe92d27fdff6cd99eed85b51d5e9a \ - --hash=sha256:8ee3838dcb656602c3b51e16aed9bfb0822f8d8d6d1c5966d32ec8c104be8e20 \ - --hash=sha256:8fa4de68e2a752468ff14b4e15db7def689a71be759e826a31ccecbef69c5fd0 \ --hash=sha256:93a3a0b662abcc10c73a47cbc72cd60f63618d6989fb2d1286e50eacd974f303 \ --hash=sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84 \ --hash=sha256:96e257121228ec5cd2bb919276e94ac11074471bc37d68dbae0e8308cce15fff \ --hash=sha256:986be58c3ab54aae8d3496a6225eea74f760fdbe739b38bd442c7e8d133aa53b \ --hash=sha256:9e71e7bc71c686a123347ae47a0de33a175e797a85bb57b791492adf4eec8ed8 \ --hash=sha256:a0865421cfdc53654b342d515e5a233187590882d20b95752150e53f65460017 \ - --hash=sha256:a093fd37229918976f602aa07aa59e0973cde82186f220c8e197f721f5be0ce4 \ --hash=sha256:a58a94fed5da6997d258e8f7668c1e195fbd04a691d781b7558f1e468f9e68bc \ --hash=sha256:a67a9f78b2942d87ba8ce3059c642164d2aedd65337377fb52fe9803656bc5c7 \ --hash=sha256:a9447978a92f405d301123cfd39ff49895490efb769a758fe2734c7f631bf8ce \ @@ -213,7 +203,6 @@ coverage==7.15.4 \ --hash=sha256:ac41cc14bebda0dbfb0628036b7f75706935c95bcc07fefe9a0f93614aa60a57 \ --hash=sha256:af6c538498ce66c10d3fd541c2a8d5b03da5850355add34e6cba564210cb9e72 \ --hash=sha256:b10075e5421d04265766a6d1dac809bbeb8a946fbb23c8f82c227409b2190719 \ - --hash=sha256:b24e078eabcd6a9caa8b0713f9bc1eeb310bcc960a29d45a3b4fcd4b16d5b11d \ --hash=sha256:bbac5abad70df71019988f83f26ac7092ff2642975def4429e98dc7585ef3490 \ --hash=sha256:be619439dbcd31a2eab10b32de9fff62c26ed4bab69dc32b8363fdaaa0882809 \ --hash=sha256:c2de40895718f91951b86712b4c5b694acaf9a0a49be13874896f599a1eed3f4 \ @@ -222,11 +211,8 @@ coverage==7.15.4 \ --hash=sha256:c65a9e0dfc6143491879da4e13b5e30f8be192055de508d737fb14601edbd22c \ --hash=sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921 \ --hash=sha256:c7dbc748ac8a1e3e59a2b28bea47675e6e778081dbbf081bde0d75def2fcbe1d \ - --hash=sha256:cb476b2e828ecb71cb6b6a928d23fd20a7ddb501188022dae1c37499149cc338 \ --hash=sha256:cbde877ef9dd7baf272b9bfef2b8a25edd45d9170fc326951dd20eb480335e85 \ - --hash=sha256:cfe20cc8cf8821d4fe54f89106cbf06aa27f37b5bbe3535568065a81539b4150 \ --hash=sha256:d003b7a5708ddad5c206c79607a6b92abb6fc13c57d99d8a4468cc03a2941ced \ - --hash=sha256:d0be6daac4cce6b8c8dc65886bae1b082ddbca4da8e5cbb5e15166acf253e264 \ --hash=sha256:d12b33a3a50a1676b7784dc8d00a0c6d66a9f2add4b85a041c19b6a7e53ef23c \ --hash=sha256:d15715e8c46552827e5e4f30a35575a2dbcad14454cf3284c54483946bd16931 \ --hash=sha256:d28a4a899354d0ea6214cc59b4fa19eefbce1b9ff1688ab579acf49e894bd3fb \ @@ -341,9 +327,9 @@ interrogate==1.7.0 \ --hash=sha256:a320d6ec644dfd887cc58247a345054fc4d9f981100c45184470068f4b3719b0 \ --hash=sha256:b13ff4dd8403369670e2efe684066de9fcb868ad9d7f2b4095d8112142dc9d12 # via cwl-idp-account-unification -packaging==26.3 \ - --hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \ - --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via pytest pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ @@ -539,9 +525,9 @@ setuptools==84.0.0 \ --hash=sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670 \ --hash=sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73 # via cwl-idp-account-unification -starlette==1.6.0 \ - --hash=sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c \ - --hash=sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b +starlette==1.3.1 \ + --hash=sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0 \ + --hash=sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6 # via fastapi tabulate==0.10.0 \ --hash=sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d \ @@ -570,7 +556,7 @@ typing-inspection==0.4.2 \ # via # fastapi # pydantic -uvicorn==0.52.1 \ - --hash=sha256:112ec661814189acbccd3f7b86460147cc065fc92c0821afa78918780e4354dd \ - --hash=sha256:e4403f9d93188cf9d1088e9f40e3acd12630e2df8675316704379a7fc20fff6a +uvicorn==0.52.3 \ + --hash=sha256:116af2710dbf47c80f463cd20ee4884b6662f4c9f227d797ddc7279d2fcc2c7c \ + --hash=sha256:18857b9e6579300be55c91c0a1cfd37d9a2cf0cabea33b88275f199eb73b8b58 # via cwl-idp-account-unification diff --git a/services/account_unification/requirements.lock b/services/account_unification/requirements.lock index 5fccf61..f3384fe 100644 --- a/services/account_unification/requirements.lock +++ b/services/account_unification/requirements.lock @@ -364,7 +364,7 @@ typing-inspection==0.4.2 \ # via # fastapi # pydantic -uvicorn==0.52.1 \ - --hash=sha256:112ec661814189acbccd3f7b86460147cc065fc92c0821afa78918780e4354dd \ - --hash=sha256:e4403f9d93188cf9d1088e9f40e3acd12630e2df8675316704379a7fc20fff6a +uvicorn==0.52.3 \ + --hash=sha256:116af2710dbf47c80f463cd20ee4884b6662f4c9f227d797ddc7279d2fcc2c7c \ + --hash=sha256:18857b9e6579300be55c91c0a1cfd37d9a2cf0cabea33b88275f199eb73b8b58 # via cwl-idp-account-unification diff --git a/services/account_unification/uv.lock b/services/account_unification/uv.lock index bf57988..a3339e8 100644 --- a/services/account_unification/uv.lock +++ b/services/account_unification/uv.lock @@ -376,7 +376,7 @@ requires-dist = [ { name = "pyyaml", specifier = "==6.0.3" }, { name = "ruff", marker = "extra == 'dev'", specifier = "==0.16.1" }, { name = "setuptools", marker = "extra == 'dev'", specifier = "==84.0.0" }, - { name = "uvicorn", specifier = "==0.52.1" }, + { name = "uvicorn", specifier = "==0.52.3" }, ] provides-extras = ["dev"] @@ -817,13 +817,13 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.52.1" +version = "0.52.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/03/18/ccce41535dee1be77735592bd19965f3972c82e07ee703d324709496b716/uvicorn-0.52.1.tar.gz", hash = "sha256:112ec661814189acbccd3f7b86460147cc065fc92c0821afa78918780e4354dd", size = 100571, upload-time = "2026-08-01T18:19:30.732Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/28/64ca011edf31c715b4fad359c587ea52391aaffa125065695590241ff617/uvicorn-0.52.3.tar.gz", hash = "sha256:18857b9e6579300be55c91c0a1cfd37d9a2cf0cabea33b88275f199eb73b8b58", size = 100621, upload-time = "2026-08-13T16:50:02.899Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/d5/68e6e9bca63c0badf67002890a46d3784c958de45b65e1275ec583ca1f06/uvicorn-0.52.1-py3-none-any.whl", hash = "sha256:e4403f9d93188cf9d1088e9f40e3acd12630e2df8675316704379a7fc20fff6a", size = 79859, upload-time = "2026-08-01T18:19:29.294Z" }, + { url = "https://files.pythonhosted.org/packages/dc/2b/ebd108734a8204c6b4b93c681c9a38c5273b3ccd5d129fee4ffc1d97772c/uvicorn-0.52.3-py3-none-any.whl", hash = "sha256:116af2710dbf47c80f463cd20ee4884b6662f4c9f227d797ddc7279d2fcc2c7c", size = 79859, upload-time = "2026-08-13T16:50:01.323Z" }, ] From 101e7bab4c034fd9bd5f4668bf2702d7779ca050 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 07:03:29 -0700 Subject: [PATCH 06/17] build(deps-dev): bump ruff from 0.16.1 to 0.16.3 in /services/account_unification (#108) * build(deps-dev): bump ruff in /services/account_unification Bumps [ruff](https://github.com/astral-sh/ruff) from 0.16.1 to 0.16.3. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.16.1...0.16.3) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.16.3 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * build(deps): refresh uv lock for ruff update --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Seongho Bae --- services/account_unification/pyproject.toml | 2 +- .../account_unification/requirements-dev.txt | 38 ++++++++-------- services/account_unification/uv.lock | 44 +++++++++---------- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/services/account_unification/pyproject.toml b/services/account_unification/pyproject.toml index 09ee712..79d5e39 100644 --- a/services/account_unification/pyproject.toml +++ b/services/account_unification/pyproject.toml @@ -25,7 +25,7 @@ dev = [ "httpx2==2.9.1", "interrogate==1.7.0", "pytest==9.1.1", - "ruff==0.16.1", + "ruff==0.16.3", "setuptools==84.0.0", ] diff --git a/services/account_unification/requirements-dev.txt b/services/account_unification/requirements-dev.txt index bf9a29b..8189677 100644 --- a/services/account_unification/requirements-dev.txt +++ b/services/account_unification/requirements-dev.txt @@ -501,25 +501,25 @@ pyyaml==6.0.3 \ --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 # via cwl-idp-account-unification -ruff==0.16.1 \ - --hash=sha256:07bf434b1c95f4e093be4532068ef4fcf00924eb2ade8796075980902d6fd54a \ - --hash=sha256:09b05e8b90c2cb06ad63464350e7a45e8e44a2dfe52072ebfba6666ca8d3f596 \ - --hash=sha256:24e9c631573cbca9d20f1283f8f479b2afa4a8503504822bd71a293889f16743 \ - --hash=sha256:26e9ca5c9bc3971f20d3cf18a957f52ffd6a5f6564ff15c4912a144dcac22494 \ - --hash=sha256:30affbcedf59ad5703d9c91f82266e02b47739f797e1a7b6e158e5526a6dae38 \ - --hash=sha256:39897739f112253ee4fdd2e8aa9a4f9ded99fb2be367d5f31dfa4ded6025584c \ - --hash=sha256:4d5f2ed10f8242d83fc08d521301089364e3375375705356f20c0e31606ef3ef \ - --hash=sha256:58edb313b88f0c5460a26adf5f39a37a3be789494a15e3e411e35fa78b89f9a0 \ - --hash=sha256:67e1e1e3fa4f0c82f0e36d4cd61e661f6e7a6196cb1aa92fe0828fa7b8f257cd \ - --hash=sha256:82ae3c0c0d74daf17b968a10b7b3bb3ef297ab7de0c1f749646b25e690ccb150 \ - --hash=sha256:a4665b309891f83f3e3c25447935f1213e9abbd4b5640af7a1f2def9f8d413c1 \ - --hash=sha256:b0d1e1393b7648079e13669de1c1f4fde06d4583e84d8fd5c1551e0a77a2aa75 \ - --hash=sha256:b41bdd48fb420987a9b5212e4957c26ad4abce401fa9ea9d4d85843727945f4f \ - --hash=sha256:d31765e131295b8445caf301e3e8a85b34d1b9b211b4109b7ba457888b051806 \ - --hash=sha256:dbaadaac38c70239f056d306b7476f246b0bf000fa6b3876402acbf5b227eaf8 \ - --hash=sha256:e0d4c20532fca4f7fa609369161d968dd28f65d83dabbd61d8e9c7edbf7001f6 \ - --hash=sha256:fde5a99e2f97479af66edd6622c6d5a2a7592c77cf4153d9e4428f5eeb55b60c \ - --hash=sha256:fedad7c801dabd3fb9741d76aca39246e6ddd9ca446a015875207bf19f1e6bc7 +ruff==0.16.3 \ + --hash=sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b \ + --hash=sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7 \ + --hash=sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb \ + --hash=sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413 \ + --hash=sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a \ + --hash=sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474 \ + --hash=sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82 \ + --hash=sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84 \ + --hash=sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da \ + --hash=sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d \ + --hash=sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870 \ + --hash=sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a \ + --hash=sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506 \ + --hash=sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9 \ + --hash=sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2 \ + --hash=sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948 \ + --hash=sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50 \ + --hash=sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081 # via cwl-idp-account-unification setuptools==84.0.0 \ --hash=sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670 \ diff --git a/services/account_unification/uv.lock b/services/account_unification/uv.lock index a3339e8..7e2f71d 100644 --- a/services/account_unification/uv.lock +++ b/services/account_unification/uv.lock @@ -374,7 +374,7 @@ requires-dist = [ { name = "pydantic", specifier = "==2.13.4" }, { name = "pytest", marker = "extra == 'dev'", specifier = "==9.1.1" }, { name = "pyyaml", specifier = "==6.0.3" }, - { name = "ruff", marker = "extra == 'dev'", specifier = "==0.16.1" }, + { name = "ruff", marker = "extra == 'dev'", specifier = "==0.16.3" }, { name = "setuptools", marker = "extra == 'dev'", specifier = "==84.0.0" }, { name = "uvicorn", specifier = "==0.52.3" }, ] @@ -731,27 +731,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/70/25/7113f6d5498888c5fb7db34081cba7d5971c4cb1bfb26819966eee68f003/ruff-0.16.1.tar.gz", hash = "sha256:fedad7c801dabd3fb9741d76aca39246e6ddd9ca446a015875207bf19f1e6bc7", size = 4877500, upload-time = "2026-07-30T19:37:01.379Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/bd/694da69368e0973de65df2ddc73ab18d43c469d5963d9b150911de6bc513/ruff-0.16.1-py3-none-linux_armv6l.whl", hash = "sha256:58edb313b88f0c5460a26adf5f39a37a3be789494a15e3e411e35fa78b89f9a0", size = 10839126, upload-time = "2026-07-30T19:36:13.697Z" }, - { url = "https://files.pythonhosted.org/packages/3f/f0/b626e5d5bd0dd9576263658ef12885e2288afd1029a48e26ffed65ec1ac1/ruff-0.16.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fde5a99e2f97479af66edd6622c6d5a2a7592c77cf4153d9e4428f5eeb55b60c", size = 11070253, upload-time = "2026-07-30T19:36:17.14Z" }, - { url = "https://files.pythonhosted.org/packages/83/63/f40acfb6b35b88623e71684942b552c3edd96035f5d98f313815f7b277de/ruff-0.16.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e0d4c20532fca4f7fa609369161d968dd28f65d83dabbd61d8e9c7edbf7001f6", size = 10561425, upload-time = "2026-07-30T19:36:20.04Z" }, - { url = "https://files.pythonhosted.org/packages/aa/dd/14ec0e9c2b4d315547dd38765004b4863e354e1b52cb308272215d9f6f6d/ruff-0.16.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30affbcedf59ad5703d9c91f82266e02b47739f797e1a7b6e158e5526a6dae38", size = 10948879, upload-time = "2026-07-30T19:36:22.476Z" }, - { url = "https://files.pythonhosted.org/packages/33/e9/9d870cbae575030fdef595f04b4b97573c525b5497cce4f4498cf2f85446/ruff-0.16.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24e9c631573cbca9d20f1283f8f479b2afa4a8503504822bd71a293889f16743", size = 10643691, upload-time = "2026-07-30T19:36:24.914Z" }, - { url = "https://files.pythonhosted.org/packages/c4/09/12743d544e2173f53ecd27217c65f90d2bc0f8424a66a60339e56bbc0457/ruff-0.16.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b41bdd48fb420987a9b5212e4957c26ad4abce401fa9ea9d4d85843727945f4f", size = 11435354, upload-time = "2026-07-30T19:36:28.447Z" }, - { url = "https://files.pythonhosted.org/packages/7f/89/a1652b2daee52083c9554a6333b678a8b01d0400f976827bb87857f9449a/ruff-0.16.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0d1e1393b7648079e13669de1c1f4fde06d4583e84d8fd5c1551e0a77a2aa75", size = 12259033, upload-time = "2026-07-30T19:36:31.326Z" }, - { url = "https://files.pythonhosted.org/packages/16/96/ecdcb8c54ee7b123b487f807eb014e6e019155a0b81dfb669acd52f28ce3/ruff-0.16.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07bf434b1c95f4e093be4532068ef4fcf00924eb2ade8796075980902d6fd54a", size = 11667981, upload-time = "2026-07-30T19:36:34.394Z" }, - { url = "https://files.pythonhosted.org/packages/cd/90/c52e12e0d862e9572f2a33aa227409143520abe53111e9a6babbac7b4af8/ruff-0.16.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39897739f112253ee4fdd2e8aa9a4f9ded99fb2be367d5f31dfa4ded6025584c", size = 11468183, upload-time = "2026-07-30T19:36:37.339Z" }, - { url = "https://files.pythonhosted.org/packages/2c/6b/4ffb7ad1d83eb16cf8cbb3c8815d3f11c88460fd162d4b372a2059be1c2a/ruff-0.16.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:82ae3c0c0d74daf17b968a10b7b3bb3ef297ab7de0c1f749646b25e690ccb150", size = 11470071, upload-time = "2026-07-30T19:36:39.91Z" }, - { url = "https://files.pythonhosted.org/packages/9c/72/32ae7db4c0b5e32ab611787caa19d1546800676d79f7483b7100a3561bf4/ruff-0.16.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4d5f2ed10f8242d83fc08d521301089364e3375375705356f20c0e31606ef3ef", size = 10919503, upload-time = "2026-07-30T19:36:42.65Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ca/3d901ba6ad6fc38da39c3448fc6c59ac945679293a17c3ceb6d6c1cba13e/ruff-0.16.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a4665b309891f83f3e3c25447935f1213e9abbd4b5640af7a1f2def9f8d413c1", size = 10649861, upload-time = "2026-07-30T19:36:45.18Z" }, - { url = "https://files.pythonhosted.org/packages/92/79/894ef1ced26552d5f8c9cf6d85b0687840e1128c55aeab7b9c2d54a0d880/ruff-0.16.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26e9ca5c9bc3971f20d3cf18a957f52ffd6a5f6564ff15c4912a144dcac22494", size = 11148137, upload-time = "2026-07-30T19:36:47.936Z" }, - { url = "https://files.pythonhosted.org/packages/2d/69/3609a09fa1cb46cc28b762363e440a354204e5dff01bd0c8d7437874d6b9/ruff-0.16.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:67e1e1e3fa4f0c82f0e36d4cd61e661f6e7a6196cb1aa92fe0828fa7b8f257cd", size = 11559211, upload-time = "2026-07-30T19:36:50.448Z" }, - { url = "https://files.pythonhosted.org/packages/fc/8a/fb22af2fd78a736e241fabf67e30ce1799a64244026377a49e133af90762/ruff-0.16.1-py3-none-win32.whl", hash = "sha256:d31765e131295b8445caf301e3e8a85b34d1b9b211b4109b7ba457888b051806", size = 10838258, upload-time = "2026-07-30T19:36:53.298Z" }, - { url = "https://files.pythonhosted.org/packages/d4/35/e57fd9fb5d423961df087a00b12d42c0a830288dc2f3b45ecca299158b4f/ruff-0.16.1-py3-none-win_amd64.whl", hash = "sha256:09b05e8b90c2cb06ad63464350e7a45e8e44a2dfe52072ebfba6666ca8d3f596", size = 11961111, upload-time = "2026-07-30T19:36:56.107Z" }, - { url = "https://files.pythonhosted.org/packages/cb/46/240ea004bf6dc4feb40e9832f2205a476a47dd5b8a3f8211a5fc5f95e20e/ruff-0.16.1-py3-none-win_arm64.whl", hash = "sha256:dbaadaac38c70239f056d306b7476f246b0bf000fa6b3876402acbf5b227eaf8", size = 11309414, upload-time = "2026-07-30T19:36:58.79Z" }, +version = "0.16.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/b3/3213589383f8f1b3938781bd1278713f6d18621a14992b3e81fefb8a5ef9/ruff-0.16.3.tar.gz", hash = "sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2", size = 4891904, upload-time = "2026-08-13T15:17:13.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/96/493770daebd68c0a67f1549fdf519f53be51fc435186c0585bcc272fd76c/ruff-0.16.3-py3-none-linux_armv6l.whl", hash = "sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7", size = 10902799, upload-time = "2026-08-13T15:16:27.382Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/2becf3942fddc29a29b8df47691d456fb1085391a694f74d84513251418c/ruff-0.16.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081", size = 11135539, upload-time = "2026-08-13T15:16:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1e/4b8b72f0d006dbf19326aa99f9ca0ee2ff374187c4d301cf529a51aa06fe/ruff-0.16.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9", size = 10475095, upload-time = "2026-08-13T15:16:33.259Z" }, + { url = "https://files.pythonhosted.org/packages/92/32/2201fa49ba1f6c101ee321e83f051ac7a4b8d07b0ef6b4d3f2772b302275/ruff-0.16.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84", size = 10668771, upload-time = "2026-08-13T15:16:35.65Z" }, + { url = "https://files.pythonhosted.org/packages/c3/66/4afc5c8363bd04d45effce1b7c8713ca037d7a6740b7451a2403a6e3a972/ruff-0.16.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870", size = 10699568, upload-time = "2026-08-13T15:16:38.195Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/c67d246bf36bf1698551c56de39e95cd07f70e64433e0098e6267d77061b/ruff-0.16.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b", size = 11499365, upload-time = "2026-08-13T15:16:40.623Z" }, + { url = "https://files.pythonhosted.org/packages/67/0b/00ecbceb99a263af7b12f6f05ac3c92bc47b905e91adc3f207a836e3bc01/ruff-0.16.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413", size = 12311728, upload-time = "2026-08-13T15:16:43.564Z" }, + { url = "https://files.pythonhosted.org/packages/54/b2/b7b3bb54f4d3f7db504e476ad4ab8de530dceebe2c061384b2757ee419e8/ruff-0.16.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82", size = 11699896, upload-time = "2026-08-13T15:16:46.209Z" }, + { url = "https://files.pythonhosted.org/packages/c7/30/4c468429ac195addc5ee1b717b6ab1b66632786737ca3b2ed3443fb0c26a/ruff-0.16.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb", size = 11058736, upload-time = "2026-08-13T15:16:48.823Z" }, + { url = "https://files.pythonhosted.org/packages/43/67/7a113cdaddf24b64d7f75b1242a99d04c82fcef4f6921fdbb832beaffb5f/ruff-0.16.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474", size = 11586911, upload-time = "2026-08-13T15:16:51.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c1/2e66f24c0f3ead25a5e660111778685e505e5da353c82802bf49f0cbe7b9/ruff-0.16.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da", size = 10954265, upload-time = "2026-08-13T15:16:54.763Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ba/4cee23bf52cba9a058d3726de623624daf50ef9638868edd86f4126157f6/ruff-0.16.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50", size = 10709886, upload-time = "2026-08-13T15:16:57.339Z" }, + { url = "https://files.pythonhosted.org/packages/82/df/7da7194fa5d9dc0a285f7e6fa5a4722e7c63faac0b45b614ded9314363a1/ruff-0.16.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506", size = 11210392, upload-time = "2026-08-13T15:17:00.171Z" }, + { url = "https://files.pythonhosted.org/packages/35/85/7795f6e817af050e7517bf3e7aa9b061cce70ef33d280aad902c956c1ecf/ruff-0.16.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d", size = 11626910, upload-time = "2026-08-13T15:17:03.299Z" }, + { url = "https://files.pythonhosted.org/packages/78/9b/475b927cf27a5cbbda3c7bafb69ed6ff77e1d7923d5d85f17c2749d7ae32/ruff-0.16.3-py3-none-win32.whl", hash = "sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a", size = 10931415, upload-time = "2026-08-13T15:17:05.726Z" }, + { url = "https://files.pythonhosted.org/packages/b2/99/e2a2bfc4fbf0a1e8a916bc9ebe6fe6c58cc34c28e0ffc6ce281d572d1c2e/ruff-0.16.3-py3-none-win_amd64.whl", hash = "sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948", size = 11445993, upload-time = "2026-08-13T15:17:08.353Z" }, + { url = "https://files.pythonhosted.org/packages/69/3e/4132e539aed78c148854d4997a2685b0ed4dc4e87110b59ce528564e184e/ruff-0.16.3-py3-none-win_arm64.whl", hash = "sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a", size = 11399302, upload-time = "2026-08-13T15:17:10.908Z" }, ] [[package]] From 43406e7b746d3d175f50dddfd39c5ad37ba587a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 07:04:08 -0700 Subject: [PATCH 07/17] build(deps-dev): bump typing-inspection from 0.4.2 to 0.4.4 in /services/account_unification (#109) * build(deps-dev): bump typing-inspection in /services/account_unification Bumps [typing-inspection](https://github.com/pydantic/typing-inspection) from 0.4.2 to 0.4.4. - [Release notes](https://github.com/pydantic/typing-inspection/releases) - [Changelog](https://github.com/pydantic/typing-inspection/blob/main/HISTORY.md) - [Commits](https://github.com/pydantic/typing-inspection/compare/v0.4.2...v0.4.4) --- updated-dependencies: - dependency-name: typing-inspection dependency-version: 0.4.4 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * build(account-unification): resync uv lockfile * build(account-unification): regenerate exports for typing-inspection update --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Seongho Bae --- services/account_unification/requirements-dev.txt | 6 +++--- services/account_unification/requirements.lock | 6 +++--- services/account_unification/uv.lock | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/services/account_unification/requirements-dev.txt b/services/account_unification/requirements-dev.txt index 8189677..e071985 100644 --- a/services/account_unification/requirements-dev.txt +++ b/services/account_unification/requirements-dev.txt @@ -550,9 +550,9 @@ typing-extensions==4.16.0 \ # pydantic-core # starlette # typing-inspection -typing-inspection==0.4.2 \ - --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ - --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 +typing-inspection==0.4.4 \ + --hash=sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47 \ + --hash=sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147 # via # fastapi # pydantic diff --git a/services/account_unification/requirements.lock b/services/account_unification/requirements.lock index f3384fe..c826b76 100644 --- a/services/account_unification/requirements.lock +++ b/services/account_unification/requirements.lock @@ -358,9 +358,9 @@ typing-extensions==4.16.0 \ # pydantic-core # starlette # typing-inspection -typing-inspection==0.4.2 \ - --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ - --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 +typing-inspection==0.4.4 \ + --hash=sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47 \ + --hash=sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147 # via # fastapi # pydantic diff --git a/services/account_unification/uv.lock b/services/account_unification/uv.lock index 7e2f71d..09330e5 100644 --- a/services/account_unification/uv.lock +++ b/services/account_unification/uv.lock @@ -805,14 +805,14 @@ wheels = [ [[package]] name = "typing-inspection" -version = "0.4.2" +version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, ] [[package]] From e217b736c773f536eb3deaa611661c263a23a38b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 07:04:46 -0700 Subject: [PATCH 08/17] build(deps): bump github/codeql-action/analyze from 4.37.6 to 4.37.7 (#110) * build(deps): bump github/codeql-action/analyze from 4.37.6 to 4.37.7 Bumps [github/codeql-action/analyze](https://github.com/github/codeql-action) from 4.37.6 to 4.37.7. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/5595ccaf912efad79be6eef63a5619ff05969be3...ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd) --- updated-dependencies: - dependency-name: github/codeql-action/analyze dependency-version: 4.37.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * fix(codeql): keep init and analyze versions aligned --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Seongho Bae --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0a68285..2a4dcef 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -26,8 +26,8 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Initialize CodeQL - uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 + uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 + uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 From 4c7963b70c58894a99ed2ce62db31b5a31810251 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 07:05:21 -0700 Subject: [PATCH 09/17] build(deps): bump github/codeql-action/init from 4.37.6 to 4.37.7 (#111) * build(deps): bump github/codeql-action/init from 4.37.6 to 4.37.7 Bumps [github/codeql-action/init](https://github.com/github/codeql-action) from 4.37.6 to 4.37.7. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/5595ccaf912efad79be6eef63a5619ff05969be3...ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd) --- updated-dependencies: - dependency-name: github/codeql-action/init dependency-version: 4.37.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * fix(codeql): keep init and analyze versions aligned --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Seongho Bae From 036eb5f425c29296b0d5b9fb2666897417487983 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 23:09:48 +0900 Subject: [PATCH 10/17] docs(ci): refresh setup-uv cache comment --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9396458..e404c58 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: python-version: "3.12" - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: - # v9 changed this default to false; retain bounded cache usage. + # Keep cache pruning enabled to bound runner disk usage. prune-cache: true - name: Install locked dependencies run: uv sync --locked --extra dev From 31dd486cb97ca215da451151f618a954a07b0ea5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 23:14:02 +0900 Subject: [PATCH 11/17] test(ci): execute repository documentation contracts --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e404c58..6518166 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,9 @@ jobs: run: uv run interrogate . - name: Compile Python sources run: uv run python -m compileall -q app tests tools + - name: Test repository documentation contracts + working-directory: ${{ github.workspace }} + run: uv run --project services/account_unification pytest tests/test_documentation_contract.py -q - name: Test and enforce production coverage run: | uv run coverage run --branch --source=app -m pytest -q From 44c2adb18687f8df457bd4bafade551533cee5b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 09:24:24 -0700 Subject: [PATCH 12/17] =?UTF-8?q?docs:=20expand=20ADRs=200001=E2=80=930007?= =?UTF-8?q?=20and=20rewrite=20buyer=20README=20(#104)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: expand ADRs 0001-0007 and rewrite buyer README Rewrite README as a customer/operator page for standalone Keyverse and published OIDC/OAuth/SCIM contracts. Expand accepted ADRs 0001-0007 with Context, Decision, Consequences, and independently opened official APA 7th records in docs/REFERENCES.md. Leave ADR 0008 and reserved 0009-0012 untouched. Co-authored-by: Seongho Bae * build(account-unification): resync uv lockfile * docs: keep ADR expansion free of dependency changes * docs: correct official standards links * docs: keep ADR metadata diff-clean * docs: add product technical gap baseline * docs: bind baseline to successor head --------- Co-authored-by: Cursor Agent Co-authored-by: Seongho Bae --- CHANGELOG.md | 8 + DOCUMENTATION.md | 3 +- README.md | 243 ++++++++++-------- docs/REFERENCES.md | 120 +++++++++ docs/TRACEABILITY.md | 1 + docs/adr/0001-keycloak-hub.md | 95 ++++++- docs/adr/0002-passwordless-local-accounts.md | 73 +++++- docs/adr/0003-identity-matching.md | 77 +++++- docs/adr/0004-desired-state-reconciliation.md | 92 ++++++- docs/adr/0005-secret-ownership.md | 82 +++++- docs/adr/0006-user-operation-lock.md | 59 ++++- docs/adr/0007-automation-authority.md | 57 +++- docs/adr/README.md | 5 + .../product-technical-gap-baseline.md | 70 +++++ docs/product-technical-gap-baseline.md | 166 ++++++++++++ tests/test_documentation_contract.py | 2 + 16 files changed, 1026 insertions(+), 127 deletions(-) create mode 100644 docs/REFERENCES.md create mode 100644 docs/doctoring/product-technical-gap-baseline.md create mode 100644 docs/product-technical-gap-baseline.md diff --git a/CHANGELOG.md b/CHANGELOG.md index db2e3ab..23819a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,10 +55,18 @@ Keep a Changelog, and releases use semantic versioning. ### Changed +- Buyer README and accepted ADRs 0001–0007 now describe Keyverse as a + standalone identity leaf/hub, point operators at published OIDC/OAuth + 2.0, SAML, LDAP, and SCIM contracts, and cite independently opened + official records in `docs/REFERENCES.md`. OAuth 2.1 is labeled an IETF + Internet-Draft, not a final RFC. - Updated the design-only MCP authorization contract to MCP Authorization 2026-07-28, RFC 9207 callback-issuer validation, and RFC 9068 JWT access-token header, claim, signature, and algorithm rejection evidence; runtime acceptance remains unimplemented. +- Added the product/technical gap baseline and its APA 7th doctoring record, + including the current exact-head PR/Issue inventory and explicit + `gap-not-claimed` runtime and release boundaries. - Relying-party deployment controllers now send validated, secret-free metadata to Keyverse desired-state PUT instead of applying client representations directly to Keycloak; confidential credential placement remains a separate diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 6f7f97a..150342c 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -14,6 +14,7 @@ Keyverse already has strong feature-specific specifications, doctoring, federati | Test strategy | [`docs/TEST_STRATEGY.md`](docs/TEST_STRATEGY.md) | | Operability/recovery/release | [`docs/OPERABILITY.md`](docs/OPERABILITY.md) | | Requirements/evidence traceability | [`docs/TRACEABILITY.md`](docs/TRACEABILITY.md) | +| Product and technical gap baseline | [`docs/product-technical-gap-baseline.md`](docs/product-technical-gap-baseline.md) and [`docs/doctoring/product-technical-gap-baseline.md`](docs/doctoring/product-technical-gap-baseline.md) | | Architecture decisions | [`docs/adr/README.md`](docs/adr/README.md) | | Federation onboarding | [`docs/federation-onboarding.md`](docs/federation-onboarding.md) | | RP onboarding | [`docs/rp-onboarding.md`](docs/rp-onboarding.md) | @@ -34,4 +35,4 @@ Keyverse already has strong feature-specific specifications, doctoring, federati - **external-system** — Keycloak/ADFS/LDAP/external OIDC/HR/IGA behavior not implemented by Keyverse itself. - **planned** — accepted target without executable implementation. -Open PR #72 OIDC RP claim mapper profile and PR #74 hourly GitHub API remediation remain active-PR until merged. Keyverse's current protected-main desired-state/reconciliation capabilities are documented independently from those changes. \ No newline at end of file +Open PR #72 OIDC RP claim mapper profile and PR #74 hourly GitHub API remediation remain active-PR until merged. Keyverse's current protected-main desired-state/reconciliation capabilities are documented independently from those changes. diff --git a/README.md b/README.md index 5e24601..2c022e7 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,72 @@ -# cwl-idp — ecosystem central IdP - -The **ContextualWisdom ecosystem's central Identity Provider**, a standalone -component built on [**Keycloak**](https://www.keycloak.org) (Apache-2.0). It: - -- issues **OIDC / OAuth 2.1** to ecosystem relying parties (`naruon`, - `pg-erd-cloud`, `semantic-data-portal`, `clearfolio`, `contextual-orchestrator`, - and `newsdom-api` through the WAF edge); -- is **passwordless-first**: FIDO2 / passkeys are the default and the **password - authenticator is removed** from the login flow for ecosystem-local accounts; -- runs a **SCIM v2 server shim** for inbound provisioning into Keycloak; +# Keyverse (cwl-idp) + +Keyverse is the ContextualWisdomLab **identity leaf and hub**. It is the system of +record for **who a person is in this ecosystem**: local passwordless accounts, +inbound federation, inbound SCIM provisioning, and outbound OpenID Connect +tokens that relying parties consume. + +It is **not** the employment or org-tree system of record. Orgmetra owns +employment and organizational-tree truth. Keyverse does not copy Orgmetra +tables. Composition hubs such as **naruon** and **gyeot** may call this leaf; +they are not required to boot it. + +Keyverse must run **from this repository alone** (Compose or Helm in this repo) +and remain **callable** by relying parties over published OIDC/OAuth, SAML +broker, LDAP/AD user-storage, and SCIM contracts. + +## What this IdP does + +Built on [Keycloak](https://www.keycloak.org) (Apache-2.0) plus a Keyverse +account-unification admin service, the product: + +- issues **OpenID Connect** on **OAuth 2.0** to ecosystem relying parties + (`naruon`, `pg-erd-cloud`, `semantic-data-portal`, `clearfolio`, + `contextual-orchestrator`, and `newsdom-api` through the WAF edge); +- is **passwordless-first**: FIDO2 / passkeys are the default, and the + **password authenticator is removed** from the bound browser flow for + ecosystem-local accounts; +- runs a **SCIM 2.0 server shim** for inbound provisioning into Keycloak; - **federates external IdPs in** — employer ADFS via SAML, corporate LDAP/AD, - and optional personal OIDC — while keeping unverified email ineligible for - account linking; and -- adds an **account-unification** admin service to link one human's many external - identities and to **merge** two pre-existing accounts into one. + and optional personal OIDC — as **deployment data**, never as portable realm + code; and +- links one human's many external identities and **merges** two pre-existing + accounts into one survivor, never on an unverified email. + +> Employer ADFS and corporate directories are **external compatibility +> targets**, not peer hubs. Customer-specific federation stays in the +> deployment controller and KV store. -> Employer ADFS and corporate directories are **external, proprietary** -> compatibility targets—not the hub. cwl-idp is the hub, and customer-specific -> federation remains deployment data rather than portable realm code. +OAuth 2.0 ([RFC 6749](https://www.rfc-editor.org/rfc/rfc6749)) is the official +authorization-framework record. [OAuth 2.1](https://datatracker.ietf.org/doc/draft-ietf-oauth-v2-1/) +is an IETF Internet-Draft (`draft-ietf-oauth-v2-1-15`, work in progress) and +is not cited here as a final RFC. -RP client registrations and secrets live in the **IdP DB / KV**, never in an RP's -environment. +RP client registrations and confidential values live in the **IdP DB / KV**, +never in an RP's environment. Authorized identity data stays usable under +purpose-bound access control, encryption, and audit. ## Architecture ```text -external IdPs ──► cwl-idp (Keycloak) ──► OIDC to ecosystem RPs - ADFS (SAML) passwordless OIDC/OAuth +external IdPs ──► Keyverse (Keycloak + admin service) ──► OIDC to RPs + ADFS (SAML) passwordless OIDC / OAuth 2.0 LDAP/AD FIDO2 passkeys - OIDC (opt) SCIM v2 shim (inbound) + OIDC (opt) SCIM 2.0 shim (inbound) HR/IGA (SCIM) account-unification admin service -``` - -Architecture and trust boundaries: [`ARCHITECTURE.md`](ARCHITECTURE.md). Full -network diagram: [`docs/topology.md`](docs/topology.md). -## Repository layout +composition hubs (naruon, gyeot) MAY call this leaf +Orgmetra owns employment / org-tree truth (not copied here) +``` -| Path | What | -| --- | --- | -| `docker-compose.yml` | Standalone bring-up: Keycloak + Postgres + admin service (pinned by digest) | -| `deploy/keycloak/` | Portable Keycloak realm config-as-code, passwordless flows, shared scopes, concrete Naruon RP, and service-account bootstrap | -| `deploy/templates/` | Private deployment templates split between Keyverse preflight/desired state and explicit Keycloak Admin REST apply contracts | -| `deploy/bootstrap/` | Bootstrap pointer to the KV/DB config store | -| `deploy/scripts/healthz.sh` | Cross-component readiness probe | -| `scripts/validate_realm.py` | Realm config-as-code validator (CI gate) | -| `services/account_unification/` | FastAPI admin service (link + merge + SCIM + federation validation/desired state) with unit tests | -| `helm/cwl-idp/` | Helm chart (templated Keycloak + Postgres + admin service) | -| `docs/operations/` | Scheduled maintenance and product-development operating procedures | -| `docs/doctoring/` | Standards interpretation and APA 7th engineering traceability | -| `docs/` | Topology, passwordless policy, federation, merge flow, RP onboarding, and papers | +Trust boundaries: [`ARCHITECTURE.md`](ARCHITECTURE.md). Network diagram: +[`docs/topology.md`](docs/topology.md). Architecture decisions: +[`docs/adr/`](docs/adr/README.md). Standards bibliography: +[`docs/REFERENCES.md`](docs/REFERENCES.md). -## Quick start (standalone) +## Run this repository alone -Requires Docker or Podman with the compose plugin. +No sibling repository checkout is required. Docker or Podman with the compose +plugin is enough: ```bash cp .env.example .env # populate values from your KV (bootstrap transport) @@ -71,93 +84,99 @@ The stack imports the **passwordless-first** realm at first start WebAuthn passwordless authenticator and **no password authenticator**, plus `registrationAllowed:false` / `resetPasswordAllowed:false`. -### Register external federation +Production-shaped clusters use [`helm/cwl-idp/`](helm/cwl-idp/). -The portable realm contains no employer ADFS, LDAP/AD source, or other -customer-specific federation. Render deployment values from KV and preflight -every private payload before apply: +### Optional parent include -- SAML and external OIDC: - `POST /federation/identity-providers:validate`, followed by the Keyverse - desired-state `PUT` and reconciliation flow. -- LDAP and Active Directory: - `POST /federation/user-directories:validate`, followed by deployment-owned - private Keycloak component apply. The first profile is LDAPS-only, - read-only, Kerberos-disabled, and `trustEmail=false`. +A parent Compose or Helm chart **may** include this repo's +`docker-compose.yml` or depend on `helm/cwl-idp`. That is an optional +embed of **this** repository. Keyverse does not require naruon, gyeot, +Orgmetra, or any other sibling checkout in order to start. -LDAP preflight performs no DNS lookup, socket connection, bind, search, KV/DB -write, or Keycloak call. Its response redacts `bindDn` and `bindCredential` and -must never be used as the apply payload; apply the original private file only. +## How a relying party calls Keyverse -See [`docs/federation-onboarding.md`](docs/federation-onboarding.md), -[`deploy/keycloak/README.md`](deploy/keycloak/README.md), and -[`deploy/templates/README.md`](deploy/templates/README.md). +Each RP is a separate trust boundary. A README listing, repository +relationship, or client ID is not authorization. The RP validates issuer, +signature, audience, subject, and expiry, then applies its own +access-control policy ([ADR-0008](docs/adr/0008-keyverse-rp-authorization-boundary.md)). -### Onboard a relying party +Published operator contracts that already ship: -See [`docs/rp-onboarding.md`](docs/rp-onboarding.md). +| Contract | Purpose | +| --- | --- | +| Keycloak OIDC endpoints on the WAF edge | Authorization, token, JWKS, and logout for registered RPs | +| `POST /clients/relying-parties:validate` | Side-effect-free RP client preflight | +| `PUT /clients/relying-parties/{client_id}` | Secret-free RP desired state and reconcile | +| `POST /federation/identity-providers:validate` | Side-effect-free SAML/OIDC IdP preflight | +| `PUT /federation/identity-providers/{alias}` | Persist and converge an external IdP | +| `POST /federation/user-directories:validate` | Side-effect-free LDAP/AD preflight (no DNS, socket, bind, search, store, or Keycloak call) | +| `PUT /federation/user-directories/{name}` | Persist and converge a directory component | +| `/scim/v2/Users` | Inbound SCIM 2.0 user lifecycle | +| `POST /registration/accounts` | Password-free account create plus enrollment email | +| `GET /users/{user_id}`, `POST /merges` | Inspect and merge accounts | + +Confidential client secrets are placed by the deployment controller, not +returned in ordinary Keyverse responses. See +[`docs/rp-onboarding.md`](docs/rp-onboarding.md). -## Account unification & merge +### Register external federation -```bash -cd services/account_unification -python -m venv .venv && . .venv/bin/activate -pip install -e '.[dev]' -pytest -q -``` +The portable realm contains no employer ADFS, LDAP/AD source, or other +customer-specific federation. Render deployment values from KV and preflight +every private payload before apply. -Design: [`docs/merge-unification-flow.md`](docs/merge-unification-flow.md). -Matching precedence is **exact (idp, subject) → verified email → explicit link**, -and the engine **never merges on an unverified email**. +LDAP preflight redacts `bindDn` and `bindCredential` and must never be used +as the apply payload; apply the original private file only. The first +directory profile is LDAPS-only, read-only, Kerberos-disabled, and +`trustEmail=false`. -## Standalone AND submodule-embeddable +See [`docs/federation-onboarding.md`](docs/federation-onboarding.md) and +[`docs/ldap-directory-onboarding.md`](docs/ldap-directory-onboarding.md). -- **Standalone:** the compose file or the Helm chart. -- **Submodule:** add this repo as a git submodule and `include:` its - `docker-compose.yml`, or depend on `helm/cwl-idp`. Every component exposes a - `/healthz`-style readiness probe so the parent can gate on it. +## Account unification -## Configuration & secrets +Matching precedence is **exact `(identity_provider, subject)` → verified +email → explicit operator link**. The engine **never merges on an unverified +email**. Merged duplicates remain disabled tombstones with survivor lineage. +Design: [`docs/merge-unification-flow.md`](docs/merge-unification-flow.md). + +## Configuration and secrets Config and secrets are read from the **KV / DB store**, not from runtime -`os.getenv`. Environment variables are used **only as bootstrap transport** to -reach that store (`CWL_IDP_BOOTSTRAP` → `deploy/bootstrap/bootstrap.yaml`). -Database objects use two-word snake_case names (`idp_config_entries`, -`account_merge_audit`). +`os.getenv`. Environment variables are **bootstrap transport** only +(`CWL_IDP_BOOTSTRAP` → `deploy/bootstrap/bootstrap.yaml`). Database objects +use two-word-or-longer snake_case names (`idp_config_entries`, +`account_merge_audit`, `user_operation_lock_state`). -## Engine & licensing +## Engine and licensing - Engine: **Keycloak** (Apache-2.0). This repo: **Apache-2.0** (`LICENSE`). -- **Permissive OSS only** — no GPL/AGPL dependencies. cwl-idp deliberately does - **not** use ZITADEL (AGPL-3.0) nor the commercial scim-for-keycloak plugin; - the SCIM shim in this repo is our own Apache-2.0 code. - -## References - -Standards and papers live under `docs/papers/` and `docs/doctoring/`, including -NIST SP 800-63C federation, RFC 7644 SCIM, OIDC Core, SAML V2.0, and the LDAP -RFC 4511–4515 family. - ---- +- **Permissive OSS only** — no GPL/AGPL dependencies. The SCIM shim is + Apache-2.0 code in this repository. -🤖 Generated with [Claude Code](https://claude.com/claude-code) +## Where decisions and standards live -## Hourly OpenCode product development - -At minute 41 UTC, and only when no pull request exists and the exact `main` SHA -is healthy, Keyverse may run one bounded OpenCode development cycle through a -loopback NVIDIA NIM credential broker. The model works from a disposable -`git archive` without `.git`, GitHub credentials, Actions OIDC, publication -authority, or the upstream NIM credential. +| Path | What | +| --- | --- | +| [`docs/adr/`](docs/adr/README.md) | Accepted architecture decisions (0001–0008 on this branch) | +| [`docs/REFERENCES.md`](docs/REFERENCES.md) | APA 7th bibliography for ADR 0001–0007 | +| [`docs/doctoring/`](docs/doctoring/) | Feature-specific standards interpretation | +| [`docs/product-technical-gap-baseline.md`](docs/product-technical-gap-baseline.md) | Current buyer-visible product and technical gap register | +| [`docs/papers/`](docs/papers/README.md) | Offline copies of selected primary sources | +| [`docs/operations/`](docs/operations/) | Operator runbooks, including hourly product development | +| [`ARCHITECTURE.md`](ARCHITECTURE.md) | Runtime topology and trust boundaries | +| [`docs/rp-onboarding.md`](docs/rp-onboarding.md) | RP onboarding | +| [`docs/passwordless-policy.md`](docs/passwordless-policy.md) | Passwordless realm invariants | -A fresh job independently validates the sealed patch and re-runs the complete -100% production docstring, statement, and branch coverage gates plus package, -realm, Compose, and provider-template checks. Only then may a dedicated -`OPENCODE_PRODUCT_DEVELOPMENT_TOKEN` create one draft PR. Existing review-agent -workflows and credentials are unchanged; the development workflow cannot -approve, merge, tag, or release. +## Repository layout -Operations are documented in -[`docs/operations/hourly-product-development.md`](docs/operations/hourly-product-development.md). -Standards traceability and APA 7th references are recorded in -[`docs/doctoring/hourly-opencode-product-development.md`](docs/doctoring/hourly-opencode-product-development.md). +| Path | What | +| --- | --- | +| `docker-compose.yml` | Standalone bring-up: Keycloak + Postgres + admin service (pinned by digest) | +| `deploy/keycloak/` | Portable Keycloak realm config-as-code and service-account bootstrap | +| `deploy/templates/` | Private deployment templates for preflight and desired state | +| `deploy/bootstrap/` | Bootstrap pointer to the KV/DB config store | +| `deploy/scripts/healthz.sh` | Cross-component readiness probe | +| `scripts/validate_realm.py` | Realm config-as-code validator | +| `services/account_unification/` | FastAPI admin service (link, merge, SCIM, federation, RP desired state) | +| `helm/cwl-idp/` | Helm chart for the same three components | diff --git a/docs/REFERENCES.md b/docs/REFERENCES.md new file mode 100644 index 0000000..778725d --- /dev/null +++ b/docs/REFERENCES.md @@ -0,0 +1,120 @@ +# References for ADR 0001–0007 + +This file is the APA 7th bibliography for the expansions of the accepted +architecture decisions [`0001`](adr/0001-keycloak-hub.md)–[`0007`](adr/0007-automation-authority.md). +Every entry was opened on an official catalog (RFC Editor, OpenID Foundation, +W3C TR, OASIS, NIST CSRC / nvlpubs) before citation. Feature-specific +doctoring records under [`docs/doctoring/`](doctoring/) keep their own +bibliographies; do not treat this file as a rewrite of those records. + +Internet-Drafts and W3C Candidate Recommendations are labeled as such and +are not treated as final RFCs or Recommendations. + +## Official records + +Cantor, S., Kemp, J., Philpott, R., & Maler, E. (Eds.). (2005, March 15). +*Assertions and protocols for the OASIS Security Assertion Markup Language +(SAML) V2.0* (OASIS Standard, document identifier saml-core-2.0-os). +Organization for the Advancement of Structured Information Standards. +https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf + +Denniss, W., & Bradley, J. (2017). *OAuth 2.0 for native apps* (BCP 212, +RFC 8252). Internet Engineering Task Force. +https://doi.org/10.17487/RFC8252 + +Hardt, D. (Ed.). (2012). *The OAuth 2.0 authorization framework* (RFC 6749). +Internet Engineering Task Force. https://doi.org/10.17487/RFC6749 + +Harrison, R. (Ed.). (2006). *Lightweight Directory Access Protocol (LDAP): +Authentication methods and security mechanisms* (RFC 4513). Internet +Engineering Task Force. https://doi.org/10.17487/RFC4513 + +Hodges, J., Jones, J. C., Jones, M. B., Kumar, A., & Lundberg, E. (Eds.). +(2021, April 8). *Web Authentication: An API for accessing Public Key +Credentials Level 2* (W3C Recommendation). World Wide Web Consortium. +https://www.w3.org/TR/2021/REC-webauthn-2-20210408/ + +Hunt, P., Grizzle, K., Ansari, M., Wahlstroem, E., & Mortimore, C. (2015). +*System for Cross-domain Identity Management: Protocol* (RFC 7644). +Internet Engineering Task Force. https://doi.org/10.17487/RFC7644 + +Hunt, P., Grizzle, K., Wahlstroem, E., & Mortimore, C. (2015). *System for +Cross-domain Identity Management: Core schema* (RFC 7643). Internet +Engineering Task Force. https://doi.org/10.17487/RFC7643 + +Lodderstedt, T., Bradley, J., Labunets, A., & Fett, D. (2025). *Best current +practice for OAuth 2.0 security* (BCP 240, RFC 9700). Internet Engineering +Task Force. https://doi.org/10.17487/RFC9700 + +OASIS Open. (2005, March 1). *Security Assertion Markup Language (SAML) +v2.0* [Standards catalog entry]. +https://www.oasis-open.org/standard/saml/ + +Sakimura, N., Bradley, J., & Agarwal, N. (2015). *Proof key for code +exchange by OAuth public clients* (RFC 7636). Internet Engineering Task +Force. https://doi.org/10.17487/RFC7636 + +Sakimura, N., Bradley, J., Jones, M., de Medeiros, B., & Mortimore, C. +(2023, December 15). *OpenID Connect Core 1.0 incorporating errata set 2*. +OpenID Foundation. https://openid.net/specs/openid-connect-core-1_0.html + +Sermersheim, J. (Ed.). (2006). *Lightweight Directory Access Protocol +(LDAP): The protocol* (RFC 4511). Internet Engineering Task Force. +https://doi.org/10.17487/RFC4511 + +Souppaya, M., Scarfone, K., & Dodson, D. (2022). *Secure Software +Development Framework (SSDF) version 1.1: Recommendations for mitigating +the risk of software vulnerabilities* (NIST Special Publication 800-218). +National Institute of Standards and Technology. +https://doi.org/10.6028/NIST.SP.800-218 + +Temoshok, D., Fenton, J. L., Choong, Y.-Y., Lefkovitz, N., Regenscheid, A., +Galluzzo, R., & Richer, J. P. (2025). *Digital identity guidelines: +Authentication and authenticator management* (NIST Special Publication +800-63B-4). National Institute of Standards and Technology. +https://doi.org/10.6028/NIST.SP.800-63B-4 + +Temoshok, D., Richer, J. P., Choong, Y.-Y., Fenton, J. L., Lefkovitz, N., +Regenscheid, A., & Galluzzo, R. (2025). *Digital identity guidelines: +Federation and assertions* (NIST Special Publication 800-63C-4). National +Institute of Standards and Technology. +https://doi.org/10.6028/NIST.SP.800-63C-4 + +Zeilenga, K. (Ed.). (2006). *Lightweight Directory Access Protocol (LDAP): +String representation of distinguished names* (RFC 4514). Internet +Engineering Task Force. https://doi.org/10.17487/RFC4514 + +## Vendor documentation (engine) + +Keycloak. (n.d.). *Server administration guide* (Version 26.7.1). Retrieved +August 18, 2026, from https://www.keycloak.org/docs/latest/server_admin/ + +## Work in progress (not final) + +Hardt, D., Parecki, A., & Lodderstedt, T. (2026, March 2). *The OAuth 2.1 +authorization framework* (Internet-Draft draft-ietf-oauth-v2-1-15). +Internet Engineering Task Force. +https://datatracker.ietf.org/doc/draft-ietf-oauth-v2-1/ + +World Wide Web Consortium. (2026, May 26). *Web Authentication: An API for +accessing Public Key Credentials Level 3* (W3C Candidate Recommendation +Snapshot). https://www.w3.org/TR/webauthn-3/ + +OAuth 2.1 remains an IETF Working Group Internet-Draft (IESG state: +I-D Exists; intended RFC status unset). WebAuthn Level 3 is a Candidate +Recommendation Snapshot, not a W3C Recommendation. Neither is cited as a +normative final record in ADR 0001–0007. + +## Catalog notes + +NIST SP 800-63B and SP 800-63C (June 2017; updated 2 March 2020) were +withdrawn on 1 August 2025 and superseded by SP 800-63B-4 and SP 800-63C-4 +(Final 31 July 2025). This bibliography cites the current finals opened at +https://csrc.nist.gov/pubs/sp/800/63/B/4/final, +https://csrc.nist.gov/pubs/sp/800/63/C/4/final, +https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-63B-4.pdf, +and +https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-63C-4.pdf + +Offline attachments under [`docs/papers/`](papers/README.md) may still +hold the withdrawn 2017 texts; ADR 0001–0007 use the 2025 finals. diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 77d1dee..c337485 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,6 +28,7 @@ | contextual-orchestrator Keyverse identity and tenant boundary | ADR-0008; deployment-owned OIDC validation plus downstream scope RBAC and org/workspace ABAC | PR #109 `32ba3a9` adds the injected verifier seam; stacked PR #110 `8607eba` requires `VerifiedIdentity`, rejects boolean-only authorization, binds workflow/evaluation/batch resources to secret-free tenant context, and denies cross-tenant or ownerless reads; main remains unchanged until both protected PRs merge | active-PR | | sidecar anonymous-access boundary | ADR-0008; private service-boundary and least-privilege policy | newsdom-api protected `develop` `3d0426b` (PR #595) fail-closed token gate, startup credential registry, explicit anonymous opt-in, review-fixed authenticated examples/healthcheck/401 contract, and pypdf Trivy remediation; Keyverse-aware gateway evidence remains required for exposure | implemented-main | | 100% production statement/branch/docstring | CWL quality contract | CI/pytest/interrogate | implemented-main | +| product and technical gap baseline | PRD/TRD/ADR/operability and live exact-head evidence | [`docs/product-technical-gap-baseline.md`](product-technical-gap-baseline.md) plus [`docs/doctoring/product-technical-gap-baseline.md`](doctoring/product-technical-gap-baseline.md) | active-PR | ## Research, standards, and operations records diff --git a/docs/adr/0001-keycloak-hub.md b/docs/adr/0001-keycloak-hub.md index 1dd0af9..cf35b72 100644 --- a/docs/adr/0001-keycloak-hub.md +++ b/docs/adr/0001-keycloak-hub.md @@ -2,5 +2,98 @@ **Status:** Accepted **Date:** 2026-08-09 +**Last expanded:** 2026-08-18 -Keyverse uses Keycloak as the standards-based identity engine and adds CWL-owned control services around it. Employer/customer ADFS, LDAP/AD, external OIDC, and HR/IGA are federation/provisioning sources rather than peer hubs. CWL relying parties trust the Keyverse/Keycloak boundary instead of administering those external systems directly. Customer-specific federation remains deployment data, not portable realm code. \ No newline at end of file +## Context + +ContextualWisdomLab products need one identity leaf that can run on its own +and still be called by relying parties. OpenID Connect Core defines an +OpenID Provider as an OAuth 2.0 authorization server that authenticates the +end-user and issues claims to a relying party (Sakimura et al., 2023). +OAuth 2.0 is the official authorization-framework record (Hardt, 2012). +SAML 2.0 defines assertions and protocols that a service provider uses to +accept authentication from an external asserting party (Cantor et al., +2005; OASIS Open, 2005). LDAP is a directory-access protocol, not an +identity hub (Sermersheim, 2006). SCIM 2.0 is an HTTP protocol for +cross-domain user lifecycle (Hunt, Grizzle, Ansari, et al., 2015). + +NIST SP 800-63C-4 describes federation as one credential service provider +supplying authentication attributes to separately administered relying +parties, and those relying parties using one or more providers (Temoshok, +Richer, et al., 2025). That model fits a single Keyverse issuer with +external employer ADFS, LDAP/AD, optional personal OIDC, and HR/IGA SCIM +as inbound sources. + +Keycloak's published administration guide documents OpenID Connect, OAuth +2.0, SAML, identity brokering, and LDAP/Active Directory user federation +in one Apache-2.0 engine (Keycloak, n.d.). Building the portable `cwl` +realm and Keyverse control plane on that engine avoids a second protocol +stack and keeps customer federation out of committed realm JSON. + +Composition hubs such as naruon and gyeot may call this leaf. Orgmetra +owns employment and org-tree truth; Keyverse does not copy those tables. + +## Decision + +Keyverse uses Keycloak as the standards-based identity engine and adds +CWL-owned control services around it. Employer/customer ADFS, LDAP/AD, +external OIDC, and HR/IGA are federation or provisioning sources rather +than peer hubs. CWL relying parties trust the Keyverse/Keycloak boundary +instead of administering those external systems directly. +Customer-specific federation remains deployment data, not portable realm +code. + +This repository must boot from its own Compose or Helm artifacts. Optional +parent include of this repo's Compose or Helm chart is allowed. A Keyverse +checkout must not require naruon, gyeot, Orgmetra, or any other sibling +repository. + +## Consequences + +- Ecosystem RPs implement OIDC client behavior against one issuer rather + than each administering ADFS, LDAP, or SCIM themselves. +- External IdP metadata, bind credentials, and RP client secrets stay in + the deployment KV/controller, so the portable realm stays reusable. +- Protocol coverage is bounded by what Keycloak already executes: OIDC and + OAuth 2.0 outbound, SAML and OIDC brokering inbound, LDAP/AD user + storage, plus the Keyverse SCIM shim and account-unification API. +- OAuth 2.1 is not treated as a final RFC; see + [`docs/REFERENCES.md`](../REFERENCES.md) for the current Internet-Draft + label. +- Authorization after a verified token remains an RP obligation + ([ADR-0008](0008-keyverse-rp-authorization-boundary.md)). + +## References + +See [`docs/REFERENCES.md`](../REFERENCES.md) for the full APA 7th entries +and official URLs/DOIs opened for this expansion. + +Cantor, S., Kemp, J., Philpott, R., & Maler, E. (Eds.). (2005). +*Assertions and protocols for the OASIS Security Assertion Markup Language +(SAML) V2.0*. OASIS. +https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf + +Hardt, D. (Ed.). (2012). *The OAuth 2.0 authorization framework* (RFC 6749). +https://doi.org/10.17487/RFC6749 + +Hunt, P., Grizzle, K., Ansari, M., Wahlstroem, E., & Mortimore, C. (2015). +*System for Cross-domain Identity Management: Protocol* (RFC 7644). +https://doi.org/10.17487/RFC7644 + +Keycloak. (n.d.). *Server administration guide* (Version 26.7.1). +https://www.keycloak.org/docs/latest/server_admin/ + +OASIS Open. (2005). *Security Assertion Markup Language (SAML) v2.0*. +https://www.oasis-open.org/standard/saml/ + +Sakimura, N., Bradley, J., Jones, M., de Medeiros, B., & Mortimore, C. +(2023). *OpenID Connect Core 1.0 incorporating errata set 2*. +https://openid.net/specs/openid-connect-core-1_0.html + +Sermersheim, J. (Ed.). (2006). *Lightweight Directory Access Protocol +(LDAP): The protocol* (RFC 4511). https://doi.org/10.17487/RFC4511 + +Temoshok, D., Richer, J. P., Choong, Y.-Y., Fenton, J. L., Lefkovitz, N., +Regenscheid, A., & Galluzzo, R. (2025). *Digital identity guidelines: +Federation and assertions* (NIST SP 800-63C-4). +https://doi.org/10.6028/NIST.SP.800-63C-4 diff --git a/docs/adr/0002-passwordless-local-accounts.md b/docs/adr/0002-passwordless-local-accounts.md index 0ff62ca..8974b03 100644 --- a/docs/adr/0002-passwordless-local-accounts.md +++ b/docs/adr/0002-passwordless-local-accounts.md @@ -2,5 +2,76 @@ **Status:** Accepted **Date:** 2026-08-09 +**Last expanded:** 2026-08-18 -The portable local browser flow uses WebAuthn/passkeys and does not include an ordinary password authenticator. Registration creates no password and uses a controlled enrollment action. External federation may rely on its upstream authentication policy, but Keyverse does not silently add a local password fallback for ecosystem-local accounts. Changing this boundary requires explicit security/product review and migration evidence. \ No newline at end of file +## Context + +Local Keyverse accounts are created and used inside the `cwl` realm, not at +an employer IdP. A reusable password in that flow would be phishable, +resettable, and leakable independently of any federated authenticator. + +W3C Web Authentication Level 2 is a Recommendation. It defines +origin-scoped public-key credentials for registration and authentication +ceremonies, with authenticators providing cryptographic proof of user +presence and consent (Hodges et al., 2021). WebAuthn Level 3 was opened as +a Candidate Recommendation Snapshot (26 May 2026) and is not treated as a +Recommendation here. + +NIST SP 800-63B-4 sets authenticator-assurance requirements for remote +authentication and covers phishing-resistant authenticators, including +syncable passkeys, as the current Digital Identity Guidelines +authentication volume (Temoshok, Fenton, et al., 2025). Those guidelines +are written for U.S. federal systems and are used here as authoritative +authenticator-management evidence, not as a claim that Keyverse is a +federal CSP. + +OpenID Connect Core treats authentication context as information an RP may +require before an entitlement decision; it does not require a password +authenticator at the OpenID Provider (Sakimura et al., 2023). Keycloak +already implements a WebAuthn passwordless authenticator that the portable +realm can bind as the only credential execution in the browser flow +(Keycloak, n.d.). + +## Decision + +The portable local browser flow uses WebAuthn/passkeys and does not include +an ordinary password authenticator. Registration creates no password and +uses a controlled enrollment action. External federation may rely on its +upstream authentication policy, but Keyverse does not silently add a local +password fallback for ecosystem-local accounts. Changing this boundary +requires explicit security/product review and migration evidence. + +## Consequences + +- The bound `browser-passwordless` flow and realm validator must keep + rejecting `auth-password-form` and related password authenticators. +- Headless registration sends `VERIFY_EMAIL` plus + `webauthn-register-passwordless` and rolls back the account if + enrollment initialization fails. +- Self-service password reset stays off (`resetPasswordAllowed:false`). +- Federated users authenticate at their upstream IdP; this decision does + not rewrite that upstream policy. +- A later move to WebAuthn Level 3 features needs a separate review after + that document becomes a Recommendation. + +## References + +See [`docs/REFERENCES.md`](../REFERENCES.md) for the full APA 7th entries +and official URLs/DOIs opened for this expansion. + +Hodges, J., Jones, J. C., Jones, M. B., Kumar, A., & Lundberg, E. (Eds.). +(2021, April 8). *Web Authentication: An API for accessing Public Key +Credentials Level 2* (W3C Recommendation). +https://www.w3.org/TR/2021/REC-webauthn-2-20210408/ + +Keycloak. (n.d.). *Server administration guide* (Version 26.7.1). +https://www.keycloak.org/docs/latest/server_admin/ + +Sakimura, N., Bradley, J., Jones, M., de Medeiros, B., & Mortimore, C. +(2023). *OpenID Connect Core 1.0 incorporating errata set 2*. +https://openid.net/specs/openid-connect-core-1_0.html + +Temoshok, D., Fenton, J. L., Choong, Y.-Y., Lefkovitz, N., Regenscheid, A., +Galluzzo, R., & Richer, J. P. (2025). *Digital identity guidelines: +Authentication and authenticator management* (NIST SP 800-63B-4). +https://doi.org/10.6028/NIST.SP.800-63B-4 diff --git a/docs/adr/0003-identity-matching.md b/docs/adr/0003-identity-matching.md index 4045434..94c7d9a 100644 --- a/docs/adr/0003-identity-matching.md +++ b/docs/adr/0003-identity-matching.md @@ -2,5 +2,80 @@ **Status:** Accepted **Date:** 2026-08-09 +**Last expanded:** 2026-08-18 -Account matching precedence is exact `(identity_provider, subject)`, then verified email under policy, then explicit operator link. Unverified email never authorizes automatic linking or merge. Merged duplicate accounts remain disabled tombstones with survivor lineage. This decision is shared by account unification, federation, and SCIM so one path cannot weaken another's identity evidence. \ No newline at end of file +## Context + +The same human can arrive through a federated SAML or OIDC IdP, an LDAP +user-storage component, inbound SCIM, and a local passkey account. Those +paths must share one matching rule so a weaker path cannot link or merge +accounts that a stronger path would refuse. + +OpenID Connect Core defines a subject identifier as a locally unique and +never-reassigned identifier within the issuer for the end-user (Sakimura +et al., 2023). An exact `(identity_provider, subject)` pair is therefore +stronger evidence than an email string that may be unverified, recycled, +or typed by an operator. + +NIST SP 800-63C-4 requires federation participants to treat assertion +attributes according to the federation assurance and attribute-validation +rules of the deployment; an RP or hub must not treat an unvalidated +attribute as proof of the same subscriber (Temoshok, Richer, et al., +2025). SAML 2.0 likewise carries subject and attribute statements from an +asserting party; those statements are only as trustworthy as the +configured signature, issuer, and attribute contract (Cantor et al., +2005). SCIM 2.0 can create or replace a User resource, including emails, +without proving mailbox control (Hunt, Grizzle, Ansari, et al., 2015; +Hunt, Grizzle, Wahlstroem, & Mortimore, 2015). + +## Decision + +Account matching precedence is exact `(identity_provider, subject)`, then +verified email under policy, then explicit operator link. Unverified email +never authorizes automatic linking or merge. Merged duplicate accounts +remain disabled tombstones with survivor lineage. This decision is shared +by account unification, federation, and SCIM so one path cannot weaken +another's identity evidence. + +## Consequences + +- Auto-link and merge APIs reject an unverified-email coincidence even + when a caller sets `explicit_link=true` unless a stronger rule also + holds; `allow_unverified_email_link` remains audit evidence and must + stay false at startup. +- `trustEmail` on external IdP and LDAP sources defaults to false until + the upstream verification contract is independently reviewed. +- SCIM provisioning must honor tombstone and survivor pointers so a later + replace cannot resurrect a merged duplicate as a second live account. +- Email remains usable identity data under purpose-bound access control, + encryption, and audit. This decision does not prescribe display masking. +- Orgmetra employment or org-tree identifiers are out of scope for this + matcher; Keyverse does not copy those tables to invent a fourth + precedence key. + +## References + +See [`docs/REFERENCES.md`](../REFERENCES.md) for the full APA 7th entries +and official URLs/DOIs opened for this expansion. + +Cantor, S., Kemp, J., Philpott, R., & Maler, E. (Eds.). (2005). +*Assertions and protocols for the OASIS Security Assertion Markup Language +(SAML) V2.0*. OASIS. +https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf + +Hunt, P., Grizzle, K., Ansari, M., Wahlstroem, E., & Mortimore, C. (2015). +*System for Cross-domain Identity Management: Protocol* (RFC 7644). +https://doi.org/10.17487/RFC7644 + +Hunt, P., Grizzle, K., Wahlstroem, E., & Mortimore, C. (2015). *System for +Cross-domain Identity Management: Core schema* (RFC 7643). +https://doi.org/10.17487/RFC7643 + +Sakimura, N., Bradley, J., Jones, M., de Medeiros, B., & Mortimore, C. +(2023). *OpenID Connect Core 1.0 incorporating errata set 2*. +https://openid.net/specs/openid-connect-core-1_0.html + +Temoshok, D., Richer, J. P., Choong, Y.-Y., Fenton, J. L., Lefkovitz, N., +Regenscheid, A., & Galluzzo, R. (2025). *Digital identity guidelines: +Federation and assertions* (NIST SP 800-63C-4). +https://doi.org/10.6028/NIST.SP.800-63C-4 diff --git a/docs/adr/0004-desired-state-reconciliation.md b/docs/adr/0004-desired-state-reconciliation.md index 35ba95c..21b3559 100644 --- a/docs/adr/0004-desired-state-reconciliation.md +++ b/docs/adr/0004-desired-state-reconciliation.md @@ -2,5 +2,95 @@ **Status:** Accepted **Date:** 2026-08-09 +**Last expanded:** 2026-08-18 -Federation, directory, and relying-party onboarding separate deterministic local preflight from external apply. Where Keyverse owns desired state, intent is persisted before remote mutation, duplicate remote matches fail closed, and a canonical apply receipt is written only after exact live re-observation. Delete uses remote-first ordering where local-first deletion could create false success. Preflight success never means external login/bind/provisioning success. \ No newline at end of file +## Context + +Operators must register SAML/OIDC identity providers, LDAP/AD user-storage +components, and OIDC relying-party clients without turning a validation +call into a live bind, metadata fetch, or silent Keycloak write. + +OpenID Connect Core assumes the RP already has issuer, authorization, +token, and related endpoint locations. Those values are normally obtained +via Discovery **or may be obtained via other mechanisms** (Sakimura et +al., 2023, §1). Keyverse chooses the latter for preflight: the operator +supplies pinned HTTPS endpoints. OAuth 2.0 Security BCP requires exact +redirect matching, PKCE for code flows, and rejects patterns that leak +codes or tokens (Lodderstedt et al., 2025; Sakimura, Bradley, & Agarwal, +2015). Native-app redirect guidance is recorded in RFC 8252; this +product's first RP profile does not accept loopback or private-use +schemes without a separate review (Denniss & Bradley, 2017). + +LDAP protocol operations include bind and search over a directory +connection (Sermersheim, 2006). LDAP authentication and StartTLS are +specified separately (Harrison, 2006). Distinguished-name strings used in +configuration must follow RFC 4514 syntax (Zeilenga, 2006). A local +preflight that only checks those syntactic and policy constraints is not +an LDAP session. + +SCIM 2.0 defines create, replace, patch, and delete as distinct protocol +operations whose success is determined by the resource server after the +request (Hunt, Grizzle, Ansari, et al., 2015). The same lesson applies to +Keycloak mutations: a local schema check is not an apply receipt. + +## Decision + +Federation, directory, and relying-party onboarding separate deterministic +local preflight from external apply. Where Keyverse owns desired state, +intent is persisted before remote mutation, duplicate remote matches fail +closed, and a canonical apply receipt is written only after exact live +re-observation. Delete uses remote-first ordering where local-first +deletion could create false success. Preflight success never means +external login, bind, or provisioning success. + +SAML and OIDC preflight perform no metadata or discovery fetch. LDAP +preflight performs no DNS lookup, socket, bind, search, storage write, or +Keycloak call. + +## Consequences + +- `POST ...:validate` routes stay pure functions of the submitted payload + and closed policy. +- `PUT` routes store intent, classify zero / one / many live objects, and + refuse to pick an arbitrary duplicate. +- Receipts are not written from the request body alone; operators compare + redacted live status to the original private file. +- Controlled login, bind, or SCIM evidence remains a later acceptance + step, not a preflight field. +- Mapper configuration on an RP client is issuer-side evidence only; it + does not prove the RP validated signature, issuer, expiry, or audience. + +## References + +See [`docs/REFERENCES.md`](../REFERENCES.md) for the full APA 7th entries +and official URLs/DOIs opened for this expansion. + +Denniss, W., & Bradley, J. (2017). *OAuth 2.0 for native apps* (RFC 8252). +https://doi.org/10.17487/RFC8252 + +Harrison, R. (Ed.). (2006). *Lightweight Directory Access Protocol (LDAP): +Authentication methods and security mechanisms* (RFC 4513). +https://doi.org/10.17487/RFC4513 + +Hunt, P., Grizzle, K., Ansari, M., Wahlstroem, E., & Mortimore, C. (2015). +*System for Cross-domain Identity Management: Protocol* (RFC 7644). +https://doi.org/10.17487/RFC7644 + +Lodderstedt, T., Bradley, J., Labunets, A., & Fett, D. (2025). *Best +current practice for OAuth 2.0 security* (RFC 9700). +https://doi.org/10.17487/RFC9700 + +Sakimura, N., Bradley, J., & Agarwal, N. (2015). *Proof key for code +exchange by OAuth public clients* (RFC 7636). +https://doi.org/10.17487/RFC7636 + +Sakimura, N., Bradley, J., Jones, M., de Medeiros, B., & Mortimore, C. +(2023). *OpenID Connect Core 1.0 incorporating errata set 2*. +https://openid.net/specs/openid-connect-core-1_0.html + +Sermersheim, J. (Ed.). (2006). *Lightweight Directory Access Protocol +(LDAP): The protocol* (RFC 4511). https://doi.org/10.17487/RFC4511 + +Zeilenga, K. (Ed.). (2006). *Lightweight Directory Access Protocol (LDAP): +String representation of distinguished names* (RFC 4514). +https://doi.org/10.17487/RFC4514 diff --git a/docs/adr/0005-secret-ownership.md b/docs/adr/0005-secret-ownership.md index 5e79284..c9f167d 100644 --- a/docs/adr/0005-secret-ownership.md +++ b/docs/adr/0005-secret-ownership.md @@ -2,5 +2,85 @@ **Status:** Accepted **Date:** 2026-08-09 +**Last expanded:** 2026-08-18 -Portable realm configuration and ordinary desired-state records contain only the fields needed for reproducible identity policy. Deployment-specific confidential values remain owned by the deployment controller and its approved configuration store. Public repository artifacts, ordinary responses, and routine logs do not copy those private values. This keeps the portable realm reusable across tenants and supports controlled rotation and rollback. \ No newline at end of file +## Context + +The portable `cwl` realm and ordinary desired-state records must be +reusable across tenants. Bind credentials, operator bearers, confidential +client secrets, and signing material must not travel with those records +into git, operator JSON, or routine logs. + +OAuth 2.0 defines client authentication and client secrets as +confidential-client credentials held by the client and the authorization +server (Hardt, 2012, §2.3). The OAuth 2.0 Security BCP updates that +threat model: secrets in front-channel URLs, leaked redirectors, and +weak client authentication remain first-class failures (Lodderstedt et +al., 2025). PKCE protects the authorization code for public clients; it +does not replace confidential-client secret handling (Sakimura, Bradley, +& Agarwal, 2015). + +NIST SP 800-63C-4 treats federation and assertion protection as a +deployment concern between an IdP and separately administered RPs +(Temoshok, Richer, et al., 2025). LDAP bind credentials are directory +authentication secrets (Harrison, 2006). SCIM endpoints are +bearer-protected HTTP resources (Hunt, Grizzle, Ansari, et al., 2015). +None of those standards require publishing those secrets in a realm +export. + +## Decision + +Portable realm configuration and ordinary desired-state records contain +only the fields needed for reproducible identity policy. +Deployment-specific confidential values remain owned by the deployment +controller and its approved configuration store. Public repository +artifacts, ordinary responses, and routine logs do not copy those private +values. This keeps the portable realm reusable across tenants and +supports controlled rotation and rollback. + +Environment variables are bootstrap transport to reach that store +(`CWL_IDP_BOOTSTRAP`), not the runtime source of truth. + +## Consequences + +- RP desired-state PUT is secret-free; confidential credential placement + is a separate secret-management port. +- Preflight and status responses redact `bindDn`, `bindCredential`, + client secrets, and other known secret fields. +- Hardcoded RP routing claim values (`role`, `org`, `workspace`) are + visible product data and must not carry credentials or personal + secrets. +- Rotation and rollback are controller/KV operations; Keyverse receipts + record observed public client or provider state, not the secret itself. +- Authorized identity attributes remain usable under purpose-bound access + control, encryption, and audit. This decision does not prescribe + display masking. + +## References + +See [`docs/REFERENCES.md`](../REFERENCES.md) for the full APA 7th entries +and official URLs/DOIs opened for this expansion. + +Hardt, D. (Ed.). (2012). *The OAuth 2.0 authorization framework* (RFC 6749). +https://doi.org/10.17487/RFC6749 + +Harrison, R. (Ed.). (2006). *Lightweight Directory Access Protocol (LDAP): +Authentication methods and security mechanisms* (RFC 4513). +https://doi.org/10.17487/RFC4513 + +Hunt, P., Grizzle, K., Ansari, M., Wahlstroem, E., & Mortimore, C. (2015). +*System for Cross-domain Identity Management: Protocol* (RFC 7644). +https://doi.org/10.17487/RFC7644 + +Lodderstedt, T., Bradley, J., Labunets, A., & Fett, D. (2025). *Best +current practice for OAuth 2.0 security* (RFC 9700). +https://doi.org/10.17487/RFC9700 + +Sakimura, N., Bradley, J., & Agarwal, N. (2015). *Proof key for code +exchange by OAuth public clients* (RFC 7636). +https://doi.org/10.17487/RFC7636 + +Temoshok, D., Richer, J. P., Choong, Y.-Y., Fenton, J. L., Lefkovitz, N., +Regenscheid, A., & Galluzzo, R. (2025). *Digital identity guidelines: +Federation and assertions* (NIST SP 800-63C-4). +https://doi.org/10.6028/NIST.SP.800-63C-4 diff --git a/docs/adr/0006-user-operation-lock.md b/docs/adr/0006-user-operation-lock.md index 1a912ad..b434fac 100644 --- a/docs/adr/0006-user-operation-lock.md +++ b/docs/adr/0006-user-operation-lock.md @@ -2,22 +2,65 @@ **Status:** Accepted **Date:** 2026-08-09 +**Last expanded:** 2026-08-18 ## Context -Account merge/link operations and a SCIM full user replacement can target the same Keycloak user. The full replacement path reads tombstone state and then writes the user representation, so it must not race a merge that creates the tombstone between those operations. +Account merge/link operations and a SCIM full user replacement can target +the same Keycloak user. The full replacement path reads tombstone state +and then writes the user representation, so it must not race a merge that +creates the tombstone between those operations. -Protected `main` also supports the narrower `PATCH active=false` deprovisioning path. That PATCH path currently performs its read/deactivate/read sequence outside the shared cross-process lock. This ADR therefore must not imply that every SCIM mutation is serialized with merge. +SCIM 2.0 defines HTTP `PUT` as replace of the entire resource and `PATCH` +as a partial modification protocol with its own operation list (Hunt, +Grizzle, Ansari, et al., 2015, §§3.5.1–3.5.2). The core User schema +includes `active` and multi-valued emails but does not define merge or +tombstone semantics (Hunt, Grizzle, Wahlstroem, & Mortimore, 2015). +Keyverse therefore has to impose a product lock around the replace path +that can observe or overwrite survivor/tombstone attributes. + +Protected `main` also supports the narrower `PATCH active=false` +deprovisioning path. That PATCH path currently performs its +read/deactivate/read sequence outside the shared cross-process lock. This +ADR therefore must not imply that every SCIM mutation is serialized with +merge. ## Decision -Keyverse uses one cross-process user-operation lock boundary for account merge/link and SCIM `PUT /Users/{id}` full replacement. Those operations serialize consistently, preserve tombstone/survivor invariants, and can be retried or recovered from observed durable state. +Keyverse uses one cross-process user-operation lock boundary for account +merge/link and SCIM `PUT /Users/{id}` full replacement. Those operations +serialize consistently, preserve tombstone/survivor invariants, and can +be retried or recovered from observed durable state. -The current SCIM `PATCH active=false` path is explicitly outside this Accepted shared-lock guarantee. If PATCH or any future SCIM read-modify-write operation can affect tombstone, survivor, or reactivation invariants, it must join the same lock boundary and add a concurrency regression before documentation may claim equivalent serialization. +The current SCIM `PATCH active=false` path is explicitly outside this +Accepted shared-lock guarantee. If PATCH or any future SCIM +read-modify-write operation can affect tombstone, survivor, or +reactivation invariants, it must join the same lock boundary and add a +concurrency regression before documentation may claim equivalent +serialization. ## Consequences -- Merge and SCIM full replacement share one documented concurrency authority. -- The protected-main PATCH behavior remains usable but must not be described as transactionally serialized with merge. -- Expanding the lock guarantee requires a source/test change, not a documentation-only promotion. -- Clustered deployments must provide the same shared-lock semantics for every operation included in this boundary. \ No newline at end of file +- Merge and SCIM full replacement share one documented concurrency + authority. +- The protected-main PATCH behavior remains usable but must not be + described as transactionally serialized with merge. +- Expanding the lock guarantee requires a source/test change, not a + documentation-only promotion. +- Clustered deployments must provide the same shared-lock semantics for + every operation included in this boundary. +- Durable lock state uses `user_operation_lock_state`; merge audit uses + `account_merge_audit`. + +## References + +See [`docs/REFERENCES.md`](../REFERENCES.md) for the full APA 7th entries +and official URLs/DOIs opened for this expansion. + +Hunt, P., Grizzle, K., Ansari, M., Wahlstroem, E., & Mortimore, C. (2015). +*System for Cross-domain Identity Management: Protocol* (RFC 7644). +https://doi.org/10.17487/RFC7644 + +Hunt, P., Grizzle, K., Wahlstroem, E., & Mortimore, C. (2015). *System for +Cross-domain Identity Management: Core schema* (RFC 7643). +https://doi.org/10.17487/RFC7643 diff --git a/docs/adr/0007-automation-authority.md b/docs/adr/0007-automation-authority.md index 8e1b264..ac553bb 100644 --- a/docs/adr/0007-automation-authority.md +++ b/docs/adr/0007-automation-authority.md @@ -2,5 +2,60 @@ **Status:** Accepted **Date:** 2026-08-09 +**Last expanded:** 2026-08-18 -Autonomous development may inspect exact repository state, produce a bounded patch, and submit ordinary reviewable work after independent verification. It cannot create its own qualifying approval, bypass branch protection, merge protected main, tag, or publish a release. Model-provider credentials remain separate from reviewer, publication, and release credentials. PR #74 refines the hourly implementation while preserving this authority boundary. \ No newline at end of file +## Context + +Keyverse may use scheduled automation to inspect exact repository state +and propose bounded product work. Generated model output is untrusted. +If the same credential that writes a patch can also approve, merge, tag, +or publish a release, a single compromised or hallucinated run becomes a +release path. + +NIST SP 800-218 (SSDF 1.1) recommends defining roles and separating +duties across the software life cycle, reviewing changes before release, +and protecting the build and publication environment (Souppaya et al., +2022). Those practices are used here as secure-development evidence, not +as a claim that Keyverse is a federal information system. + +This decision is about **authority**. Operator procedures for the hourly +OpenCode loop live in +[`docs/operations/hourly-product-development.md`](../operations/hourly-product-development.md) +and are not restated in the buyer README. + +## Decision + +Autonomous development may inspect exact repository state, produce a +bounded patch, and submit ordinary reviewable work after independent +verification. It cannot create its own qualifying approval, bypass branch +protection, merge protected main, tag, or publish a release. +Model-provider credentials remain separate from reviewer, publication, +and release credentials. PR #74 refines the hourly implementation while +preserving this authority boundary. + +Existing review-agent workflows and their credentials stay on their +current system. They must not be repurposed, renamed, or broadened as a +side effect of product-development automation. + +## Consequences + +- A draft PR from automation is ordinary reviewable work, not a merge + grant. +- Independent verification (fresh checkout, complete quality gates) is + required before publication of a generated patch. +- Release tagging, image digest, SBOM, and rollback evidence remain a + human-owned release process after exact-main regression. +- Buyer-facing README does not describe the bot loop; operators follow + the operations guide. +- This ADR does not authorize stacking documentation or product work onto + unrelated open feature PRs. + +## References + +See [`docs/REFERENCES.md`](../REFERENCES.md) for the full APA 7th entries +and official URLs/DOIs opened for this expansion. + +Souppaya, M., Scarfone, K., & Dodson, D. (2022). *Secure Software +Development Framework (SSDF) version 1.1: Recommendations for mitigating +the risk of software vulnerabilities* (NIST SP 800-218). +https://doi.org/10.6028/NIST.SP.800-218 diff --git a/docs/adr/README.md b/docs/adr/README.md index 1a087a4..93e8b2f 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -2,6 +2,11 @@ `Accepted` means the decision governs architecture; it does not imply an active PR has merged or a customer deployment has completed acceptance. +Accepted ADRs 0001–0007 are expanded in place with Context, Decision, +Consequences, and APA 7th references. The shared bibliography is +[`docs/REFERENCES.md`](../REFERENCES.md). ADR 0008 remains the RP +authorization boundary and is not rewritten by that expansion. + | ADR | Decision | Status | |---|---|---| | [0001](0001-keycloak-hub.md) | Keep Keycloak/Keyverse as the ecosystem identity hub | Accepted | diff --git a/docs/doctoring/product-technical-gap-baseline.md b/docs/doctoring/product-technical-gap-baseline.md new file mode 100644 index 0000000..1e4bd57 --- /dev/null +++ b/docs/doctoring/product-technical-gap-baseline.md @@ -0,0 +1,70 @@ +# Product-technical gap baseline doctoring record + +**Date:** 2026-08-21 +**Scope:** Keyverse product, trust-boundary, PR queue, and release evidence + +## Interpretation + +This baseline separates deterministic repository evidence from live protocol, +consumer authorization, deployment, and release acceptance. Source/tests can +prove validators, reconciliation, locking, and documentation contracts; they +cannot prove a controlled passwordless login, downstream token acceptance, +production sizing, or immutable release provenance without an approved runtime +lane. Missing evidence is therefore `gap-not-claimed`, not synthetic success. + +The mapper policy remains closed: `role`, `org`, and `workspace` are issuer-side +claims, not tenant authorization. Consumers must independently validate issuer, +signature/JWKS, expiry, audience, resource, tenant, purpose, and RBAC. The +LineageWeave profile keeps `org` as one opaque tenant key and `workspace` as one +child namespace; ambiguous membership denies before authorization. + +## Current evidence interpretation + +- The protected-main head observed for this snapshot is + `ce207dfd42975db61c82a5963e206fc1db14ac2b`. +- The #112 root stack is at `31dd486cb97ca215da451151f618a954a07b0ea5` with + hosted checks pending and `REVIEW_REQUIRED`; local 100% evidence does not + replace hosted checks or independent approval. +- #104 was normally restacked onto #112 at `c623a3d8df6e0f6da0e9623b23e3178e0f0296f0`. + Its documentation-only conflict resolution preserved both CHANGELOG entries; + the baseline addition then advanced it to successor + `8077aa46e120ea5977464f2e611d44ab44bab695`; fresh hosted checks are required + for the successor and neither head is protected-main evidence. +- Central `.github#1203` has a cancelled scheduler predecessor and a queued + retry. Cancellation is normal concurrency behavior; the observed age and + evidence do not satisfy D1–D5 emergency bypass criteria. +- Open issues #114, #102, #99, #71, and #2 remain tracked. No issue is treated + as implemented-main evidence merely because a design PR exists. + +## Standards interpretation + +OIDC exact issuer and audience validation, RFC 8725 token validation, RFC 9700 +authorization-code and PKCE guidance, RFC 8707 resource indicators, RFC 9728 +protected-resource metadata, RFC 9068 JWT access-token requirements, and RFC +9207 authorization-response issuer comparison are interpreted as consumer or +resource-server acceptance requirements where applicable. Keycloak mapper +configuration remains projection evidence only. + +No new frontend behavior is claimed by this baseline. A future UI change must +record its Figma File ID and Storybook scene/edge-event, accessibility, +interaction, performance, responsive, typography/color, animation, form, +navigation, and chart acceptance in the owning ADR. + +## Verification rule + +Every refresh must re-query the exact PR head, base, open review threads, formal +review decision, required CheckRun conclusions, and merge state. A changed head +invalidates prior evidence. Protected merges remain normal PR merges; guarded +force merge is permitted only after all D1–D5 and emergency acceptance criteria +are independently proven. No bypass, force push, direct protected push, fake +status, or self-approval is part of this record. + +## APA 7th references + +- OpenID Foundation. (2014). *OpenID Connect Core 1.0*. https://openid.net/specs/openid-connect-core-1_0-18.html +- Internet Engineering Task Force. (2020). *JSON Web Token best current practices* (RFC 8725). https://www.rfc-editor.org/rfc/rfc8725.html +- Lodderstedt, T., Bradley, J., Labunets, A., & Fett, D. (2025). *Best current practice for OAuth 2.0 security* (RFC 9700). https://www.rfc-editor.org/rfc/rfc9700.html +- Internet Engineering Task Force. (2018). *OAuth 2.0 authorization server metadata* (RFC 8414). https://doi.org/10.17487/RFC8414 +- Internet Engineering Task Force. (2020). *Resource indicators for OAuth 2.0* (RFC 8707). https://doi.org/10.17487/RFC8707 +- Internet Engineering Task Force. (2025). *OAuth 2.0 protected resource metadata* (RFC 9728). https://doi.org/10.17487/RFC9728 +- Model Context Protocol. (2026, July 28). *Authorization*. https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md new file mode 100644 index 0000000..167cb21 --- /dev/null +++ b/docs/product-technical-gap-baseline.md @@ -0,0 +1,166 @@ +# Keyverse product and technical gap baseline + +**Evidence snapshot:** 2026-08-21T14:24:31Z (UTC) +**Repository:** `ContextualWisdomLab/keyverse` +**Protected-main head observed:** `ce207dfd42975db61c82a5963e206fc1db14ac2b` +**Status:** live inventory and gap register; not a release acceptance record + +This baseline joins the product, architecture, ADR, standards, operations, +and exact-head GitHub evidence into one executable backlog. It distinguishes +protected-main evidence from open-PR work, accepted contracts, and claims that +remain intentionally unverified. + +## Product and authority boundary + +Keyverse is a standalone and embeddable identity control plane for CWL, Naruon, +and sibling products. It owns passwordless-first Keycloak policy, federation +and directory preflight/reconciliation, account unification, SCIM lifecycle, +relying-party desired state, audit, and safe deployment operations. + +Downstream applications own token signature/issuer/audience validation, +tenant/resource/purpose ABAC, and bounded RBAC. A Keycloak mapper receipt is +issuer-side configuration evidence, never proof that a relying party accepts a +token or enforces authorization. + +## Evidence vocabulary + +| Classification | Meaning | +|---|---| +| `implemented-main` | Source and representative tests are on protected `main`. | +| `active-PR` | Work exists only in an open PR and is not released evidence. | +| `active-issue` | An open issue records a product or operational gap. | +| `accepted-contract` | An ADR or standard defines policy; runtime acceptance may still be absent. | +| `gap-not-claimed` | The repository makes no success claim until stronger evidence exists. | + +Queued, cancelled, skipped-required, stale, predecessor-head, and +rate-limited checks are not successful evidence. Formal approval must bind to +the exact current head and satisfy the latest-pusher and independent-review +rules. + +## Capability and buyer acceptance map + +| Capability | Current maturity | Buyer-visible boundary | +|---|---|---| +| Passwordless local identity | `implemented-main` | Realm validators and tests protect WebAuthn/passwordless policy; live login remains separate evidence. | +| Federation and LDAP preflight | `implemented-main` | Validators are side-effect-free; external bind/discovery and apply remain separate. | +| Account merge and SCIM full replacement | `implemented-main` | Verified identity matching, tombstones, audit, and shared merge/PUT locking are covered on main. | +| SCIM `PATCH active=false` lock parity | `active-PR` | PR #113 is not protected-main evidence until its current head passes all gates and merges. | +| Closed RP mapper profile | `implemented-main` / `accepted-contract` | Canonical `role`, `org`, and `workspace` claims remain closed; consumers must prove their own authorization. | +| Real login and token acceptance | `gap-not-claimed` | No live controlled passwordless browser flow, token exchange, downstream ABAC/RBAC, or revocation acceptance is claimed. | +| Standalone Compose/Helm operation | `implemented-main` / `gap-not-claimed` | Repository validators exist; deployment secret/configuration, rollback, and immutable artifact evidence remain required. | +| Product loop and protected merge | `active-PR` | The scheduler and review path must bind every decision to a current exact head. | +| Release artifact acceptance | `gap-not-claimed` | Version, immutable image digest, SBOM/provenance, rollback, and exact-main regression are still release gates. | + +## Current exact-head PR inventory + +This table was queried from the live GitHub state at the snapshot time. Counts +exclude informational CodeRabbit/Devin contexts and count only CheckRun +success, skipped, or non-terminal results. + +| PR | Scope | Base | Exact head | Checks | Gate / next safe action | +|---:|---|---|---|---|---| +| [#113](https://github.com/ContextualWisdomLab/keyverse/pull/113) | SCIM deactivation shared lock | `ce207dfd42975db61c82a5963e206fc1db14ac2b` | `9bd33ee0d00ef1874fd5efabac3462f678a256ed` | 21 success / 8 skipped / 1 pending | `REVIEW_REQUIRED`; obtain exact-head independent approval and finish the pending review gate. | +| [#112](https://github.com/ContextualWisdomLab/keyverse/pull/112) | Account-unification lockfile and stacked contract updates | `ce207dfd42975db61c82a5963e206fc1db14ac2b` | `31dd486cb97ca215da451151f618a954a07b0ea5` | 0 success / 7 skipped / 14 pending | `REVIEW_REQUIRED`; local evidence is complete, but hosted Checks and independent approval are pending. | +| [#104](https://github.com/ContextualWisdomLab/keyverse/pull/104) | ADR 0001–0007 and operator README expansion | `31dd486cb97ca215da451151f618a954a07b0ea5` | `c623a3d8df6e0f6da0e9623b23e3178e0f0296f0` | 0 success / 0 skipped / 4 pending | Restacked normally; wait for fresh Checks and independent review before merging into the #112 stack. | +| [#103](https://github.com/ContextualWisdomLab/keyverse/pull/103) | Hierarchical authorization, login helper, and PATs | `ce207dfd42975db61c82a5963e206fc1db14ac2b` | `77b8f4ea9995329f1c55b916d110b460b4bc7649` | 20 success / 8 skipped / 1 pending | `REVIEW_REQUIRED`; retain fail-closed security boundary and obtain current approval. | +| [#101](https://github.com/ContextualWisdomLab/keyverse/pull/101) | Coupled Python dependency updates | `ce207dfd42975db61c82a5963e206fc1db14ac2b` | `50dd9c96cab5c230f775685e8baea939fba390dd` | 22 success / 8 skipped / 0 pending | `REVIEW_REQUIRED`; revalidate against the final lockfile stack and obtain approval. | +| [#100](https://github.com/ContextualWisdomLab/keyverse/pull/100) | LineageWeave account-derived RP profile | `ce207dfd42975db61c82a5963e206fc1db14ac2b` | `ede8075f82bb082b7d992b824992bf44792f744e` | 18 success / 7 skipped / 2 pending | `REVIEW_REQUIRED`; downstream issuer/audience/tenant acceptance remains unclaimed. | +| [#83](https://github.com/ContextualWisdomLab/keyverse/pull/83) | Remove runtime application RPs from portable realm | `ce207dfd42975db61c82a5963e206fc1db14ac2b` | `dd1ab7444a75342b42e3af013ccda6d1dbfb359d` | 22 success / 8 skipped / 0 pending | `REVIEW_REQUIRED`; confirm exact-head approval and latest-pusher policy before merge. | + +The central coordination PR [`.github#1203`](https://github.com/ContextualWisdomLab/.github/pull/1203) +is open at exact head `babb411e5132f67a665e302eb492da69f7d91afb` against +`731af58e954901c4f1cc853231c592abb1eaf617`. Its `scan-pr-queue` run +`32487969901` was cancelled by the normal concurrency scheduler; successor run +`32488287515` is queued. This is not a source failure or D1–D5 emergency +evidence, so no bypass or guarded force merge is allowed. + +This baseline is itself carried by #104. The table records that PR at the +pre-baseline snapshot head `c623a3d8df6e0f6da0e9623b23e3178e0f0296f0`; adding +this baseline advanced it normally to successor `8077aa46e120ea5977464f2e611d44ab44bab695`. +The successor's hosted Checks and review state must be read from the live PR, +and neither snapshot is protected-main evidence. + +## Open issue inventory + +| Issue | Signal | Classification | Required outcome | +|---:|---|---|---| +| [#114](https://github.com/ContextualWisdomLab/keyverse/issues/114) | MCP-compatible OAuth authorization for headless agents | `active-issue` | Independently review the design, then prove a real resource-bound client flow before runtime implementation. | +| [#102](https://github.com/ContextualWisdomLab/keyverse/issues/102) | Hierarchical authorization plane and PATs | `active-PR` | Prove tenant/resource fail-closed behavior and current-head security review. | +| [#99](https://github.com/ContextualWisdomLab/keyverse/issues/99) | Orphaned federation and product-loop identities | `active-issue` | Preserve the registry recurrence detector and central coordination evidence. | +| [#71](https://github.com/ContextualWisdomLab/keyverse/issues/71) | Remove runtime application RPs from portable import | `active-PR` | Merge #83 only after exact protected evidence. | +| [#2](https://github.com/ContextualWisdomLab/keyverse/issues/2) | Central IdP and external-IdP federation | `accepted-contract` | Complete approved-environment acceptance without weakening preflight boundaries. | + +## Buyer-visible gap order + +### G0 — Protected queue convergence + +The repository must distinguish current, reviewed, passing artifacts from stale +or coupled proposals. The loop is inventory, review disposition, focused fix, +exact-head local and hosted checks, independent approval, protected merge, merge +SHA verification, and re-listing. Never self-approve, force-push, admin-merge, +publish fake status, or reuse predecessor evidence. + +### G1 — Controlled real login and authorization acceptance + +In an approved environment, prove discovery/issuer, JWKS signature and allowed +algorithm, authorization-code + PKCE `S256`, passwordless browser login, token +`iss`/`sub`/`aud`/time claims, logout, tenant/resource ABAC, role/scope RBAC, +cross-tenant denial, and verifier-unavailable fail-closed behavior. An +unavailable issuer stays `unavailable`; it is never replaced with a synthetic +success. + +### G2 — Downstream tenant semantics + +For `lineageweave-web`, `org` is one opaque external tenant key and `workspace` +is one child namespace. Ambiguous or missing membership denies before ABAC/RBAC; +membership changes require a new token or session. Generic tenant claims must +not be added to the closed mapper profile. + +### G3 — SCIM concurrency and database evidence + +After #113, prove real concurrent PATCH/merge behavior on protected main. For +production storage, add PostgreSQL migration/rollback, tenant-qualified +constraints, concentrated-tenant skew measurements, partition/index decisions, +backup/restore, and recovery evidence. Local SQLite tests are not that proof. + +### G4 — MCP resource authorization + +The design-only ADR requires Keycloak authorization code + PKCE, exact redirects, +RFC 8707 resource binding, RFC 9728 protected-resource metadata, RFC 9207 +callback issuer comparison, RFC 9068 JWT validation, revocation, and negative +evidence. Runtime MCP acceptance remains `gap-not-claimed`. + +### G5 — Release and module acceptance + +On exact protected main, complete regression and controlled deployment +acceptance, publish immutable image digest plus SBOM/provenance, and prove +rollback. A green feature PR is not a release. + +## Loop and design boundary + +The hourly PR steward may advance only trusted same-repository PRs with exact +head, independent approval, and required Checks. The hourly product loop may +create at most one bounded draft product-gap PR only after the open queue is +empty and protected-main evidence is healthy. GitHub review/check waiting is not +a reason to stop independent review, documentation, or test design, but queued +results are never promoted to success. + +This repository has no current frontend change in this baseline. Therefore no +Figma file or Storybook inventory is claimed. If a future buyer gap changes a +web surface, its ADR must record the Figma File ID, design tokens, reusable +components, Storybook scene/edge events, and accessibility/interaction/ +performance/responsive/form/navigation/chart acceptance before implementation +is claimed. + +## APA 7th references + +- OpenID Foundation. (2014). *OpenID Connect Core 1.0*. https://openid.net/specs/openid-connect-core-1_0-18.html +- Internet Engineering Task Force. (2020). *JSON Web Token best current practices* (RFC 8725). https://www.rfc-editor.org/rfc/rfc8725.html +- Lodderstedt, T., Bradley, J., Labunets, A., & Fett, D. (2025). *Best current practice for OAuth 2.0 security* (RFC 9700). https://www.rfc-editor.org/rfc/rfc9700.html +- Internet Engineering Task Force. (2018). *OAuth 2.0 authorization server metadata* (RFC 8414). https://doi.org/10.17487/RFC8414 +- Internet Engineering Task Force. (2020). *Resource indicators for OAuth 2.0* (RFC 8707). https://doi.org/10.17487/RFC8707 +- Internet Engineering Task Force. (2025). *OAuth 2.0 protected resource metadata* (RFC 9728). https://doi.org/10.17487/RFC9728 +- Model Context Protocol. (2026, July 28). *Authorization*. https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization + +Interpretations and evidence boundaries are maintained in +[`docs/doctoring/product-technical-gap-baseline.md`](doctoring/product-technical-gap-baseline.md). diff --git a/tests/test_documentation_contract.py b/tests/test_documentation_contract.py index c5c0657..6d8ab7b 100644 --- a/tests/test_documentation_contract.py +++ b/tests/test_documentation_contract.py @@ -17,6 +17,8 @@ "docs/TEST_STRATEGY.md", "docs/OPERABILITY.md", "docs/TRACEABILITY.md", + "docs/product-technical-gap-baseline.md", + "docs/doctoring/product-technical-gap-baseline.md", "docs/adr/README.md", "README.md", "AGENTS.md", From e1ada50f91d0d841b39cac5a6faeaf12210d76fe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 01:54:14 +0900 Subject: [PATCH 13/17] fix: require exact federation re-observation --- CHANGELOG.md | 4 ++ README.md | 8 ++-- docs/adr/0004-desired-state-reconciliation.md | 4 +- docs/adr/0005-secret-ownership.md | 12 ++--- .../product-technical-gap-baseline.md | 20 +++++---- docs/federation-onboarding.md | 7 ++- docs/product-technical-gap-baseline.md | 45 +++++++++++-------- .../account_unification/app/federation.py | 20 +++++++++ .../tests/test_federation.py | 45 +++++++++++++++++++ tests/test_documentation_contract.py | 34 ++++++++++++-- 10 files changed, 154 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23819a5..39bcacb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,10 @@ Keep a Changelog, and releases use semantic versioning. ### Changed +- Federation PUT and apply now report `applied_to_keycloak: true` only after a + fresh live Keycloak identity-provider observation matches the desired + representation; mutation or observation drift retains the desired state for + retry. - Buyer README and accepted ADRs 0001–0007 now describe Keyverse as a standalone identity leaf/hub, point operators at published OIDC/OAuth 2.0, SAML, LDAP, and SCIM contracts, and cite independently opened diff --git a/README.md b/README.md index 2c022e7..1961d5e 100644 --- a/README.md +++ b/README.md @@ -96,9 +96,11 @@ Orgmetra, or any other sibling checkout in order to start. ## How a relying party calls Keyverse Each RP is a separate trust boundary. A README listing, repository -relationship, or client ID is not authorization. The RP validates issuer, -signature, audience, subject, and expiry, then applies its own -access-control policy ([ADR-0008](docs/adr/0008-keyverse-rp-authorization-boundary.md)). +relationship, or client ID is not authorization. The RP validates the issuer, +signature, allowed algorithm, audience, subject, expiry, `iat`, exact resource, +tenant, and purpose. All of those token and request-context checks must +complete before applying its own access-control policy, including RBAC +([ADR-0008](docs/adr/0008-keyverse-rp-authorization-boundary.md)). Published operator contracts that already ship: diff --git a/docs/adr/0004-desired-state-reconciliation.md b/docs/adr/0004-desired-state-reconciliation.md index 21b3559..ece808f 100644 --- a/docs/adr/0004-desired-state-reconciliation.md +++ b/docs/adr/0004-desired-state-reconciliation.md @@ -16,8 +16,8 @@ via Discovery **or may be obtained via other mechanisms** (Sakimura et al., 2023, §1). Keyverse chooses the latter for preflight: the operator supplies pinned HTTPS endpoints. OAuth 2.0 Security BCP requires exact redirect matching, PKCE for code flows, and rejects patterns that leak -codes or tokens (Lodderstedt et al., 2025; Sakimura, Bradley, & Agarwal, -2015). Native-app redirect guidance is recorded in RFC 8252; this +codes or tokens (Lodderstedt et al., 2025; Sakimura et al., 2015). +Native-app redirect guidance is recorded in RFC 8252; this product's first RP profile does not accept loopback or private-use schemes without a separate review (Denniss & Bradley, 2017). diff --git a/docs/adr/0005-secret-ownership.md b/docs/adr/0005-secret-ownership.md index c9f167d..4b60d58 100644 --- a/docs/adr/0005-secret-ownership.md +++ b/docs/adr/0005-secret-ownership.md @@ -17,16 +17,16 @@ server (Hardt, 2012, §2.3). The OAuth 2.0 Security BCP updates that threat model: secrets in front-channel URLs, leaked redirectors, and weak client authentication remain first-class failures (Lodderstedt et al., 2025). PKCE protects the authorization code for public clients; it -does not replace confidential-client secret handling (Sakimura, Bradley, -& Agarwal, 2015). +does not replace confidential-client secret handling (Sakimura et al., 2015). NIST SP 800-63C-4 treats federation and assertion protection as a deployment concern between an IdP and separately administered RPs (Temoshok, Richer, et al., 2025). LDAP bind credentials are directory -authentication secrets (Harrison, 2006). SCIM endpoints are -bearer-protected HTTP resources (Hunt, Grizzle, Ansari, et al., 2015). -None of those standards require publishing those secrets in a realm -export. +authentication secrets (Harrison, 2006). Keyverse account-unification SCIM +endpoints are bearer-protected HTTP resources; RFC 7644 does not require +bearer tokens for every SCIM deployment and leaves the authentication scheme +to the underlying HTTP security boundary (Hunt et al., 2015). None of those +standards require publishing those secrets in a realm export. ## Decision diff --git a/docs/doctoring/product-technical-gap-baseline.md b/docs/doctoring/product-technical-gap-baseline.md index 1e4bd57..b104e7c 100644 --- a/docs/doctoring/product-technical-gap-baseline.md +++ b/docs/doctoring/product-technical-gap-baseline.md @@ -1,6 +1,6 @@ # Product-technical gap baseline doctoring record -**Date:** 2026-08-21 +**Date:** 2026-08-21T16:47:10Z **Scope:** Keyverse product, trust-boundary, PR queue, and release evidence ## Interpretation @@ -22,14 +22,14 @@ child namespace; ambiguous membership denies before authorization. - The protected-main head observed for this snapshot is `ce207dfd42975db61c82a5963e206fc1db14ac2b`. -- The #112 root stack is at `31dd486cb97ca215da451151f618a954a07b0ea5` with - hosted checks pending and `REVIEW_REQUIRED`; local 100% evidence does not - replace hosted checks or independent approval. -- #104 was normally restacked onto #112 at `c623a3d8df6e0f6da0e9623b23e3178e0f0296f0`. - Its documentation-only conflict resolution preserved both CHANGELOG entries; - the baseline addition then advanced it to successor - `8077aa46e120ea5977464f2e611d44ab44bab695`; fresh hosted checks are required - for the successor and neither head is protected-main evidence. +- The #112 root stack is at exact head `44c2adb18687f8df457bd4bafade551533cee5b9` + with 14 queued and 7 skipped Checks, six valid unresolved review threads, + and no exact-head formal approval; local 100% evidence does not replace + hosted checks or independent approval. +- #104 is closed by squash merge at `44c2adb18687f8df457bd4bafade551533cee5b9` + on the feature base, while protected `main` remains + `ce207dfd42975db61c82a5963e206fc1db14ac2b`. This is audit evidence of a + feature-base governance gap, not protected-main approval evidence. - Central `.github#1203` has a cancelled scheduler predecessor and a queued retry. Cancellation is normal concurrency behavior; the observed age and evidence do not satisfy D1–D5 emergency bypass criteria. @@ -66,5 +66,7 @@ status, or self-approval is part of this record. - Lodderstedt, T., Bradley, J., Labunets, A., & Fett, D. (2025). *Best current practice for OAuth 2.0 security* (RFC 9700). https://www.rfc-editor.org/rfc/rfc9700.html - Internet Engineering Task Force. (2018). *OAuth 2.0 authorization server metadata* (RFC 8414). https://doi.org/10.17487/RFC8414 - Internet Engineering Task Force. (2020). *Resource indicators for OAuth 2.0* (RFC 8707). https://doi.org/10.17487/RFC8707 +- Bertocci, V. (2021). *JSON Web Token (JWT) profile for OAuth 2.0 access tokens* (RFC 9068). https://doi.org/10.17487/RFC9068 +- Meyer zu Selhausen, K., & Fett, D. (2022). *OAuth 2.0 authorization server issuer identification* (RFC 9207). https://doi.org/10.17487/RFC9207 - Internet Engineering Task Force. (2025). *OAuth 2.0 protected resource metadata* (RFC 9728). https://doi.org/10.17487/RFC9728 - Model Context Protocol. (2026, July 28). *Authorization*. https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization diff --git a/docs/federation-onboarding.md b/docs/federation-onboarding.md index 28324ae..730aa1f 100644 --- a/docs/federation-onboarding.md +++ b/docs/federation-onboarding.md @@ -120,8 +120,11 @@ curl --config "$AUTH_CONFIG" \ ``` A successful `PUT` persists desired state even when Keycloak is temporarily -unavailable and returns `applied_to_keycloak: false`. This makes the outage -visible without losing the intended configuration. +unavailable and returns `applied_to_keycloak: false`. When it returns +`applied_to_keycloak: true`, Keyverse has freshly re-observed the live +Keycloak identity-provider representation and matched every desired observable +field. This makes mutation or observation drift visible without losing the +intended configuration. ## Convergence and recovery diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 167cb21..9604326 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,6 +1,6 @@ # Keyverse product and technical gap baseline -**Evidence snapshot:** 2026-08-21T14:24:31Z (UTC) +**Evidence snapshot:** 2026-08-21T16:47:10Z (UTC) **Repository:** `ContextualWisdomLab/keyverse` **Protected-main head observed:** `ce207dfd42975db61c82a5963e206fc1db14ac2b` **Status:** live inventory and gap register; not a release acceptance record @@ -59,26 +59,31 @@ success, skipped, or non-terminal results. | PR | Scope | Base | Exact head | Checks | Gate / next safe action | |---:|---|---|---|---|---| -| [#113](https://github.com/ContextualWisdomLab/keyverse/pull/113) | SCIM deactivation shared lock | `ce207dfd42975db61c82a5963e206fc1db14ac2b` | `9bd33ee0d00ef1874fd5efabac3462f678a256ed` | 21 success / 8 skipped / 1 pending | `REVIEW_REQUIRED`; obtain exact-head independent approval and finish the pending review gate. | -| [#112](https://github.com/ContextualWisdomLab/keyverse/pull/112) | Account-unification lockfile and stacked contract updates | `ce207dfd42975db61c82a5963e206fc1db14ac2b` | `31dd486cb97ca215da451151f618a954a07b0ea5` | 0 success / 7 skipped / 14 pending | `REVIEW_REQUIRED`; local evidence is complete, but hosted Checks and independent approval are pending. | -| [#104](https://github.com/ContextualWisdomLab/keyverse/pull/104) | ADR 0001–0007 and operator README expansion | `31dd486cb97ca215da451151f618a954a07b0ea5` | `c623a3d8df6e0f6da0e9623b23e3178e0f0296f0` | 0 success / 0 skipped / 4 pending | Restacked normally; wait for fresh Checks and independent review before merging into the #112 stack. | -| [#103](https://github.com/ContextualWisdomLab/keyverse/pull/103) | Hierarchical authorization, login helper, and PATs | `ce207dfd42975db61c82a5963e206fc1db14ac2b` | `77b8f4ea9995329f1c55b916d110b460b4bc7649` | 20 success / 8 skipped / 1 pending | `REVIEW_REQUIRED`; retain fail-closed security boundary and obtain current approval. | -| [#101](https://github.com/ContextualWisdomLab/keyverse/pull/101) | Coupled Python dependency updates | `ce207dfd42975db61c82a5963e206fc1db14ac2b` | `50dd9c96cab5c230f775685e8baea939fba390dd` | 22 success / 8 skipped / 0 pending | `REVIEW_REQUIRED`; revalidate against the final lockfile stack and obtain approval. | -| [#100](https://github.com/ContextualWisdomLab/keyverse/pull/100) | LineageWeave account-derived RP profile | `ce207dfd42975db61c82a5963e206fc1db14ac2b` | `ede8075f82bb082b7d992b824992bf44792f744e` | 18 success / 7 skipped / 2 pending | `REVIEW_REQUIRED`; downstream issuer/audience/tenant acceptance remains unclaimed. | -| [#83](https://github.com/ContextualWisdomLab/keyverse/pull/83) | Remove runtime application RPs from portable realm | `ce207dfd42975db61c82a5963e206fc1db14ac2b` | `dd1ab7444a75342b42e3af013ccda6d1dbfb359d` | 22 success / 8 skipped / 0 pending | `REVIEW_REQUIRED`; confirm exact-head approval and latest-pusher policy before merge. | +| [#113](https://github.com/ContextualWisdomLab/keyverse/pull/113) | SCIM deactivation shared lock | `ce207dfd42975db61c82a5963e206fc1db14ac2b` | `9bd33ee0d00ef1874fd5efabac3462f678a256ed` | 22 success / 8 skipped | `REVIEW_REQUIRED`; obtain exact-head independent approval. | +| [#112](https://github.com/ContextualWisdomLab/keyverse/pull/112) | Account-unification lockfile and stacked contract updates | `ce207dfd42975db61c82a5963e206fc1db14ac2b` | `44c2adb18687f8df457bd4bafade551533cee5b9` | 14 queued / 7 skipped | `REVIEW_REQUIRED`; six valid unresolved review threads were observed on this head and are being dispositioned; no approval. | +| [#103](https://github.com/ContextualWisdomLab/keyverse/pull/103) | Hierarchical authorization, login helper, and PATs | `ce207dfd42975db61c82a5963e206fc1db14ac2b` | `77b8f4ea9995329f1c55b916d110b460b4bc7649` | 22 success / 8 skipped | `REVIEW_REQUIRED`; retain fail-closed security boundary and obtain current approval. | +| [#101](https://github.com/ContextualWisdomLab/keyverse/pull/101) | Coupled Python dependency updates | `ce207dfd42975db61c82a5963e206fc1db14ac2b` | `50dd9c96cab5c230f775685e8baea939fba390dd` | 22 success / 8 skipped | `REVIEW_REQUIRED`; obtain exact-head approval. | +| [#100](https://github.com/ContextualWisdomLab/keyverse/pull/100) | LineageWeave account-derived RP profile | `ce207dfd42975db61c82a5963e206fc1db14ac2b` | `2fd5a77cf3765f933debd244f457e13241726929` | 14 queued / 7 skipped | `REVIEW_REQUIRED`; downstream issuer/audience/tenant acceptance remains unclaimed. | +| [#83](https://github.com/ContextualWisdomLab/keyverse/pull/83) | Remove runtime application RPs from portable realm | `ce207dfd42975db61c82a5963e206fc1db14ac2b` | `dd1ab7444a75342b42e3af013ccda6d1dbfb359d` | 22 success / 8 skipped | `REVIEW_REQUIRED`; confirm exact-head approval and latest-pusher policy before merge. | + +PR #104 is closed by squash merge at +`44c2adb18687f8df457bd4bafade551533cee5b9`, which advanced the #112 feature +base without changing protected `main`. Its feature-base merge was outside the +default-branch ruleset scope, so it is retained as governance audit evidence, +not as a protected approval or force-merge precedent. The central coordination PR [`.github#1203`](https://github.com/ContextualWisdomLab/.github/pull/1203) -is open at exact head `babb411e5132f67a665e302eb492da69f7d91afb` against -`731af58e954901c4f1cc853231c592abb1eaf617`. Its `scan-pr-queue` run -`32487969901` was cancelled by the normal concurrency scheduler; successor run -`32488287515` is queued. This is not a source failure or D1–D5 emergency -evidence, so no bypass or guarded force merge is allowed. - -This baseline is itself carried by #104. The table records that PR at the -pre-baseline snapshot head `c623a3d8df6e0f6da0e9623b23e3178e0f0296f0`; adding -this baseline advanced it normally to successor `8077aa46e120ea5977464f2e611d44ab44bab695`. -The successor's hosted Checks and review state must be read from the live PR, -and neither snapshot is protected-main evidence. +is open at exact head `94c09152a843db1a0d3a3463900ef4d30467f085` against +`dd58a88391e44a32fb399f7407f508d8e73cc1c7`; `pip-audit` and `strix` are failed +while the provider outage and shared pip root are remediated. Central #1198 is +at `d2490ad594bd2ab8cccd5ff9e0b6f2a3fa8e23d4` with no failed Checks and its +normal auto-merge armed, but it still lacks required approval. Central #1026 +is at `71c0cc890bd06a0ff97aa10267cb075b02c62f9e` with no failed Checks and +running/queued jobs. None supplies D1–D5 emergency evidence. + +This review update is prepared from exact #112 head `44c2adb`; its successor +will invalidate the table's predecessor Checks and review evidence and must be +re-queried before any merge decision. ## Open issue inventory @@ -159,6 +164,8 @@ is claimed. - Lodderstedt, T., Bradley, J., Labunets, A., & Fett, D. (2025). *Best current practice for OAuth 2.0 security* (RFC 9700). https://www.rfc-editor.org/rfc/rfc9700.html - Internet Engineering Task Force. (2018). *OAuth 2.0 authorization server metadata* (RFC 8414). https://doi.org/10.17487/RFC8414 - Internet Engineering Task Force. (2020). *Resource indicators for OAuth 2.0* (RFC 8707). https://doi.org/10.17487/RFC8707 +- Bertocci, V. (2021). *JSON Web Token (JWT) profile for OAuth 2.0 access tokens* (RFC 9068). https://doi.org/10.17487/RFC9068 +- Meyer zu Selhausen, K., & Fett, D. (2022). *OAuth 2.0 authorization server issuer identification* (RFC 9207). https://doi.org/10.17487/RFC9207 - Internet Engineering Task Force. (2025). *OAuth 2.0 protected resource metadata* (RFC 9728). https://doi.org/10.17487/RFC9728 - Model Context Protocol. (2026, July 28). *Authorization*. https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization diff --git a/services/account_unification/app/federation.py b/services/account_unification/app/federation.py index d9ceb9f..d64678f 100644 --- a/services/account_unification/app/federation.py +++ b/services/account_unification/app/federation.py @@ -278,6 +278,15 @@ def _try_apply(self, registration: IdentityProviderRegistration) -> bool: """Attempt convergence and report failure without losing desired state.""" try: self._apply(registration) + observed = self._api.get_identity_provider( + registration.provider_alias + ) + if not _identity_provider_matches(registration, observed): + logger.error( + "identity-provider post-apply observation drift alias=%s", + registration.provider_alias, + ) + return False except Exception: logger.exception( "identity-provider convergence failed alias=%s", @@ -646,6 +655,17 @@ def _to_keycloak_payload( } +def _identity_provider_matches( + registration: IdentityProviderRegistration, + observed: dict | None, +) -> bool: + """Compare every desired observable field with the live representation.""" + if not isinstance(observed, dict): + return False + desired = _to_keycloak_payload(registration) + return all(observed.get(key) == value for key, value in desired.items()) + + federation_router = APIRouter(prefix="/federation", tags=["federation"]) diff --git a/services/account_unification/tests/test_federation.py b/services/account_unification/tests/test_federation.py index d97b85e..b43f0b9 100644 --- a/services/account_unification/tests/test_federation.py +++ b/services/account_unification/tests/test_federation.py @@ -90,6 +90,51 @@ def test_put_updates_existing_provider_in_place(federation, api) -> None: ) +def test_put_requires_exact_live_reobservation_after_apply( + federation, store, api, monkeypatch +) -> None: + """A successful mutation is not enough when live state still differs.""" + registration = _employer_adfs_registration() + observations = iter( + [ + None, + {"alias": "employer-adfs", "enabled": False}, + ] + ) + + def observe_provider(provider_alias: str) -> dict | None: + """Return the pre-apply absence and a mismatched post-apply state.""" + assert provider_alias == "employer-adfs" + return next(observations) + + monkeypatch.setattr(api, "get_identity_provider", observe_provider) + + status = federation.put_registration("employer-adfs", registration) + + assert status.applied_to_keycloak is False + assert store.get(FEDERATION_PROVIDER_NAMESPACE, "employer-adfs") is not None + + +def test_put_requires_live_provider_after_apply( + federation, store, api, monkeypatch +) -> None: + """A provider disappearing after mutation remains unapplied.""" + registration = _employer_adfs_registration() + observations = iter([None, None]) + + def observe_provider(provider_alias: str) -> dict | None: + """Return absence before and after the attempted mutation.""" + assert provider_alias == "employer-adfs" + return next(observations) + + monkeypatch.setattr(api, "get_identity_provider", observe_provider) + + status = federation.put_registration("employer-adfs", registration) + + assert status.applied_to_keycloak is False + assert store.get(FEDERATION_PROVIDER_NAMESPACE, "employer-adfs") is not None + + def test_put_retains_desired_state_when_keycloak_is_unavailable( federation, store, api, monkeypatch ) -> None: diff --git a/tests/test_documentation_contract.py b/tests/test_documentation_contract.py index 6d8ab7b..b472c59 100644 --- a/tests/test_documentation_contract.py +++ b/tests/test_documentation_contract.py @@ -101,16 +101,42 @@ def test_mcp_authorization_contract_tracks_current_issuer_and_token_rules() -> N changelog = _read("CHANGELOG.md") for text in (adr, doctoring): normalized = " ".join(text.split()) - assert "authorization_response_iss_parameter_supported" in text + assert "`authorization_response_iss_parameter_supported=true`" in normalized assert "simple string comparison" in normalized - assert "at+jwt" in text - assert "application/at+jwt" in text + assert "`at+jwt`" in normalized + assert "`application/at+jwt`" in normalized assert "alg=none" in text assert "missing `iat`/`jti`" in text assert "MCP Authorization 2026-07-28" in traceability assert "RFC 9207" in traceability assert "mismatch rejects the authorization code" in traceability - assert "MCP Authorization\n 2026-07-28" in changelog + assert "MCP Authorization 2026-07-28" in " ".join(changelog.split()) + + +def test_baseline_carries_mcp_reference_and_current_rp_checklist() -> None: + """Keep product evidence and README guidance aligned with standards.""" + + baseline = _read("docs/product-technical-gap-baseline.md") + doctoring = _read("docs/doctoring/product-technical-gap-baseline.md") + readme = _read("README.md") + for text in (baseline, doctoring): + assert "RFC 9068" in text + assert "RFC 9207" in text + normalized_readme = " ".join(readme.split()) + for requirement in ( + "issuer", + "signature", + "allowed algorithm", + "audience", + "subject", + "expiry", + "iat", + "exact resource", + "tenant", + "purpose", + ): + assert requirement in normalized_readme + assert "before applying its own access-control policy" in normalized_readme def test_adr_index_contains_governing_identity_decisions() -> None: From 49d2fb9ee31ab298e2d6856558b0916638303824 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 01:57:54 +0900 Subject: [PATCH 14/17] ci: pin documentation contract environment --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6518166..655ff06 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: run: uv run python -m compileall -q app tests tools - name: Test repository documentation contracts working-directory: ${{ github.workspace }} - run: uv run --project services/account_unification pytest tests/test_documentation_contract.py -q + run: uv run --locked --project services/account_unification --extra dev pytest tests/test_documentation_contract.py -q - name: Test and enforce production coverage run: | uv run coverage run --branch --source=app -m pytest -q From f2aeb9044922bbd6510f548512c42741fcebdc4a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 02:01:13 +0900 Subject: [PATCH 15/17] fix: tolerate keycloak default provider fields --- .../account_unification/app/federation.py | 14 ++++++++++++- .../tests/test_federation.py | 21 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/services/account_unification/app/federation.py b/services/account_unification/app/federation.py index d64678f..d6e9c75 100644 --- a/services/account_unification/app/federation.py +++ b/services/account_unification/app/federation.py @@ -663,7 +663,19 @@ def _identity_provider_matches( if not isinstance(observed, dict): return False desired = _to_keycloak_payload(registration) - return all(observed.get(key) == value for key, value in desired.items()) + observed_config = observed.get("config") + return ( + all( + observed.get(key) == value + for key, value in desired.items() + if key != "config" + ) + and isinstance(observed_config, dict) + and all( + observed_config.get(key) == value + for key, value in desired["config"].items() + ) + ) federation_router = APIRouter(prefix="/federation", tags=["federation"]) diff --git a/services/account_unification/tests/test_federation.py b/services/account_unification/tests/test_federation.py index b43f0b9..6f1587c 100644 --- a/services/account_unification/tests/test_federation.py +++ b/services/account_unification/tests/test_federation.py @@ -135,6 +135,27 @@ def observe_provider(provider_alias: str) -> dict | None: assert store.get(FEDERATION_PROVIDER_NAMESPACE, "employer-adfs") is not None +def test_put_accepts_keycloak_default_config_fields( + federation, api, monkeypatch +) -> None: + """Vendor defaults do not invalidate fields owned by desired state.""" + registration = _employer_adfs_registration() + original_get = api.get_identity_provider + + def observe_provider(provider_alias: str) -> dict | None: + """Return the live payload with a vendor-owned default field.""" + observed = original_get(provider_alias) + if observed is not None: + observed["config"]["keycloakDefaultOption"] = "IMPORT" + return observed + + monkeypatch.setattr(api, "get_identity_provider", observe_provider) + + status = federation.put_registration("employer-adfs", registration) + + assert status.applied_to_keycloak is True + + def test_put_retains_desired_state_when_keycloak_is_unavailable( federation, store, api, monkeypatch ) -> None: From 567a4c963f231c4ef70bda486af3a9e187682fb9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 02:02:56 +0900 Subject: [PATCH 16/17] fix: reuse exact state checks for federation reads --- .../account_unification/app/federation.py | 6 +++--- .../tests/test_federation.py | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/services/account_unification/app/federation.py b/services/account_unification/app/federation.py index d6e9c75..0b41c2a 100644 --- a/services/account_unification/app/federation.py +++ b/services/account_unification/app/federation.py @@ -304,11 +304,11 @@ def _status_for( """Build a redacted status, tolerating temporary Keycloak outages.""" if applied is None: try: - applied = ( + applied = _identity_provider_matches( + registration, self._api.get_identity_provider( registration.provider_alias - ) - is not None + ), ) except Exception: logger.warning( diff --git a/services/account_unification/tests/test_federation.py b/services/account_unification/tests/test_federation.py index 6f1587c..f21b4c8 100644 --- a/services/account_unification/tests/test_federation.py +++ b/services/account_unification/tests/test_federation.py @@ -156,6 +156,27 @@ def observe_provider(provider_alias: str) -> dict | None: assert status.applied_to_keycloak is True +def test_get_and_list_report_observable_drift( + federation, api, monkeypatch +) -> None: + """Read paths use the same exact live-state contract as apply.""" + registration = _employer_adfs_registration() + federation.put_registration("employer-adfs", registration) + original_get = api.get_identity_provider + + def observe_drift(provider_alias: str) -> dict | None: + """Return a present provider whose enabled field drifted.""" + observed = original_get(provider_alias) + if observed is not None: + observed["enabled"] = False + return observed + + monkeypatch.setattr(api, "get_identity_provider", observe_drift) + + assert federation.get_registration("employer-adfs").applied_to_keycloak is False + assert federation.list_registrations()[0].applied_to_keycloak is False + + def test_put_retains_desired_state_when_keycloak_is_unavailable( federation, store, api, monkeypatch ) -> None: From ec34ac14fd38c9c7c463cddbd0ced04b4dfccafd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 02:15:26 +0900 Subject: [PATCH 17/17] fix: handle masked federation client secrets --- ARCHITECTURE.md | 4 +++ CHANGELOG.md | 4 ++- docs/doctoring/federation-desired-state.md | 29 +++++++++++++++++++ docs/federation-onboarding.md | 7 +++-- .../account_unification/app/federation.py | 20 +++++++++++-- .../tests/test_oidc_federation_preflight.py | 26 +++++++++++++++++ 6 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 docs/doctoring/federation-desired-state.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 8a54a23..bab4518 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -114,6 +114,10 @@ private rendered payload Validation never fetches metadata or discovery documents. Deployment egress policy and Keycloak perform remote interaction only after explicit apply. +Reconciliation compares desired observable fields exactly; the fixed Keycloak +mask for the known non-observable `clientSecret` field is the sole exception and +does not prove secret equality. Missing, changed, or unknown fields remain +drift. ### LDAP and Active Directory diff --git a/CHANGELOG.md b/CHANGELOG.md index 39bcacb..5916812 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,7 +57,9 @@ Keep a Changelog, and releases use semantic versioning. - Federation PUT and apply now report `applied_to_keycloak: true` only after a fresh live Keycloak identity-provider observation matches the desired - representation; mutation or observation drift retains the desired state for + observable representation. Keycloak's fixed mask for the known + non-observable `clientSecret` field is accepted without claiming secret + equality; mutation or any other observation drift retains desired state for retry. - Buyer README and accepted ADRs 0001–0007 now describe Keyverse as a standalone identity leaf/hub, point operators at published OIDC/OAuth diff --git a/docs/doctoring/federation-desired-state.md b/docs/doctoring/federation-desired-state.md new file mode 100644 index 0000000..8b8c6e0 --- /dev/null +++ b/docs/doctoring/federation-desired-state.md @@ -0,0 +1,29 @@ +# Federation Desired-State Reconciliation — Doctoring Record + +## Decision + +Keyverse reports a federation provider as applied only after a fresh Keycloak +identity-provider representation matches every desired observable field. The +one deliberate non-observable exception is the known `clientSecret` field when +Keycloak returns its fixed mask. This accepts the vendor read-back boundary but +does not claim plaintext secret equality. A missing secret, a different mask, an +unknown secret-bearing key, or any changed observable field remains drift. + +## Evidence and limits + +The repository regression test simulates the live read-back mask after a +successful OIDC provider mutation and verifies `applied_to_keycloak: true`. +Negative drift tests continue to reject missing providers, changed fields, and +unrecognized configuration. The Keycloak Admin REST contract identifies the +identity-provider instance representation used by this comparison; the mask +behavior is recorded here as measured adapter evidence, not as a broader +Keycloak conformance claim. + +The comparison never logs or returns the configured secret. It only recognizes +the fixed mask for the single validated key, so the secret-management system +remains responsible for provisioning, rotation, and equality evidence. + +## Reference — APA 7th + +Keycloak. (n.d.). *Keycloak Admin REST API*. Retrieved August 22, 2026, from +https://www.keycloak.org/docs-api/latest/rest-api/index.html diff --git a/docs/federation-onboarding.md b/docs/federation-onboarding.md index 730aa1f..b8e5a3b 100644 --- a/docs/federation-onboarding.md +++ b/docs/federation-onboarding.md @@ -123,8 +123,11 @@ A successful `PUT` persists desired state even when Keycloak is temporarily unavailable and returns `applied_to_keycloak: false`. When it returns `applied_to_keycloak: true`, Keyverse has freshly re-observed the live Keycloak identity-provider representation and matched every desired observable -field. This makes mutation or observation drift visible without losing the -intended configuration. +field. Keycloak masks the known non-observable `clientSecret` value on +read-back; its fixed mask is accepted for that field only and does not prove +secret equality. Missing, changed, or unknown fields remain drift. This makes +mutation or observation drift visible without losing the intended +configuration. ## Convergence and recovery diff --git a/services/account_unification/app/federation.py b/services/account_unification/app/federation.py index 0b41c2a..54b3569 100644 --- a/services/account_unification/app/federation.py +++ b/services/account_unification/app/federation.py @@ -32,6 +32,8 @@ _MAX_PROVIDER_CONFIG_KEY_LENGTH = 128 _MAX_PROVIDER_CONFIG_VALUE_LENGTH = 16_384 _REDACTED_VALUE = "" +_KEYCLOAK_MASKED_VALUE = "**********" +_NON_OBSERVABLE_PROVIDER_CONFIG_KEYS = frozenset({"clientSecret"}) _ALIAS_ALPHABET = frozenset("abcdefghijklmnopqrstuvwxyz0123456789-") _ALIAS_EDGE_ALPHABET = frozenset("abcdefghijklmnopqrstuvwxyz0123456789") _HTTP_SCHEMES = frozenset({"http", "https"}) @@ -671,10 +673,22 @@ def _identity_provider_matches( if key != "config" ) and isinstance(observed_config, dict) - and all( - observed_config.get(key) == value - for key, value in desired["config"].items() + and _provider_config_matches(desired["config"], observed_config) + ) + + +def _provider_config_matches( + desired_config: dict[str, str], + observed_config: dict, +) -> bool: + """Compare config while preserving Keycloak's known secret mask boundary.""" + return all( + observed_config.get(key) == value + or ( + key in _NON_OBSERVABLE_PROVIDER_CONFIG_KEYS + and observed_config.get(key) == _KEYCLOAK_MASKED_VALUE ) + for key, value in desired_config.items() ) diff --git a/services/account_unification/tests/test_oidc_federation_preflight.py b/services/account_unification/tests/test_oidc_federation_preflight.py index 89c9e0e..5b8e784 100644 --- a/services/account_unification/tests/test_oidc_federation_preflight.py +++ b/services/account_unification/tests/test_oidc_federation_preflight.py @@ -114,6 +114,32 @@ def test_oidc_preflight_allows_optional_endpoints_to_be_absent( _assert_no_side_effects(store, api) +def test_oidc_put_accepts_keycloak_masked_client_secret_readback(api) -> None: + """A masked Keycloak client secret remains an applied provider state.""" + store = InMemoryKvStore() + federation = FederationService(store, api) + registration = IdentityProviderRegistration.model_validate(_oidc_body()) + original_get = api.get_identity_provider + + def observe_with_mask(provider_alias: str) -> dict | None: + """Return live state with Keycloak's non-observable secret marker.""" + observed = original_get(provider_alias) + if observed is not None: + config = observed.get("config") + assert isinstance(config, dict) + config["clientSecret"] = "**********" + return observed + + api.get_identity_provider = observe_with_mask + + status = federation.put_registration( + registration.provider_alias, + registration, + ) + + assert status.applied_to_keycloak is True + + @pytest.mark.parametrize( "required_field", [