Files
broker/tests/test_auth_audit.py
T
coreci 7f7868f7cf docs(milestone): complete coreci-broker
---ci---
phase: 5
milestone: v0.1
status: complete
requirements:
  covered: [REQ-01, REQ-02, REQ-03, REQ-04, REQ-05, REQ-06, REQ-07, REQ-08, REQ-09, REQ-10, REQ-11, REQ-12, REQ-13, REQ-14, REQ-15, REQ-16, REQ-17, REQ-18, REQ-19, REQ-20]
  partial: []
---/ci---
2026-10-10 15:30:14 +00:00

188 lines
7.7 KiB
Python

import base64
import contextlib
import datetime
import io
import json
import os
import sys
import tempfile
import unittest
import urllib.error
import urllib.parse
import urllib.request
import uuid as uuidlib
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)))
import coreci_broker
ADMIN_USER = "admin"
ADMIN_PASS = "correct-horse-battery-staple"
REGISTER_PATH = "/api/broker/agents/register"
APPS_PATH = "/api/broker/app_registrations"
USERS_PATH = "/api/broker/user_allowlist"
AUDIT_PATH = "/api/broker/audit"
STATE_PATH = "/api/broker/agents/%s/state"
AUTH_401_BODY = {"error": 401, "message": "invalid app credentials"}
BAD_JSON_BODY = {"error": 400, "message": "invalid JSON body"}
NEST_DEPTH = 50000
def _basic_header(username, password):
raw = "%s:%s" % (username, password)
return "Basic " + base64.b64encode(raw.encode("utf-8")).decode("ascii")
class AuthAuditTests(unittest.TestCase):
def setUp(self):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
self.tmp = tmp.name
self.env = {
"BROKER_ENV_PATH": os.path.join(self.tmp, ".env"),
"BROKER_DB_PATH": os.path.join(self.tmp, "broker.db"),
"BROKER_AUDIT_LOG_PATH": os.path.join(self.tmp, "broker.audit.log"),
"BROKER_ADMIN_USER": ADMIN_USER,
"BROKER_ADMIN_PASS": ADMIN_PASS,
}
with contextlib.redirect_stdout(io.StringIO()):
self.broker = coreci_broker.boot(dict(self.env))
self.addCleanup(self.broker.audit.close)
self.addCleanup(self.broker.store.close)
self.server = coreci_broker.serve("127.0.0.1", 0, self.broker)
self.addCleanup(self.server.shutdown)
self.addCleanup(self.server.server_close)
self.base = "http://127.0.0.1:%d" % self.server.server_address[1]
self.admin = {"Authorization": _basic_header(ADMIN_USER, ADMIN_PASS)}
self.app_uuid = str(uuidlib.uuid4())
self.user_id = "ceo"
self.agent_id = "agent-1"
self._seed(self.app_uuid, self.user_id)
self.token = self._register()["token"]
self.app = {"Authorization": "App %s %s" % (self.app_uuid, self.token)}
def _raw(self, method, path, data, headers):
request = urllib.request.Request(self.base + path, data=data, method=method)
for key, value in (headers or {}).items():
request.add_header(key, value)
try:
with urllib.request.urlopen(request, timeout=10) as response:
return response.status, json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as error:
return error.code, json.loads(error.read().decode("utf-8"))
def _request(self, method, path, payload=None, headers=None):
data = None if payload is None else json.dumps(payload).encode("utf-8")
return self._raw(method, path, data, headers)
def _seed(self, app_uuid, user_id):
status, _ = self._request("POST", APPS_PATH, {"uuid": app_uuid}, self.admin)
self.assertEqual(201, status)
status, _ = self._request("POST", USERS_PATH, {"user_id": user_id}, self.admin)
self.assertEqual(201, status)
def _register(self):
payload = {"agent_id": self.agent_id, "app_registration_uuid": self.app_uuid,
"user_id": self.user_id, "tags": ["coreci"]}
status, body = self._request("POST", REGISTER_PATH, payload)
self.assertEqual(201, status)
return body
def _denied(self):
status, body = self._request("GET", AUDIT_PATH + "?action=denied",
headers=self.admin)
self.assertEqual(200, status)
return body["entries"]
def _assert_denied_line(self, entry, app_uuid, path):
self.assertEqual("denied", entry["action"])
self.assertEqual("denied", entry["result"])
self.assertEqual(app_uuid, entry["app_registration_uuid"])
self.assertEqual(path, entry["path"])
self.assertIsNone(entry["user_id"])
self.assertIsNone(entry["agent_id"])
def test_deep_nested_json_body_is_400_not_500(self):
data = ("[" * NEST_DEPTH).encode("utf-8")
self.assertLess(len(data), 1024 * 1024)
status, body = self._raw("PUT", STATE_PATH % self.agent_id, data, self.app)
self.assertEqual(400, status)
self.assertEqual(BAD_JSON_BODY, body)
status, body = self._request("GET", "/healthz")
self.assertEqual(200, status)
self.assertEqual({"ok": True}, body)
def test_wrong_token_denied_audit_carries_uuid_and_path(self):
path = STATE_PATH % self.agent_id
status, body = self._request(
"GET", path, headers={"Authorization": "App %s %s" % (self.app_uuid, "A" * 44)}
)
self.assertEqual(401, status)
self.assertEqual(AUTH_401_BODY, body)
entries = self._denied()
self.assertEqual(1, len(entries))
self._assert_denied_line(entries[0], self.app_uuid, path)
def test_expired_or_absent_token_same_denied_audit(self):
path = STATE_PATH % self.agent_id
past = (datetime.datetime.now(datetime.timezone.utc)
- datetime.timedelta(seconds=60)).isoformat()
self.broker.store.conn.execute(
"UPDATE agent_registrations SET token_expires_at = ? WHERE agent_id = ?",
(past, self.agent_id),
)
self.broker.store.conn.commit()
status, body = self._request("GET", path, headers=self.app)
self.assertEqual(401, status)
self.assertEqual(AUTH_401_BODY, body)
status, body = self._request(
"GET", path, headers={"Authorization": "App %s" % self.app_uuid}
)
self.assertEqual(401, status)
self.assertEqual(AUTH_401_BODY, body)
entries = self._denied()
self.assertEqual(2, len(entries))
for entry in entries:
self._assert_denied_line(entry, self.app_uuid, path)
def test_missing_app_header_401_writes_no_denied_audit(self):
self.assertEqual(0, len(self._denied()))
path = STATE_PATH % self.agent_id
status, body = self._request("GET", path, headers={})
self.assertEqual(401, status)
self.assertEqual(AUTH_401_BODY, body)
status, body = self._request("GET", path, headers=self.admin)
self.assertEqual(401, status)
self.assertEqual(AUTH_401_BODY, body)
self.assertEqual(0, len(self._denied()))
def test_malformed_uuid_denied_audit_carries_null_uuid(self):
path = STATE_PATH % self.agent_id
status, body = self._request(
"GET", path, headers={"Authorization": "App not-a-uuid %s" % self.token}
)
self.assertEqual(401, status)
self.assertEqual(AUTH_401_BODY, body)
entries = self._denied()
self.assertEqual(1, len(entries))
self._assert_denied_line(entries[0], None, path)
def test_admin_audit_action_filter_returns_denied_lines(self):
path = STATE_PATH % self.agent_id
for header in ("App %s wrong" % self.app_uuid,
"App not-a-uuid %s" % self.token):
status, _ = self._request("GET", path, headers={"Authorization": header})
self.assertEqual(401, status)
query = urllib.parse.urlencode({"action": "denied"})
status, body = self._request("GET", AUDIT_PATH + "?" + query,
headers=self.admin)
self.assertEqual(200, status)
self.assertEqual(2, len(body["entries"]))
self.assertEqual([self.app_uuid, None],
[entry["app_registration_uuid"] for entry in body["entries"]])
for entry in body["entries"]:
self.assertEqual(path, entry["path"])
if __name__ == "__main__":
unittest.main()