railiance-telemetry/tests/test_alert_ack.py
tegwick e7282e493d Implement authenticated alert receipt acknowledgments and audit delivery
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0e6f1-443f-7783-9920-a16b2ffc467f
2026-09-28 11:11:33 +02:00

158 lines
7.3 KiB
Python

from dataclasses import replace
import io
import json
import os
from pathlib import Path
import sqlite3
import sys
import tempfile
import unittest
from urllib.parse import urlencode
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'scripts'))
from alert_ack import Actor, Application, Store, occurrence
NOW = 1790553600 # 2026-09-28 UTC
START = '2026-09-28T00:00:00Z'
class AcknowledgmentTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.path = Path(self.temp.name) / 'ack.db'
self.store = Store(self.path)
self.actor = Actor('https://issuer.example', 'test-human', 'tenant:platform',
('railiance-admin',), NOW + 900, 'c' * 32)
self.decision = {'id': 'test-decision', 'effect': 'allow', 'expires_at': NOW + 30}
self.app = Application(self.store, 'https://telemetry.example', 'w' * 32,
lambda env: self.actor, lambda *args: self.decision,
clock=lambda: NOW)
self.alert = {'status': 'firing', 'fingerprint': '0123456789abcdef',
'startsAt': START, 'labels': {'alertname': 'TestFailure', 'owner': 'railiance-telemetry'}}
self.payload = {'version': '4', 'receiver': 'railiance-admin-email', 'alerts': [self.alert]}
self.identity = occurrence(self.alert['fingerprint'], START)
self.query = urlencode({'fingerprint': self.alert['fingerprint'], 'starts_at': START})
def request(self, method='GET', path='/ack/alerts', body=b'', **extra):
env = {'REQUEST_METHOD': method, 'PATH_INFO': path, 'QUERY_STRING': self.query,
'CONTENT_LENGTH': str(len(body)), 'wsgi.input': io.BytesIO(body),
'HTTP_ORIGIN': 'https://telemetry.example'}
env.update(extra)
result = {}
raw = b''.join(self.app(env, lambda status, headers: result.update(status=status, headers=headers)))
return result['status'], raw.decode()
def receive(self):
self.store.receive(self.payload, NOW)
def post(self, **kw):
return self.request('POST', body=urlencode({'csrf': self.actor.csrf}).encode(), **kw)
def test_scanner_get_does_not_acknowledge(self):
self.receive()
status, page = self.request()
self.assertEqual(status, '200 OK')
self.assertIn('Acknowledge receipt', page)
self.assertIsNone(self.store.get(self.identity)['occurred_at'])
with self.store.connect() as db:
self.assertEqual(db.execute('SELECT COUNT(*) FROM outbox').fetchone()[0], 0)
def test_click_commits_ack_and_audit_once_across_restart(self):
self.receive()
self.assertIn('Audit delivery pending', self.post()[1])
self.store = Store(self.path)
self.app.store = self.store
self.post()
with self.store.connect() as db:
rows = db.execute('SELECT body FROM outbox').fetchall()
self.assertEqual(len(rows), 1)
event = json.loads(rows[0]['body'])
self.assertEqual(event['data']['actor_subject'], 'test-human')
self.assertEqual(event['data']['role'], 'railiance-admin')
self.assertEqual(event['subject'], 'alert:' + self.identity)
self.assertEqual(event['type'], 'telemetry.alert.acknowledged')
def test_failed_outbox_rolls_back_ack(self):
self.receive()
with self.store.connect() as db:
db.execute("CREATE TRIGGER simulate_full BEFORE INSERT ON outbox BEGIN SELECT RAISE(ABORT, 'full'); END")
self.assertEqual(self.post()[0], '503 Service Unavailable')
self.assertIsNone(self.store.get(self.identity)['occurred_at'])
def test_wrong_actor_role_tenant_service_expiry_denied(self):
self.receive()
original = self.actor
for change in ({'roles': ()}, {'tenant': 'tenant:other'}, {'principal_type': 'service'}, {'expires_at': NOW}):
self.actor = replace(original, **change)
self.assertIn(self.post()[0], ('401 Unauthorized', '403 Forbidden'))
self.assertIsNone(self.store.get(self.identity)['occurred_at'])
def test_email_or_headers_do_not_grant_role(self):
self.receive()
self.actor = replace(self.actor, roles=())
self.assertEqual(self.post(HTTP_X_EMAIL='bernd.worsch@gmail.com', HTTP_X_ROLE='railiance-admin')[0], '403 Forbidden')
def test_origin_csrf_and_expired_pdp_denied(self):
self.receive()
self.assertEqual(self.post(HTTP_ORIGIN='https://attacker.example')[0], '403 Forbidden')
self.assertEqual(self.request('POST', body=b'csrf=wrong')[0], '403 Forbidden')
self.decision['expires_at'] = NOW
self.assertEqual(self.post()[0], '403 Forbidden')
self.assertIsNone(self.store.get(self.identity)['occurred_at'])
def test_webhook_auth_and_retry_and_batch_rollback(self):
raw = json.dumps(self.payload).encode()
self.assertEqual(self.request('POST', '/webhook', raw)[0], '401 Unauthorized')
for _ in range(2):
self.assertEqual(self.request('POST', '/webhook', raw, HTTP_AUTHORIZATION='Bearer ' + 'w' * 32)[0], '200 OK')
changed = json.loads(raw)
changed['alerts'][0]['labels']['alertname'] = 'Different'
with self.assertRaises(ValueError): self.store.receive(changed, NOW)
self.assertEqual(self.store.get(self.identity)['alertname'], 'TestFailure')
def test_new_firing_occurrence_requires_new_ack(self):
self.receive()
self.post()
self.alert['startsAt'] = '2026-09-27T23:59:59Z'
other = self.store.receive(self.payload, NOW)[0]
self.assertNotEqual(other, self.identity)
self.assertIsNone(self.store.get(other)['occurred_at'])
def test_lost_audit_receipt_replays_original_event(self):
self.receive()
self.post()
sent = []
def lost(event):
sent.append(event)
raise TimeoutError()
self.store.drain(lost)
def duplicate(event):
self.assertEqual(event, sent[0])
return 200, {'status': 'duplicate', 'reference': 'audit:' + event['id']}
self.store.drain(duplicate)
self.assertEqual(self.store.get(self.identity)['audit_status'], 'delivered')
self.assertIn('Audit record archived', self.request()[1])
def test_audit_refusal_retained_and_wrong_receipt_not_accepted(self):
self.receive()
self.post()
self.store.drain(lambda event: (202, {'status': 'accepted', 'reference': 'wrong'}))
self.assertEqual(self.store.get(self.identity)['audit_status'], 'pending')
self.store.drain(lambda event: (403, {}))
self.assertEqual(self.store.get(self.identity)['audit_status'], 'blocked')
self.assertIn('Audit delivery pending', self.request()[1])
self.assertEqual(self.store.audit_debt(), {'blocked': 1})
with self.store.connect() as db:
event = json.loads(db.execute('SELECT body FROM outbox').fetchone()[0])
self.assertTrue(self.store.requeue(event['id']))
self.store.drain(lambda value: (202, {'status': 'accepted', 'reference': 'audit:' + value['id']}))
self.assertEqual(self.store.audit_debt(), {})
def test_unsafe_database_refused(self):
os.chmod(self.path, 0o644)
with self.assertRaises(ValueError): Store(self.path)
if __name__ == '__main__':
unittest.main()