#!/usr/bin/env python3 """Migrate only the two reviewed baseline registrations; never export Secret values. Runtime policy overrides are issuer-owned in its policy PVC. This migration is for initial activation; subsequent changes use the authenticated policy journey. """ import argparse import base64 import copy import importlib.util import json from pathlib import Path import subprocess import yaml ROOT = Path(__file__).resolve().parent spec = importlib.util.spec_from_file_location('config_pin', ROOT/'openbao-client-config.py') pin = importlib.util.module_from_spec(spec); spec.loader.exec_module(pin) CLIENTS = {'vergabe-demo-company', 'user-engine-portal'} CLUSTER_UID = 'a553c742-0115-43d4-99a4-a5ca56fe0786' class Refused(Exception): pass def require(condition, reason): if not condition: raise Refused(reason) def replacement(secret): try: raw,config,_ = pin.issuer_document(secret) except Exception: raise Refused("issuer_configuration_unreadable") from None clients = config.get('clients',[]) ids = [c.get('clientId') for c in clients] require(len(ids)==len(set(ids)) and CLIENTS.issubset(ids), 'reviewed_clients_required') document = yaml.compose(raw) nodes = next(v for k,v in document.value if k.value=='clients') edits=[]; expected=copy.deepcopy(config) for index,client in enumerate(clients): if client['clientId'] not in CLIENTS: continue require(client.get('clientType')=='public' and client.get('grantTypes')==['authorization_code'], 'client_shape_changed') require('mfaRequired' not in client, 'explicit_policy_requires_review') if client.get('mfaOptional') is True: continue require('mfaOptional' not in client, 'explicit_policy_requires_review') node=nodes.value[index] require(not node.flow_style, 'block_client_required') column=node.value[0][0].start_mark.column position=node.end_mark.index line_start=raw.rfind('\n',0,position)+1 if not raw[line_start:position].strip(): position=line_start addition=('' if position==0 or raw[position-1]=='\n' else '\n')+' '*column+'mfaOptional: true\n' edits.append((position,addition));expected['clients'][index]['mfaOptional']=True updated=raw for position,addition in sorted(edits,reverse=True):updated=updated[:position]+addition+updated[position:] require(yaml.load(updated,Loader=pin.UniqueLoader)==expected, 'unrelated_configuration_changed') return base64.b64encode(updated.encode()).decode(),bool(edits) def kube(args,payload=None): result=subprocess.run(['kubectl','--request-timeout=20s',*args],input=json.dumps(payload) if payload is not None else None,capture_output=True,text=True,timeout=30) require(result.returncode==0,'kubernetes_operation_failed') return json.loads(result.stdout) def run(args): require(kube(['get','namespace','kube-system','-o','json'])['metadata']['uid']==CLUSTER_UID,'cluster_changed') secret=kube(['-n','sso','get','secret','keycape-config','-o','json']) require(not secret['metadata'].get('ownerReferences'),'controller_owned_configuration') value,changed=replacement(secret);metadata=pin.safe_metadata(secret) receipt={'clients':sorted(CLIENTS),'before':metadata,'change_needed':changed,'other_clients_unchanged':True,'secret_values_emitted':False,'mode':args.mode} if args.mode=='inspect':return receipt require(metadata=={'uid':args.expected_uid,'resource_version':args.expected_resource_version},'observed_revision_changed') if changed: patch=[{'op':'test','path':'/metadata/uid','value':metadata['uid']},{'op':'test','path':'/metadata/resourceVersion','value':metadata['resource_version']},{'op':'replace','path':'/data/config.yaml','value':value}] command=['-n','sso','patch','secret','keycape-config','--type=json','--patch-file=/dev/stdin','-o','json'] if args.mode=='dry-run':command.append('--dry-run=server') response=kube(command,patch) require(response['data']==dict(secret['data'],**{'config.yaml':value}),'patch_readback_mismatch') if args.mode=='apply': response=kube(['-n','sso','get','secret','keycape-config','-o','json']) require(response['metadata']['uid']==metadata['uid'] and response['data']==dict(secret['data'],**{'config.yaml':value}),'readback_changed') receipt['after']=pin.safe_metadata(response) return receipt if __name__=='__main__': parser=argparse.ArgumentParser();parser.add_argument('mode',choices=['inspect','dry-run','apply']);parser.add_argument('--expected-uid');parser.add_argument('--expected-resource-version') try: print(json.dumps(run(parser.parse_args()))) except Refused as error:print(json.dumps({'success':False,'failure':str(error)}));raise SystemExit(1)