net-kingdom/sso-mfa/k8s/keycape/playwright-activation.py
tegwick 276b95f6ba
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
security: reject Playwright audience and delivery alias collisions
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a1346b-72a4-7d40-b8bd-8b8ca346cfa2
2026-10-13 23:44:41 +02:00

236 lines
13 KiB
Python

"""Private, digest-bound Playwright registry/Deployment patch preparation.
No CLI or Kubernetes client: the existing protected owner execution path must
provide fresh objects, an independently approved proposal digest, admitted
custody, exclusive maintenance, and readiness/login verification. Proposal and
patch bodies contain private registry data; never log or serialize them to chat.
Only summary() is an output receipt. This library grants no execution authority.
"""
from __future__ import annotations
import base64
import copy
import hashlib
import importlib.util
import json
from pathlib import Path
import re
import subprocess
spec = importlib.util.spec_from_file_location('playwright_registration', Path(__file__).with_name('playwright-registration.py'))
registration = importlib.util.module_from_spec(spec)
spec.loader.exec_module(registration)
require = registration.require
Refused = registration.Refused
def digest(value):
return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':'),
allow_nan=False).encode()).hexdigest()
def target(obj, kind, name):
require(obj.get('kind') == kind and obj.get('metadata', {}).get('name') == name
and obj['metadata'].get('namespace') == 'sso', 'unexpected_target')
require(not obj['metadata'].get('deletionTimestamp') and
not obj['metadata'].get('ownerReferences'), 'target_not_independently_available')
return registration.registry.pin.safe_metadata(obj)
def container(specification):
items = specification['template']['spec']['containers']
matches = [item for item in items if item.get('name') == 'keycape']
require(len(matches) == 1, 'keycape_container_required')
return matches[0]
def stopped(specification):
result = copy.deepcopy(specification)
result['replicas'] = 0
return result
def build(secret, deployment, identities, source_revision):
secret_meta = target(secret, 'Secret', 'keycape-config')
deploy_meta = target(deployment, 'Deployment', 'keycape')
require(not secret.get('immutable'), 'mutable_registry_required')
require(re.fullmatch(r'[0-9a-f]{40}', source_revision or '') is not None, 'exact_source_revision_required')
original = deployment['spec']
require(original.get('replicas') == 1 and original.get('strategy', {}).get('type') == 'Recreate'
and not original.get('paused'), 'single_recreate_issuer_required')
current = container(original)
require(re.fullmatch(r'forgejo\.coulomb\.social/coulomb/key-cape@sha256:[0-9a-f]{64}',
current.get('image', '')) is not None, 'immutable_owner_image_required')
require(not current.get('envFrom'), 'explicit_env_bindings_required')
env = current.get('env', [])
require(isinstance(env, list) and all(isinstance(e, dict) and isinstance(e.get('name'), str) for e in env),
'env_shape_invalid')
names = [e['name'] for e in env]
require(len(names) == len(set(names)), 'duplicate_env_name')
require([e for e in env if e['name'] == 'KEYCAPE_CONFIG'] ==
[{'name': 'KEYCAPE_CONFIG', 'value': '/etc/keycape/config.yaml'}], 'config_path_drift')
volumes = original['template']['spec'].get('volumes', [])
mounts = current.get('volumeMounts', [])
expected = [v['name'] for v in volumes if v.get('secret', {}).get('secretName') == 'keycape-config']
require(len(expected) == 1 and any(m.get('name') == expected[0] and
m.get('mountPath') == '/etc/keycape' and m.get('readOnly') is True and
not m.get('subPath') and not m.get('subPathExpr') for m in mounts), 'registry_mount_drift')
encoded, created = registration.prepare_registry(secret, identities)
proposed_data = dict(secret['data'], **{'config.yaml': encoded})
proposed_spec = copy.deepcopy(original)
proposed_env = container(proposed_spec).setdefault('env', [])
added_env = []
records = registration.clients(identities)
for record in records[1:]:
binding = registration.registry.env_binding(record)
target_ref = binding['valueFrom']['secretKeyRef']
for entry in proposed_env:
reference = entry.get('valueFrom', {}).get('secretKeyRef', {})
require(entry['name'] == binding['name'] or
(reference.get('name'), reference.get('key')) != (target_ref['name'], target_ref['key']),
'delivery_secret_already_bound')
existing = [e for e in proposed_env if e['name'] == binding['name']]
require(not existing or existing == [binding], 'existing_binding_requires_reconciliation')
if not existing:
proposed_env.append(binding)
added_env.append(binding['name'])
return {'schema': 'netkingdom.playwright-activation.v1', 'source_revision': source_revision,
'secret_identity': secret_meta, 'deployment_identity': deploy_meta,
'before_data': copy.deepcopy(secret['data']), 'after_data': proposed_data,
'before_spec': copy.deepcopy(original), 'after_spec': proposed_spec,
'identity_inventory': copy.deepcopy(identities), 'created_client_ids': created,
'created_env_names': added_env}
def summary(proposal):
changed = proposal['before_data'] != proposal['after_data'] or proposal['before_spec'] != proposal['after_spec']
return {'schema': proposal['schema'], 'proposal_digest': digest(proposal),
'source_revision': proposal['source_revision'],
'created_client_ids': proposal['created_client_ids'],
'created_env_names': proposal['created_env_names'],
'change_needed': changed, 'maintenance_required': changed,
'live_activated': False, 'custody_admitted': False}
def check_registry(proposal, binary, expected_binary_sha256):
"""Run the exact admitted native verifier, no ambient env or raw diagnostics."""
path = Path(binary)
require(path.is_file() and not path.is_symlink() and
hashlib.sha256(path.read_bytes()).hexdigest() == expected_binary_sha256,
'native_verifier_digest_mismatch')
for stage in ('before', 'after'):
env_names = [e['name'] for e in container(proposal[stage + '_spec']).get('env', [])]
command = [str(path), 'check-config', '--config', '/dev/stdin', '--env-names',
','.join(env_names) or '__NO_ENV_NAMES__']
if 'KEYCAPE_POLICY_PATH' in env_names:
command.append('--policy-store')
try:
result = subprocess.run(command, input=base64.b64decode(proposal[stage + '_data']['config.yaml'], validate=True),
capture_output=True, timeout=60, env={'PATH': '/usr/bin:/bin'})
report = json.loads(result.stdout)
valid = result.returncode == 0 and report.get('valid') is True and not report.get('errors')
except Exception:
valid = False
require(valid, 'native_registry_check_failed')
return {'original_registry_valid': True, 'proposed_registry_valid': True,
'verifier_sha256': expected_binary_sha256}
def quiescent(deployment, pods):
"""Caller supplies an unfiltered complete sso PodList from the same cluster."""
status = deployment.get('status', {})
require(status.get('observedGeneration') == deployment['metadata'].get('generation')
and deployment['spec'].get('replicas') == 0 and
all(status.get(k, 0) == 0 for k in ['replicas', 'readyReplicas', 'availableReplicas', 'updatedReplicas']),
'issuer_not_stopped')
require(pods.get('kind') == 'PodList' and not pods.get('metadata', {}).get('continue')
and isinstance(pods.get('items'), list), 'complete_namespace_pod_list_required')
# Terminating pods are still consumers. Do not infer quiescence from readiness.
for pod in pods['items']:
require(pod.get('metadata', {}).get('namespace') == 'sso', 'pod_inventory_namespace_mismatch')
pod_spec = pod.get('spec', {})
uses_registry = any(v.get('secret', {}).get('secretName') == 'keycape-config' or
any(s.get('secret', {}).get('name') == 'keycape-config'
for s in v.get('projected', {}).get('sources', []))
for v in pod_spec.get('volumes', []))
for c in pod_spec.get('containers', []) + pod_spec.get('initContainers', []) + pod_spec.get('ephemeralContainers', []):
uses_registry = uses_registry or any(e.get('secretRef', {}).get('name') == 'keycape-config'
for e in c.get('envFrom', [])) or any(
e.get('valueFrom', {}).get('secretKeyRef', {}).get('name') == 'keycape-config'
for e in c.get('env', []))
require(not uses_registry, 'registry_consumer_still_present')
def propose_step(proposal, approved_digest, phase, secret, deployment, pods):
"""Return one private CAS patch or None for an exact no-op; never execute it.
The caller must freshly read BOTH objects before EACH phase, verify every
response, and stop on unknown outcomes. The digest binds content, not authority.
Kubernetes has no cross-object transaction; exclusive maintenance is required.
"""
require(digest(proposal) == approved_digest, 'proposal_revision_changed')
secret_meta = target(secret, 'Secret', 'keycape-config')
deploy_meta = target(deployment, 'Deployment', 'keycape')
require(secret_meta['uid'] == proposal['secret_identity']['uid'] and
deploy_meta['uid'] == proposal['deployment_identity']['uid'], 'target_identity_changed')
old_data, new_data = proposal['before_data'], proposal['after_data']
old_spec, new_spec = proposal['before_spec'], proposal['after_spec']
data, current = secret['data'], deployment['spec']
require(data == old_data or data == new_data, 'registry_drift_stop_without_stale_replay')
require(any(current == item for item in [old_spec, new_spec, stopped(old_spec), stopped(new_spec)]),
'deployment_drift_stop_without_stale_replay')
require(phase in ('stop', 'registry', 'bindings', 'resume', 'rollback-stop',
'rollback-registry', 'rollback-bindings', 'rollback-resume'), 'phase_invalid')
if phase.startswith('rollback-') and data == old_data and current == old_spec:
return None
if old_data == new_data and old_spec == new_spec:
require(data == old_data and current == old_spec, 'noop_state_changed')
return None
if phase == 'stop':
require(secret_meta == proposal['secret_identity'] and deploy_meta == proposal['deployment_identity']
and data == old_data and current == old_spec, 'initial_revision_changed')
status = deployment.get('status', {})
require(status.get('observedGeneration') == deployment['metadata'].get('generation') and
all(status.get(k) == 1 for k in ['replicas', 'readyReplicas', 'availableReplicas', 'updatedReplicas']),
'initial_issuer_not_ready')
obj, field, wanted = deployment, 'spec', stopped(old_spec)
elif phase == 'rollback-stop':
obj, field, wanted = deployment, 'spec', stopped(current)
else:
quiescent(deployment, pods)
if phase == 'registry':
require(current == stopped(old_spec) and data == old_data, 'registry_phase_out_of_order')
obj, field, wanted = secret, 'data', new_data
elif phase == 'bindings':
require(current == stopped(old_spec) and data == new_data, 'bindings_phase_out_of_order')
obj, field, wanted = deployment, 'spec', stopped(new_spec)
elif phase == 'resume':
require(current == stopped(new_spec) and data == new_data, 'resume_phase_out_of_order')
obj, field, wanted = deployment, 'spec', new_spec
elif phase == 'rollback-registry':
obj, field, wanted = secret, 'data', old_data
elif phase == 'rollback-bindings':
require(data == old_data, 'restore_registry_before_bindings')
obj, field, wanted = deployment, 'spec', stopped(old_spec)
else:
require(data == old_data and current == stopped(old_spec), 'rollback_resume_out_of_order')
obj, field, wanted = deployment, 'spec', old_spec
if obj[field] == wanted:
return None
metadata = registration.registry.pin.safe_metadata(obj)
patch = [{'op': 'test', 'path': '/metadata/uid', 'value': metadata['uid']},
{'op': 'test', 'path': '/metadata/resourceVersion', 'value': metadata['resource_version']},
{'op': 'test', 'path': '/' + field, 'value': copy.deepcopy(obj[field])},
{'op': 'replace', 'path': '/' + field, 'value': copy.deepcopy(wanted)}]
return {'kind': obj['kind'], 'namespace': 'sso', 'name': obj['metadata']['name'],
'uid': metadata['uid'], 'field': field, 'expected': copy.deepcopy(wanted),
'proposal_digest': approved_digest, 'phase': phase, 'patch': patch}
def verify_response(operation, obj):
"""Also use on a fresh GET after an acknowledged patch; failures stop the caller."""
metadata = target(obj, operation['kind'], operation['name'])
require(metadata['uid'] == operation['uid'] and obj.get(operation['field']) == operation['expected'],
'mutation_readback_mismatch')
return {'phase': operation['phase'], 'proposal_digest': operation['proposal_digest'],
'uid': metadata['uid'], 'resource_version': metadata['resource_version'], 'readback_matches': True}