net-kingdom/sso-mfa/k8s/keycape/playwright-registration.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

162 lines
7.8 KiB
Python

#!/usr/bin/env python3
"""Prepare Playwright client records; no credential access or live mutation.
The registry adapter below is for an owner-controlled activation ceremony. Its
encoded result contains the original private registry and must stay in memory.
The CLI emits only newly constructed non-secret client/binding proposals.
"""
from __future__ import annotations
import argparse
import copy
from decimal import Decimal
import importlib.util
import json
from pathlib import Path
import re
import sys
spec = importlib.util.spec_from_file_location('playwright_registry', Path(__file__).with_name('keycape-client.py'))
registry = importlib.util.module_from_spec(spec)
spec.loader.exec_module(registry)
require = registry.require
Refused = registry.Refused
AUDIENCE = 'playwright-kubernetes'
SCOPE = 'playwright:connect'
CALLBACK = 'http://127.0.0.1:18764/callback'
ISSUER = 'https://kc.coulomb.social'
SLUG = re.compile(r'[a-z0-9][a-z0-9-]{0,39}\Z')
IDENTIFIER = re.compile(r'[a-z0-9][a-z0-9:._-]{0,159}\Z')
def public_client():
return {'clientId': 'playwright-cli', 'audience': AUDIENCE,
'displayName': 'Playwright platform consumer CLI',
'redirectUris': [CALLBACK],
'allowedScopes': ['openid', 'profile', 'groups', SCOPE],
'grantTypes': ['authorization_code'], 'clientType': 'public'}
def agent_client(identity):
"""Reference an existing registered identity; never manufacture its subject/tenant."""
require(isinstance(identity, dict) and set(identity) ==
{'agent_id', 'service_subject', 'tenant', 'identity_record'}, 'agent_identity_fields_required')
require(all(isinstance(v, str) for v in identity.values()), 'agent_identity_strings_required')
require(SLUG.fullmatch(identity['agent_id']) is not None, 'agent_id_invalid')
require(identity['service_subject'].startswith('service:') and
IDENTIFIER.fullmatch(identity['service_subject']) is not None and
len(identity['service_subject']) > len('service:'), 'registered_service_subject_required')
require(identity['tenant'].startswith('tenant:') and
IDENTIFIER.fullmatch(identity['tenant']) is not None and
len(identity['tenant']) > len('tenant:'), 'registered_tenant_required')
# This is a reference for owner review, not proof that registration exists.
reference = identity['identity_record']
require(1 <= len(reference) <= 512 and all(32 < ord(c) < 127 for c in reference),
'identity_record_reference_required')
cid = 'playwright-agent-' + identity['agent_id']
return {'clientId': cid, 'audience': AUDIENCE,
'displayName': 'Playwright agent ' + identity['agent_id'],
'allowedScopes': [SCOPE], 'grantTypes': ['client_credentials'],
'clientType': 'confidential', 'secretRef': 'env:' + registry.env_name(cid),
'serviceSubject': identity['service_subject'], 'tenant': identity['tenant'],
'roles': ['coding-agent'], 'tokenLifetime': '15m'}
def clients(identities):
require(isinstance(identities, list) and len(identities) <= 100, 'agent_inventory_invalid')
result = [public_client()] + [agent_client(identity) for identity in identities]
for field in ['clientId', 'serviceSubject', 'secretRef']:
values = [c[field] for c in result if field in c]
require(len(values) == len(set(values)), 'duplicate_agent_identity_or_binding')
return result
def human_lifetime(config):
"""Bound the existing issuer setting; do not alter all humans' token lifetime."""
raw = config.get('tokenLifetime')
require(isinstance(raw, str) and 0 < len(raw) <= 128, 'explicit_human_token_lifetime_required')
units = {'h': Decimal(3600), 'm': Decimal(60), 's': Decimal(1),
'ms': Decimal('.001'), 'us': Decimal('.000001'),
'µs': Decimal('.000001'), 'μs': Decimal('.000001'), 'ns': Decimal('.000000001')}
parts = re.findall(r'([0-9]+(?:\.[0-9]*)?|\.[0-9]+)(ns|us|µs|μs|ms|s|m|h)', raw)
require(parts and ''.join(value + unit for value, unit in parts) == raw,
'human_token_lifetime_invalid')
seconds = sum((Decimal(value) * units[unit] for value, unit in parts), Decimal(0))
require(1 <= seconds <= 3600, 'human_token_lifetime_outside_cluster_bound')
return seconds
def prepare_registry(secret, identities, operation='add'):
"""Byte-preserving proposal only. Caller owns CAS, check-config and readback.
Refuse drift before constructing any result. A missing client during removal
is an idempotent no-op. Removal does not revoke issued tokens/connections.
"""
require(operation in ('add', 'remove'), 'operation_invalid')
wanted = clients(identities)
_, config, _ = registry.pin.issuer_document(secret)
require(config.get('issuer') == ISSUER, 'keycape_issuer_mismatch')
human_lifetime(config)
require(isinstance(config.get('clients'), list), 'client_sequence_required')
for existing in config['clients']:
require(isinstance(existing, dict), 'client_shape_unexpected')
if (existing.get('audience') or existing.get('clientId')) == AUDIENCE:
require(existing in wanted, 'unreviewed_playwright_audience_registration')
# Preflight every record against the same original, including collisions.
for client in wanted:
_, current, _ = registry._inspect(secret, client)
if 'serviceSubject' in client:
for other in current['clients']:
if other['clientId'] != client['clientId'] and (other.get('audience') or other.get('clientId')) == AUDIENCE:
require(other.get('serviceSubject') != client['serviceSubject'],
'playwright_subject_already_registered')
matching = [c for c in current['clients'] if c['clientId'] == client['clientId']]
require(not matching or matching == [client], 'existing_registration_differs_requires_reconciliation')
candidate = copy.deepcopy(secret)
changed_ids = []
merge = registry.replacement if operation == 'add' else registry.removal
for client in wanted:
encoded, changed = merge(candidate, client)
candidate['data']['config.yaml'] = encoded
if changed:
changed_ids.append(client['clientId'])
return candidate['data']['config.yaml'], changed_ids
def unique_object(pairs):
result = {}
for key, value in pairs:
require(key not in result, 'duplicate_input_field')
result[key] = value
return result
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__.split('\n', 1)[0])
parser.add_argument('--agents', help='Non-secret JSON array of existing agent identity references')
args = parser.parse_args(argv)
try:
identities = []
if args.agents:
with Path(args.agents).open('rb') as stream:
raw = stream.read(65537)
require(len(raw) <= 65536, 'agent_inventory_too_large')
identities = json.loads(raw, object_pairs_hook=unique_object)
records = clients(identities)
print(json.dumps({'schema': 'netkingdom.playwright-registration.v1',
'status': 'source_proposal', 'clients': records,
'identity_references': {c['clientId']: identity['identity_record']
for c, identity in zip(records[1:], identities)},
'deployment_env': [registry.env_binding(c) for c in records if 'secretRef' in c],
'identity_evidence_verified': False,
'custody_admitted': False, 'live_activated': False}, indent=2))
return 0
except Refused as error:
print(json.dumps({'status': 'refused', 'reason': str(error)}))
except Exception:
print(json.dumps({'status': 'refused', 'reason': 'invalid_registration_input'}))
return 1
if __name__ == '__main__':
sys.exit(main())