80 lines
4.3 KiB
Python
80 lines
4.3 KiB
Python
|
|
"""Disposable actual Flex Auth HTTP process, with synthetic Kubernetes custody.
|
||
|
|
|
||
|
|
The evaluator and inbound caller authenticator are real. TokenReview and its
|
||
|
|
identities are fixtures; this does not admit any production caller or policy.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from contextlib import contextmanager
|
||
|
|
from datetime import datetime, timedelta, timezone
|
||
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
from pathlib import Path
|
||
|
|
import socket
|
||
|
|
import subprocess
|
||
|
|
import threading
|
||
|
|
import time
|
||
|
|
|
||
|
|
from cryptography import x509
|
||
|
|
from cryptography.hazmat.primitives import hashes, serialization
|
||
|
|
from cryptography.x509.oid import NameOID
|
||
|
|
|
||
|
|
from informed_decision.http_transport import JSONTransport, TransportError
|
||
|
|
from informed_decision.policy import PolicyClient
|
||
|
|
|
||
|
|
FIXTURE = Path(__file__).parent / 'fixtures/review-policy'
|
||
|
|
|
||
|
|
|
||
|
|
@contextmanager
|
||
|
|
def flex_component(binary, directory, signing_key):
|
||
|
|
tokens={'synthetic-workload-token':'system:serviceaccount:informed-decision:review',
|
||
|
|
'synthetic-wrong-caller':'system:serviceaccount:other:other'}
|
||
|
|
calls=[]
|
||
|
|
class Reviewer(BaseHTTPRequestHandler):
|
||
|
|
def log_message(self,*args):pass
|
||
|
|
def do_POST(self):
|
||
|
|
body=json.loads(self.rfile.read(int(self.headers['Content-Length'])))
|
||
|
|
token=body['spec']['token'];calls.append({'known':token in tokens,'audiences':body['spec']['audiences']})
|
||
|
|
valid=(self.path=='/apis/authentication.k8s.io/v1/tokenreviews'
|
||
|
|
and self.headers.get('Authorization')=='Bearer synthetic-reviewer'
|
||
|
|
and body['spec']['audiences']==['flex-auth'] and token in tokens)
|
||
|
|
raw=json.dumps({'status':{'authenticated':valid,'audiences':['flex-auth'] if valid else [],
|
||
|
|
'user':{'username':tokens.get(token,'')}}}).encode()
|
||
|
|
self.send_response(201);self.send_header('Content-Type','application/json');self.end_headers();self.wfile.write(raw)
|
||
|
|
server=ThreadingHTTPServer(('127.0.0.1',0),Reviewer)
|
||
|
|
thread=threading.Thread(target=server.serve_forever,daemon=True);thread.start()
|
||
|
|
now=datetime.now(timezone.utc)
|
||
|
|
name=x509.Name([x509.NameAttribute(NameOID.COMMON_NAME,'synthetic-tokenreview-fixture')])
|
||
|
|
cert=(x509.CertificateBuilder().subject_name(name).issuer_name(name).public_key(signing_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.BasicConstraints(ca=True,path_length=None),critical=True)
|
||
|
|
.sign(signing_key,hashes.SHA256()))
|
||
|
|
ca=directory/'fixture-ca.pem';ca.write_bytes(cert.public_bytes(serialization.Encoding.PEM))
|
||
|
|
reviewer=directory/'reviewer-token';reviewer.write_text('synthetic-reviewer');reviewer.chmod(0o600)
|
||
|
|
with socket.socket() as listener:
|
||
|
|
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'),
|
||
|
|
'--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)
|
||
|
|
try:
|
||
|
|
transport=JSONTransport(allow_internal_http=True)
|
||
|
|
for _ in range(100):
|
||
|
|
if process.poll() is not None:
|
||
|
|
raise RuntimeError('fixture evaluator exited: '+process.stderr.read().decode()[:1000])
|
||
|
|
try:
|
||
|
|
if transport.request('GET',origin+'/healthz')[0]==200:break
|
||
|
|
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')
|
||
|
|
client=PolicyClient(origin,lambda:'synthetic-workload-token',allow_internal_http=True,**pin)
|
||
|
|
yield client,calls
|
||
|
|
finally:
|
||
|
|
process.terminate()
|
||
|
|
try:process.wait(timeout=5)
|
||
|
|
except subprocess.TimeoutExpired:process.kill();process.wait(timeout=5)
|
||
|
|
process.stderr.close();server.shutdown();server.server_close();thread.join(timeout=5)
|