Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 34 additions & 5 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,18 @@ application relying-party registration.
- SAML/OIDC identity-provider desired-state validation and reconciliation;
- LDAP/Active Directory component preflight and desired-state reconciliation;
- OIDC relying-party preflight and secret-free desired-state reconciliation;
- audit and user-operation lock boundaries.
- audit and user-operation lock boundaries;
- hierarchical software-unit, menu, inheritance, and SSO-combination
authorization decisions consumed from Orgmetra assignment snapshots;
- app start-login / IdP discovery helper for relying parties;
- hashed programmable application tokens scoped to one software unit and API.

The hierarchical authorization router carries the existing operator bearer and
privileged-path dependencies itself, so an embedding application cannot make
grant administration public by mounting the module without the application
factory's outer dependency list. The operator credential is a coarse
operator-admin boundary; ``actor_identity_id`` on a grant is policy metadata,
not an end-user principal extracted from that bearer request.

The core merge and SCIM layer depends on the narrow `AdminApi` protocol.
Product extensions are isolated behind `ProductAdminApi`; relying-party client
Expand Down Expand Up @@ -93,7 +104,11 @@ modules require neither protocol nor any network client.
`relying_party_sources`, `relying_party_apply_receipts`;
- merge audit: `account_merge_audit`;
- cross-process user mutation lock sidecar:
`user_operation_lock_state`.
`user_operation_lock_state`;
- hierarchical authorization grants:
`authorization_software_unit_grants`, `authorization_menu_grants`;
- SSO combination scopes: `authorization_sso_combination_scopes`;
- hashed programmable tokens: `application_access_tokens`.

Database objects and namespaces use descriptive two-word-or-longer snake_case
names.
Expand Down Expand Up @@ -178,9 +193,21 @@ profiles.
Each downstream RP is a separate trust boundary. The RP must validate the
Keyverse issuer, signature/algorithm, expiry, subject, and audience, map the
verified tenant (`org`/deployment mapping), apply resource and purpose ABAC,
and then apply bounded role/scope/group RBAC. A registered client or accepted
mapper receipt never grants authorization by itself; see ADR-0008 for the
non-fork application matrix and remediation gates.
and then apply bounded role/scope/group RBAC. A registered client, accepted
mapper receipt, or Keyverse PDP decision never grants authorization by itself;
see ADR-0008 for the non-fork application matrix and remediation gates.
ADR-0010 adds issuer-side hierarchical attributes (`group_company`,
`legal_entity`, `business_unit`, `team`, `person`, `org_path`) and decisions.
Those names are distinct from the unmerged LineageWeave `role`/`org`/`workspace`
profile reserved as ADR-0009 on PR #100. Orgmetra remains employment truth;
Keyverse binds an opaque subject and does not copy the Orgmetra tree.

Relying applications start brokered login through the Keyverse start-login
helper (ADR-0011) and may present software-unit-scoped programmable tokens
(ADR-0012) that are hashed at rest, never inherit org-tree grants, and cannot be
rotated after revocation, prior rotation, or expiry. Rotation writes the
replacement and predecessor through one KV-store transaction, then compensates
the pair through one atomic upsert/delete operation if audit persistence fails.

## Account and provisioning invariants

Expand Down Expand Up @@ -246,6 +273,8 @@ explicitly documented deployment-controller responsibility.

Detailed decisions and evidence are maintained under:

- `docs/adr/` — accepted architecture decisions (0001–0008 plus 0010–0012;
0009 reserved for the unmerged LineageWeave profile);
- `docs/superpowers/specs/` — approved feature architecture;
- `docs/superpowers/plans/` — executable implementation plans;
- `docs/doctoring/` — standards interpretation and APA 7th traceability;
Expand Down
49 changes: 49 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,31 @@ Keep a Changelog, and releases use semantic versioning.

### Added

- Hierarchical authorization plane (ADR-0010): software-unit ACL, menu
ABAC/RBAC decisions, SSO combination scopes, and most-specific org-path
inheritance consumed from Orgmetra assignment snapshots. ADR-0008 stays
the PEP boundary. Hierarchical attributes use `group_company`,
`legal_entity`, `business_unit`, `team`, `person`, and `org_path` so they
do not collide with the unmerged LineageWeave `role`/`org`/`workspace`
profile reserved as ADR-0009 on PR #100.
- App start-login helper (ADR-0011) that discovers enabled brokered IdPs from
the local registry and returns a Keycloak `kc_idp_hint` authorization URL
without metadata or discovery fetch; encoded discovery markers are rejected
after URL normalization at the same boundary.
- Programmable application tokens (ADR-0012) hashed at rest, purpose-bound,
software-unit and API scoped, rotatable, auditable, and never a password
substitute or inherited secret; failed issue/rotation audit and storage
writes are compensated, and runtime verification uses a separate service
credential.
- Authorization decisions now require a tenant-qualified assignment snapshot;
grant matching, ABAC constraints, duplicate identity, and KV keys preserve
tenant boundaries.
- Authorization decision metadata now marks strict menu-prefix inheritance
correctly when the org path is an exact match.
- Ambiguous same-named authorization grants can now be read or deleted through
an explicit tenant-scoped GET/DELETE query without weakening fail-closed
behavior.
- Start-login issuer input is bound to configured Keyverse public issuer state.
- ADR-0008 and the non-fork RP authorization matrix, requiring explicit
Keyverse token validation, tenant/resource ABAC, bounded RBAC, and
cross-tenant acceptance evidence per application.
Expand Down Expand Up @@ -55,6 +80,21 @@ Keep a Changelog, and releases use semantic versioning.

### Changed

- Authorization decisions now require explicit tenant-bound assignment
snapshots, grants, SSO combinations, and application-token verification;
software-unit grants reject menu-only ABAC constraints.
- Start-login now uses a configured Keycloak public issuer and is exposed as a
front-channel runtime helper, while PAT management remains operator-gated
and PAT verification remains token-gated.
- Application-token rotation now persists the replacement and rotated
predecessor through one atomic KV-store batch before recording the audit
event, and restores that pair with one atomic upsert/delete compensation if
audit persistence fails.

- The hierarchical authorization router now carries its operator-authentication
and privileged-path dependencies at the module boundary, so direct CWL/Naruon
embedding cannot accidentally mount grant administration without the existing
operator gate.
- 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
Expand Down Expand Up @@ -89,6 +129,12 @@ Keep a Changelog, and releases use semantic versioning.

### Fixed

- Application-token rotation now rejects revoked, already-rotated, and expired
predecessors instead of reviving retired credentials.
- Prevented cross-tenant authorization selection, SSO-name collisions, PAT
tenant confusion, expired-token revival, and audit-failure state leakage;
start-login can no longer reflect an attacker-selected issuer.

- Prevented relying-party inventory from silently accepting a KV key/body
identity mismatch, rejected unsafe live or `Location`-derived client UUIDs,
and aligned exact client discovery with Keycloak's documented
Expand Down Expand Up @@ -118,6 +164,9 @@ Keep a Changelog, and releases use semantic versioning.
state storage lock is held.
- Prevented unknown federation configuration keys, credentials, and private
values from being echoed through list, get, or update responses.
- Application-token rotation now validates the replacement purpose,
capabilities, lifetime, and software-unit binding before revoking the
active token, so invalid rotation requests preserve the working credential.
- Rejected Unicode-confusable federation aliases outside the explicit ASCII
slug alphabet.
- Rejected raw C0 controls, DEL, invalid ports, insecure HTTP SSO or metadata
Expand Down
1 change: 1 addition & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Keyverse already has strong feature-specific specifications, doctoring, federati
| Requirements/evidence traceability | [`docs/TRACEABILITY.md`](docs/TRACEABILITY.md) |
| Architecture decisions | [`docs/adr/README.md`](docs/adr/README.md) |
| Federation onboarding | [`docs/federation-onboarding.md`](docs/federation-onboarding.md) |
| Authorization onboarding | [`docs/authorization-onboarding.md`](docs/authorization-onboarding.md) |
| RP onboarding | [`docs/rp-onboarding.md`](docs/rp-onboarding.md) |
| Account merge/unification | [`docs/merge-unification-flow.md`](docs/merge-unification-flow.md) |
| Standards/APA 7 evidence | [`docs/doctoring/`](docs/doctoring/) and [`docs/papers/`](docs/papers/) |
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,17 @@ 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).

An application backend starts brokered login through
`POST /federation/identity-providers:start-login` with the separately
provisioned `X-Keyverse-Runtime-Token`; it then adds PKCE locally. The operator
Bearer token is reserved for grant and token-management endpoints. See
[`docs/authorization-onboarding.md`](docs/authorization-onboarding.md).

### Onboard a relying party

See [`docs/rp-onboarding.md`](docs/rp-onboarding.md).
See [`docs/rp-onboarding.md`](docs/rp-onboarding.md). Software-unit ACL, menu
decisions, SSO combinations, and programmable application tokens are documented
in [`docs/authorization-onboarding.md`](docs/authorization-onboarding.md).

## Account unification & merge

Expand Down
4 changes: 3 additions & 1 deletion deploy/bootstrap/bootstrap.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
# Copy to bootstrap.yaml and mount read-only at /bootstrap/bootstrap.yaml.
# The store is expected to hold, under the namespace below, the Keycloak wiring
# keys: keycloak_server_url, keycloak_realm, keycloak_client_id,
# keycloak_client_secret (+ merge_conflict_policy, allow_unverified_email_link).
# keycloak_client_secret, operator_api_token. Optional keys include
# public_issuer_url and a distinct runtime_api_token for app-facing start-login
# and token verification.

config_store:
# Backends: "sqlite" (dev/standalone), "postgres" (prod), "env-kv" (12-factor
Expand Down
7 changes: 7 additions & 0 deletions deploy/keycloak/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ following account-unification KV entries:
- `registration_redirect_uri`
- `registration_action_lifespan_seconds`

Application backends use a separate optional `runtime_api_token` for
start-login and programmable-token verification. It must differ from both
`operator_api_token` and `registration_api_token`; do not put it in browser
code or source-controlled templates. Set `public_issuer_url` when the public
Keycloak issuer differs from the service URL so start-login cannot redirect to
an untrusted host.

Without the registration token the endpoint is unavailable rather than open.
See [`../../docs/passwordless-policy.md`](../../docs/passwordless-policy.md).

Expand Down
71 changes: 68 additions & 3 deletions docs/ERD.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
# Keyverse Logical and Persistence ERD

**Status:** Accepted cross-cutting data model. Exact Keycloak internal schema remains Keycloak-owned.
**Last reviewed:** 2026-08-09
**Last reviewed:** 2026-08-18

Keyverse persists its own configuration, desired-state, receipts, merge audit, and user-operation locks while Keycloak/PostgreSQL owns canonical IdP users/sessions/clients/federation runtime state. This ERD models Keyverse-owned durable records and their relation to external Keycloak identities without pretending to own Keycloak's internal tables.
Keyverse persists its own configuration, desired-state, receipts, merge audit, user-operation locks, authorization grants, SSO combination scopes, and hashed application tokens while Keycloak/PostgreSQL owns canonical IdP users/sessions/clients/federation runtime state. Orgmetra remains the employment-tree system of record; Keyverse stores grants against org-path nodes and does not persist Orgmetra `organization_unit` rows as source of record. This ERD models Keyverse-owned durable records and their relation to external Keycloak identities without pretending to own Keycloak's internal tables.

```mermaid
erDiagram
IDP_CONFIG_ENTRY }o--|| TENANT_DEPLOYMENT : scoped_to
FEDERATION_SOURCE }o--|| TENANT_DEPLOYMENT : scoped_to
DIRECTORY_FEDERATION_SOURCE }o--|| TENANT_DEPLOYMENT : scoped_to
RELYING_PARTY_SOURCE }o--|| TENANT_DEPLOYMENT : scoped_to
AUTHORIZATION_SOFTWARE_UNIT_GRANT }o--|| TENANT_DEPLOYMENT : scoped_to
AUTHORIZATION_MENU_GRANT }o--|| TENANT_DEPLOYMENT : scoped_to
SSO_COMBINATION_SCOPE }o--|| TENANT_DEPLOYMENT : scoped_to
APPLICATION_ACCESS_TOKEN }o--|| TENANT_DEPLOYMENT : scoped_to

FEDERATION_SOURCE ||--o{ FEDERATION_APPLY_RECEIPT : produces
DIRECTORY_FEDERATION_SOURCE ||--o{ DIRECTORY_FEDERATION_APPLY_RECEIPT : produces
Expand Down Expand Up @@ -136,18 +140,71 @@ erDiagram
timestamptz acquired_at
timestamptz lease_expires_at
}

AUTHORIZATION_SOFTWARE_UNIT_GRANT {
text grant_key PK
text tenant_deployment_id FK
text org_path
text software_unit_id
text effect_code
jsonb capability_codes
jsonb attribute_constraints
text actor_identity_id
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

AUTHORIZATION_MENU_GRANT {
text grant_key PK
text tenant_deployment_id FK
text org_path
text software_unit_id
text menu_path
text effect_code
jsonb capability_codes
jsonb attribute_constraints
text actor_identity_id
}

SSO_COMBINATION_SCOPE {
text combination_name
text tenant_deployment_id FK
jsonb software_unit_ids
text actor_identity_id
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

APPLICATION_ACCESS_TOKEN {
text application_token_id
text tenant_deployment_id FK
text software_unit_id
text token_prefix
text token_hash
text purpose_code
jsonb capability_codes
text lifecycle_status_code
timestamptz expires_at
timestamptz created_at
timestamptz revoked_at
text actor_identity_id
text replaced_token_id
}
```

## Logical uniqueness constraints

UUID primary identifiers are globally unique. Human/provider identifiers are scoped to the owning tenant or federation source and MUST NOT be interpreted as global keys.
UUID primary identifiers are globally unique. The authorization-plane identifiers
are tenant-qualified text keys; `tenant_deployment_id` is a validated lowercase
slug, not a UUID. Human/provider identifiers are scoped to the owning tenant or
federation source and MUST NOT be interpreted as global keys.

| Entity | Required logical uniqueness |
|---|---|
| `IDP_CONFIG_ENTRY` | `(tenant_deployment_id, config_key)` |
| `FEDERATION_SOURCE` | `(tenant_deployment_id, federation_alias)` |
| `DIRECTORY_FEDERATION_SOURCE` | `(tenant_deployment_id, directory_alias)` |
| `RELYING_PARTY_SOURCE` | `(tenant_deployment_id, client_id)` |
| `AUTHORIZATION_SOFTWARE_UNIT_GRANT` | `(tenant_deployment_id, org_path, software_unit_id)` |
| `AUTHORIZATION_MENU_GRANT` | `(tenant_deployment_id, org_path, software_unit_id, menu_path)` |
| `SSO_COMBINATION_SCOPE` | `(tenant_deployment_id, combination_name)` |
| `APPLICATION_ACCESS_TOKEN` | `(tenant_deployment_id, application_token_id)` and unique `token_hash` |
| `KEYCLOAK_USER_REFERENCE` | `(tenant_deployment_id, keycloak_user_uuid)` |
| `EXTERNAL_IDENTITY_LINK` | `(federation_source_id, external_subject_hash)` |

Expand Down Expand Up @@ -177,7 +234,15 @@ or documentation labels are bypassed.
- Keycloak UUIDs, federation aliases, RP client IDs, email values, and external subjects are data identifiers, not authorization by themselves.
- Exact external identity key is `(identity_provider, subject)`; verified email may support matching under policy but unverified email never authorizes linking.
- `tenant_deployment_id` is explicit in Keyverse-owned records; deployment/customer separation must not be inferred from realm/resource names.
- The non-persisted `AssignmentSnapshot` decision input also requires an
explicit `tenant_deployment_id`. Software-unit `attribute_constraints` are
intentionally absent: ABAC belongs to `AUTHORIZATION_MENU_GRANT`; a
software-unit payload carrying those constraints is rejected.
- Secrets are referenced through protected values/handles where possible; secret-free desired-state tables must never gain client/bind credentials accidentally.
- Application access tokens store the hashed secret and lifecycle metadata only;
plaintext tokens and org-tree secrets never appear on grant or combination
rows. KV storage keys for grants and combinations are tenant-qualified.
- Hierarchical grant paths use `group_company` / `legal_entity` / `business_unit` / `team` / `person`. They do not persist Orgmetra trees and do not reuse LineageWeave `role` / `org` / `workspace` claim names.

## Desired-state and receipt invariant

Expand Down
24 changes: 23 additions & 1 deletion docs/OPERABILITY.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Keyverse Operability, Recovery, and Release Guide

**Status:** Accepted cross-cutting operating baseline
**Last reviewed:** 2026-08-11
**Last reviewed:** 2026-08-18

Feature-specific procedures under `docs/operations/`, federation/RP onboarding, and deployment READMEs remain authoritative for their slices. This guide defines the shared operating model and evidence needed before declaring the identity platform healthy or release-ready.

Expand Down Expand Up @@ -66,6 +66,28 @@ must test the **Naruon** product login/token/authorization journey using the
`naruon-web` RP client ID and verify the expected audience and bounded claims.
Mapper unit tests alone do not prove Naruon product authorization readiness.

## Authorization-plane and token runbook

1. Obtain an Orgmetra assignment snapshot for the subject, including its
validated `tenant_deployment_id`; do not copy the Orgmetra tree into
Keyverse.
2. PUT software-unit and menu grants at the intended org-path node.
3. PUT an SSO combination when several RPs should share one session.
4. Call the matching `:decide` endpoint and keep the RP as PEP.
5. For app login, the application backend calls
`POST /federation/identity-providers:start-login` with the separately
provisioned `X-Keyverse-Runtime-Token`, then adds PKCE locally and
redirects. Do not fetch IdP metadata from the app.
6. An operator calls `POST /application-tokens` with the operator bearer,
stores the one-time plaintext response in the relying application's secret
manager, and discards the response. The application presents that PAT to
`POST /application-tokens:verify` with the runtime service token. Rotate an
active, unexpired token or revoke it; retired and expired tokens cannot be
revived. Never share a password.

See `docs/authorization-onboarding.md` and
`docs/operations/authorization-plane.md`.

## Account merge recovery

Merge and SCIM full replacement (`PUT`) must hold the shared operation lock. Protected-main `PATCH active=false` is not currently inside that shared-lock guarantee and must not be treated as transactionally serialized with merge. On failure, classify whether state changed in Keycloak, Keyverse audit, linked identities, or tombstone status. Re-observe before retry. Never infer a retry is safe solely from the previous HTTP response. Preserve survivor and duplicate lineage in audit.
Expand Down
Loading
Loading