Skip to content
Open
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
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ jobs:
run: |
uv run coverage run --branch --source=app -m pytest -q
uv run coverage report --show-missing --fail-under=100
uv run coverage erase
uv run coverage run --branch -m pytest -q tests/test_validate_realm.py
uv run coverage report --include='*/scripts/validate_realm.py' --show-missing --fail-under=100
- name: Build distribution
run: uv build --out-dir dist

Expand All @@ -53,7 +56,7 @@ jobs:
with:
python-version: "3.12"
- name: Validate Keycloak realm config-as-code
run: python scripts/validate_realm.py deploy/keycloak/realm-cwl.json
run: python scripts/validate_realm.py deploy/keycloak/cwl-realm.json
- name: Validate deployment template JSON
run: |
python - <<'PY'
Expand Down
11 changes: 7 additions & 4 deletions .github/workflows/hourly-product-development.yml
Original file line number Diff line number Diff line change
Expand Up @@ -498,9 +498,9 @@ jobs:
install -d -m 0750 "$agent_workspace" "$agent_home" "$agent_home/tmp"
git archive HEAD | tar -x -C "$agent_workspace"

cat >"${agent_workspace}/opencode.json" <<'CONFIG'
cat >"${agent_workspace}/opencode.json" <<CONFIG
{
"$schema": "https://opencode.ai/config.json",
"\$schema": "https://opencode.ai/config.json",
"enabled_providers": ["nvidia-nim"],
"lsp": false,
"mcp": {},
Expand Down Expand Up @@ -562,7 +562,7 @@ jobs:
NVIDIA_API_KEY=keyverse-local-broker \
OPENCODE_DISABLE_AUTOUPDATE=1 \
/bin/bash -c \
'ulimit -u 256; ulimit -n 1024; cd "$1"; exec opencode run "$2" --model "$3"' \
"ulimit -u 256; ulimit -n 1024; cd \"\$1\"; exec opencode run \"\$2\" --model \"\$3\"" \
bash "$agent_workspace" "$prompt" "$model"; then
sudo pkill -KILL -u 65532 >/dev/null 2>&1 || true
successful_workspace="$agent_workspace"
Expand Down Expand Up @@ -739,9 +739,12 @@ jobs:
uv run coverage erase
uv run coverage run --branch --source=app -m pytest -q
uv run coverage report --show-missing --fail-under=100
uv run coverage erase
uv run coverage run --branch -m pytest -q tests/test_validate_realm.py
uv run coverage report --include='*/scripts/validate_realm.py' --show-missing --fail-under=100
uv build --out-dir dist
)
python scripts/validate_realm.py deploy/keycloak/realm-cwl.json
python scripts/validate_realm.py deploy/keycloak/cwl-realm.json
docker compose -f docker-compose.yml config >/dev/null
while IFS= read -r -d '' template; do
python -m json.tool "$template" >/dev/null
Expand Down
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ queue owner.
- SAML/OIDC preflight performs no metadata/discovery fetch. LDAP preflight
performs no DNS, socket, bind, search, storage, or Keycloak call.
- OIDC relying-party mapper support stays closed: one self-pinned audience plus
only canonical `role`, `org`, and `workspace` hardcoded claims. Do not add
scripts, user attributes, groups, regex, arbitrary claims, new audiences, or
only canonical `role`, `org`, and `workspace` hardcoded claims, except the
ADR-0009 `lineageweave-web` profile which maps only a same-client account role
and the exact `org`/`workspace` account attributes. Do not add scripts,
other user attributes, groups, regex, arbitrary claims, new audiences, or
extra token destinations without a separately reviewed profile and RED test.
- Treat generated Keycloak mapper IDs and vendor ordering as normalization-only
metadata. Unknown, malformed, duplicate, or semantically changed live mappers
Expand Down
22 changes: 13 additions & 9 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,18 @@ redirect/origin/logout policy, public/confidential client consistency, bounded
token metadata, and an exact portable scope set.

An optional closed `protocolMappers` profile carries exactly one self-pinned
`oidc-audience-mapper` plus zero to three canonical hardcoded claims named
`role`, `org`, and `workspace`. Mapper count, names, classes, destinations,
claim values, and ordering are bounded; scripts, user attributes, groups, regex,
arbitrary claims, unknown fields, and credential material are rejected.
`deploy/templates/oidc-rp-naruon.json` is the reviewed public-client instance of
that profile. Its routing claim values are deployment data and must not contain
credentials or personal secrets.
`oidc-audience-mapper` plus either zero to three canonical hardcoded claims or
the ADR-0009 LineageWeave account-derived trio. The latter is limited to a
same-client `oidc-usermodel-client-role-mapper` for multivalued `role` and two
scalar `oidc-usermodel-attribute-mapper` entries from exact `org` and
`workspace` account attributes. The three dynamic claims must appear together
and cannot mix with hardcoded claims. Mapper count, names, classes,
destinations, claim values, and ordering are bounded; scripts, other user
attributes, groups, regex, arbitrary claims, unknown fields, and credential
material are rejected. `deploy/templates/oidc-rp-naruon.json` is the reviewed
public-client instance of the static profile; `deploy/templates/oidc-rp-lineageweave.json`
is the confidential account-derived contract. Neither template contains a
credential or proves a live account login.

Stateful reconciliation keys intent by validated `clientId`, classifies zero,
one, or multiple exact Keycloak clients, and never mutates duplicates. Create or
Expand All @@ -172,8 +177,7 @@ The accepted representation has no client-secret field; credential provisioning
remains an independent secret-management responsibility.

Native loopback/private-use redirects, different resource audiences, and claim
expansion beyond `role`, `org`, and `workspace` remain separate reviewed
profiles.
expansion beyond the two closed profiles remain separate reviewed profiles.

Each downstream RP is a separate trust boundary. The RP must validate the
Keyverse issuer, signature/algorithm, expiry, subject, and audience, map the
Expand Down
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,24 @@ Keep a Changelog, and releases use semantic versioning.

### Added

- A dated product and technical gap baseline that records the live PR/Issue
queue, exact-head Check evidence, buyer-visible authorization and runtime
acceptance gaps, and the protected hourly loop.
- 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.
- A closed optional OIDC relying-party mapper profile with one self-pinned
access-token audience, bounded `role`, `org`, and `workspace` hardcoded claims,
canonical mapper ordering, Keycloak-generated-ID/order normalization, and a
secret-free `naruon-web` runtime desired-state template.
- ADR-0009's confidential `lineageweave-web` account-derived mapper profile:
same-client roles plus exact scalar `org` and `workspace` account attributes,
with no static/dynamic mixing, a secret-free deployment template, and
reconciliation regression coverage.
- A normative LineageWeave tenant mapping: `org` is the opaque external tenant
key, `workspace` is its child namespace, ambiguous or multi-membership
resolution fails closed, and lifecycle changes require a new token or
session renewal; no generic `tenant` mapper was introduced.
- Durable, secret-free OIDC relying-party desired-state CRUD and reconciliation
with exact `clientId` matching, duplicate fail-closed behavior, post-mutation
re-observation, canonical apply receipts, realm-rebuild recovery, per-client
Expand Down Expand Up @@ -55,6 +66,13 @@ Keep a Changelog, and releases use semantic versioning.

### Changed

- The Helm realm-import operator runbook now migrates the legacy
`realm-cwl.json` ConfigMap key to `cwl-realm.json` before rollout, preserving
a rollback copy and requiring post-rollout realm discovery verification.
- Account-derived OIDC claim mappers are now limited to the ADR-0009
`lineageweave-web` profile, and a non-string observed mapper type is treated
as reconciliation drift rather than causing an exception. Operator guides now
consistently name issued `org` (company) and `workspace` (PU) claims.
- 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 +107,9 @@ Keep a Changelog, and releases use semantic versioning.

### Fixed

- Packaged the portable Keycloak realm under the required `cwl-realm.json`
directory-import name in Compose and mapped it in Helm, with a deployment
contract that prevents a healthy-but-empty identity realm.
- 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
18 changes: 10 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ make ready # poll readiness (deploy/scripts/healthz.sh)
make install # install the admin service development environment
make test # run account-unification unit tests
make lint # run Ruff + interrogate docstring coverage
make validate-realm # validate deploy/keycloak/realm-cwl.json
make validate-realm # validate deploy/keycloak/cwl-realm.json
make seed-bootstrap # create a local SQLite KV bootstrap store
```

Expand Down Expand Up @@ -110,10 +110,11 @@ is required by the normal suite.
`kcadm-bootstrap.sh`. The realm contains no employer-specific federation.
- `deploy/templates/` — explicit private deployment contracts. SAML/OIDC use
Keyverse desired-state endpoints. `oidc-rp-naruon.json` is the reviewed public
Naruon runtime RP profile with one audience mapper and bounded routing claims.
LDAP is preflighted through Keyverse and then applied through private Keycloak
Admin REST in this release. All `{{placeholders}}` are resolved from KV before
use.
Naruon runtime profile; `oidc-rp-lineageweave.json` is the ADR-0009
confidential profile that projects an account's same-client role and exact
`org`/`workspace` attributes. LDAP is preflighted through Keyverse and then
applied through private Keycloak Admin REST in this release. All
`{{placeholders}}` are resolved from KV before use.
- `deploy/bootstrap/` — the bootstrap pointer locating the KV/DB config store.
- `helm/cwl-idp/` — the same three components; Keycloak and Postgres may be
disabled in favor of externally managed services. Secrets come from
Expand Down Expand Up @@ -142,9 +143,10 @@ is required by the normal suite.
- **OIDC relying-party metadata is secret-free desired state.** Validate with
`POST /clients/relying-parties:validate`, persist with `PUT`, and require exact
post-mutation observation before accepting a receipt. The optional mapper
profile permits exactly one audience mapper plus only canonical `role`, `org`,
and `workspace` hardcoded claims. Never expand mapper classes, claim names,
resource audiences, or token destinations by configuration alone.
profile permits static canonical claims, plus the separately reviewed
ADR-0009 `lineageweave-web` account-derived profile. Never expand mapper
classes, claim names, resource audiences, or token destinations by
configuration alone.
- **Treat mapper normalization narrowly.** Ignore only a valid generated mapper
`id` and canonicalize known mapper order. Unknown, malformed, duplicate, or
semantically changed live mapper state is drift. Mapper configuration does not
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ seed-bootstrap: ## Create a local sqlite KV bootstrap store for dev
python $(SERVICE_DIR)/tools/seed_config_store.py

validate-realm: ## Validate the Keycloak realm config-as-code
python scripts/validate_realm.py deploy/keycloak/realm-cwl.json
python scripts/validate_realm.py deploy/keycloak/cwl-realm.json

install: ## Install the admin service with dev extras
cd $(SERVICE_DIR) && python -m pip install -e '.[dev]'
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,15 @@ docker compose up -d # or: podman compose up -d
- Admin service health: `http://localhost:8099/healthz`

The stack imports the **passwordless-first** realm at first start
(`deploy/keycloak/realm-cwl.json`): a `browser-passwordless` flow with a
(`deploy/keycloak/cwl-realm.json`): a `browser-passwordless` flow with a
WebAuthn passwordless authenticator and **no password authenticator**, plus
`registrationAllowed:false` / `resetPasswordAllowed:false`.

For Helm installations created with the former `realm-cwl.json` ConfigMap key,
perform the zero-gap key migration in
[`deploy/keycloak/README.md`](deploy/keycloak/README.md#helm-configmap-key-migration)
before upgrading the chart.

### Register external federation

The portable realm contains no employer ADFS, LDAP/AD source, or other
Expand All @@ -96,6 +101,10 @@ See [`docs/federation-onboarding.md`](docs/federation-onboarding.md),
### Onboard a relying party

See [`docs/rp-onboarding.md`](docs/rp-onboarding.md).
The ADR-0009 LineageWeave profile uses real account-derived `role`, `org`
(company), and `workspace` (PU) claims; it is not enabled until private Keyverse
apply and controlled
downstream authorization evidence are recorded.

## Account unification & merge

Expand Down
7 changes: 7 additions & 0 deletions deploy/keycloak/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM quay.io/keycloak/keycloak:26.3.2@sha256:98fab020a3a490aba0978f237e2a06cd0ea42bf149c6cf10f11c0aaf27728ff2
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed

# Keycloak directory import recognizes <realm>-realm.json at container start.
COPY cwl-realm.json /opt/keycloak/data/import/cwl-realm.json
COPY lineageweave-user-profile.json /opt/keycloak/lineageweave-user-profile.json
COPY --chmod=755 reconcile-lineageweave-user-profile.sh /opt/keycloak/reconcile-lineageweave-user-profile.sh
USER 1000
41 changes: 40 additions & 1 deletion deploy/keycloak/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ providers are converged afterwards from the KV/DB source of truth.

| File | Responsibility |
| --- | --- |
| `realm-cwl.json` | Portable passwordless realm, shared client scopes, RP template, concrete `naruon-web` PKCE client, and account-unification service client |
| `cwl-realm.json` | Portable passwordless realm, shared client scopes, RP template, concrete `naruon-web` PKCE client, and account-unification service client |
| `kcadm-bootstrap.sh` | Idempotently inject the service-client secret, grant least-privilege realm-management roles, and reconcile the role mapper |
| `../templates/` | Reference payloads for runtime federation and additional relying-party registrations |

Expand Down Expand Up @@ -58,6 +58,45 @@ URLs or invalid placeholder LDAP distinguished names.
than 900 seconds;
- committed client secrets are placeholders only.

## Helm ConfigMap key migration

Older Helm installations used the ConfigMap data key `realm-cwl.json`. The
current chart requires the source key `cwl-realm.json` and mounts it at the
Keycloak-required target filename of the same name. Add the new key **before**
upgrading the StatefulSet so an existing Pod never references a missing
ConfigMap item.

```bash
NAMESPACE=identity
RELEASE=cwl-idp
CONFIG_MAP=cwl-idp-realm

# Keep a rollback copy of the dedicated realm-import ConfigMap.
kubectl -n "$NAMESPACE" get configmap "$CONFIG_MAP" -o yaml \
> "${CONFIG_MAP}.before-cwl-realm.yaml"

# Server-side apply adds the new data key without deleting the legacy key first.
kubectl -n "$NAMESPACE" create configmap "$CONFIG_MAP" \
--from-file=cwl-realm.json=deploy/keycloak/cwl-realm.json \
--dry-run=client -o yaml \
| kubectl -n "$NAMESPACE" apply --server-side \
--field-manager=cwl-idp-realm-migration -f -

helm upgrade "$RELEASE" helm/cwl-idp -n "$NAMESPACE" \
--set keycloak.realmImport.configMapName="$CONFIG_MAP" \
--set keycloak.realmImport.fileName=cwl-realm.json
kubectl -n "$NAMESPACE" rollout status statefulset/"${RELEASE}-keycloak"

# Only after the new StatefulSet is healthy may the obsolete source key go.
kubectl -n "$NAMESPACE" patch configmap "$CONFIG_MAP" --type=json \
--patch='[{"op":"remove","path":"/data/realm-cwl.json"}]'
```

This ConfigMap is dedicated to realm import. If a deployment added unrelated
keys, preserve or relocate them under its own ownership before the migration.
Verify the `cwl` realm discovery endpoint after rollout; container health alone
does not prove import succeeded.

## RP clients

`ecosystem-rp-template` is a confidential PKCE S256 blueprint. It uses the
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion deploy/keycloak/kcadm-bootstrap.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# cwl-idp — Keycloak post-import bootstrap (config-as-code, secrets from KV).
#
# The realm SHAPE lives in realm-cwl.json and is imported at container start.
# The realm SHAPE lives in cwl-realm.json and is imported at container start.
# This script patches the pieces that must NOT be committed (secrets, env URLs)
# by reading them from the KV store and applying them with Keycloak's admin CLI
# (`kcadm.sh`, shipped in the Keycloak image, Apache-2.0). Run it after the realm
Expand Down
Loading