NK-WP-0056-T10: set cas_required by metadata write (platform-admin has no patch)
First attended setup stopped at cas_required: platform-admin holds create/read/update/delete/list on platform/*, not patch, so 'kv metadata patch' was denied. Policies were already applied; nothing after. Now a metadata write naming only cas_required (KV v2 keeps unnamed fields; checked on the pinned OpenBao image), guarded by a before/after comparison of max_versions, delete_version_after and custom_metadata. Receipt of the stopped attempt kept as evidence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 5502@bnt-lap001 Assistant-Session: 68b3b02f-5d0f-4e71-8421-796f62e50196
This commit is contained in:
parent
634b3a41df
commit
12c9b2df43
3 changed files with 44 additions and 7 deletions
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"action": "setup",
|
||||
"phase": "cas_required",
|
||||
"reason": "LaneError",
|
||||
"source_sha256": "a9289918254bc98d84b4b573c7ed2895fe50d7fbc538f650167e4d84557d44ae",
|
||||
"status": "failed",
|
||||
"values_recorded": false
|
||||
}
|
||||
|
|
@ -30,7 +30,10 @@ def platform_copy(tmp_path, confirm=True):
|
|||
(root / 'credential-change-requests').mkdir()
|
||||
for name in CCRS.values():
|
||||
shutil.copy(PLATFORM / 'credential-change-requests' / name, root / 'credential-change-requests' / name)
|
||||
if confirm:
|
||||
copy = root / 'credential-change-requests' / name
|
||||
if not confirm: # the real CCRs are confirmed since 2026-10-31; undo it in the copy
|
||||
copy.write_text(copy.read_text().replace('bound_claims_confirmed: true', 'bound_claims_confirmed: false'))
|
||||
elif 'bound_claims_confirmed: true' not in copy.read_text():
|
||||
subprocess.run([sys.executable, str(root / 'scripts/credential-change.py'), 'confirm-binding',
|
||||
'--reviewer', 'platform-owner', '--comment', 'synthetic test',
|
||||
str(root / 'credential-change-requests' / name)], check=True, capture_output=True)
|
||||
|
|
@ -41,11 +44,17 @@ def digests(root):
|
|||
return {k: hashlib.sha256((root / 'credential-change-requests' / v).read_bytes()).hexdigest() for k, v in CCRS.items()}
|
||||
|
||||
|
||||
def m_key(m):
|
||||
return m.CAS_KEY
|
||||
|
||||
|
||||
class FakeBao:
|
||||
def __init__(self, m):
|
||||
self.m = m
|
||||
self.store = {m.BOUNDARY: {'policy': 'path "x" { capabilities = ["deny"] }\n' + FENCE}}
|
||||
self.cas = False
|
||||
self.meta = {'max_versions': 0, 'delete_version_after': '0s', 'custom_metadata': None}
|
||||
self.meta_write_clobbers = False
|
||||
self.identity = {'policies': ['platform-admin'], 'path': 'auth/netkingdom/oidc/callback'}
|
||||
self.login_ok = {('nk-bind-executor', 'openbao', m.LANES['journey']['role'])}
|
||||
self.wide = False
|
||||
|
|
@ -56,9 +65,8 @@ class FakeBao:
|
|||
missing = out({}, 2, b'No value found')
|
||||
if args[:2] == ['token', 'lookup']: return out({'data': self.identity})
|
||||
if args[:2] == ['token', 'create']: return out({'auth': {'client_token': 'child'}})
|
||||
if args[:3] == ['kv', 'metadata', 'get']: return out({'data': {'cas_required': self.cas}})
|
||||
if args[:3] == ['kv', 'metadata', 'patch']:
|
||||
assert '-cas-required=true' in args; self.cas = True; return out({})
|
||||
if args[0] == 'read' and args[-1] == m_key(self.m):
|
||||
return out({'data': {'cas_required': self.cas, **self.meta}})
|
||||
if args[0] == 'read':
|
||||
path = args[-1]
|
||||
if path == JOURNEY and token == 'reader':
|
||||
|
|
@ -78,6 +86,11 @@ class FakeBao:
|
|||
return out({'auth': {'client_token': 'reader', 'lease_duration': 900,
|
||||
'token_policies': [self.m.LANES['journey']['policy']]}})
|
||||
return out({}, 2, b'Code: 403')
|
||||
if path == m_key(self.m):
|
||||
assert payload == {'cas_required': True}
|
||||
self.cas = True
|
||||
if self.meta_write_clobbers: self.meta = {**self.meta, 'max_versions': 10}
|
||||
return out({})
|
||||
if path.startswith('auth/kubernetes/role/'):
|
||||
self.store[path] = {k: (900 if v == '15m' else v) for k, v in payload.items()}; return out({})
|
||||
self.store[path] = payload; return out({})
|
||||
|
|
@ -136,6 +149,7 @@ def test_verify_after_setup(tmp_path, monkeypatch):
|
|||
'write_role_exists_outside_a_window'),
|
||||
(lambda m, f, d: f.store.update({'sys/policies/acl/' + m.LANES['journey']['policy']: {'policy': 'path "*" {}'}}),
|
||||
'net-kingdom-bind-executor-syn-journey-read:policy_drift'),
|
||||
(lambda m, f, d: setattr(f, 'meta_write_clobbers', True), 'metadata_changed_beyond_cas_required'),
|
||||
])
|
||||
def test_setup_refusals(tmp_path, monkeypatch, mutate, reason):
|
||||
m, fake, d = build(tmp_path, monkeypatch)
|
||||
|
|
|
|||
|
|
@ -61,7 +61,8 @@ for _lane in LANES.values():
|
|||
_lane.update(role=_lane['policy'], service_account=SA, namespace=NS,
|
||||
data_path=_lane['kv'].replace('platform/', 'platform/data/', 1),
|
||||
metadata_path=_lane['kv'].replace('platform/', 'platform/metadata/', 1))
|
||||
CAS_KEY = ['-mount=platform', 'workloads/net-kingdom/directory-binds/svc-authelia']
|
||||
CAS_KEY = 'platform/metadata/workloads/net-kingdom/directory-binds/svc-authelia'
|
||||
KEPT_METADATA = ('max_versions', 'delete_version_after', 'custom_metadata')
|
||||
|
||||
|
||||
class Refused(Exception):
|
||||
|
|
@ -114,8 +115,22 @@ def policy_check(lane):
|
|||
return check
|
||||
|
||||
|
||||
def key_metadata():
|
||||
return data(bao(['read', '-format=json', CAS_KEY]))['data']
|
||||
|
||||
|
||||
def cas_required():
|
||||
return data(bao(['kv', 'metadata', 'get', '-format=json', *CAS_KEY]))['data'].get('cas_required') is True
|
||||
return key_metadata().get('cas_required') is True
|
||||
|
||||
|
||||
def set_cas_required():
|
||||
"""A metadata write that names only cas_required. platform-admin holds update, not patch, on platform/*.
|
||||
KV v2 changes only the fields a write names (checked on the pinned OpenBao, 2026-10-31); the other settings
|
||||
are compared before and after anyway."""
|
||||
before = {k: key_metadata().get(k) for k in KEPT_METADATA}
|
||||
bao(['write', CAS_KEY, '-'], payload={'cas_required': True})
|
||||
after = key_metadata()
|
||||
if {k: after.get(k) for k in KEPT_METADATA} != before: raise Refused('metadata_changed_beyond_cas_required')
|
||||
|
||||
|
||||
def write_role_absent():
|
||||
|
|
@ -137,7 +152,7 @@ def setup(digests, receipt):
|
|||
ensure('sys/policies/acl/' + lane['policy'], {'policy': lane['policy_text']}, policy_check(lane))
|
||||
receipt['phase'] = 'cas_required'
|
||||
if not cas_required():
|
||||
bao(['kv', 'metadata', 'patch', '-cas-required=true', *CAS_KEY])
|
||||
set_cas_required()
|
||||
if not cas_required(): raise Refused('cas_required_not_set')
|
||||
receipt['phase'] = 'journey_role'
|
||||
journey = LANES['journey']
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue