"""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()