7f7868f7cf
---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---
187 lines
7.8 KiB
Python
187 lines
7.8 KiB
Python
import base64
|
|
import contextlib
|
|
import io
|
|
import json
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
import urllib.error
|
|
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"
|
|
BASE_PATH = "/api/broker/app_registrations"
|
|
|
|
|
|
def _basic_header(username, password):
|
|
raw = "%s:%s" % (username, password)
|
|
return "Basic " + base64.b64encode(raw.encode("utf-8")).decode("ascii")
|
|
|
|
|
|
class AppRegistrationsTests(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.server_close)
|
|
self.addCleanup(self.server.shutdown)
|
|
self.base = "http://127.0.0.1:%d" % self.server.server_address[1]
|
|
self.admin = {"Authorization": _basic_header(ADMIN_USER, ADMIN_PASS)}
|
|
|
|
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=5) 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 _add(self, app_uuid, name=None, headers=None):
|
|
payload = {"uuid": app_uuid}
|
|
if name is not None:
|
|
payload["name"] = name
|
|
return self._request("POST", BASE_PATH, payload, headers or self.admin)
|
|
|
|
def _row(self, app_uuid):
|
|
status, body = self._request("GET", BASE_PATH, headers=self.admin)
|
|
self.assertEqual(200, status)
|
|
for row in body["registrations"]:
|
|
if row["uuid"] == app_uuid:
|
|
return row
|
|
return None
|
|
|
|
def _audit(self):
|
|
with open(self.env["BROKER_AUDIT_LOG_PATH"], "r", encoding="utf-8") as handle:
|
|
return [json.loads(raw) for raw in handle if raw.strip()]
|
|
|
|
def test_list_empty_then_add_then_list(self):
|
|
status, body = self._request("GET", BASE_PATH, headers=self.admin)
|
|
self.assertEqual(200, status)
|
|
self.assertEqual({"registrations": []}, body)
|
|
app_uuid = str(uuidlib.uuid4())
|
|
status, body = self._add(app_uuid, "proxmox-tool")
|
|
self.assertEqual(201, status)
|
|
self.assertEqual({"uuid": app_uuid, "status": "active"}, body)
|
|
row = self._row(app_uuid)
|
|
self.assertEqual("proxmox-tool", row["name"])
|
|
self.assertEqual("active", row["status"])
|
|
self.assertTrue(row["created_at"])
|
|
self.assertIsNone(row["revoked_at"])
|
|
|
|
def test_add_without_name_stores_null_name(self):
|
|
app_uuid = str(uuidlib.uuid4())
|
|
status, _ = self._add(app_uuid)
|
|
self.assertEqual(201, status)
|
|
self.assertIsNone(self._row(app_uuid)["name"])
|
|
|
|
def test_add_duplicate_active_or_revoked_is_409(self):
|
|
app_uuid = str(uuidlib.uuid4())
|
|
self._add(app_uuid)
|
|
status, body = self._add(app_uuid)
|
|
self.assertEqual(409, status)
|
|
self.assertEqual(409, body["error"])
|
|
self.assertTrue(body["message"])
|
|
status, _ = self._request("DELETE", BASE_PATH + "/" + app_uuid, headers=self.admin)
|
|
self.assertEqual(200, status)
|
|
status, body = self._add(app_uuid)
|
|
self.assertEqual(409, status)
|
|
|
|
def test_add_invalid_or_noncanonical_uuid_is_400(self):
|
|
canonical = str(uuidlib.uuid4())
|
|
for bad in ("not-a-uuid", "", "{" + canonical.upper() + "}", canonical.upper(),
|
|
canonical.replace("-", ""), "urn:uuid:" + canonical):
|
|
status, body = self._add(bad)
|
|
self.assertEqual(400, status, bad)
|
|
self.assertEqual(400, body["error"])
|
|
status, _ = self._request("POST", BASE_PATH, {}, self.admin)
|
|
self.assertEqual(400, status)
|
|
status, _ = self._request("POST", BASE_PATH, {"uuid": 12345}, self.admin)
|
|
self.assertEqual(400, status)
|
|
status, _ = self._request("POST", BASE_PATH, {"name": "no-uuid"}, self.admin)
|
|
self.assertEqual(400, status)
|
|
|
|
def test_revoke_revoke_again_reinstate_lifecycle(self):
|
|
app_uuid = str(uuidlib.uuid4())
|
|
self._add(app_uuid)
|
|
status, body = self._request("DELETE", BASE_PATH + "/" + app_uuid, headers=self.admin)
|
|
self.assertEqual(200, status)
|
|
self.assertEqual("revoked", body["status"])
|
|
revoked_at = self._row(app_uuid)["revoked_at"]
|
|
self.assertIsNotNone(revoked_at)
|
|
self.assertEqual("revoked", self._row(app_uuid)["status"])
|
|
status, body = self._request("DELETE", BASE_PATH + "/" + app_uuid, headers=self.admin)
|
|
self.assertEqual(200, status)
|
|
self.assertEqual(revoked_at, self._row(app_uuid)["revoked_at"])
|
|
status, body = self._request(
|
|
"POST", BASE_PATH + "/" + app_uuid + "/reinstate", headers=self.admin
|
|
)
|
|
self.assertEqual(200, status)
|
|
self.assertEqual("active", body["status"])
|
|
row = self._row(app_uuid)
|
|
self.assertEqual("active", row["status"])
|
|
self.assertIsNone(row["revoked_at"])
|
|
|
|
def test_revoke_missing_is_404(self):
|
|
status, body = self._request(
|
|
"DELETE", BASE_PATH + "/" + str(uuidlib.uuid4()), headers=self.admin
|
|
)
|
|
self.assertEqual(404, status)
|
|
self.assertEqual(404, body["error"])
|
|
|
|
def test_reinstate_missing_is_404(self):
|
|
status, body = self._request(
|
|
"POST", BASE_PATH + "/" + str(uuidlib.uuid4()) + "/reinstate", headers=self.admin
|
|
)
|
|
self.assertEqual(404, status)
|
|
self.assertEqual(404, body["error"])
|
|
|
|
def test_admin_actions_audited_with_null_identity(self):
|
|
app_uuid = str(uuidlib.uuid4())
|
|
self._add(app_uuid)
|
|
self._request("DELETE", BASE_PATH + "/" + app_uuid, headers=self.admin)
|
|
self._request("POST", BASE_PATH + "/" + app_uuid + "/reinstate", headers=self.admin)
|
|
entries = self._audit()
|
|
self.assertEqual(["add_app", "remove_app", "add_app"], [e["action"] for e in entries])
|
|
for entry in entries:
|
|
self.assertEqual("ok", entry["result"])
|
|
self.assertIsNone(entry["user_id"])
|
|
self.assertIsNone(entry["app_registration_uuid"])
|
|
self.assertIsNone(entry["agent_id"])
|
|
self.assertEqual("/api/broker/app_registrations/" + app_uuid, entry["path"])
|
|
|
|
def test_unauthenticated_requests_are_401(self):
|
|
checks = (
|
|
("GET", BASE_PATH, None),
|
|
("POST", BASE_PATH, {"uuid": str(uuidlib.uuid4())}),
|
|
("DELETE", BASE_PATH + "/" + str(uuidlib.uuid4()), None),
|
|
("POST", BASE_PATH + "/" + str(uuidlib.uuid4()) + "/reinstate", None),
|
|
)
|
|
for method, path, payload in checks:
|
|
status, body = self._request(method, path, payload)
|
|
self.assertEqual(401, status, method + " " + path)
|
|
self.assertEqual(401, body["error"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main() |