net-kingdom/sso-mfa/k8s/privacyidea/deploy-onboarding-guard.py
tegwick a5496170cf
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
Implement scoped P06 authentication policy and guarded optional onboarding
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a09cbb-87c6-7900-a145-4ce53ba9f1a6
2026-09-14 00:00:09 +02:00

64 lines
4.6 KiB
Python

#!/usr/bin/env python3
"""Deploy the reviewed provider enrollment hook while preserving runtime secrets."""
import argparse
import copy
import hashlib
import json
from pathlib import Path
import subprocess
CLUSTER='a553c742-0115-43d4-99a4-a5ca56fe0786'
PROVIDER='58c7f96d-61cb-4dd4-bca2-54661c0ac375'
CONFIG='423d857b-6b10-40ad-aba8-2cda2d3645ef'
SUFFIX='\n# P06: fail startup if the enrollment guard is unavailable.\nfrom keycape_onboarding_guard import check as _p06_onboarding_check\nPI_INIT_CHECK_HOOK = "keycape_onboarding_guard.check"\n'
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)
if result.returncode:raise RuntimeError('kubernetes_operation_failed')
return json.loads(result.stdout)
def plan(source,config,deployment):
code=(source/'scripts/keycape_onboarding_guard.py').read_text()
digest=hashlib.sha256(code.encode()).hexdigest();name='keycape-onboarding-'+digest[:16]
cm={'apiVersion':'v1','kind':'ConfigMap','metadata':{'name':name,'namespace':'mfa'},'immutable':True,'data':{'keycape_onboarding_guard.py':code}}
original=config['data']['pi.cfg']
if 'PI_INIT_CHECK_HOOK' in original and not original.endswith(SUFFIX):raise RuntimeError('existing_hook_requires_review')
updated=original if original.endswith(SUFFIX) else original+SUFFIX
spec=copy.deepcopy(deployment['spec']['template']['spec'])
for container in spec['containers']:
if container['name'] not in {'privacyidea','factor-recovery'}:continue
env=container.setdefault('env',[])
path=next((e for e in env if e['name']=='PYTHONPATH'),None)
if path and path.get('value') not in {'/opt/keycape-onboarding'}:raise RuntimeError('existing_python_path_requires_review')
if not path:env.append({'name':'PYTHONPATH','value':'/opt/keycape-onboarding'})
mounts=container.setdefault('volumeMounts',[])
existing=next((m for m in mounts if m['name']=='onboarding-guard'),None)
desired={'name':'onboarding-guard','mountPath':'/opt/keycape-onboarding','readOnly':True}
if existing and existing!=desired:raise RuntimeError('existing_guard_mount_differs')
if not existing:mounts.append(desired)
spec['volumes']=[v for v in spec['volumes'] if v['name']!='onboarding-guard']+[{'name':'onboarding-guard','configMap':{'name':name}}]
return cm,updated,spec
def main():
parser=argparse.ArgumentParser();parser.add_argument('--source',type=Path,required=True);parser.add_argument('--apply',action='store_true');args=parser.parse_args()
if kube(['get','namespace','kube-system','-o','json'])['metadata']['uid']!=CLUSTER:raise RuntimeError('cluster_changed')
config=kube(['-n','mfa','get','configmap','privacyidea-cfg','-o','json']);deployment=kube(['-n','mfa','get','deployment','privacyidea','-o','json'])
if config['metadata']['uid']!=CONFIG or deployment['metadata']['uid']!=PROVIDER:raise RuntimeError('target_identity_changed')
cm,updated,spec=plan(args.source,config,deployment)
print(json.dumps({'apply':args.apply,'guard_config':cm['metadata']['name'],'target':'mfa/privacyidea','config_uid':CONFIG,'provider_uid':PROVIDER,'secret_values_emitted':False}),flush=True)
if not args.apply:return
committed=subprocess.run(['git','show','HEAD:scripts/keycape_onboarding_guard.py'],cwd=args.source,capture_output=True,text=True)
if committed.returncode or committed.stdout!=cm['data']['keycape_onboarding_guard.py']:raise RuntimeError('guard_source_must_be_committed')
kube(['apply','-f','-','-o','json'],cm)
if updated!=config['data']['pi.cfg']:
kube(['-n','mfa','patch','configmap','privacyidea-cfg','--type=json','--patch-file=/dev/stdin','-o','json'],[
{'op':'test','path':'/metadata/uid','value':CONFIG},{'op':'test','path':'/metadata/resourceVersion','value':config['metadata']['resourceVersion']},
{'op':'replace','path':'/data/pi.cfg','value':updated}])
kube(['-n','mfa','patch','deployment','privacyidea','--type=json','--patch-file=/dev/stdin','-o','json'],[
{'op':'test','path':'/metadata/uid','value':PROVIDER},{'op':'test','path':'/metadata/resourceVersion','value':deployment['metadata']['resourceVersion']},
{'op':'replace','path':'/spec/template/spec','value':spec}])
after=kube(['-n','mfa','get','configmap','privacyidea-cfg','-o','json'])
if after['data']!=dict(config['data'],**{'pi.cfg':updated}):raise RuntimeError('configuration_readback_changed')
print(json.dumps({'guard_deployed':True,'wait_for_rollout_and_policy_activation':True}))
if __name__=='__main__':main()