Merge NK-WP-0048-T30 1.2 descriptor composer and candidate artifact
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 903097@bnt-lap001 Assistant-Session: aa320c8f-c3fa-46e7-8541-0b58e1e45d23
This commit is contained in:
commit
3c62bec31a
3 changed files with 583 additions and 0 deletions
231
sso-mfa/k8s/keycape/compose-1.2-descriptors.py
Normal file
231
sso-mfa/k8s/keycape/compose-1.2-descriptors.py
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
"""Compose the seven-step source descriptor set for oidc-browser-relying-party@1.2.
|
||||
|
||||
Source candidate for NK-WP-0048-T30. Read-only: it loads the 1.2 type only
|
||||
through the validator's explicit version-root selection, asks each step adapter
|
||||
for its descriptor, closes each one through Warden's own Descriptor.decode, and
|
||||
checks step, owner and lane against the 1.2 follow_through. It refuses a partial
|
||||
map, a duplicate or unknown step, any mismatch, and any change to canonical 1.1.
|
||||
|
||||
Nothing here writes a type file, pins a descriptor, admits anything or touches
|
||||
a live system. A composed map is a successor candidate that needs a reviewed
|
||||
revision under canon section 6 and owner review (T16) before any pinning.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from warden.case_run import Descriptor, digest
|
||||
from warden.case_receipt_state import Refused
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
REPO = ROOT.parents[2]
|
||||
DEFAULT_ROOT = REPO / 'security-case-types'
|
||||
VERSION_ROOTS = {'1.2': REPO / 'security-case-type-revisions' / '1.2'}
|
||||
TYPE_ID = 'oidc-browser-relying-party'
|
||||
TYPE_REF = f'{TYPE_ID}@1.2'
|
||||
CANONICAL_REF = f'{TYPE_ID}@1.1'
|
||||
SECTION6 = ('successor candidate only: new descriptor bytes need a reviewed successor '
|
||||
'revision under canon section 6 and owner review (NK-WP-0048-T16) before pinning')
|
||||
|
||||
# step -> (adapter file, needs explicit 1.2 version-root support)
|
||||
ADAPTERS = {
|
||||
'ensure-groups': ('ensure-groups-adapter.py', True),
|
||||
'mint-secret': ('mint-secret-adapter.py', False),
|
||||
'deliver-secret': ('deliver-secret-adapter.py', False),
|
||||
'bind-env': ('bind-env-adapter.py', False),
|
||||
'register-client': ('keycape-registration-adapter.py', False),
|
||||
'deliver-consumer-secret': ('deliver-consumer-secret-adapter.py', False),
|
||||
'hand-over': ('handover-adapter.py', True),
|
||||
}
|
||||
|
||||
# Worker-proposed names (T25-T29) that the type does not fix. Each needs owner
|
||||
# confirmation before any pin; the artifact marks them owner_confirmation_required.
|
||||
PROPOSED = {
|
||||
'mint-secret': {'task': 'NK-WP-0048-T25', 'actor': 'warden',
|
||||
'schema': 'railiance-platform.custody-mint.v1'},
|
||||
'deliver-secret': {'task': 'NK-WP-0048-T26', 'lane': 'platform-secret-delivery',
|
||||
'schema': 'netkingdom.deliver-secret.v1'},
|
||||
'bind-env': {'task': 'NK-WP-0048-T27', 'schema': 'netkingdom.keycape-bind-env.attempt.v1'},
|
||||
'deliver-consumer-secret': {'task': 'NK-WP-0048-T28', 'lane': 'consumer-owner-deployment',
|
||||
'schema': 'netkingdom.consumer-secret-delivery.v1'},
|
||||
'hand-over': {'task': 'NK-WP-0048-T29', 'owner': 'apps', 'lane': 'apps-consumer-handover',
|
||||
'schema': 'netkingdom.apps-handover.attempt.v1'},
|
||||
}
|
||||
|
||||
|
||||
def _load(name, path):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _validator():
|
||||
return _load('security_case_validator',
|
||||
REPO / 'tools' / 'security-case-types' / 'security_case_validator.py')
|
||||
|
||||
|
||||
def encode(desc: Descriptor) -> bytes:
|
||||
return json.dumps(asdict(desc), sort_keys=True, separators=(',', ':')).encode()
|
||||
|
||||
|
||||
def load_types(default_root=DEFAULT_ROOT, version_roots=None):
|
||||
"""Return (1.2 spec, canonical 1.1 file digest) via explicit selection only."""
|
||||
v = _validator()
|
||||
roots = dict(VERSION_ROOTS if version_roots is None else version_roots)
|
||||
if set(roots) != {'1.2'}:
|
||||
raise Refused('only an explicit 1.2 version root may be selected')
|
||||
issues, data, _ = v.load_type(Path(default_root), TYPE_REF, version_roots=roots)
|
||||
if issues or data['spec'].get('version') != '1.2':
|
||||
raise Refused('1.2 type not loadable through explicit version-root selection')
|
||||
issues, default_12, _ = v.load_type(Path(default_root), TYPE_REF)
|
||||
if not issues:
|
||||
raise Refused('1.2 is reachable by default; canonical 1.1 default changed')
|
||||
issues, canonical, type_dir = v.load_type(Path(default_root), CANONICAL_REF)
|
||||
if issues or str(canonical['spec'].get('version')) != '1.1':
|
||||
raise Refused('canonical 1.1 default not loadable')
|
||||
return data['spec'], digest((type_dir / 'type.yaml').read_bytes())
|
||||
|
||||
|
||||
def _resolve(value, params):
|
||||
try:
|
||||
return value.format(**params) if isinstance(value, str) else value
|
||||
except (KeyError, IndexError, ValueError):
|
||||
raise Refused('type owner template has no bound parameter') from None
|
||||
|
||||
|
||||
def adapter_descriptors(check_config_digest, consumer_owner):
|
||||
"""Ask every step adapter for its descriptor; T13 needs the checker digest."""
|
||||
out = []
|
||||
for step, (filename, needs_12) in ADAPTERS.items():
|
||||
m = _load(f'compose_{step.replace("-", "_")}', ROOT / filename)
|
||||
if needs_12 and getattr(m, 'SELECTABLE_TYPES', {}).get('1.2') != TYPE_REF:
|
||||
raise Refused(f'{step}: adapter has no explicit 1.2 version-root path')
|
||||
if step == 'register-client':
|
||||
desc, source = m.descriptor(check_config_digest), m.source_digest(check_config_digest)
|
||||
elif step == 'deliver-consumer-secret':
|
||||
desc, source = m.descriptor(consumer_owner), m.source_digest()
|
||||
else:
|
||||
desc, source = m.descriptor(), m.source_digest()
|
||||
if desc.adapter_source != source:
|
||||
raise Refused(f'{step}: descriptor does not pin its own source digest')
|
||||
out.append((step, desc))
|
||||
return out
|
||||
|
||||
|
||||
def compose(type_spec, candidates, params, canonical_digest_before, *, default_root=DEFAULT_ROOT):
|
||||
"""Return {step: descriptor digest} for the complete 1.2 set, or raise Refused."""
|
||||
steps = [s['id'] for s in type_spec.get('follow_through', ())]
|
||||
if len(steps) != 7 or len(set(steps)) != 7:
|
||||
raise Refused('1.2 type does not declare exactly seven distinct steps')
|
||||
by_step, sources = {}, {}
|
||||
for step, desc in candidates:
|
||||
if step in by_step:
|
||||
raise Refused(f'duplicate step {step!r}')
|
||||
if step not in steps:
|
||||
raise Refused(f'unknown step {step!r}')
|
||||
try:
|
||||
source = encode(desc)
|
||||
Descriptor.decode(source)
|
||||
except (TypeError, ValueError, Refused):
|
||||
raise Refused(f'{step}: descriptor refused by the Warden v1 kernel') from None
|
||||
if desc.step != step:
|
||||
raise Refused(f'{step}: descriptor names step {desc.step!r}')
|
||||
by_step[step], sources[step] = desc, source
|
||||
missing = [s for s in steps if s not in by_step]
|
||||
if missing:
|
||||
raise Refused(f'partial descriptor map; missing {missing}')
|
||||
for entry in type_spec['follow_through']:
|
||||
desc = by_step[entry['id']]
|
||||
if desc.owner != _resolve(entry['owner'], params):
|
||||
raise Refused(f"{entry['id']}: owner {desc.owner!r} does not match the 1.2 type")
|
||||
if 'lane' in entry and desc.lane != entry['lane']:
|
||||
raise Refused(f"{entry['id']}: lane {desc.lane!r} does not match the 1.2 type")
|
||||
_, after = load_types(default_root)
|
||||
if after != canonical_digest_before:
|
||||
raise Refused('canonical 1.1 type changed during composition')
|
||||
return {s: digest(sources[s]) for s in steps}
|
||||
|
||||
|
||||
def review(type_spec, candidates, params):
|
||||
"""Per-step findings without stopping at the first refusal (for the artifact)."""
|
||||
entries = {e['id']: e for e in type_spec.get('follow_through', ())}
|
||||
rows = []
|
||||
for step, desc in candidates:
|
||||
entry, findings = entries.get(step), []
|
||||
source = encode(desc)
|
||||
try:
|
||||
Descriptor.decode(source)
|
||||
except Refused:
|
||||
findings.append('refused by Warden v1 Descriptor.decode')
|
||||
if entry is None:
|
||||
findings.append('step not in 1.2 type')
|
||||
else:
|
||||
try:
|
||||
owner = _resolve(entry['owner'], params)
|
||||
except Refused as exc:
|
||||
owner = None
|
||||
findings.append(str(exc))
|
||||
if owner is not None and desc.owner != owner:
|
||||
findings.append(f'owner {desc.owner!r} != type owner {owner!r}')
|
||||
if 'lane' in entry and desc.lane != entry['lane']:
|
||||
findings.append(f"lane {desc.lane!r} != type lane {entry['lane']!r}")
|
||||
proposed = dict(PROPOSED.get(step, {}))
|
||||
if entry is not None and 'lane' not in entry:
|
||||
proposed.setdefault('lane', desc.lane)
|
||||
rows.append({'step': step, 'descriptor': asdict(desc),
|
||||
'candidate_descriptor_digest': digest(source),
|
||||
'type_owner': entry and entry['owner'], 'type_lane': entry and entry.get('lane'),
|
||||
'findings': findings,
|
||||
'owner_confirmation_required': proposed or None})
|
||||
return rows
|
||||
|
||||
|
||||
def artifact(check_config_digest, params):
|
||||
type_spec, canonical = load_types()
|
||||
candidates = adapter_descriptors(check_config_digest, params['consumer_owner_repo'])
|
||||
try:
|
||||
composed, refusal = compose(type_spec, candidates, params, canonical), None
|
||||
except Refused as exc:
|
||||
composed, refusal = None, str(exc)
|
||||
return {
|
||||
'schema': 'netkingdom.descriptor-composition-candidate.v1',
|
||||
'task': 'NK-WP-0048-T30',
|
||||
'type': TYPE_REF,
|
||||
'selection': 'explicit version root 1.2=security-case-type-revisions/1.2; default root unchanged',
|
||||
'canonical_default': {'type': CANONICAL_REF, 'type_yaml_digest': canonical,
|
||||
'unchanged': True},
|
||||
'status': 'not_an_admission',
|
||||
'review_requirement': SECTION6,
|
||||
'check_config_digest': check_config_digest,
|
||||
'params': params,
|
||||
'complete_executable_descriptor_set': composed is not None,
|
||||
'execution_descriptors': composed,
|
||||
'refusal': refusal,
|
||||
'steps': review(type_spec, candidates, params),
|
||||
'owner_review': False, 'admitted': False, 'dispatch_permitted': False,
|
||||
}
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
p.add_argument('--check-config-digest', required=True)
|
||||
p.add_argument('--consumer-owner-repo', required=True)
|
||||
p.add_argument('--out', type=Path)
|
||||
a = p.parse_args(argv)
|
||||
doc = artifact(a.check_config_digest, {'consumer_owner_repo': a.consumer_owner_repo})
|
||||
text = json.dumps(doc, indent=2, sort_keys=True) + '\n'
|
||||
if a.out:
|
||||
a.out.write_text(text)
|
||||
else:
|
||||
sys.stdout.write(text)
|
||||
return 0 if doc['complete_executable_descriptor_set'] else 2
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
234
sso-mfa/k8s/keycape/descriptor-composition-1.2.candidate.json
Normal file
234
sso-mfa/k8s/keycape/descriptor-composition-1.2.candidate.json
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
{
|
||||
"admitted": false,
|
||||
"canonical_default": {
|
||||
"type": "oidc-browser-relying-party@1.1",
|
||||
"type_yaml_digest": "sha256:f705c64e4875e9b8e18a912fb51c135e77898926d6a451f5cb74ab3c0cbe4d1c",
|
||||
"unchanged": true
|
||||
},
|
||||
"check_config_digest": "sha256:be37738fe1c08259fb4f9ca0daf0f9301bfffc237e5bb38435525dfbf11ccff5",
|
||||
"complete_executable_descriptor_set": false,
|
||||
"dispatch_permitted": false,
|
||||
"execution_descriptors": null,
|
||||
"owner_review": false,
|
||||
"params": {
|
||||
"consumer_owner_repo": "railiance-apps"
|
||||
},
|
||||
"refusal": "ensure-groups: descriptor refused by the Warden v1 kernel",
|
||||
"review_requirement": "successor candidate only: new descriptor bytes need a reviewed successor revision under canon section 6 and owner review (NK-WP-0048-T16) before pinning",
|
||||
"schema": "netkingdom.descriptor-composition-candidate.v1",
|
||||
"selection": "explicit version root 1.2=security-case-type-revisions/1.2; default root unchanged",
|
||||
"status": "not_an_admission",
|
||||
"steps": [
|
||||
{
|
||||
"candidate_descriptor_digest": "sha256:0a35e4bf7ce1b98a7538bda8b36bdce88d152880df05218020274a38c0f89c16",
|
||||
"descriptor": {
|
||||
"actor": "human",
|
||||
"adapter": "net-kingdom/ensure-groups@v1",
|
||||
"adapter_source": "sha256:e38067a40486379620d8ea37de48d436e9e7eac193541ddd73808e98af54be32",
|
||||
"allowed_created": [
|
||||
"service-group",
|
||||
"service-group-membership"
|
||||
],
|
||||
"effect": "change",
|
||||
"lane": "key-cape-oidc-login",
|
||||
"owner": "net-kingdom",
|
||||
"retry_safe": false,
|
||||
"revocation_scopes": [],
|
||||
"step": "ensure-groups",
|
||||
"target_parameter": "client_id",
|
||||
"version": "warden.case.execution.v1"
|
||||
},
|
||||
"findings": [
|
||||
"refused by Warden v1 Descriptor.decode"
|
||||
],
|
||||
"owner_confirmation_required": null,
|
||||
"step": "ensure-groups",
|
||||
"type_lane": "key-cape-oidc-login",
|
||||
"type_owner": "net-kingdom"
|
||||
},
|
||||
{
|
||||
"candidate_descriptor_digest": "sha256:d33f16653dfa5d226d8cfb82bc5b95f447a3d8ad439d65b9437b1656e1abb875",
|
||||
"descriptor": {
|
||||
"actor": "warden",
|
||||
"adapter": "net-kingdom/keycape-mint-secret@v1",
|
||||
"adapter_source": "sha256:ede3c2b3edfa453233fc6fd4eb4f713b26240ff38b497d38d63d90b01d797cf4",
|
||||
"allowed_created": [
|
||||
"custody-secret-version"
|
||||
],
|
||||
"effect": "mint",
|
||||
"lane": "keycape-client-secret-generate",
|
||||
"owner": "railiance-platform",
|
||||
"retry_safe": false,
|
||||
"revocation_scopes": [
|
||||
"parent-session",
|
||||
"broker-grant"
|
||||
],
|
||||
"step": "mint-secret",
|
||||
"target_parameter": "client_id",
|
||||
"version": "warden.case.execution.v1"
|
||||
},
|
||||
"findings": [
|
||||
"refused by Warden v1 Descriptor.decode"
|
||||
],
|
||||
"owner_confirmation_required": {
|
||||
"actor": "warden",
|
||||
"schema": "railiance-platform.custody-mint.v1",
|
||||
"task": "NK-WP-0048-T25"
|
||||
},
|
||||
"step": "mint-secret",
|
||||
"type_lane": "keycape-client-secret-generate",
|
||||
"type_owner": "railiance-platform"
|
||||
},
|
||||
{
|
||||
"candidate_descriptor_digest": "sha256:1e23b05058a61b3558070805329b6906f0ba9b989b98d9df35c621471a7c4938",
|
||||
"descriptor": {
|
||||
"actor": "human",
|
||||
"adapter": "net-kingdom/deliver-secret@v1",
|
||||
"adapter_source": "sha256:0e5ea5440f7f51d499c75b44e047aaeb9ddeb8eba1ce58ae4457cffb072c223b",
|
||||
"allowed_created": [
|
||||
"delivered-secret"
|
||||
],
|
||||
"effect": "change",
|
||||
"lane": "platform-secret-delivery",
|
||||
"owner": "railiance-platform",
|
||||
"retry_safe": false,
|
||||
"revocation_scopes": [
|
||||
"parent-session"
|
||||
],
|
||||
"step": "deliver-secret",
|
||||
"target_parameter": "client_id",
|
||||
"version": "warden.case.execution.v1"
|
||||
},
|
||||
"findings": [],
|
||||
"owner_confirmation_required": {
|
||||
"lane": "platform-secret-delivery",
|
||||
"schema": "netkingdom.deliver-secret.v1",
|
||||
"task": "NK-WP-0048-T26"
|
||||
},
|
||||
"step": "deliver-secret",
|
||||
"type_lane": null,
|
||||
"type_owner": "railiance-platform"
|
||||
},
|
||||
{
|
||||
"candidate_descriptor_digest": "sha256:55898e15f5851525910f6ee3b2802d9167f0c0dd1bfcf921a81470161773f70a",
|
||||
"descriptor": {
|
||||
"actor": "human",
|
||||
"adapter": "net-kingdom/keycape-bind-env@v1",
|
||||
"adapter_source": "sha256:80bb389a2c91b32c7295f22c56ea7451d4bd9a0bf27d4b6519661d5eb9be0371",
|
||||
"allowed_created": [
|
||||
"keycape-env-binding"
|
||||
],
|
||||
"effect": "change",
|
||||
"lane": "key-cape-oidc-login",
|
||||
"owner": "net-kingdom",
|
||||
"retry_safe": false,
|
||||
"revocation_scopes": [
|
||||
"parent-session"
|
||||
],
|
||||
"step": "bind-env",
|
||||
"target_parameter": "client_id",
|
||||
"version": "warden.case.execution.v1"
|
||||
},
|
||||
"findings": [],
|
||||
"owner_confirmation_required": {
|
||||
"schema": "netkingdom.keycape-bind-env.attempt.v1",
|
||||
"task": "NK-WP-0048-T27"
|
||||
},
|
||||
"step": "bind-env",
|
||||
"type_lane": "key-cape-oidc-login",
|
||||
"type_owner": "net-kingdom"
|
||||
},
|
||||
{
|
||||
"candidate_descriptor_digest": "sha256:3ccc8eed545cf31f8a48feb8afa0c1355e126d9549eef9737d92d0c1b87708a8",
|
||||
"descriptor": {
|
||||
"actor": "human",
|
||||
"adapter": "net-kingdom/keycape-registration@v1",
|
||||
"adapter_source": "sha256:8af23cb5d5fff963d2eff65d8b16dcfd395931ad860357846151019a70b2ed34",
|
||||
"allowed_created": [
|
||||
"keycape-client"
|
||||
],
|
||||
"effect": "change",
|
||||
"lane": "key-cape-oidc-login",
|
||||
"owner": "net-kingdom",
|
||||
"retry_safe": false,
|
||||
"revocation_scopes": [
|
||||
"parent-session"
|
||||
],
|
||||
"step": "register-client",
|
||||
"target_parameter": "client_id",
|
||||
"version": "warden.case.execution.v1"
|
||||
},
|
||||
"findings": [],
|
||||
"owner_confirmation_required": null,
|
||||
"step": "register-client",
|
||||
"type_lane": "key-cape-oidc-login",
|
||||
"type_owner": "net-kingdom"
|
||||
},
|
||||
{
|
||||
"candidate_descriptor_digest": "sha256:a74220eddab2709e90e6349b45940f68233c169009cccd0cdeea25a5541774a6",
|
||||
"descriptor": {
|
||||
"actor": "agent",
|
||||
"adapter": "net-kingdom/deliver-consumer-secret@v1",
|
||||
"adapter_source": "sha256:7dd5509d7dee83c2a8ff1bcd4704c336f8dbc4586223830cad34b92e6fa0bfff",
|
||||
"allowed_created": [
|
||||
"consumer-eso-sa",
|
||||
"consumer-secretstore",
|
||||
"consumer-externalsecret"
|
||||
],
|
||||
"effect": "change",
|
||||
"lane": "consumer-owner-deployment",
|
||||
"owner": "railiance-apps",
|
||||
"retry_safe": false,
|
||||
"revocation_scopes": [
|
||||
"parent-session"
|
||||
],
|
||||
"step": "deliver-consumer-secret",
|
||||
"target_parameter": "client_id",
|
||||
"version": "warden.case.execution.v1"
|
||||
},
|
||||
"findings": [],
|
||||
"owner_confirmation_required": {
|
||||
"lane": "consumer-owner-deployment",
|
||||
"schema": "netkingdom.consumer-secret-delivery.v1",
|
||||
"task": "NK-WP-0048-T28"
|
||||
},
|
||||
"step": "deliver-consumer-secret",
|
||||
"type_lane": null,
|
||||
"type_owner": "{consumer_owner_repo}"
|
||||
},
|
||||
{
|
||||
"candidate_descriptor_digest": "sha256:366e8829dd6363e69abd817ea0d4ea966ac1ebcf994493b9bf9eee1a648a6ce0",
|
||||
"descriptor": {
|
||||
"actor": "human",
|
||||
"adapter": "net-kingdom/apps-handover@v1",
|
||||
"adapter_source": "sha256:4ad0396928f71bfb57760c414237b6cfa06aea57ccca2ef1293e63ab22f2953a",
|
||||
"allowed_created": [
|
||||
"handover-receipt"
|
||||
],
|
||||
"effect": "change",
|
||||
"lane": "apps-consumer-handover",
|
||||
"owner": "apps",
|
||||
"retry_safe": false,
|
||||
"revocation_scopes": [],
|
||||
"step": "hand-over",
|
||||
"target_parameter": "client_id",
|
||||
"version": "warden.case.execution.v1"
|
||||
},
|
||||
"findings": [
|
||||
"refused by Warden v1 Descriptor.decode",
|
||||
"owner 'apps' != type owner 'railiance-apps'"
|
||||
],
|
||||
"owner_confirmation_required": {
|
||||
"lane": "apps-consumer-handover",
|
||||
"owner": "apps",
|
||||
"schema": "netkingdom.apps-handover.attempt.v1",
|
||||
"task": "NK-WP-0048-T29"
|
||||
},
|
||||
"step": "hand-over",
|
||||
"type_lane": null,
|
||||
"type_owner": "{consumer_owner_repo}"
|
||||
}
|
||||
],
|
||||
"task": "NK-WP-0048-T30",
|
||||
"type": "oidc-browser-relying-party@1.2"
|
||||
}
|
||||
118
sso-mfa/k8s/keycape/test_compose_1_2_descriptors.py
Normal file
118
sso-mfa/k8s/keycape/test_compose_1_2_descriptors.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
"""Synthetic positive and refusal fixtures for the T30 1.2 descriptor composer."""
|
||||
from dataclasses import replace
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
from warden.case_run import Descriptor
|
||||
from warden.case_receipt_state import Refused
|
||||
|
||||
ROOT = Path(__file__).parent
|
||||
spec = importlib.util.spec_from_file_location('compose_12', ROOT / 'compose-1.2-descriptors.py')
|
||||
c = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(c)
|
||||
|
||||
CHECKER = 'sha256:' + 'b' * 64
|
||||
PARAMS = {'consumer_owner_repo': 'railiance-apps'}
|
||||
|
||||
|
||||
def synthetic(type_spec):
|
||||
"""One kernel-valid descriptor per 1.2 step, owner/lane taken from the type."""
|
||||
out = []
|
||||
for entry in type_spec['follow_through']:
|
||||
out.append((entry['id'], Descriptor(
|
||||
'warden.case.execution.v1', entry['id'], entry['owner'].format(**PARAMS),
|
||||
f"net-kingdom/{entry['id']}@v1", 'sha256:' + 'a' * 64, 'human',
|
||||
entry.get('lane', 'proposed-lane'), 'client_id', 'change', ('created',),
|
||||
False, ('parent-session',))))
|
||||
return out
|
||||
|
||||
|
||||
class ComposeTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.spec, cls.canonical = c.load_types()
|
||||
cls.good = synthetic(cls.spec)
|
||||
|
||||
def compose(self, candidates, canonical=None):
|
||||
return c.compose(self.spec, candidates, PARAMS, canonical or self.canonical)
|
||||
|
||||
def test_complete_seven_step_map(self):
|
||||
composed = self.compose(self.good)
|
||||
self.assertEqual(list(composed), [e['id'] for e in self.spec['follow_through']])
|
||||
self.assertEqual(len(composed), 7)
|
||||
self.assertTrue(all(v.startswith('sha256:') for v in composed.values()))
|
||||
|
||||
def test_partial_map_refused(self):
|
||||
with self.assertRaisesRegex(Refused, 'partial'):
|
||||
self.compose(self.good[:-1])
|
||||
|
||||
def test_duplicate_step_refused(self):
|
||||
with self.assertRaisesRegex(Refused, 'duplicate'):
|
||||
self.compose(self.good + [self.good[0]])
|
||||
|
||||
def test_unknown_step_refused(self):
|
||||
bogus = ('rotate-secret', replace(self.good[0][1], step='rotate-secret'))
|
||||
with self.assertRaisesRegex(Refused, 'unknown'):
|
||||
self.compose(self.good[:-1] + [bogus])
|
||||
|
||||
def test_step_name_mismatch_refused(self):
|
||||
bad = [(s, replace(d, step='bind-env') if s == 'ensure-groups' else d)
|
||||
for s, d in self.good]
|
||||
with self.assertRaisesRegex(Refused, 'names step'):
|
||||
self.compose(bad)
|
||||
|
||||
def test_owner_mismatch_refused(self):
|
||||
bad = [(s, replace(d, owner='apps') if s == 'hand-over' else d) for s, d in self.good]
|
||||
with self.assertRaisesRegex(Refused, 'owner'):
|
||||
self.compose(bad)
|
||||
|
||||
def test_lane_mismatch_refused(self):
|
||||
bad = [(s, replace(d, lane='other-lane') if s == 'register-client' else d)
|
||||
for s, d in self.good]
|
||||
with self.assertRaisesRegex(Refused, 'lane'):
|
||||
self.compose(bad)
|
||||
|
||||
def test_kernel_invalid_descriptor_refused(self):
|
||||
bad = [(s, replace(d, revocation_scopes=()) if s == 'ensure-groups' else d)
|
||||
for s, d in self.good]
|
||||
with self.assertRaisesRegex(Refused, 'kernel'):
|
||||
self.compose(bad)
|
||||
|
||||
def test_canonical_change_refused(self):
|
||||
with self.assertRaisesRegex(Refused, '1.1'):
|
||||
self.compose(self.good, canonical='sha256:' + '0' * 64)
|
||||
|
||||
def test_only_explicit_12_root(self):
|
||||
with self.assertRaisesRegex(Refused, 'explicit 1.2'):
|
||||
c.load_types(version_roots={'1.1': c.VERSION_ROOTS['1.2']})
|
||||
with self.assertRaisesRegex(Refused, 'explicit 1.2'):
|
||||
c.load_types(version_roots={})
|
||||
|
||||
def test_canonical_default_bytes_untouched(self):
|
||||
path = c.DEFAULT_ROOT / c.TYPE_ID / 'type.yaml'
|
||||
before = path.read_bytes()
|
||||
c.artifact(CHECKER, PARAMS)
|
||||
self.assertEqual(path.read_bytes(), before)
|
||||
|
||||
def test_current_adapters_do_not_compose(self):
|
||||
# Pins the present source findings; this test must change when they are fixed.
|
||||
doc = c.artifact(CHECKER, PARAMS)
|
||||
self.assertFalse(doc['complete_executable_descriptor_set'])
|
||||
self.assertIsNone(doc['execution_descriptors'])
|
||||
self.assertFalse(doc['admitted'] or doc['owner_review'] or doc['dispatch_permitted'])
|
||||
findings = {r['step']: r['findings'] for r in doc['steps']}
|
||||
self.assertTrue(findings['ensure-groups'])
|
||||
self.assertTrue(findings['mint-secret'])
|
||||
self.assertTrue(any('owner' in f for f in findings['hand-over']))
|
||||
self.assertEqual(findings['register-client'], [])
|
||||
flagged = {r['step'] for r in doc['steps'] if r['owner_confirmation_required']}
|
||||
self.assertTrue({'mint-secret', 'deliver-secret', 'bind-env', 'hand-over'} <= flagged)
|
||||
|
||||
def test_adapters_need_bounded_inputs(self):
|
||||
with self.assertRaises(Refused):
|
||||
c.adapter_descriptors('not-a-digest', 'railiance-apps')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue