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:
parent
f3b71b1521
commit
4f546ae3b1
41 changed files with 2598 additions and 52 deletions
|
|
@ -26,7 +26,7 @@ FIXTURE = Path(__file__).parent / 'fixtures/review-policy'
|
|||
|
||||
|
||||
@contextmanager
|
||||
def flex_component(binary, directory, signing_key):
|
||||
def flex_component(binary, directory, signing_key, *, fixture=FIXTURE):
|
||||
tokens={'synthetic-workload-token':'system:serviceaccount:informed-decision:review',
|
||||
'synthetic-wrong-caller':'system:serviceaccount:other:other'}
|
||||
calls=[]
|
||||
|
|
@ -55,7 +55,7 @@ def flex_component(binary, directory, signing_key):
|
|||
listener.bind(('127.0.0.1',0));port=listener.getsockname()[1]
|
||||
origin=f'http://127.0.0.1:{port}'
|
||||
process=subprocess.Popen([str(binary),'serve','--addr',f'127.0.0.1:{port}',
|
||||
'--registry',str(FIXTURE/'registry.json'),'--policy',str(FIXTURE/'policy.md'),
|
||||
'--registry',str(fixture/'registry.json'),'--policy',str(fixture/'policy.md'),
|
||||
'--caller-auth-mode','enforce','--caller-binding','informed-decision=system:serviceaccount:informed-decision:review',
|
||||
'--caller-kubernetes-url',f'http://127.0.0.1:{server.server_port}',
|
||||
'--caller-reviewer-token-file',str(reviewer),'--caller-ca-file',str(ca)],stdout=subprocess.DEVNULL,stderr=subprocess.PIPE)
|
||||
|
|
@ -69,7 +69,7 @@ def flex_component(binary, directory, signing_key):
|
|||
except TransportError:pass
|
||||
time.sleep(.05)
|
||||
else:raise RuntimeError('fixture evaluator did not start')
|
||||
pin=json.loads((FIXTURE/'pin.json').read_text());pin.pop('fixture_only')
|
||||
pin=json.loads((fixture/'pin.json').read_text());pin.pop('fixture_only', None)
|
||||
client=PolicyClient(origin,lambda:'synthetic-workload-token',allow_internal_http=True,**pin)
|
||||
yield client,calls
|
||||
finally:
|
||||
|
|
|
|||
15
tests/test_dependency_pins.py
Normal file
15
tests/test_dependency_pins.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
"""Prevent a tested dependency and its shipped container pin from diverging."""
|
||||
from pathlib import Path
|
||||
import re
|
||||
import tomllib
|
||||
|
||||
|
||||
def test_container_dependencies_match_project_lock():
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
lock = tomllib.loads((root/'uv.lock').read_text())
|
||||
locked = {p['name'].lower(): p['version'] for p in lock['package']}
|
||||
requirements = dict(re.findall(r'^([a-zA-Z0-9_-]+)==([^ ;\\\n]+)',
|
||||
(root/'requirements.lock').read_text(), re.MULTILINE))
|
||||
assert requirements
|
||||
for name, version in requirements.items():
|
||||
assert locked[name.lower()] == version, name
|
||||
|
|
@ -97,3 +97,53 @@ def test_optional_workspace_requires_distinct_pins_and_exact_policy_network_peer
|
|||
data['lifecycle_policy']['package'] = data['policy']['package']
|
||||
with pytest.raises(ValueError):
|
||||
module.render(data)
|
||||
|
||||
|
||||
def workload_inputs():
|
||||
data = inputs()
|
||||
data['lifecycle_policy'] = {**data['policy'], 'package': 'informed-decision.lifecycle', 'pod_name': 'flex-auth-lifecycle'}
|
||||
data['lifecycle_delivery'] = True
|
||||
data['intake_attributes'] = True
|
||||
data['intake_delivery'] = {'hub_origin': 'http://state-hub.state-hub.svc.cluster.local:8000'}
|
||||
data['workload_api'] = {'intake': True, 'lifecycle': True,
|
||||
'registries_configmap': 'informed-decision-workload-registry',
|
||||
'peers': [{'namespace': 'state-hub', 'app': 'state-hub'}],
|
||||
'api_egress': [{'ip': '10.43.0.1', 'port': 443}, {'ip': '92.205.62.239', 'port': 6443}]}
|
||||
return data
|
||||
|
||||
|
||||
def test_private_workload_candidate_has_exact_rbac_peers_and_projection():
|
||||
objects = module.render(workload_inputs())['items']
|
||||
services = [o for o in objects if o['kind'] == 'Service']
|
||||
assert len(services) == 2
|
||||
assert services[0]['spec']['ports'][0]['port'] == 80
|
||||
assert services[1]['spec']['ports'][0]['port'] == 8081
|
||||
assert all(o['kind'] not in ('Secret', 'Ingress') for o in objects)
|
||||
role = next(o for o in objects if o['kind'] == 'ClusterRole')
|
||||
assert role['rules'] == [{'apiGroups':['authentication.k8s.io'],'resources':['tokenreviews'],'verbs':['create']}]
|
||||
binding = next(o for o in objects if o['kind'] == 'ClusterRoleBinding')
|
||||
assert binding['subjects'] == [{'kind':'ServiceAccount','name':'review','namespace':'informed-decision'}]
|
||||
policy = next(o for o in objects if o['kind']=='NetworkPolicy' and o['metadata']['name']=='informed-decision-review')
|
||||
rule = policy['spec']['ingress'][-1]
|
||||
assert rule['ports'] == [{'protocol':'TCP','port':8081}]
|
||||
assert rule['from'][0]['namespaceSelector']['matchLabels'] == {'kubernetes.io/metadata.name':'state-hub'}
|
||||
pod = next(o for o in objects if o['kind']=='Deployment')['spec']['template']['spec']
|
||||
registry = next(m for m in pod['containers'][0]['volumeMounts'] if m['name']=='workload-registry')
|
||||
assert registry['readOnly'] and 'subPath' not in registry
|
||||
requester = next(v for v in pod['volumes'] if v['name']=='sitting-requester')
|
||||
assert requester['secret']['secretName'] == 'informed-decision-sitting-requester'
|
||||
assert requester['secret']['defaultMode'] == 0o440
|
||||
config = json.loads(next(o for o in objects if o['kind']=='ConfigMap')['data']['runtime.json'])
|
||||
assert config['workload_api']['token_review']['origin'] == 'https://kubernetes.default.svc:443'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('fault', ['wildcard','missing_delivery','no_policy','broad_api','false_type','empty_peers'])
|
||||
def test_private_workload_candidate_refuses_incomplete_or_broad_configuration(fault):
|
||||
data = workload_inputs()
|
||||
if fault == 'wildcard': data['workload_api']['peers'][0]['app']='*'
|
||||
if fault == 'missing_delivery': del data['intake_delivery']
|
||||
if fault == 'no_policy': del data['lifecycle_policy']
|
||||
if fault == 'broad_api': data['workload_api']['api_egress'][0]['ip']='10.0.0.0/8'
|
||||
if fault == 'false_type': data['workload_api']['intake']='true'
|
||||
if fault == 'empty_peers': data['workload_api']['peers']=[]
|
||||
with pytest.raises(ValueError): module.render(data)
|
||||
|
|
|
|||
56
tests/test_lifecycle_policy_component.py
Normal file
56
tests/test_lifecycle_policy_component.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
"""Real candidate Access Engine policy; authentication custody remains synthetic."""
|
||||
from dataclasses import replace
|
||||
import os
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from informed_decision.disposition import Actor, ActorKind
|
||||
from informed_decision.lifecycle import Attribution, LifecycleRepository
|
||||
from informed_decision.lifecycle_service import Caller, LifecycleError, LifecycleService
|
||||
from informed_decision.records import dumps
|
||||
from informed_decision.store import Store
|
||||
from review_components import flex_component
|
||||
from test_browser_auth import signing_key
|
||||
from test_lifecycle import workspace
|
||||
from test_policy_client import human
|
||||
|
||||
|
||||
def test_initial_audience_collaborates_across_revisions_with_real_pdp(workspace, tmp_path, signing_key):
|
||||
binary = os.environ.get('INFD_FLEX_AUTH_BINARY')
|
||||
source = os.environ.get('INFD_FLEX_AUTH_SOURCE')
|
||||
if not binary or not source:
|
||||
pytest.skip('set INFD_FLEX_AUTH_BINARY and INFD_FLEX_AUTH_SOURCE for candidate owner policy')
|
||||
fixture = Path(source) / 'examples/informed-decision-lifecycle'
|
||||
pins = (fixture / 'pin.json').read_bytes()
|
||||
with flex_component(binary, tmp_path, signing_key, fixture=fixture) as (pdp, calls):
|
||||
repo, memo = workspace
|
||||
app = LifecycleService(repo, pdp)
|
||||
hub = Caller(Attribution('tenant:platform', Actor('system:serviceaccount:state-hub:state-hub', ActorKind.SERVICE)),
|
||||
time.time() + 600, dumps({'principal_type_source': 'authentication-derived',
|
||||
'tenant_source': 'registration-supplied', 'authentication_audience': 'informed-decision-lifecycle',
|
||||
'workload_uid': 'synthetic-verified-uid'}))
|
||||
person = Caller.from_human_session(human())
|
||||
app.register(hub, memo, key='first')
|
||||
with pytest.raises(LifecycleError):
|
||||
app.retrieve(person, memo.id)
|
||||
app.publish(hub, memo.id, 1, audience={'groups': ['net-kingdom-admins'], 'subjects': []}, key='publish')
|
||||
app.annotate(person, memo.id, 1, kind='comment', body={'text': 'Consider the alternative'}, key='comment')
|
||||
app.revise(person, memo.next_version(brief='Alternative assessed'), expected_version=1, summary='Refined', key='refine')
|
||||
# The successor is unpublished; its editor and original author retain access.
|
||||
assert app.retrieve(person, memo.id).version == 2
|
||||
assert app.retrieve(hub, memo.id).version == 2
|
||||
other = Caller.from_human_session(replace(human(), subject='another-admin'))
|
||||
with pytest.raises(LifecycleError):
|
||||
app.retrieve(other, memo.id)
|
||||
app.publish(person, memo.id, 2, audience={'groups': ['net-kingdom-admins'], 'subjects': []}, key='republish')
|
||||
assert app.retrieve(other, memo.id).version == 2
|
||||
app.register(hub, replace(memo, id='second-decision'), key='second')
|
||||
reopened = LifecycleService(LifecycleRepository(Store(repo.store.path)), pdp)
|
||||
assert reopened.retrieve(person, memo.id).version == 2
|
||||
foreign = replace(hub, attribution=Attribution('tenant:platform', Actor('system:serviceaccount:other:other', ActorKind.SERVICE)))
|
||||
with pytest.raises(LifecycleError):
|
||||
reopened.retrieve(foreign, memo.id)
|
||||
assert calls and all(c['known'] and c['audiences'] == ['flex-auth'] for c in calls)
|
||||
assert (fixture / 'pin.json').read_bytes() == pins
|
||||
|
|
@ -186,8 +186,11 @@ def test_actual_engine_human_entry_and_independent_audit_link_to_source_revision
|
|||
status, loaded = request(intake, receipt['envelope'])
|
||||
assert status == 201, loaded
|
||||
controller = ReviewController(app.repository.store, policy(),
|
||||
lambda session: ApprovalHTTPClient('https://approval.test', session, transport=transport))
|
||||
lambda session: ApprovalHTTPClient('https://approval.test', session, transport=transport), intake_attributes=True)
|
||||
page = controller.open(human, receipt['review_memo_id'])
|
||||
context = controller.policy.transport.calls[-1][3]['context']
|
||||
assert context['intake_submitter'] == 'state-hub'
|
||||
assert context['approval_requester'] == 'informed-decision' and context['approval_human_control'] is True
|
||||
controller.acknowledge(human, page.presentation.id, memo.required_ack_ids)
|
||||
controller.act(human, page.presentation.id, Verb.ACCEPT, operation_id='human-accept')
|
||||
result = app.outcomes(actor, memo.id, 2)
|
||||
|
|
@ -278,3 +281,41 @@ def test_schema_four_upgrade_preserves_evidence_and_fences_old_readers(handoff):
|
|||
assert app.events(actor, memo.id) == before
|
||||
receipt = prepare(handoff)
|
||||
assert prepare(handoff) == receipt
|
||||
|
||||
|
||||
def test_attribute_review_checks_live_requester_before_policy(handoff):
|
||||
from informed_decision.review import ReviewController, ReviewError
|
||||
from test_policy_client import human
|
||||
app, actor, memo, options, intake, calls = handoff
|
||||
receipt = prepare(handoff)
|
||||
assert request(intake, receipt['envelope'])[0] == 201
|
||||
stored = app.repository.store.memo(receipt['review_memo_id'])
|
||||
session = replace(human(), subject=stored.binding.principal.id)
|
||||
class Engine:
|
||||
def get_approval(self, approval_id):
|
||||
return {'binding': {'digest': stored.approval_binding_digest, 'actor': 'wrong-requester', 'human_control': True}}
|
||||
controller = ReviewController(app.repository.store, policy(), lambda session: Engine(), intake_attributes=True)
|
||||
with pytest.raises(ReviewError, match='binding_changed'):
|
||||
controller.open(session, stored.id)
|
||||
assert controller.policy.transport.calls == []
|
||||
|
||||
|
||||
def test_intake_policy_guard_rechecks_retained_metadata(handoff):
|
||||
from informed_decision.policy import build_request
|
||||
from test_policy_client import human
|
||||
app, actor, memo, options, intake, calls = handoff
|
||||
receipt = prepare(handoff)
|
||||
assert request(intake, receipt['envelope'])[0] == 201
|
||||
store = app.repository.store
|
||||
review = store.memo(receipt['review_memo_id'])
|
||||
session = replace(human(), subject=review.binding.principal.id)
|
||||
request_body = build_request(session, review, 'read', 'v1', intake_context=store.intake_context(review))
|
||||
observation = policy().check(request_body)
|
||||
policy_id = store.observe_policy(observation)
|
||||
with store._transaction() as db:
|
||||
row = db.execute('SELECT metadata FROM intake_ledger WHERE memo_id=?', (review.id,)).fetchone()
|
||||
metadata = json.loads(row['metadata']); metadata['review_group'] = 'changed'
|
||||
db.execute('UPDATE intake_ledger SET metadata=? WHERE memo_id=?', (json.dumps(metadata), review.id))
|
||||
with pytest.raises(Conflict, match='policy observation'):
|
||||
store.present(review.id, principal_sub=session.subject, tenant=session.tenant,
|
||||
principal_type=session.principal_type, policy_id=policy_id)
|
||||
|
|
|
|||
191
tests/test_workload_runtime.py
Normal file
191
tests/test_workload_runtime.py
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import io
|
||||
import json
|
||||
import ssl
|
||||
import time
|
||||
|
||||
import jwt
|
||||
import pytest
|
||||
|
||||
from informed_decision.disposition import ActorKind
|
||||
from informed_decision.intake_service import IntakeError, AUDIENCE
|
||||
from informed_decision.workload import KubernetesTokenReview, LifecycleWorkloadIdentity, LIFECYCLE_AUDIENCE, PrivateWorkloadApp
|
||||
from informed_decision.runtime import Runtime
|
||||
from test_review_runtime import config
|
||||
|
||||
SA = 'system:serviceaccount:state-hub:state-hub'
|
||||
|
||||
|
||||
def verified(body):
|
||||
return {'status': {'authenticated': True, 'audiences': body['spec']['audiences'],
|
||||
'user': {'username': SA, 'uid': 'actual-sa-uid'}}}
|
||||
|
||||
|
||||
def token(exp=None):
|
||||
return jwt.encode({'exp': int(time.time())+300 if exp is None else exp}, 'synthetic-test-key-with-at-least-thirty-two-bytes', algorithm='HS256')
|
||||
|
||||
|
||||
def test_workload_identity_uses_verified_subject_and_separate_audience(tmp_path):
|
||||
path = tmp_path/'callers.json'
|
||||
path.write_text(json.dumps({SA: {'tenant': 'tenant:platform'}}))
|
||||
requests = []
|
||||
def review(body):
|
||||
requests.append(body)
|
||||
return verified(body)
|
||||
identity = LifecycleWorkloadIdentity(review, path)
|
||||
caller = identity(token())
|
||||
assert requests[0]['spec']['audiences'] == [LIFECYCLE_AUDIENCE]
|
||||
assert caller.attribution.actor.sub == SA and caller.attribution.actor.kind is ActorKind.SERVICE
|
||||
assert caller.expires_at <= time.time()+30
|
||||
assert json.loads(caller.attributes_json)['workload_uid'] == 'actual-sa-uid'
|
||||
assert 'token' not in caller.attributes_json
|
||||
path.write_text('{}')
|
||||
with pytest.raises(ValueError):
|
||||
identity(token())
|
||||
|
||||
|
||||
@pytest.mark.parametrize('fault', ['wrong_audience', 'unauthenticated', 'human', 'missing_uid', 'expired', 'no_expiry', 'tenant'])
|
||||
def test_workload_identity_fails_closed(tmp_path, fault):
|
||||
path = tmp_path/'callers.json'
|
||||
path.write_text(json.dumps({SA: {'tenant': 'tenant:other' if fault == 'tenant' else 'tenant:platform'}}))
|
||||
def review(body):
|
||||
result = verified(body)
|
||||
if fault == 'wrong_audience': result['status']['audiences'] = [AUDIENCE]
|
||||
if fault == 'unauthenticated': result['status']['authenticated'] = False
|
||||
if fault == 'human': result['status']['user']['username'] = 'human'
|
||||
if fault == 'missing_uid': result['status']['user']['uid'] = ''
|
||||
return result
|
||||
value = token(int(time.time())-1) if fault == 'expired' else token()
|
||||
if fault == 'no_expiry': value = jwt.encode({}, 'synthetic-test-key-with-at-least-thirty-two-bytes', algorithm='HS256')
|
||||
with pytest.raises(ValueError):
|
||||
LifecycleWorkloadIdentity(review, path)(value)
|
||||
|
||||
|
||||
def test_tokenreview_uses_owned_ca_rotating_caller_and_bounded_transport(tmp_path, monkeypatch):
|
||||
seen = []
|
||||
sentinel = ssl.create_default_context()
|
||||
def context(*, cafile):
|
||||
assert cafile == str(tmp_path/'ca.crt')
|
||||
return sentinel
|
||||
monkeypatch.setattr(ssl, 'create_default_context', context)
|
||||
current = ['synthetic-caller-one']
|
||||
class Transport:
|
||||
def __init__(self, **kwargs):
|
||||
assert kwargs['ssl_context'] is sentinel
|
||||
def request(self, method, url, **kwargs):
|
||||
seen.append((method, url, kwargs))
|
||||
return 201, verified(json.loads(kwargs['body']))
|
||||
review = KubernetesTokenReview('https://kubernetes.default.svc:443', tmp_path/'ca.crt',
|
||||
lambda: current[0], transport_factory=Transport)
|
||||
body = {'spec': {'token': 'synthetic-submitter', 'audiences': [AUDIENCE]}}
|
||||
assert review(body)['status']['authenticated']
|
||||
current[0] = 'synthetic-caller-two'
|
||||
review(body)
|
||||
assert seen[0][2]['headers']['Authorization'] != seen[1][2]['headers']['Authorization']
|
||||
assert seen[0][1] == 'https://kubernetes.default.svc:443/apis/authentication.k8s.io/v1/tokenreviews'
|
||||
def fail(**kwargs): raise OSError('private-path-or-response')
|
||||
monkeypatch.setattr(ssl, 'create_default_context', fail)
|
||||
with pytest.raises(IntakeError, match='workload_identity_unavailable') as error:
|
||||
review(body)
|
||||
assert 'private-path' not in str(error.value)
|
||||
|
||||
|
||||
def test_explicit_runtime_configuration_does_not_read_custody_or_start_listener(tmp_path):
|
||||
path, data = config(tmp_path)
|
||||
data['lifecycle_policy'] = {**data['policy'], 'package': 'informed-decision.lifecycle'}
|
||||
data['workload_api'] = {'token_review': {'origin': 'https://kubernetes.default.svc:443',
|
||||
'ca_file': str(tmp_path/'absent-ca'), 'token_file': str(tmp_path/'absent-token')},
|
||||
'lifecycle_callers_file': str(tmp_path/'absent-callers'),
|
||||
'intake': {'secret_file': str(tmp_path/'absent-secret'), 'submitters_file': str(tmp_path/'absent-submitters'),
|
||||
'groups_file': str(tmp_path/'absent-groups')}}
|
||||
data['intake_attributes'] = True
|
||||
path.write_text(json.dumps(data))
|
||||
runtime = Runtime.from_file(path)
|
||||
assert isinstance(runtime.workload, PrivateWorkloadApp)
|
||||
assert runtime.workload.intake.store is runtime.controller.store
|
||||
assert runtime.workload.lifecycle.service is runtime.lifecycle
|
||||
assert runtime.controller.intake_attributes
|
||||
del data['lifecycle_policy']
|
||||
path.write_text(json.dumps(data))
|
||||
with pytest.raises(ValueError): Runtime.from_file(path)
|
||||
|
||||
|
||||
def test_private_router_does_not_expose_browser_or_unknown_routes():
|
||||
calls = []
|
||||
def app(environ, start):
|
||||
calls.append(environ['PATH_INFO']); start('200 OK', []); return [b'ok']
|
||||
router = PrivateWorkloadApp(intake=app, lifecycle=app)
|
||||
for path in ('/v1/memos', '/v1/decisions', '/v1/decisions/example/events'):
|
||||
assert b''.join(router({'PATH_INFO': path}, lambda s, h: None)) == b'ok'
|
||||
for path in ('/', '/auth/start', '/review', '/v1/decisionsevil'):
|
||||
statuses = []
|
||||
router({'PATH_INFO': path}, lambda s, h: statuses.append(s))
|
||||
assert statuses == ['404 Not Found']
|
||||
assert len(calls) == 3
|
||||
|
||||
|
||||
def test_two_listener_startup_failure_closes_first_socket_and_dispatcher(monkeypatch):
|
||||
import waitress
|
||||
from informed_decision.web import serve_with_workload
|
||||
calls = []
|
||||
class Server:
|
||||
def close(self): calls.append('close')
|
||||
def shutdown(self): calls.append('shutdown')
|
||||
@property
|
||||
def task_dispatcher(self): return self
|
||||
def create(app, **kwargs):
|
||||
calls.append(kwargs)
|
||||
if kwargs['port'] == 8081: raise OSError('occupied')
|
||||
return Server()
|
||||
monkeypatch.setattr(waitress, 'create_server', create)
|
||||
with pytest.raises(OSError): serve_with_workload(object(), object(), host='127.0.0.1')
|
||||
assert calls[0]['map'] is calls[1]['map']
|
||||
assert calls[-2:] == ['close', 'shutdown']
|
||||
|
||||
|
||||
def test_real_tls_tokenreview_requires_ca_and_hostname(tmp_path):
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
import ipaddress
|
||||
import threading
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.x509.oid import NameOID
|
||||
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
now = datetime.now(timezone.utc)
|
||||
name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, 'synthetic-api')])
|
||||
certificate = (x509.CertificateBuilder().subject_name(name).issuer_name(name).public_key(key.public_key())
|
||||
.serial_number(x509.random_serial_number()).not_valid_before(now-timedelta(minutes=1))
|
||||
.not_valid_after(now+timedelta(hours=1))
|
||||
.add_extension(x509.SubjectAlternativeName([x509.IPAddress(ipaddress.ip_address('127.0.0.1'))]), critical=False)
|
||||
.sign(key, hashes.SHA256()))
|
||||
ca, private_key = tmp_path/'ca.pem', tmp_path/'key.pem'
|
||||
ca.write_bytes(certificate.public_bytes(serialization.Encoding.PEM))
|
||||
private_key.write_bytes(key.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8,
|
||||
serialization.NoEncryption()))
|
||||
received = []
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, *args): pass
|
||||
def do_POST(self):
|
||||
received.append(self.path)
|
||||
assert self.headers['Authorization'] == 'Bearer synthetic-reviewer'
|
||||
body = json.loads(self.rfile.read(int(self.headers['Content-Length'])))
|
||||
raw = json.dumps(verified(body)).encode()
|
||||
self.send_response(201); self.end_headers(); self.wfile.write(raw)
|
||||
server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
|
||||
tls = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
tls.load_cert_chain(ca, private_key)
|
||||
server.socket = tls.wrap_socket(server.socket, server_side=True)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True); thread.start()
|
||||
body = {'spec': {'token': 'synthetic-caller', 'audiences': [LIFECYCLE_AUDIENCE]}}
|
||||
try:
|
||||
origin = f'https://127.0.0.1:{server.server_port}'
|
||||
review = KubernetesTokenReview(origin, ca, lambda: 'synthetic-reviewer')
|
||||
assert review(body)['status']['authenticated']
|
||||
wrong_host = KubernetesTokenReview(origin.replace('127.0.0.1', 'localhost'), ca, lambda: 'synthetic-reviewer')
|
||||
with pytest.raises(IntakeError): wrong_host(body)
|
||||
untrusted = KubernetesTokenReview(origin, '/etc/ssl/certs/ca-certificates.crt', lambda: 'synthetic-reviewer')
|
||||
with pytest.raises(IntakeError): untrusted(body)
|
||||
assert len(received) == 1
|
||||
finally:
|
||||
server.shutdown(); server.server_close(); thread.join(timeout=5)
|
||||
Loading…
Add table
Add a link
Reference in a new issue