railiance-platform/tests/test_keycape_client_secret_generate.py

529 lines
29 KiB
Python
Raw Normal View History

"""Offline tests for credential.py generate (RPF-WP-0054-T03) against a fake ``bao``.
Runs ``credential.py generate`` as a real subprocess, so the silence contract and
the delegation from credential.py are tested end to end. The fake records which
token each call used: KV calls must use the minted grant token, never the
caller's session.
"""
import hashlib
import importlib.util
import shutil
from unittest import mock
import yaml
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import textwrap
import unittest
ROOT = Path(__file__).resolve().parents[1]
CLI = ROOT / 'scripts/credential.py'
GRANT_TOKEN = 'hvs.grant-' + 'G7h8' * 6
FAKE_BAO = textwrap.dedent(f'''\
#!/usr/bin/env python3
import json, os, sys
sf = os.environ['FAKE_BAO_STATE']
st = json.load(open(sf))
a = sys.argv[1:]
tok = os.environ.get('BAO_TOKEN', 'caller-session')
st['calls'].append({{'args': a, 'token': tok}})
def save(): json.dump(st, open(sf, 'w'))
def out(obj): save(); print(json.dumps(obj)); sys.exit(0)
def missing(): save(); sys.stderr.write('No value found at x\\n'); sys.exit(2)
if a[:2] == ['token', 'lookup']: out({{'data': {{'policies': st['caller_policies']}}}})
if a[:2] == ['token', 'create']:
if st.get('fail_create'): save(); sys.exit(2)
out({{'auth': {{'client_token': '{GRANT_TOKEN}', 'accessor': 'acc'}}}})
if a[:2] == ['token', 'revoke']: st['revoked'] = tok; save(); sys.exit(0)
if a[0] == 'read':
rest = a[-1].split('/', 2)[2]
if rest not in st['kv']: missing()
n = len(st['kv'][rest])
out({{'data': {{'current_version': n, 'versions': {{str(i + 1): {{}} for i in range(n)}}}}}})
if a[0] == 'write':
rest = a[-2].split('/', 2)[2]
body = json.loads(sys.stdin.read())
st['written'] = body
if st.get('race'): st['kv'][rest] = [{{'x': 1}}]; save(); sys.exit(2)
if st.get('fail_write'): save(); sys.exit(2)
e = st['kv'].setdefault(rest, [])
if body['options']['cas'] != len(e): save(); sys.exit(2)
e.append(body['data'])
out({{'data': {{'version': len(e)}}}})
save(); sys.exit(9)
''')
# Exact accepted Forgejo @1.1 shape; materialized only in this fake test.
CASE = '# First KnownSecurityCase of oidc-browser-relying-party 1.1 (NK-WP-0048-T06).\n# Parameters are the request the founder accepted on 2026-10-08\n# (key-cape docs/forgejo-oidc-client-request.yaml, KEY-WP-0038-T01); that\n# acceptance carries over as this case\'s acceptance. One difference: custody is\n# derived by the type (platform/workloads/keycape/forgejo), not the request\'s\n# proposed platform/workloads/forgejo/netkingdom-oidc-client. The founder\n# acknowledged that derivation with the type (memo r4, 2026-10-09).\n# Re-pinned from 1.0 to 1.1 on 2026-10-10 on the founder\'s go, before any\n# apply, so no 1.0 run overlaps (minor change: consumer-enforces-groups).\napiVersion: netkingdom.io/security-case/v0.1\nkind: SecurityCase\nspec:\n id: forgejo-netkingdom-oidc\n type: oidc-browser-relying-party@1.1\n status: materialized\n acceptance:\n at: "2026-10-08"\n record: "key-cape docs/forgejo-oidc-client-request.yaml (status accepted-2026-10-08, KEY-WP-0038-T01); type acknowledged via informed-decision memo nk-wp-0048-t05-oidc-browser-relying-party-r4, hub decision e418263e"\n by:\n - {role: owner, name: "founder (for railiance-apps)"}\n - {role: founder, name: founder}\n owner:\n repo: railiance-apps\n person: founder\n parameters:\n client_id: forgejo\n display_name: Forgejo source forge\n redirect_uris: https://forgejo.coulomb.social/user/oauth2/netkingdom/callback\n allowed_scopes: openid,profile,email,groups\n client_type: confidential\n mfa_required: "true"\n access_groups: forge-admins,forge-coulomb-owners,forge-coulomb-readers\n initial_members: forge-admins=tegwick;forge-coulomb-owners=tegwick\n consuming_service: forgejo\n consumer_owner_repo: railiance-apps\n'
SERVICE_TYPE = Path('/home/worsch/net-kingdom/security-case-types/oidc-service-client/type.yaml')
# The first oidc-service-client case (net-kingdom examples/nk-bind-executor-approval.yaml),
# materialized with an acceptance only in this fake test.
SERVICE_CASE = '''apiVersion: netkingdom.io/security-case/v0.1
kind: SecurityCase
spec:
id: nk-bind-executor-approval
type: oidc-service-client@0.2
status: materialized
acceptance:
at: "2026-11-01"
record: "test fixture only"
by:
- {role: owner, name: founder}
- {role: founder, name: founder}
owner:
repo: net-kingdom
person: founder
parameters:
client_id: nk-bind-executor-approval
display_name: NetKingdom bounded executor (approval-engine)
audience: approval-engine
allowed_scopes: approval:create,approval:read,approval:consume
service_subject: service:nk-bind-executor
tenant: tenant:platform
roles: bounded-executor
token_lifetime: 15m
consuming_service: bounded-executor
consumer_namespace: sso
consumer_service_account: nk-bind-executor
consumer_owner_repo: net-kingdom
'''
class Harness(unittest.TestCase):
CASE_TEXT = CASE
CLIENT_ID = 'forgejo'
def setUp(self):
self.tmp = Path(tempfile.mkdtemp())
(self.tmp / 'bin').mkdir()
(self.tmp / 'bin/bao').write_text(FAKE_BAO)
(self.tmp / 'bin/bao').chmod(0o755)
self.state = {'calls': [], 'kv': {},
'caller_policies': ['default', 'credential-broker-keycape-client-secret-generate-issuer']}
self.write_case(self.CASE_TEXT)
def write_case(self, text):
self.case = self.tmp / 'case.yaml'
self.case.write_text(text)
case_id = yaml.safe_load(text)['spec']['id']
self.ref = case_id + '@sha256:' + hashlib.sha256(text.encode()).hexdigest()
def run_cli(self, *extra, client_id=None, ref=None, env_extra=None):
client_id = client_id or self.CLIENT_ID
(self.tmp / 'state.json').write_text(json.dumps(self.state))
receipt = self.tmp / f'r{len(list(self.tmp.glob("r*.json")))}.json'
empty_home = self.tmp / 'empty-home'
empty_home.mkdir(exist_ok=True)
env = {'PATH': os.defpath, 'HOME': str(empty_home)}
env.update(PATH=f'{self.tmp / "bin"}:{env["PATH"]}', FAKE_BAO_STATE=str(self.tmp / 'state.json'),
**(env_extra or {}))
proc = subprocess.run([sys.executable, str(CLI), 'generate', '--grant', 'keycape/client-secret-generate',
'--client-id', client_id, '--case-file', str(self.case),
'--case-ref', ref or self.ref, '--receipt', str(receipt), *extra],
env=env, capture_output=True, text=True)
self.assertEqual((proc.stdout, proc.stderr), ('', ''))
self.state = json.loads((self.tmp / 'state.json').read_text())
text = receipt.read_text()
self.assertNotIn(GRANT_TOKEN, text)
written = self.state.get('written')
if written:
self.assertNotIn(written['data']['CLIENT_SECRET'], text)
return proc.returncode, json.loads(text)
def kv_calls(self):
return [c for c in self.state['calls'] if c['args'][0] in ('read', 'write')]
class Generate(Harness):
def test_generates_once_with_grant_token_and_revokes_it(self):
code, receipt = self.run_cli()
self.assertEqual((code, receipt['outcome']), (0, 'generated'))
self.assertEqual(receipt['kv_version'], 1)
self.assertTrue(receipt['grant_token_revoked'])
self.assertEqual(self.state['revoked'], GRANT_TOKEN)
self.assertTrue(all(c['token'] == GRANT_TOKEN for c in self.kv_calls()))
self.assertFalse(any(c['args'][0] == 'read' and '/data/' in c['args'][-1] for c in self.kv_calls()))
value = self.state['written']['data']['CLIENT_SECRET']
self.assertEqual(len(value), 64)
self.assertEqual(self.state['written']['options'], {'cas': 0})
def test_existing_entry_is_never_touched(self):
self.state['kv']['workloads/keycape/forgejo'] = [{'CLIENT_SECRET': 'old'}]
code, receipt = self.run_cli()
self.assertEqual((code, receipt['outcome']), (3, 'already_present'))
self.assertNotIn('written', self.state)
self.assertTrue(receipt['grant_token_revoked'])
def test_lost_cas_race_reports_already_present(self):
self.state['race'] = True
code, receipt = self.run_cli()
self.assertEqual((code, receipt['outcome']), (3, 'already_present_lost_race'))
def test_dry_run_writes_nothing(self):
code, receipt = self.run_cli('--dry-run')
self.assertEqual((code, receipt['outcome']), (0, 'dry_run_would_generate'))
self.assertNotIn('written', self.state)
def test_case_checks_refuse_before_any_bao_call(self):
cases = [
('case_ref_mismatch', {'ref': 'forgejo-netkingdom-oidc@sha256:' + '0' * 64}),
('case_client_id_mismatch', {'client_id': 'gitea'}),
('client_id_invalid_or_reserved', {'client_id': 'secrets-engine-openbao'}),
('client_id_invalid_or_reserved', {'client_id': '../forgejo'}),
('case_ref_malformed', {'ref': 'forgejo'}),
]
for outcome, kwargs in cases:
with self.subTest(outcome=outcome, **kwargs):
self.state['calls'] = []
code, receipt = self.run_cli(**kwargs)
self.assertEqual((code, receipt['outcome']), (2, outcome))
self.assertEqual(self.state['calls'], [])
def test_unmaterialized_or_public_case_refused(self):
for text, outcome in ((CASE.replace('materialized', 'accepted'), 'case_not_materialized'),
(CASE.replace('confidential', 'public'), 'case_client_not_confidential')):
with self.subTest(outcome=outcome):
self.write_case(text)
code, receipt = self.run_cli()
self.assertEqual((code, receipt['outcome']), (2, outcome))
def test_only_exact_acknowledged_type_version_is_admitted(self):
for type_ref in ('oidc-browser-relying-party@1.0',
'oidc-browser-relying-party@1.2',
'oidc-browser-relying-party@1.3',
'oidc-browser-relying-party@99.0',
'confidential-service-client@1.1'):
with self.subTest(type_ref=type_ref):
self.write_case(CASE.replace('oidc-browser-relying-party@1.1', type_ref))
self.state['calls'] = []
code, receipt = self.run_cli()
self.assertEqual((code, receipt['outcome']), (2, 'case_type_unacknowledged'))
self.assertEqual(self.state['calls'], [])
def test_source_acknowledgment_alone_does_not_admit_12(self):
self.write_case(CASE.replace('oidc-browser-relying-party@1.1',
'oidc-browser-relying-party@1.2')
+ ' consumer_namespace: forgejo\n')
code, receipt = self.run_cli()
self.assertEqual((code, receipt['outcome']), (2, 'case_type_unacknowledged'))
self.assertEqual(self.state['calls'], [])
def test_caller_environment_cannot_select_type_or_source_root(self):
code, receipt = self.run_cli(env_extra={
'NETKINGDOM_ROOT': '/nonexistent',
'WARDEN_NETKINGDOM_ROOT': '/nonexistent',
'PYTHONPATH': '/nonexistent',
})
self.assertEqual((code, receipt['outcome']), (0, 'generated'))
def test_acknowledged_11_still_requires_its_exact_parameters(self):
self.write_case(CASE + ' unexpected_target: forgejo\n')
code, receipt = self.run_cli()
self.assertEqual((code, receipt['outcome']), (2, 'case_source_validation_failed'))
self.assertEqual(self.state['calls'], [])
def test_future_revision_requires_explicit_owner_binding_and_acknowledgment(self):
# Synthetic owner configuration exercises the extension mechanism only.
# This is not the actual proposed 1.3 type, acknowledgment or parameter pin.
spec = importlib.util.spec_from_file_location(
'future_fixture_generator', ROOT / 'scripts/keycape_client_secret_generate.py')
broker = importlib.util.module_from_spec(spec)
spec.loader.exec_module(broker)
source = self.tmp / 'synthetic-owner-root'
source.mkdir()
relative, digest = broker.ACKNOWLEDGED_TYPES['oidc-browser-relying-party@1.1']
for rel in (relative, *broker.SOURCE_PINS):
dest = source / rel
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(broker.NETKINGDOM_ROOT / rel, dest)
typ = source / relative
original = typ.read_bytes()
doc = yaml.safe_load(CASE)
doc['spec']['type'] = 'oidc-browser-relying-party@1.3'
typ.write_bytes(original.replace(b'version: "1.1"', b'version: "1.3"'))
# YAML numeric spelling is owner-authored, never an inferred revision.
type_doc = yaml.safe_load(original)
type_doc['spec']['version'] = '1.3'
typ.write_text(yaml.safe_dump(type_doc))
binding = {doc['spec']['type']: (relative, hashlib.sha256(typ.read_bytes()).hexdigest())}
with mock.patch.object(broker, 'NETKINGDOM_ROOT', source):
with self.assertRaises(broker.Exit):
broker.check_acknowledged_case(doc) # no explicit binding
with mock.patch.dict(broker.ACKNOWLEDGED_TYPES, binding):
with self.assertRaises(broker.Exit):
broker.check_acknowledged_case(doc) # no parameter admission
with mock.patch.dict(broker.PARAMETER_PINS, {
'oidc-browser-relying-party@1.3': broker.PARAMETER_PINS['oidc-browser-relying-party@1.1']}):
broker.check_acknowledged_case(doc)
type_doc['spec']['status'] = 'proposed'
typ.write_text(yaml.safe_dump(type_doc))
proposed = {doc['spec']['type']: (relative, hashlib.sha256(typ.read_bytes()).hexdigest())}
with mock.patch.dict(broker.ACKNOWLEDGED_TYPES, proposed):
with self.assertRaises(broker.Exit) as caught:
broker.check_acknowledged_case(doc)
self.assertEqual(caught.exception.reason, 'case_source_validation_failed')
def test_wrong_sources_proposed_unknown_versions_and_symlinks_fail_closed(self):
spec = importlib.util.spec_from_file_location(
'candidate_generator', ROOT / 'scripts/keycape_client_secret_generate.py')
broker = importlib.util.module_from_spec(spec)
spec.loader.exec_module(broker)
source = self.tmp / 'net-kingdom'
source.mkdir()
relative, digest = broker.ACKNOWLEDGED_TYPES['oidc-browser-relying-party@1.1']
for rel in (relative, *broker.SOURCE_PINS):
dest = source / rel
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(broker.NETKINGDOM_ROOT / rel, dest)
doc = yaml.safe_load(CASE)
with mock.patch.object(broker, 'NETKINGDOM_ROOT', source):
broker.check_acknowledged_case(doc)
producer = source / next(iter(broker.SOURCE_PINS))
raw = producer.read_bytes()
producer.write_bytes(raw + b'\n# drift\n')
with self.assertRaises(broker.Exit) as caught:
broker.check_acknowledged_case(doc)
self.assertEqual(caught.exception.reason, 'case_source_validation_failed')
producer.write_bytes(raw)
typ = source / relative
raw = typ.read_bytes()
typ.write_bytes(raw.replace(b'status: acknowledged', b'status: proposed'))
# Even a hypothetical owner pin update cannot admit proposed status.
with mock.patch.dict(broker.ACKNOWLEDGED_TYPES, {
doc['spec']['type']: (relative, hashlib.sha256(typ.read_bytes()).hexdigest())}):
with self.assertRaises(broker.Exit) as caught:
broker.check_acknowledged_case(doc)
self.assertEqual(caught.exception.reason, 'case_source_validation_failed')
typ.write_bytes(raw + b'\n# source drift\n')
with self.assertRaises(broker.Exit) as caught:
broker.check_acknowledged_case(doc)
self.assertEqual(caught.exception.reason, 'case_source_validation_failed')
typ.unlink()
typ.symlink_to(broker.__file__)
with self.assertRaises(broker.Exit) as caught:
broker.check_acknowledged_case(doc)
self.assertEqual(caught.exception.reason, 'case_source_validation_failed')
for ref in ('oidc-browser-relying-party@1.3', 'oidc-browser-relying-party@99.0'):
doc['spec']['type'] = ref
with self.assertRaises(broker.Exit) as caught:
broker.check_acknowledged_case(doc)
self.assertEqual(caught.exception.reason, 'case_type_unacknowledged')
def test_exact_accepted_case_still_requires_materialization(self):
self.write_case(CASE.replace('status: materialized', 'status: accepted'))
code, receipt = self.run_cli()
self.assertEqual((code, receipt['outcome']), (2, 'case_not_materialized'))
self.assertEqual(self.state['calls'], [])
def test_agents_root_and_unbound_callers_refused(self):
code, receipt = self.run_cli(env_extra={'WARDEN_AGENT_ID': 'codex'})
self.assertEqual((code, receipt['outcome']), (2, 'agent_callers_refused'))
self.state['caller_policies'] = ['root']
code, receipt = self.run_cli()
self.assertEqual((code, receipt['outcome']), (2, 'root_refused'))
self.state['caller_policies'] = ['default']
code, receipt = self.run_cli()
self.assertEqual((code, receipt['outcome']), (2, 'caller_lacks_issuer_policy'))
self.assertFalse(any(c['args'][:2] == ['token', 'create'] for c in self.state['calls']))
def test_write_failure_is_reported_and_token_revoked(self):
self.state['fail_write'] = True
code, receipt = self.run_cli()
self.assertEqual((code, receipt['outcome']), (4, 'write_failed'))
self.assertTrue(receipt['grant_token_revoked'])
class ServiceClientGenerate(Harness):
"""oidc-service-client@0.2 (RPF-WP-0069-T08): admitted, confidential by type, create-only."""
CASE_TEXT = SERVICE_CASE
CLIENT_ID = 'nk-bind-executor-approval'
def test_generates_once_at_the_case_path_and_revokes(self):
code, receipt = self.run_cli()
self.assertEqual((code, receipt['outcome']), (0, 'generated'))
self.assertEqual(receipt['path'], 'platform/workloads/keycape/nk-bind-executor-approval')
self.assertTrue(receipt['case_verified'])
self.assertTrue(receipt['grant_token_revoked'])
self.assertTrue(all(c['token'] == GRANT_TOKEN for c in self.kv_calls()))
self.assertFalse(any(c['args'][0] == 'read' and '/data/' in c['args'][-1] for c in self.kv_calls()))
self.assertEqual(self.state['written']['options'], {'cas': 0})
self.assertEqual(list(self.state['written']['data']), ['CLIENT_SECRET'])
def test_existing_path_is_refused_and_never_touched(self):
self.state['kv']['workloads/keycape/nk-bind-executor-approval'] = [{'CLIENT_SECRET': 'old'}]
code, receipt = self.run_cli()
self.assertEqual((code, receipt['outcome']), (3, 'already_present'))
self.assertNotIn('written', self.state)
self.assertTrue(receipt['grant_token_revoked'])
def test_lost_cas_race_and_dry_run(self):
code, receipt = self.run_cli('--dry-run')
self.assertEqual((code, receipt['outcome']), (0, 'dry_run_would_generate'))
self.assertNotIn('written', self.state)
self.state['race'] = True
code, receipt = self.run_cli()
self.assertEqual((code, receipt['outcome']), (3, 'already_present_lost_race'))
def test_confidentiality_comes_from_the_type_not_a_parameter(self):
# No client_type parameter is needed ...
self.assertNotIn('client_type', SERVICE_CASE)
# ... and a case cannot claim one, either way.
for value in ('confidential', 'public'):
with self.subTest(value=value):
self.write_case(SERVICE_CASE + f' client_type: {value}\n')
self.state['calls'] = []
code, receipt = self.run_cli()
self.assertEqual((code, receipt['outcome']), (2, 'case_client_type_not_admitted'))
self.assertEqual(self.state['calls'], [])
def test_type_refusals_are_refused_before_any_bao_call(self):
bad = {
'service_subject: service:nk-bind-executor': 'service_subject: nk-bind-executor',
'token_lifetime: 15m': 'token_lifetime: 30m',
'allowed_scopes: approval:create,approval:read,approval:consume': 'allowed_scopes: openid,approval:read',
'tenant: tenant:platform': 'tenant: platform',
'audience: approval-engine': 'audience: Approval_Engine',
'consumer_namespace: sso': 'consumer_namespace: SSO',
'consumer_service_account: nk-bind-executor': 'consumer_service_account: ../nk',
}
for old, new in bad.items():
with self.subTest(new=new):
self.assertIn(old, SERVICE_CASE)
self.write_case(SERVICE_CASE.replace(old, new))
self.state['calls'] = []
code, receipt = self.run_cli()
self.assertEqual((code, receipt['outcome']), (2, 'case_source_validation_failed'))
self.assertEqual(self.state['calls'], [])
for extra in (' issuer: https://evil.example\n', ' redirect_uris: https://x.example/cb\n',
' unexpected_target: forgejo\n'):
with self.subTest(extra=extra):
self.write_case(SERVICE_CASE + extra)
self.state['calls'] = []
code, receipt = self.run_cli()
self.assertEqual((code, receipt['outcome']), (2, 'case_source_validation_failed'))
self.assertEqual(self.state['calls'], [])
for required in ('consumer_service_account', 'service_subject', 'audience'):
with self.subTest(missing=required):
text = '\n'.join(l for l in SERVICE_CASE.splitlines() if not l.strip().startswith(required + ':')) + '\n'
self.write_case(text)
self.state['calls'] = []
code, receipt = self.run_cli()
self.assertEqual((code, receipt['outcome']), (2, 'case_source_validation_failed'))
self.assertEqual(self.state['calls'], [])
def test_case_and_client_checks_still_apply(self):
for outcome, kwargs in (('case_client_id_mismatch', {'client_id': 'forgejo'}),
('client_id_invalid_or_reserved', {'client_id': 'secrets-engine-openbao'})):
with self.subTest(outcome=outcome):
self.state['calls'] = []
code, receipt = self.run_cli(**kwargs)
self.assertEqual((code, receipt['outcome']), (2, outcome))
self.assertEqual(self.state['calls'], [])
for text, outcome in ((SERVICE_CASE.replace('status: materialized', 'status: proposed'), 'case_not_materialized'),
(SERVICE_CASE.replace(' acceptance:\n at: "2026-11-01"\n record: "test fixture only"\n'
' by:\n - {role: owner, name: founder}\n'
' - {role: founder, name: founder}\n', ''),
'case_source_validation_failed')):
with self.subTest(outcome=outcome):
self.write_case(text)
self.state['calls'] = []
code, receipt = self.run_cli()
self.assertEqual((code, receipt['outcome']), (2, outcome))
self.assertEqual(self.state['calls'], [])
def test_only_exact_acknowledged_service_revision_is_admitted(self):
for type_ref in ('oidc-service-client@0.1', 'oidc-service-client@0.3', 'oidc-service-client@1.1'):
with self.subTest(type_ref=type_ref):
self.write_case(SERVICE_CASE.replace('oidc-service-client@0.2', type_ref))
self.state['calls'] = []
code, receipt = self.run_cli()
# An unlisted type keeps the browser parameter check, which a service case
# (no client_type) fails first; either way nothing reaches bao.
self.assertEqual((code, receipt['outcome']), (2, 'case_client_not_confidential'))
self.assertEqual(self.state['calls'], [])
def _broker_with_copied_sources(self):
spec = importlib.util.spec_from_file_location(
'service_generator', ROOT / 'scripts/keycape_client_secret_generate.py')
broker = importlib.util.module_from_spec(spec)
spec.loader.exec_module(broker)
source = self.tmp / 'net-kingdom'
relative, _ = broker.ACKNOWLEDGED_TYPES['oidc-service-client@0.2']
for rel in (relative, *broker.SOURCE_PINS):
dest = source / rel
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(broker.NETKINGDOM_ROOT / rel, dest)
return broker, source, source / relative
def test_drifted_or_proposed_type_fails_closed(self):
broker, source, typ = self._broker_with_copied_sources()
doc = yaml.safe_load(SERVICE_CASE)
raw = typ.read_bytes()
with mock.patch.object(broker, 'NETKINGDOM_ROOT', source):
broker.check_acknowledged_case(doc)
typ.write_bytes(raw + b'\n# drift\n')
with self.assertRaises(broker.Exit) as caught:
broker.check_acknowledged_case(doc)
self.assertEqual(caught.exception.reason, 'case_source_validation_failed')
typ.write_bytes(raw.replace(b'status: acknowledged', b'status: proposed'))
with mock.patch.dict(broker.ACKNOWLEDGED_TYPES, {
'oidc-service-client@0.2': (str(typ.relative_to(source)), hashlib.sha256(typ.read_bytes()).hexdigest())}):
with self.assertRaises(broker.Exit) as caught:
broker.check_acknowledged_case(doc)
self.assertEqual(caught.exception.reason, 'case_source_validation_failed')
def test_parameter_pin_is_per_type_reference(self):
broker, source, typ = self._broker_with_copied_sources()
doc = yaml.safe_load(SERVICE_CASE)
with mock.patch.object(broker, 'NETKINGDOM_ROOT', source):
with mock.patch.dict(broker.PARAMETER_PINS, {'oidc-service-client@0.2': '0' * 64}):
with self.assertRaises(broker.Exit) as caught:
broker.check_acknowledged_case(doc)
self.assertEqual(caught.exception.reason, 'case_source_validation_failed')
def test_type_that_is_not_confidential_by_construction_is_refused(self):
broker, source, typ = self._broker_with_copied_sources()
doc = yaml.safe_load(SERVICE_CASE)
type_doc = yaml.safe_load(typ.read_bytes())
for item in type_doc['spec']['materialization']:
if item['id'] == 'client-secret-path':
item['when'] = 'client_type == confidential'
typ.write_text(yaml.safe_dump(type_doc))
with mock.patch.object(broker, 'NETKINGDOM_ROOT', source):
with mock.patch.dict(broker.ACKNOWLEDGED_TYPES, {
'oidc-service-client@0.2': (str(typ.relative_to(source)), hashlib.sha256(typ.read_bytes()).hexdigest())}):
with self.assertRaises(broker.Exit) as caught:
broker.check_acknowledged_case(doc)
self.assertEqual(caught.exception.reason, 'case_type_not_confidential')
def test_pins_match_the_acknowledged_source(self):
spec = importlib.util.spec_from_file_location(
'pin_generator', ROOT / 'scripts/keycape_client_secret_generate.py')
broker = importlib.util.module_from_spec(spec)
spec.loader.exec_module(broker)
raw = SERVICE_TYPE.read_bytes()
self.assertEqual(hashlib.sha256(raw).hexdigest(), broker.ACKNOWLEDGED_TYPES['oidc-service-client@0.2'][1])
typ = yaml.safe_load(raw)['spec']
self.assertEqual((typ['version'], typ['status']), ('0.2', 'acknowledged'))
names = {p['name'] for p in typ['parameters']}
self.assertTrue({'client_id', 'audience', 'allowed_scopes', 'service_subject', 'tenant',
'token_lifetime', 'consuming_service', 'consumer_namespace',
'consumer_service_account', 'consumer_owner_repo'} <= names)
self.assertNotIn('client_type', names)
client_id = next(p for p in typ['parameters'] if p['name'] == 'client_id')
self.assertEqual(client_id['constraints']['pattern'], broker.CLIENT_ID_RE.pattern)
self.assertTrue(broker.type_is_confidential(typ))
self.assertEqual(broker.CONFIDENTIALITY['oidc-browser-relying-party@1.1'], 'parameter')
if __name__ == '__main__':
unittest.main()