diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a1eca6..655ff06 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,9 +26,9 @@ 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. + # Keep cache pruning enabled to bound runner disk usage. prune-cache: true - name: Install locked dependencies run: uv sync --locked --extra dev @@ -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 --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 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 diff --git a/.github/workflows/hourly-product-development.yml b/.github/workflows/hourly-product-development.yml index a612bdc..0a15d1d 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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f730590..f68965d 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 e06bd8e..4639f1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,24 @@ 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 + 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 + 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..1961d5e 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,101 @@ 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 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)). -### 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 ee831fb..61c5e18 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 | |---|---|---|---| @@ -20,12 +20,14 @@ | 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 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 | | 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..ece808f 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 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). + +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..4b60d58 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 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). 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 + +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/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/0013-mcp-oauth-client-authorization.md b/docs/adr/0013-mcp-oauth-client-authorization.md new file mode 100644 index 0000000..108ab26 --- /dev/null +++ b/docs/adr/0013-mcp-oauth-client-authorization.md @@ -0,0 +1,234 @@ +# 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 (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 + +### 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. + +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 +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 | +| 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 | + +## 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 02dd751..aad6a6a 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 | @@ -12,6 +17,14 @@ | [0006](0006-user-operation-lock.md) | Merge/link, SCIM full replacement, and supported `PATCH active=false` 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/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/doctoring/mcp-oauth-authorization.md b/docs/doctoring/mcp-oauth-authorization.md new file mode 100644 index 0000000..0060931 --- /dev/null +++ b/docs/doctoring/mcp-oauth-authorization.md @@ -0,0 +1,252 @@ +# 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 + 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 + +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. + +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 +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; +- 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; +- 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 + +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. (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 + +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. diff --git a/docs/doctoring/product-technical-gap-baseline.md b/docs/doctoring/product-technical-gap-baseline.md new file mode 100644 index 0000000..b104e7c --- /dev/null +++ b/docs/doctoring/product-technical-gap-baseline.md @@ -0,0 +1,72 @@ +# Product-technical gap baseline doctoring record + +**Date:** 2026-08-21T16:47:10Z +**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 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. +- 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 +- 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..b8e5a3b 100644 --- a/docs/federation-onboarding.md +++ b/docs/federation-onboarding.md @@ -120,8 +120,14 @@ 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. 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/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/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md new file mode 100644 index 0000000..9604326 --- /dev/null +++ b/docs/product-technical-gap-baseline.md @@ -0,0 +1,173 @@ +# Keyverse product and technical gap baseline + +**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 + +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` | 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 `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 + +| 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 +- 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 + +Interpretations and evidence boundaries are maintained in +[`docs/doctoring/product-technical-gap-baseline.md`](doctoring/product-technical-gap-baseline.md). diff --git a/services/account_unification/app/federation.py b/services/account_unification/app/federation.py index d9ceb9f..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"}) @@ -278,6 +280,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", @@ -295,11 +306,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( @@ -646,6 +657,41 @@ 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) + 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 _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() + ) + + federation_router = APIRouter(prefix="/federation", tags=["federation"]) diff --git a/services/account_unification/pyproject.toml b/services/account_unification/pyproject.toml index 3afc27f..79d5e39 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] @@ -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 a1f2d38..e071985 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 \ @@ -515,33 +501,33 @@ 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 \ --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 \ @@ -564,13 +550,13 @@ 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 -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..c826b76 100644 --- a/services/account_unification/requirements.lock +++ b/services/account_unification/requirements.lock @@ -358,13 +358,13 @@ 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 -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/tests/test_federation.py b/services/account_unification/tests/test_federation.py index d97b85e..f21b4c8 100644 --- a/services/account_unification/tests/test_federation.py +++ b/services/account_unification/tests/test_federation.py @@ -90,6 +90,93 @@ 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_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_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: 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", [ diff --git a/services/account_unification/uv.lock b/services/account_unification/uv.lock index bf57988..09330e5 100644 --- a/services/account_unification/uv.lock +++ b/services/account_unification/uv.lock @@ -374,9 +374,9 @@ 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.1" }, + { name = "uvicorn", specifier = "==0.52.3" }, ] provides-extras = ["dev"] @@ -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]] @@ -805,25 +805,25 @@ 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]] 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" }, ] diff --git a/tests/test_documentation_contract.py b/tests/test_documentation_contract.py index 47ef4cd..b472c59 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", @@ -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", @@ -90,6 +92,53 @@ 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=true`" in normalized + assert "simple string comparison" in normalized + 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 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: """Keep every indexed architecture decision present and reviewable."""