Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a10780-6d41-7952-9718-dfbc1f41236f
133 lines
6.7 KiB
Python
133 lines
6.7 KiB
Python
"""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)
|