Files
broker/tests/test_acceptance_p4.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

290 lines
12 KiB
Python

import ast
import base64
import contextlib
import importlib.util
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"
APPS_PATH = "/api/broker/app_registrations"
USERS_PATH = "/api/broker/user_allowlist"
REGISTER_PATH = "/api/broker/agents/register"
AUDIT_PATH = "/api/broker/audit"
SECRETS_PREFIX = "/api/broker/secrets/"
CLIENT_STATIC_PATH = "/static/coreci_broker_client.py"
PROXMOX_PATH = "coreci/proxmox"
GITEA_PATH = "coreci/gitea"
PROXMOX_SECRET = {"url": "https://pve.coreci.dev:8006", "token": "pve-tok"}
GITEA_SECRET = {"url": "https://git.coreci.dev", "token": "git-tok"}
AUTH_401_BODY = {"error": 401, "message": "invalid app credentials"}
GITEA_403_BODY = {"error": 403, "message": "forbidden"}
REFERENCE_DIR = os.path.abspath(
os.path.join(os.path.dirname(__file__), os.pardir, "reference_tools")
)
STANDALONE_CLIENT_IMPORTS = {"json", "time", "urllib.parse", "urllib.request"}
def _basic_header(username, password):
raw = "%s:%s" % (username, password)
return "Basic " + base64.b64encode(raw.encode("utf-8")).decode("ascii")
def _flip(token):
replacement = "x" if token[21] != "x" else "y"
return token[:21] + replacement + token[22:]
class AcceptanceP4Tests(unittest.TestCase):
def setUp(self):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
self.tmp = tmp.name
self.creds_path = os.path.join(self.tmp, "creds.json")
with open(self.creds_path, "w", encoding="utf-8") as handle:
handle.write(json.dumps(
{PROXMOX_PATH: PROXMOX_SECRET, GITEA_PATH: GITEA_SECRET}
))
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,
"BROKER_CREDS_SOURCE": "file",
"BROKER_CREDS_FILE": self.creds_path,
}
with contextlib.redirect_stdout(io.StringIO()):
self.broker = coreci_broker.boot(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._seed()
def _request(self, method, path, payload=None, headers=None):
data = None if payload is None else json.dumps(payload).encode("utf-8")
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:
raw = response.read().decode("utf-8")
return response.status, json.loads(raw) if raw else None
except urllib.error.HTTPError as error:
raw = error.read().decode("utf-8")
return error.code, json.loads(raw) if raw else None
def _seed(self):
status, _ = self._request("POST", APPS_PATH, {"uuid": self.app_uuid}, self.admin)
self.assertEqual(201, status)
status, _ = self._request("POST", USERS_PATH, {"user_id": self.user_id}, self.admin)
self.assertEqual(201, status)
def _register(self, tags):
payload = {"agent_id": "proxmox", "app_registration_uuid": self.app_uuid,
"user_id": self.user_id, "tags": list(tags)}
status, body = self._request("POST", REGISTER_PATH, payload)
self.assertEqual(201, status)
return body
def _app_header(self, token):
return {"Authorization": "App %s %s" % (self.app_uuid, token)}
def _secret(self, suffix, headers):
return self._request("GET", SECRETS_PREFIX + suffix, headers=headers)
def _audit(self, action):
query = urllib.parse.urlencode({"action": action, "limit": "1000"})
status, body = self._request(
"GET", AUDIT_PATH + "?" + query, headers=self.admin
)
self.assertEqual(200, status)
return body["entries"]
def _fetch_client_source(self):
with urllib.request.urlopen(
self.base + CLIENT_STATIC_PATH, timeout=10
) as response:
self.assertEqual(200, response.status)
return response.read().decode("utf-8")
def _import_module(self, name, source):
path = os.path.join(self.tmp, name + ".py")
with open(path, "w", encoding="utf-8") as handle:
handle.write(source)
spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def _assert_standalone_imports(self, source):
modules = set()
for node in ast.walk(ast.parse(source)):
if isinstance(node, ast.Import):
modules.update(alias.name for alias in node.names)
elif isinstance(node, ast.ImportFrom):
modules.add(node.module or "")
self.assertTrue(modules)
for module in modules:
self.assertIn(module, STANDALONE_CLIENT_IMPORTS, module)
def _bootstrapped_client(self):
module = self._import_module(
"p4_fetched_coreci_broker_client", self._fetch_client_source()
)
return module.BrokerClient(
self.base, "proxmox", self.app_uuid, self.user_id, ["coreci", "bootstrap"]
)
def _reference_tools(self):
source_path = os.path.join(REFERENCE_DIR, "proxmox_readonly_reference.py")
spec = importlib.util.spec_from_file_location(
"p4_proxmox_readonly_reference", source_path
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
tools = module.Tools()
tools.valves.base_url = self.base
tools.valves.agent_id = "proxmox"
tools.valves.app_uuid = self.app_uuid
tools.valves.user_id = self.user_id
return tools
def _persisted_state(self):
return self.broker.store.get_agent_state("proxmox", self.app_uuid)
def _assert_tool_identity(self, entry):
self.assertEqual(self.user_id, entry["user_id"])
self.assertIsNotNone(entry["user_id"])
self.assertEqual(self.app_uuid, entry["app_registration_uuid"])
self.assertIsNotNone(entry["app_registration_uuid"])
self.assertEqual("proxmox", entry["agent_id"])
self.assertIsNotNone(entry["agent_id"])
def test_ac5_valid_token_reads_secret_tampered_token_is_401(self):
registered = self._register(["coreci"])
status, body = self._secret(
"coreci/proxmox", self._app_header(registered["token"])
)
self.assertEqual(200, status)
self.assertEqual({"path", "data", "version", "updated_at"}, set(body))
self.assertEqual(PROXMOX_PATH, body["path"])
self.assertEqual(PROXMOX_SECRET, body["data"])
self.assertRegex(body["version"], r"^[0-9a-f]{8}$")
flipped = _flip(registered["token"])
self.assertNotEqual(registered["token"], flipped)
status, body = self._secret("coreci/proxmox", self._app_header(flipped))
self.assertEqual(401, status)
self.assertEqual(AUTH_401_BODY, body)
def test_ac6_cross_path_read_is_403_with_denied_audit(self):
registered = self._register(["coreci"])
status, body = self._secret("coreci/gitea", self._app_header(registered["token"]))
self.assertEqual(403, status)
self.assertEqual(GITEA_403_BODY, body)
denied = [
entry for entry in self._audit("read") if entry["result"] == "denied"
]
self.assertEqual(1, len(denied))
self._assert_tool_identity(denied[0])
self.assertEqual("read", denied[0]["action"])
self.assertEqual(GITEA_PATH, denied[0]["path"])
def test_client_bootstrap_public_fetch_import_register_state_secret(self):
source = self._fetch_client_source()
self.assertIn("class BrokerClient", source)
self._assert_standalone_imports(source)
client = self._bootstrapped_client()
state = {}
client.ensure_registered(state)
self.assertEqual(44, len(state["current_token"]))
self.assertEqual("proxmox", state["agent_id"])
self.assertEqual(self.app_uuid, state["app_registration_uuid"])
self.assertEqual(self.user_id, state["user_id"])
self.assertIn("pepper", state)
self.assertTrue(client.put_state(state))
document = client.read_secret(PROXMOX_PATH, state)
self.assertEqual(PROXMOX_PATH, document["path"])
self.assertEqual(PROXMOX_SECRET, document["data"])
persisted = self._persisted_state()
self.assertEqual(state["current_token"], persisted["current_token"])
def test_reference_tool_full_flow_single_register_line(self):
tools = self._reference_tools()
before = len(self._audit("register"))
response = tools.list_nodes()
self.assertIs(True, response["dry_run"])
self.assertIs(True, response["credential_present"])
registers = self._audit("register")
self.assertEqual(1, len(registers) - before)
self.assertEqual("ok", registers[-1]["result"])
self._assert_tool_identity(registers[-1])
persisted = self._persisted_state()
self.assertEqual(tools._state["current_token"], persisted["current_token"])
def test_phase_audit_completeness_after_full_drive(self):
registered = self._register(["coreci"])
token = registered["token"]
status, _ = self._secret("coreci/proxmox", self._app_header(token))
self.assertEqual(200, status)
status, _ = self._secret("coreci/proxmox", self._app_header(_flip(token)))
self.assertEqual(401, status)
status, _ = self._secret("coreci/gitea", self._app_header(token))
self.assertEqual(403, status)
client = self._bootstrapped_client()
state = {}
client.ensure_registered(state)
self.assertTrue(client.put_state(state))
self.assertEqual(PROXMOX_SECRET, client.read_secret(PROXMOX_PATH, state)["data"])
before = len(self._audit("register"))
tools = self._reference_tools()
response = tools.list_nodes()
self.assertIs(True, response["dry_run"])
self.assertIs(True, response["credential_present"])
registers = self._audit("register")
self.assertEqual(1, len(registers) - before)
self.assertEqual(3, len(registers))
for entry in registers:
self.assertEqual("ok", entry["result"])
self.assertEqual(REGISTER_PATH, entry["path"])
self._assert_tool_identity(entry)
reads = self._audit("read")
ok_reads = [entry for entry in reads if entry["result"] == "ok"]
denied_reads = [entry for entry in reads if entry["result"] == "denied"]
self.assertEqual(3, len(ok_reads))
for entry in ok_reads:
self.assertEqual(PROXMOX_PATH, entry["path"])
self._assert_tool_identity(entry)
self.assertEqual(1, len(denied_reads))
self.assertEqual(GITEA_PATH, denied_reads[0]["path"])
self._assert_tool_identity(denied_reads[0])
denied_actions = self._audit("denied")
self.assertEqual(1, len(denied_actions))
self.assertEqual("denied", denied_actions[0]["action"])
self.assertEqual("denied", denied_actions[0]["result"])
self.assertEqual(SECRETS_PREFIX + "coreci/proxmox", denied_actions[0]["path"])
self.assertEqual(self.app_uuid, denied_actions[0]["app_registration_uuid"])
self.assertIsNone(denied_actions[0]["user_id"])
self.assertIsNone(denied_actions[0]["agent_id"])
if __name__ == "__main__":
unittest.main()