Prepare authenticated lifecycle runtime and production release candidate

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a10780-6d41-7952-9718-dfbc1f41236f
This commit is contained in:
tegwick 2026-10-04 21:40:19 +02:00
parent f3b71b1521
commit 4f546ae3b1
41 changed files with 2598 additions and 52 deletions

View file

@ -7,7 +7,7 @@ import ipaddress
from http.client import HTTPException
from urllib.error import HTTPError, URLError
from urllib.parse import urlsplit
from urllib.request import HTTPRedirectHandler, ProxyHandler, Request, build_opener
from urllib.request import HTTPRedirectHandler, HTTPSHandler, ProxyHandler, Request, build_opener
class TransportError(Exception):
@ -41,9 +41,9 @@ class _NoRedirect(HTTPRedirectHandler):
class JSONTransport:
"""No redirects or ambient proxy credentials; five-second request timeout."""
def __init__(self, *, allow_internal_http=False) -> None:
def __init__(self, *, allow_internal_http=False, ssl_context=None) -> None:
self.allow_internal_http = allow_internal_http
self._opener = build_opener(ProxyHandler({}), _NoRedirect())
self._opener = build_opener(ProxyHandler({}), _NoRedirect(), HTTPSHandler(context=ssl_context))
def request(self, method: str, url: str, *, headers=None, body=None) -> tuple[int, dict]:
parsed = urlsplit(url)

View file

@ -215,7 +215,11 @@ class LifecycleRepository:
published = db.execute("SELECT body FROM lifecycle_events WHERE tenant=? AND memo_id=? "
"AND version=? AND kind='published' ORDER BY sequence DESC LIMIT 1",
(tenant, memo_id, version if version is not None else row["current_version"])).fetchone()
revision = db.execute("SELECT actor FROM lifecycle_events WHERE tenant=? AND memo_id=? "
"AND version=? AND kind IN ('registered','revised') ORDER BY sequence DESC LIMIT 1",
(tenant, memo_id, version if version is not None else row["current_version"])).fetchone()
return {"registered": True, "registered_by": json.loads(row["actor"]),
"revision_by": json.loads(revision["actor"]) if revision else None,
"current_version": row["current_version"],
"published_audience": json.loads(published["body"])["audience"] if published else None}

View file

@ -57,9 +57,12 @@ def submitted_digest(request):
return "sha256:" + hashlib.sha256(encoded.encode()).hexdigest()
def build_request(session: HumanSession, memo, action, policy_version):
def build_request(session: HumanSession, memo, action, policy_version, *, intake_context=None):
if action not in ACTIONS:
raise ValueError("unsupported review action")
extra = {} if intake_context is None else dict(intake_context)
if extra and set(extra) != {"review_group", "memo_class", "intake_submitter", "approval_requester", "approval_human_control"}:
raise ValueError("exact intake policy context required")
return {"id": str(uuid.uuid4()), "tenant": "tenant:platform",
"subject": {"id": session.subject, "type": "human", "tenant": session.tenant.value,
"attributes": {"tenant_source": session.tenant.route.value,
@ -70,7 +73,7 @@ def build_request(session: HumanSession, memo, action, policy_version):
"resource": {"id": "memo:" + memo.id, "type": "decision-memo",
"system": "informed-decision", "tenant": "tenant:platform"},
"context": {"memo_version": memo.version, "approval_id": memo.approval_id,
"approval_binding_digest": memo.approval_binding_digest},
"approval_binding_digest": memo.approval_binding_digest, **extra},
"policy_version": policy_version}

View file

@ -53,9 +53,10 @@ class OverviewRow:
class ReviewController:
def __init__(self, store, policy, approval_factory, *, clock=time.time):
def __init__(self, store, policy, approval_factory, *, clock=time.time, intake_attributes=False):
self.store, self.policy, self.approval_factory = store, policy, approval_factory
self.clock = clock
self.intake_attributes = intake_attributes
def _session(self, session):
if not isinstance(session, HumanSession) or session.expires_at <= self.clock():
@ -80,7 +81,13 @@ class ReviewController:
def _authorize(self, session, memo, action):
self._memo(session, memo)
request = build_request(session, memo, action, self.policy.version)
context = self.store.intake_context(memo) if self.intake_attributes and action != 'list' else None
if context is not None:
current = self._approval(session, memo)
if (current['binding'].get('actor') != context['approval_requester']
or current['binding'].get('human_control') is not True):
raise ReviewError(409, 'binding_changed')
request = build_request(session, memo, action, self.policy.version, intake_context=context)
observation = self.policy.check(request)
policy_id = self.store.observe_policy(observation)
try:

View file

@ -119,6 +119,7 @@ class Runtime:
controller: ReviewController
pump: AuditPump
lifecycle: object | None = None
workload: object | None = None
@classmethod
def from_file(cls, filename):
@ -129,7 +130,7 @@ class Runtime:
or info.st_size > 16384):
raise ValueError("runtime configuration must be an owned private 0600 file")
data = json.loads(path.read_text())
if ((set(data) - {"ordinary_approval_ids", "intake_delivery", "lifecycle_policy", "lifecycle_delivery"}) != {"schema", "evidence_db", "policy", "approval_origin", "audit"}
if ((set(data) - {"ordinary_approval_ids", "intake_delivery", "lifecycle_policy", "lifecycle_delivery", "workload_api", "intake_attributes"}) != {"schema", "evidence_db", "policy", "approval_origin", "audit"}
or data["schema"] != "informed-decision.review-runtime.v1"
or set(data["policy"]) != {"origin", "package", "version", "package_digest", "caller_token_file"}
or set(data["audit"]) != {"origin", "sender_token_file"}):
@ -137,6 +138,9 @@ class Runtime:
lifecycle_delivery = data.get("lifecycle_delivery", False)
if type(lifecycle_delivery) is not bool:
raise ValueError("invalid lifecycle delivery setting")
intake_attributes = data.get('intake_attributes', False)
if type(intake_attributes) is not bool:
raise ValueError('invalid intake attributes setting')
ordinary = ordinary_review_ids(data.get("ordinary_approval_ids", []))
if not Path(data["evidence_db"]).is_absolute():
raise ValueError("absolute evidence database path required")
@ -168,7 +172,8 @@ class Runtime:
package_digest=configuration["package_digest"], allow_internal_http=True)
store = Store(data["evidence_db"])
controller = ReviewController(store, client, lambda session: ApprovalHTTPClient(
approval_origin, session, allow_internal_http=True, ordinary_approval_ids=tuple(ordinary)))
approval_origin, session, allow_internal_http=True, ordinary_approval_ids=tuple(ordinary)),
intake_attributes=intake_attributes)
worker = None
if hub:
from .intake_delivery import IntakeDeliveryWorker
@ -179,5 +184,9 @@ class Runtime:
from .lifecycle import LifecycleRepository
from .lifecycle_service import LifecycleService
lifecycle = LifecycleService(LifecycleRepository(store), lifecycle_policy)
workload = None
if 'workload_api' in data:
from .workload import compose_workload
workload = compose_workload(data['workload_api'], store, lifecycle, approval_origin, token_file)
return cls(controller, AuditPump(store, sink, intake_worker=worker,
lifecycle_delivery=lifecycle_delivery), lifecycle)
lifecycle_delivery=lifecycle_delivery), lifecycle, workload)

View file

@ -259,12 +259,36 @@ class Store:
if row is None or row["outcome"] != "allow" or row["usable_until"] <= time.time():
raise Conflict("a current recorded policy observation is required")
request = json.loads(row["request"])
expected = {"memo_version": memo.version, "approval_id": memo.approval_id,
"approval_binding_digest": memo.approval_binding_digest}
if set(request['context']) - expected.keys():
context = self._intake_context(db, memo)
if context is None:
raise Conflict("intake policy observation has no ready receipt")
expected.update(context)
if (request["action"] != action or request["subject"]["id"] != subject
or request["resource"]["id"] != "memo:" + memo.id
or request["context"] != {"memo_version": memo.version, "approval_id": memo.approval_id,
"approval_binding_digest": memo.approval_binding_digest}):
or request["context"] != expected):
raise Conflict("policy observation does not name this act")
@staticmethod
def _intake_context(db, memo):
row = db.execute("SELECT submitter,metadata,receipt FROM intake_ledger WHERE memo_id=? AND state='ready'",
(memo.id,)).fetchone()
if row is None:
return None
receipt, metadata = json.loads(row['receipt']), json.loads(row['metadata'])
if (receipt['approval_id'] != memo.approval_id
or receipt['approval_binding_digest'] != memo.approval_binding_digest):
raise Conflict('intake receipt does not match memo')
return {'review_group': metadata['review_group'], 'memo_class': metadata['memo_class'],
'intake_submitter': row['submitter'], 'approval_requester': 'informed-decision',
'approval_human_control': True}
def intake_context(self, memo):
with self._connection() as db:
return self._intake_context(db, memo)
def _presentation(self, db, presentation_id):
row = db.execute("SELECT body FROM presentations WHERE id=?", (presentation_id,)).fetchone()
if row is None:

View file

@ -263,7 +263,25 @@ def main():
readiness=runtime.pump.ready if runtime else lambda: False,
lifecycle=runtime.lifecycle if runtime else None)
try:
serve(app, host=host, port=8080, threads=4)
if runtime and runtime.workload:
serve_with_workload(app, runtime.workload, host=host)
else:
serve(app, host=host, port=8080, threads=4)
finally:
if runtime:
runtime.pump.stop()
def serve_with_workload(public, private, *, host, public_port=8080, private_port=8081):
"""Two sockets, one writer/process; the private app has no browser routes."""
from waitress import create_server
servers, socket_map = [], {}
try:
servers.append(create_server(public, host=host, port=public_port, threads=4, map=socket_map))
servers.append(create_server(private, host=host, port=private_port, threads=4, map=socket_map,
max_request_body_size=1024 * 1024))
servers[0].run()
finally:
for server in servers:
server.close()
server.task_dispatcher.shutdown()

View file

@ -0,0 +1,133 @@
"""Private workload composition. Kubernetes verifies identity; PDP grants actions."""
import json
import math
from pathlib import Path
import re
import ssl
import time
import jwt
from .disposition import Actor, ActorKind
from .http_transport import JSONTransport, https_origin
from .intake_service import IntakeError
from .lifecycle import Attribution
from .lifecycle_http import LifecycleAPI
from .lifecycle_service import Caller
from .records import dumps
LIFECYCLE_AUDIENCE = 'informed-decision-lifecycle'
SA = re.compile(r'system:serviceaccount:[a-z0-9-]+:[a-z0-9-]+')
def registry(path):
with Path(path).open('rb') as handle:
raw = handle.read(65537)
if len(raw) > 65536:
raise ValueError('registry too large')
result = json.loads(raw)
if (not isinstance(result, dict) or any(not SA.fullmatch(name) or
not isinstance(row, dict) or set(row) != {'tenant'} or row['tenant'] != 'tenant:platform'
for name, row in result.items())):
raise ValueError('invalid workload registry')
return result
class KubernetesTokenReview:
def __init__(self, origin, ca_file, token_provider, *, transport_factory=JSONTransport):
self.origin = https_origin(origin)
self.ca_file = Path(ca_file)
if not self.ca_file.is_absolute():
raise ValueError('absolute Kubernetes CA path required')
self.token_provider = token_provider
self.transport_factory = transport_factory
def __call__(self, body):
try:
# Reopen projected CA/caller custody; require TLS peer and hostname validation.
context = ssl.create_default_context(cafile=str(self.ca_file))
transport = self.transport_factory(ssl_context=context)
status, result = transport.request('POST', self.origin + '/apis/authentication.k8s.io/v1/tokenreviews',
headers={'Authorization': 'Bearer ' + self.token_provider(), 'Content-Type': 'application/json'},
body=dumps(body).encode())
if status not in (200, 201):
raise ValueError()
return result
except Exception:
raise IntakeError(503, 'workload_identity_unavailable') from None
class LifecycleWorkloadIdentity:
def __init__(self, review, callers_file, *, clock=time.time):
self.review, self.callers_file, self.clock = review, Path(callers_file), clock
if not self.callers_file.is_absolute():
raise ValueError('absolute workload registry path required')
def __call__(self, token):
if not isinstance(token, str) or not 1 <= len(token) <= 16384:
raise ValueError('invalid workload token')
result = self.review({'apiVersion': 'authentication.k8s.io/v1', 'kind': 'TokenReview',
'spec': {'token': token, 'audiences': [LIFECYCLE_AUDIENCE]}})
status = result['status']
user = status['user']
if (status.get('authenticated') is not True or status.get('error')
or LIFECYCLE_AUDIENCE not in status.get('audiences', [])
or not isinstance(user.get('username'), str) or not SA.fullmatch(user['username'])
or not isinstance(user.get('uid'), str) or not user['uid']):
raise ValueError('workload authentication refused')
admitted = registry(self.callers_file).get(user['username'])
if admitted is None:
raise ValueError('workload not admitted')
# The API server has verified this Kubernetes JWT. Read only its expiry;
# subject/UID come from TokenReview and tenant from reviewed registration.
claims = jwt.decode(token, options={'verify_signature': False})
expiry = claims.get('exp')
if type(expiry) is not int or not math.isfinite(expiry) or expiry <= self.clock():
raise ValueError('workload token expired')
return Caller(Attribution(admitted['tenant'], Actor(user['username'], ActorKind.SERVICE)),
min(expiry, self.clock() + 30), dumps({
'principal_type_source': 'authentication-derived',
'tenant_source': 'registration-supplied',
'workload_uid': user['uid'], 'authentication_audience': LIFECYCLE_AUDIENCE}))
class PrivateWorkloadApp:
def __init__(self, *, intake=None, lifecycle=None):
self.intake, self.lifecycle = intake, lifecycle
def __call__(self, environ, start_response):
path = environ.get('PATH_INFO', '')
if path == '/v1/memos' and self.intake is not None:
return self.intake(environ, start_response)
if (path == '/v1/decisions' or path.startswith('/v1/decisions/')) and self.lifecycle is not None:
return self.lifecycle(environ, start_response)
raw = b'{"error":"route_not_found"}'
start_response('404 Not Found', [('Content-Type', 'application/json'), ('Cache-Control', 'no-store'),
('Content-Length', str(len(raw)))])
return [raw]
def compose_workload(data, store, lifecycle, approval_origin, token_reader):
if (not isinstance(data, dict) or set(data) - {'token_review', 'intake', 'lifecycle_callers_file'}
or 'token_review' not in data or not ({'intake', 'lifecycle_callers_file'} & data.keys())):
raise ValueError('invalid private workload configuration')
review = data['token_review']
if not isinstance(review, dict) or set(review) != {'origin', 'ca_file', 'token_file'}:
raise ValueError('exact TokenReview configuration required')
verifier = KubernetesTokenReview(review['origin'], review['ca_file'], token_reader(review['token_file']))
intake, api = None, None
if 'intake' in data:
from .requester import compose_intake
configuration = data['intake']
if not isinstance(configuration, dict) or set(configuration) != {'secret_file', 'submitters_file', 'groups_file'}:
raise ValueError('invalid private intake configuration')
if any(not isinstance(p, str) or not Path(p).is_absolute() for p in configuration.values()):
raise ValueError('absolute intake projection paths required')
intake = compose_intake(store, verifier, issuer='https://kc.coulomb.social',
secret_file=configuration['secret_file'], approval_origin=approval_origin,
submitters_path=configuration['submitters_file'], groups_path=configuration['groups_file'])
if 'lifecycle_callers_file' in data:
if lifecycle is None:
raise ValueError('private lifecycle API requires a separately pinned policy')
api = LifecycleAPI(lifecycle, LifecycleWorkloadIdentity(verifier, data['lifecycle_callers_file']))
return PrivateWorkloadApp(intake=intake, lifecycle=api)