Implement authenticated alert receipt acknowledgments and audit delivery

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0e6f1-443f-7783-9920-a16b2ffc467f
This commit is contained in:
tegwick 2026-09-28 11:11:33 +02:00
parent 67283b66c2
commit e7282e493d
25 changed files with 1881 additions and 0 deletions

View file

@ -0,0 +1,131 @@
from dataclasses import replace
from datetime import datetime, timezone
import json
from pathlib import Path
import sys
import time
import unittest
from urllib.parse import parse_qs, urlsplit
import jwt
from cryptography.hazmat.primitives.asymmetric import rsa
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'scripts'))
from alert_identity import Login, SCOPES
from alert_policy import Policy, request_digest
from alert_ack import Actor
class Issuer:
def __init__(self):
self.key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
self.jwk = json.loads(jwt.algorithms.RSAAlgorithm.to_jwk(self.key.public_key()))
self.jwk.update(kid='fixture', use='sig', alg='RS256')
self.nonce = ''
self.change = lambda c: c
def request(self, method, url, body=None, headers=None):
if url.endswith('/.well-known/openid-configuration'):
return 200, dict(issuer='https://issuer.example', authorization_endpoint='https://issuer.example/authorize',
token_endpoint='https://issuer.example/token', jwks_uri='https://issuer.example/jwks',
code_challenge_methods_supported=['S256'])
if url.endswith('/jwks'):
return 200, {'keys': [self.jwk]}
now = int(time.time())
claims = dict(iss='https://issuer.example', sub='fixture-human', aud='railiance-telemetry-admin',
iat=now, exp=now + 600, nonce=self.nonce, tenant='tenant:platform',
tenant_source='registration', principal_type='human', groups=[], roles=['railiance-admin'],
assurance=dict(level='aal2', mfa=True, source='key-cape', methods=['pwd', 'otp'], at=now))
claims = self.change(claims)
identity = jwt.encode(claims, self.key, algorithm='RS256', headers={'kid': 'fixture'})
access = dict(claims, aud='railiance-telemetry', scope=' '.join(SCOPES))
return 200, dict(id_token=identity, access_token=jwt.encode(access, self.key, algorithm='RS256', headers={'kid': 'fixture'}))
class IdentityTests(unittest.TestCase):
def setUp(self):
self.issuer = Issuer()
self.login = Login('https://issuer.example', 'https://telemetry.example', transport=self.issuer)
def start(self):
url, browser = self.login.start('/ack/alerts?fingerprint=0123456789abcdef&starts_at=2026-09-28T00:00:00Z')
params = parse_qs(urlsplit(url).query)
self.issuer.nonce = params['nonce'][0]
self.assertEqual(params['code_challenge_method'], ['S256'])
return params['state'][0], browser
def actor(self):
state, browser = self.start()
sid, target = self.login.finish(state, browser, 'fixture-code')
return self.login.session(sid)
def test_verified_human_session_and_logout(self):
state, browser = self.start()
sid, target = self.login.finish(state, browser, 'fixture-code')
actor = self.login.session(sid)
self.assertEqual(actor.subject, 'fixture-human')
self.assertEqual(actor.roles, ('railiance-admin',))
self.assertLessEqual(actor.expires_at, time.time() + 600)
self.login.logout(sid)
self.assertIsNone(self.login.session(sid))
with self.assertRaises(ValueError): self.login.finish(state, browser, 'fixture-code')
def test_wrong_browser_and_state_replay_refused(self):
state, browser = self.start()
with self.assertRaises(ValueError): self.login.finish(state, 'other', 'fixture-code')
with self.assertRaises(ValueError): self.login.finish(state, browser, 'fixture-code')
def test_nonce_wrong_issuer_wrong_tenant_and_weak_mfa(self):
for changes in ({'nonce': 'wrong'}, {'iss': 'https://other.example'}, {'tenant': 'tenant:other'},
{'principal_type': 'service'}, {'assurance': {'mfa': False}}, {'exp': 1}):
self.issuer.change = lambda claims, changes=changes: dict(claims, **changes)
state, browser = self.start()
with self.assertRaises(ValueError): self.login.finish(state, browser, 'fixture-code')
def test_return_path_cannot_escape_surface(self):
for value in ('https://attacker.example/ack/alerts', '//attacker.example/ack/alerts', '/other'):
with self.assertRaises(ValueError): self.login.start(value)
class DecisionTransport:
def __init__(self):
self.change = lambda response: response
def request(self, method, url, body=None, headers=None):
request = json.loads(body)
now = time.time()
stamp = lambda delta: datetime.fromtimestamp(now + delta, timezone.utc).isoformat()
response = dict(id='fixture-decision', request_id=request['id'], contract_version='flex-auth.decision-record.v1',
effect='allow', obligations=[], matched_policy_version='v1', subject=request['subject'], resource=request['resource'],
binding=dict(tenant=request['tenant'], action=request['action'], subject=request['subject'], resource=request['resource'],
context=request.get('context', {}),
submitted_request_digest=request_digest(request), request_digest='sha256:' + 'b' * 64),
provenance=dict(policy_package='telemetry.ack', policy_version='v1', policy_package_digest='sha256:' + 'a' * 64,
registry_snapshot_digest='sha256:' + 'c' * 64, evaluator='flex-auth/fixture', decision_time=stamp(0)),
lifetime=dict(kind='ttl', not_before=stamp(-1), expires_at=stamp(30)))
return 200, self.change(response)
class PolicyTests(unittest.TestCase):
def test_policy_pins_and_action_bindings_fail_closed(self):
actor = Actor('https://issuer.example', 'fixture-human', 'tenant:platform', ('railiance-admin',),
time.time() + 600, 'c' * 32, tenant_source='directory')
transport = DecisionTransport()
policy = Policy('https://policy.example', lambda: 'fixture-workload-token', 'telemetry.ack', 'v1',
'sha256:' + 'a' * 64, transport=transport)
resource = 'alert:11111111-1111-1111-1111-111111111111'
self.assertEqual(policy(actor, 'acknowledge', resource)['effect'], 'allow')
mutations = [lambda r: dict(r, effect='deny'), lambda r: dict(r, obligations=['unknown']),
lambda r: dict(r, request_id='other'),
lambda r: dict(r, binding=dict(r['binding'], action='read')),
lambda r: dict(r, binding=dict(r['binding'], submitted_request_digest='sha256:' + '0' * 64)),
lambda r: dict(r, provenance=dict(r['provenance'], policy_package_digest='sha256:' + '0' * 64)),
lambda r: dict(r, lifetime=dict(r['lifetime'], expires_at='2000-01-01T00:00:00Z'))]
for change in mutations:
transport.change = change
self.assertEqual(policy(actor, 'acknowledge', resource)['effect'], 'deny')
self.assertEqual(policy(actor, 'delete', resource)['effect'], 'deny')
if __name__ == '__main__':
unittest.main()

View file

@ -0,0 +1,42 @@
from dataclasses import replace
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import time
import unittest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'scripts'))
from alert_ack import Actor
from alert_policy import Policy
@unittest.skipUnless(os.environ.get('RTEL_FLEX_AUTH_BINARY'), 'set RTEL_FLEX_AUTH_BINARY')
class NativePolicyTests(unittest.TestCase):
def test_native_wire_digest_and_revoked_or_wrong_identity_denials(self):
with tempfile.TemporaryDirectory() as directory:
request_file = Path(directory) / 'request.json'
class Native:
def request(self, method, url, body=None, headers=None):
request_file.write_bytes(body)
result = subprocess.run([os.environ['RTEL_FLEX_AUTH_BINARY'], 'check', '--policy',
str(ROOT / 'integration/telemetry-policy.md'), '--registry', str(ROOT / 'integration/registry.json'),
'--request', str(request_file)], capture_output=True, check=True)
return 200, json.loads(result.stdout)
actor = Actor('https://kc.coulomb.social', 'uid=tegwick,ou=people,dc=netkingdom,dc=local',
'tenant:platform', ('railiance-admin',), time.time() + 600, 'c' * 32,
assurance=dict(level='aal2', mfa=True, source='key-cape', methods=['pwd', 'otp'], at=int(time.time())),
groups=('railiance-admins',), tenant_source='directory')
# Native digest is pinned by the owner packet, not inferred from a response.
policy = Policy('https://policy.example', lambda: 'fixture-only',
'railiance-telemetry.alert-acknowledgment', 'v1',
'sha256:02938202cf75140d6ab638b8d0fbce6ebb22832354efc89cab040b13e8943f09', transport=Native())
resource = 'alert:11111111-1111-1111-1111-111111111111'
self.assertEqual(policy(actor, 'acknowledge', resource)['effect'], 'allow')
for changed in (replace(actor, roles=()), replace(actor, groups=()), replace(actor, subject='other'),
replace(actor, tenant='tenant:other'), replace(actor, issuer='https://other.example'),
replace(actor, assurance=dict(actor.assurance, at=1))):
self.assertEqual(policy(changed, 'acknowledge', resource)['effect'], 'deny')

View file

@ -0,0 +1,57 @@
import io
from pathlib import Path
import sys
import tempfile
import time
import unittest
from urllib.parse import parse_qs, urlencode, urlsplit
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'scripts'))
from alert_ack import Store
from alert_identity import Login
from alert_service import Router
from test_identity_policy import Issuer
class ServiceTests(unittest.TestCase):
def test_browser_login_confirmation_and_logout(self):
with tempfile.TemporaryDirectory() as tmp:
token = Path(tmp) / 'webhook-token'
token.write_text('w' * 32)
store = Store(Path(tmp) / 'state.db')
issuer = Issuer()
login = Login('https://issuer.example', 'https://telemetry.example', transport=issuer)
router = Router({'origin': 'https://telemetry.example', 'webhook_token_file': str(token)}, store,
login=login, policy=lambda *args: dict(id='fixture-decision', effect='allow', expires_at=time.time() + 30))
query = urlencode(dict(fingerprint='0123456789abcdef', starts_at='2026-09-28T00:00:00Z'))
def call(path, method='GET', cookie='', query=query, body=b''):
env = dict(PATH_INFO=path, REQUEST_METHOD=method, HTTP_COOKIE=cookie, QUERY_STRING=query,
CONTENT_LENGTH=str(len(body)), HTTP_ORIGIN='https://telemetry.example')
env['wsgi.input'] = io.BytesIO(body)
result = {}
output = b''.join(router(env, lambda status, headers: result.update(status=status, headers=headers)))
return result, output.decode()
response, _ = call('/ack/alerts')
self.assertEqual(response['status'], '303 See Other')
headers = dict(response['headers'])
browser_cookie = headers['Set-Cookie'].split(';')[0]
params = parse_qs(urlsplit(headers['Location']).query)
issuer.nonce = params['nonce'][0]
response, _ = call('/ack/auth/callback', cookie=browser_cookie,
query=urlencode(dict(state=params['state'][0], code='fixture-code')))
self.assertEqual(response['status'], '303 See Other')
cookies = [value for key, value in response['headers'] if key == 'Set-Cookie']
session_cookie = next(c.split(';')[0] for c in cookies if c.startswith('__Host-rtel-session='))
actor = login.session(session_cookie.split('=', 1)[1])
identity = store.receive({'version': '4', 'receiver': 'railiance-admin-email', 'alerts': [
{'status': 'firing', 'fingerprint': '0123456789abcdef', 'startsAt': '2026-09-28T00:00:00Z',
'labels': {'alertname': 'TestFailure', 'owner': 'railiance-telemetry'}}]}, time.time())[0]
response, page = call('/ack/alerts', cookie=session_cookie)
self.assertIn('Acknowledge receipt', page)
self.assertIsNone(store.get(identity)['occurred_at'])
form = urlencode(dict(csrf=actor.csrf)).encode()
response, page = call('/ack/alerts', 'POST', session_cookie, body=form)
self.assertIn('Receipt acknowledged', page)
response, _ = call('/ack/logout', 'POST', session_cookie, query='', body=form)
self.assertEqual(response['status'], '200 OK')
self.assertIsNone(login.session(session_cookie.split('=', 1)[1]))