2026-10-09 20:12:44 +02:00
""" 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
2026-10-17 22:55:51 +02:00
import importlib . util
import shutil
from unittest import mock
import yaml
2026-10-09 20:12:44 +02:00
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 )
''' )
2026-10-11 18:46:21 +02:00
# 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). \n apiVersion: netkingdom.io/security-case/v0.1 \n kind: SecurityCase \n spec: \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 '
2026-10-09 20:12:44 +02:00
2026-11-01 10:24:17 +01:00
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 : 15 m
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 '
2026-10-09 20:12:44 +02:00
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 ' ] }
2026-11-01 10:24:17 +01:00
self . write_case ( self . CASE_TEXT )
2026-10-09 20:12:44 +02:00
def write_case ( self , text ) :
self . case = self . tmp / ' case.yaml '
self . case . write_text ( text )
2026-11-01 10:24:17 +01:00
case_id = yaml . safe_load ( text ) [ ' spec ' ] [ ' id ' ]
self . ref = case_id + ' @sha256: ' + hashlib . sha256 ( text . encode ( ) ) . hexdigest ( )
2026-10-09 20:12:44 +02:00
2026-11-01 10:24:17 +01:00
def run_cli ( self , * extra , client_id = None , ref = None , env_extra = None ) :
client_id = client_id or self . CLIENT_ID
2026-10-09 20:12:44 +02:00
( self . tmp / ' state.json ' ) . write_text ( json . dumps ( self . state ) )
receipt = self . tmp / f ' r { len ( list ( self . tmp . glob ( " r*.json " ) ) ) } .json '
2026-10-17 22:55:51 +02:00
empty_home = self . tmp / ' empty-home '
empty_home . mkdir ( exist_ok = True )
env = { ' PATH ' : os . defpath , ' HOME ' : str ( empty_home ) }
2026-10-09 20:12:44 +02:00
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 ' ) ]
2026-11-01 10:24:17 +01:00
class Generate ( Harness ) :
2026-10-09 20:12:44 +02:00
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 ) )
2026-10-11 18:46:21 +02:00
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 ' ,
2026-10-17 22:55:51 +02:00
' oidc-browser-relying-party@1.3 ' ,
2026-10-11 18:46:21 +02:00
' 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 ( )
2026-10-17 22:55:51 +02:00
self . assertEqual ( ( code , receipt [ ' outcome ' ] ) , ( 2 , ' case_type_unacknowledged ' ) )
2026-10-11 18:46:21 +02:00
self . assertEqual ( self . state [ ' calls ' ] , [ ] )
2026-10-17 22:55:51 +02:00
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
2026-11-01 10:24:17 +01:00
with mock . patch . dict ( broker . PARAMETER_PINS , {
' oidc-browser-relying-party@1.3 ' : broker . PARAMETER_PINS [ ' oidc-browser-relying-party@1.1 ' ] } ) :
2026-10-17 22:55:51 +02:00
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 ' )
2026-10-11 18:46:21 +02:00
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 ' ] , [ ] )
2026-10-09 20:12:44 +02:00
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 ' ] )
2026-11-01 10:24:17 +01:00
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 ' )
2026-10-09 20:12:44 +02:00
if __name__ == ' __main__ ' :
unittest . main ( )