MASON-WP-0005-T01: bind key-cape's confirmed claims for the operator lane

key-cape confirmed groups_claim=groups, bound group fluid-telegram-operators,
assurance aal2 and the three openbao-admin callbacks. The OIDC executor now
pins /assurance/level=aal2 (bound_claims_type=string) and accepts the exact
operator-tunnel UI callback; the retired public UI callback stays refused.
T01 now waits on the founder go for the LLDAP group and a scoped survey grant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 327203@bnt-lap001
Assistant-Session: 55028ae3-4914-4bd1-8f5c-815f3527aef0
This commit is contained in:
tegwick 2026-10-08 23:41:34 +02:00
parent 24d05c6cde
commit 894761477f
4 changed files with 49 additions and 11 deletions

View file

@ -113,9 +113,18 @@ until recovery is verified and must never be exposed through this OIDC role.
- token TTL: `15m`, maximum TTL `30m`
- user claim: `sub`; groups claim: `groups`
- scopes: `openid,profile,email,groups`
- proposed bound claim: `groups=fluid-telegram-operators` — **unconfirmed**
- callbacks: Bao UI plus localhost and loopback CLI callbacks already accepted
by the platform contract
- bound claims (`bound_claims_type=string`): `groups=[fluid-telegram-operators]`
and `/assurance/level=aal2` — confirmed by key-cape 2026-10-08 (message
`b2ed890f-a05a-4b96-95eb-2cd9d8b32caa`, key-cape `e0b5fae`). Human ID tokens
always carry `groups` (LLDAP group names); `aal2` is set only after a
verified privacyIDEA factor, the same pin as CCR-2026-0034.
- precondition: LLDAP group `fluid-telegram-operators` does not exist yet;
creating it with its members is a NetKingdom directory act needing the
founder go. KeyCape does not create groups.
- callbacks (subset of the `openbao-admin` client registration):
`http://localhost:8250/oidc/callback`, `http://127.0.0.1:8250/oidc/callback`,
`http://127.0.0.1:18200/ui/vault/auth/netkingdom/oidc/callback`. Never the
retired `https://bao.coulomb.social/...` (2026-09-15).
The role is campaign-specific because its policy is campaign-specific. A
second campaign receives a second policy and role after its own review; it is
@ -143,7 +152,8 @@ This section is incomplete while T01 and T02 are open; the plan must remain
access to KV metadata because the provisioner uses data GETs to detect
absence; metadata evidence comes from the separately scoped builder/operator
verification path.
- **Identity binding:** an unconfirmed group is a hard blocker, not a warning.
- **Identity binding:** the group and assurance claims are confirmed; the
group's existence in LLDAP is a hard blocker, not a warning.
The role must not be readable by every OIDC user.
- **Write-once semantics:** KV-v2 create/update distinction is documented, but
activation also requires an owner-run non-secret probe proving the installed
@ -177,7 +187,7 @@ This section is incomplete while T01 and T02 are open; the plan must remain
## 5. Executive summary (phase 3)
Not yet renderable. The owner-approved tenant segment, OIDC bound claim, live
structure survey, CCR/executor extension, and installed-engine create/update
Not yet renderable. The owner-approved tenant segment, the LLDAP group's
creation, the live structure survey, CCR/executor extension, and installed-engine create/update
probe are open inputs. When those close, this section must ask for one explicit
approval covering the exact objects and capability matrix above.

View file

@ -93,6 +93,11 @@ class OIDCKVSpec:
audit_log_path: Path | None = None
# KeyCape sets assurance.level=aal2 only after a verified privacyIDEA factor
# (key-cape reply 2026-10-08, as in CCR-2026-0034); every attended role pins it.
_REQUIRED_ASSURANCE = {"/assurance/level": "aal2"}
_UI_CALLBACK = "http://127.0.0.1:18200/ui/vault/auth/{mount}/oidc/callback"
_OPERATOR_MATRIX = (
("operator-app", ("read",)),
("operator-session", ("create", "read", "update")),
@ -138,6 +143,8 @@ def _validate_oidc_spec(spec: OIDCKVSpec) -> None:
for uri in spec.allowed_redirect_uris:
if not isinstance(uri, str):
raise BuildRefused("OIDC callback must be a URI")
if uri == _UI_CALLBACK.format(mount=spec.auth_mount):
continue
try:
u = urlsplit(uri)
valid = (u.scheme == "http" and u.hostname in {"localhost", "127.0.0.1"}
@ -146,7 +153,7 @@ def _validate_oidc_spec(spec: OIDCKVSpec) -> None:
except ValueError:
valid = False
if not valid:
raise BuildRefused("initial matrix engine accepts only reviewed loopback CLI callbacks on port 8250")
raise BuildRefused("matrix engine accepts only the reviewed loopback CLI callbacks on port 8250 and the operator-tunnel UI callback")
def oidc_role_document(spec: OIDCKVSpec) -> dict:
@ -154,7 +161,8 @@ def oidc_role_document(spec: OIDCKVSpec) -> dict:
return {
"role_type": "oidc", "user_claim": "sub", "groups_claim": "groups",
"oidc_scopes": ["openid", "profile", "email", "groups"],
"bound_claims": {"groups": spec.bound_group},
"bound_claims_type": "string",
"bound_claims": {"groups": [spec.bound_group], **_REQUIRED_ASSURANCE},
"allowed_redirect_uris": list(spec.allowed_redirect_uris),
"token_policies": [spec.policy_name], "token_no_default_policy": True,
"token_ttl": spec.token_ttl, "token_max_ttl": spec.token_max_ttl,

View file

@ -67,7 +67,8 @@ def test_exact_role_binding_and_value_free_build(tmp_path):
assert run.call_args_list[0].args[0]==['bao','policy','write',s.policy_name,'-']
assert run.call_args_list[1].args[0]==['bao','write','auth/netkingdom/role/'+s.role_name,'-']
role=json.loads(run.call_args_list[1].kwargs['input'])
assert role['bound_claims']=={'groups':s.bound_group}
assert role['bound_claims']=={'groups':[s.bound_group],'/assurance/level':'aal2'}
assert role['bound_claims_type']=='string'
assert role['token_no_default_policy'] is True and role['token_explicit_max_ttl']=='30m'
assert role['token_policies']==[s.policy_name]
assert build_spec_document(s)['engine']=='openbao-oidc-kv-read-write'
@ -80,3 +81,12 @@ def test_failure_suppresses_output_and_never_claims_success(tmp_path):
with patch('ops_mason.executor.subprocess.run',return_value=MagicMock(returncode=1,stdout='sensitive-marker',stderr='sensitive-marker')):
with pytest.raises(BuildError) as e:build_oidc_kv_lane(_plan(tmp_path,spec=s),s)
assert 'sensitive-marker' not in str(e.value) and not s.audit_log_path.exists()
def test_callbacks_accept_operator_ui_and_refuse_retired_public_ui(tmp_path):
s=spec(tmp_path)
ui='http://127.0.0.1:18200/ui/vault/auth/netkingdom/oidc/callback'
assert ui in oidc_role_document(replace(s,allowed_redirect_uris=(ui,)))['allowed_redirect_uris']
for bad in ('https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback',
'http://127.0.0.1:18200/ui/vault/auth/other/oidc/callback'):
with pytest.raises(BuildRefused):oidc_role_document(replace(s,allowed_redirect_uris=(bad,)))

View file

@ -44,8 +44,8 @@ consumer workplan `FT-WP-0002` (`f2373858-c932-5a4d-81b6-db9a3595135a`).
```task
id: MASON-WP-0005-T01
status: wait
blocked_on: message-from:key-cape
blocking_reason: "KeyCape/NetKingdom must confirm the OIDC group claim, assurance level and callbacks for the attended provisioner role; railiance-platform (2026-09-09) declines to decide these. Tenant/path acceptance moves to the plan approval (T03)."
blocked_on: human
blocking_reason: "Claim, assurance and callbacks confirmed by key-cape (2026-10-08). Remaining: founder go for net-kingdom to create LLDAP group fluid-telegram-operators with its members, and a scoped OpenBao grant (bao-session.sh grant MASON-WP-0005-T01) for the live survey."
priority: high
state_hub_task_id: "18c9099f-882f-575c-93b9-cc68d7fbc2af"
```
@ -72,6 +72,16 @@ plan approval); the construction contract and writer authority are ops-mason's
side. Re-qualified from `message-from:railiance-platform` to
`message-from:key-cape`; asked key-cape for the claim the same day.
2026-10-08: key-cape answered (message `b2ed890f`, source key-cape `e0b5fae`):
`groups_claim=groups`, bound `groups=[fluid-telegram-operators]`; bound
`/assurance/level=aal2` with `bound_claims_type=string` (as CCR-2026-0034);
callbacks are the two loopback CLI URIs on 8250 plus the operator-tunnel UI
callback on 18200. Recorded in the plan's OIDC role shape. The executor now
always pins `aal2` and accepts that exact UI callback, refusing the retired
public one (`tests/test_oidc_matrix.py`, 153 passed). Two inputs remain, both
human: the founder go for net-kingdom to create the LLDAP group with its
members, and a scoped grant for the live survey (no operator session today).
## Extend the construction and credential-change contracts
```task