net-kingdom/identity-provisioner/tests/test_group_provenance.py
tegwick 4f65e41ac5
Some checks failed
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
Identity provider journey acceptance / provider (push) Failing after 20s
Build and Publish identity-provisioner / build-and-push (push) Successful in 8s
fix(identity): retain group provenance and prepare sealed recovery custody
Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a1468b-63fa-7710-85dc-8f2e16b3ebfa
2026-10-17 00:28:17 +02:00

326 lines
16 KiB
Python

"""Synthetic actual provisioner/HTTP/storage proof; no live LDAP or authority."""
import concurrent.futures
from dataclasses import asdict
from http.server import ThreadingHTTPServer
import json
import os
import sqlite3
from pathlib import Path
import tempfile
import threading
import unittest
from unittest.mock import patch
from urllib.error import HTTPError
from urllib.request import Request, urlopen
from group_provenance import GroupProvenance
from provisioner import LLDAPProvisioner, dispatch
from server import Handler, configured_group_provenance
from test_service_groups import Directory, req
def rollback(**kw):
return req(ensure_idempotency_key=req()["idempotency_key"], ensure_correlation_id=req()["correlation_id"], **kw)
class ProvenanceTests(unittest.TestCase):
def test_inventory_excludes_preexisting_membership_and_empty_group(self):
d = Directory()
d.members[3].add("tegwick")
result = d.ensure_service_groups(req())
self.assertEqual([{"name": "forge-coulomb-owners", "id": 4}, {"name": "forge-coulomb-readers", "id": 5}], list(result.created_groups))
self.assertEqual([{"name": "forge-coulomb-owners", "id": 4, "username": "tegwick"}], list(result.created_memberships))
d.revoke_service_groups(rollback(delete_if_empty=True))
self.assertEqual({"tegwick"}, d.members[3])
self.assertIn("forge-admins", d.groups)
self.assertNotIn("forge-coulomb-owners", d.groups)
def test_preexisting_empty_group_never_deleted(self):
d = Directory()
d.ensure_service_groups(req())
d.revoke_service_groups(rollback(delete_if_empty=True))
self.assertIn("forge-admins", d.groups)
self.assertEqual(set(), d.members[3])
def test_unknown_receipt_and_digest_mismatch_before_login(self):
d = Directory()
with self.assertRaises(ValueError):
d.revoke_service_groups(rollback())
d.ensure_service_groups(req())
d.calls.clear()
with patch.object(d, "_login", side_effect=AssertionError("must not login")):
with self.assertRaises(ValueError):
d.revoke_service_groups(rollback(members={"forge-admins": ["agent-codex"]}))
with self.assertRaises(ValueError):
d.ensure_service_groups(req(groups=["another-group"], members={}))
self.assertEqual([], d.calls)
def test_unsigned_inventory_and_legacy_revoke_cannot_mutate(self):
d = Directory()
d.ensure_service_groups(req())
d.calls.clear()
for payload in [req(), rollback(created_groups=[{"name": "forge-admins", "id": 3}]), rollback(created_memberships=[]), rollback(provenance={})]:
with self.assertRaises(ValueError):
d.revoke_service_groups(payload)
self.assertEqual([], d.calls)
def test_shared_membership_drift_refuses_all_destructive_calls(self):
d = Directory()
d.ensure_service_groups(req())
d.members[3].add("agent-codex")
d.calls.clear()
with self.assertRaisesRegex(RuntimeError, "postimage"):
d.revoke_service_groups(rollback(delete_if_empty=True))
self.assertEqual([], d.calls)
self.assertEqual({"tegwick", "agent-codex"}, d.members[3])
def test_replaced_group_identity_refuses_all_destructive_calls(self):
d = Directory()
d.ensure_service_groups(req())
d.groups["forge-admins"] = 10
d.members[10] = {"tegwick"}
d.calls.clear()
with self.assertRaisesRegex(RuntimeError, "postimage"):
d.revoke_service_groups(rollback())
self.assertEqual([], d.calls)
def test_successful_ensure_replay_never_recreates_later_removed_state(self):
d = Directory()
first = d.ensure_service_groups(req())
d.members[3].clear()
d.calls.clear()
replay = d.ensure_service_groups(req())
self.assertTrue(replay.resumed)
self.assertEqual(first.created_memberships, replay.created_memberships)
self.assertEqual([], d.calls)
self.assertEqual(set(), d.members[3])
def test_interrupted_add_persists_intent_and_blocks_restart_retry(self):
d = Directory()
original = d._gql
def lost_response(token, query, variables):
result = original(token, query, variables)
if "addUserToGroup" in query:
raise RuntimeError("synthetic lost response")
return result
d._gql = lost_response
with self.assertRaises(RuntimeError):
d.ensure_service_groups(req())
d.group_provenance = GroupProvenance(d.group_provenance.path)
receipt = d.group_provenance.get(req()["idempotency_key"], d._group_binding(req()["groups"], req()["members"], req()["correlation_id"]))
self.assertEqual("uncertain", receipt["state"])
self.assertEqual("add_member", receipt["intent"]["operation"])
self.assertEqual([], receipt["created_memberships"])
d.calls.clear()
for operation, payload in [(d.ensure_service_groups, req()), (d.revoke_service_groups, rollback()), (d.ensure_service_groups, req(idempotency_key="different-key-is-not-recovery"))]:
with self.assertRaises(RuntimeError):
operation(payload)
self.assertEqual([], d.calls)
def test_interrupted_rollback_retains_confirmed_inventory_and_pending_intent(self):
d = Directory()
d.ensure_service_groups(req())
original = d._remove_group
def lost_response(*args):
original(*args)
raise RuntimeError("lost remove response")
d._remove_group = lost_response
with self.assertRaises(RuntimeError):
d.revoke_service_groups(rollback())
d.group_provenance = GroupProvenance(d.group_provenance.path)
receipt = d.group_provenance.get(req()["idempotency_key"], d._group_binding(req()["groups"], req()["members"], req()["correlation_id"]))
self.assertEqual("uncertain", receipt["state"])
self.assertEqual("remove_member", receipt["intent"]["operation"])
self.assertEqual(2, len(receipt["created_memberships"]))
d.calls.clear()
with self.assertRaises(RuntimeError):
d.revoke_service_groups(rollback())
self.assertEqual([], d.calls)
def test_unresolved_pre_call_intent_survives_hard_process_loss(self):
d = Directory()
d._gql = lambda *_: (_ for _ in ()).throw(SystemExit("process lost before response"))
with self.assertRaises(SystemExit):
d.ensure_service_groups(req())
d.group_provenance = GroupProvenance(d.group_provenance.path)
receipt = d.group_provenance.get(req()["idempotency_key"], d._group_binding(req()["groups"], req()["members"], req()["correlation_id"]))
self.assertEqual("pending", receipt["state"])
self.assertEqual("add_member", receipt["intent"]["operation"])
with self.assertRaises(RuntimeError):
d.ensure_service_groups(req())
def test_missing_config_private_permissions_and_lost_database_fail_closed(self):
with patch.dict(os.environ, {}, clear=True):
self.assertIsNone(configured_group_provenance())
d = Directory()
path = d.group_provenance.path
with patch.dict(os.environ, {"GROUP_PROVENANCE_DB": path}):
self.assertIsInstance(configured_group_provenance(), GroupProvenance)
os.chmod(path, 0o644)
with self.assertRaises(RuntimeError):
configured_group_provenance()
os.chmod(path, 0o600)
os.unlink(path)
with self.assertRaises(RuntimeError):
configured_group_provenance()
self.assertFalse(Path(path).exists())
provider = LLDAPProvisioner(base_url="https://invalid", admin_password="synthetic")
with patch.object(provider, "_login", side_effect=AssertionError("no directory")):
with self.assertRaises(RuntimeError):
provider.ensure_service_groups(req())
with self.assertRaises(RuntimeError):
provider.revoke_service_groups(rollback())
def test_missing_intent_row_stops_before_any_directory_effect(self):
d = Directory()
store = d.group_provenance
original_save = store.save
def lose_row(receipt):
if receipt["intent"] is not None:
with sqlite3.connect(store.path) as db:
db.execute("DELETE FROM group_receipts")
original_save(receipt)
store.save = lose_row
with self.assertRaisesRegex(RuntimeError, "group_provenance_unavailable"):
d.ensure_service_groups(req())
self.assertEqual([], d.calls)
def test_live_instance_rejects_replaced_database_before_effect(self):
d = Directory()
original = d.group_provenance
replacement = str(Path(d.storage.name) / "replacement.db")
GroupProvenance(replacement, initialize=True)
os.replace(replacement, original.path)
with self.assertRaisesRegex(RuntimeError, "group_provenance_unavailable"):
d.ensure_service_groups(req())
self.assertEqual([], d.calls)
def test_failed_before_mutation_is_retained_without_stranding_other_keys(self):
for failure in ("unknown-user", "oversized-prestate", "directory-read-failed"):
d = Directory()
old_members = d._group_members
request = req(members={"forge-admins": ["nobody"]}) if failure == "unknown-user" else req()
if failure == "oversized-prestate":
d._group_members = lambda *_: [f"synthetic{i}" for i in range(4097)]
if failure == "directory-read-failed":
d._group_members = lambda *_: (_ for _ in ()).throw(RuntimeError("read failed"))
with self.assertRaises((ValueError, RuntimeError)):
d.ensure_service_groups(request)
binding = d._group_binding(request["groups"], request["members"], request["correlation_id"])
receipt = d.group_provenance.get(request["idempotency_key"], binding)
self.assertEqual("refused", receipt["state"])
self.assertEqual("before_mutation", receipt["failure_phase"])
self.assertEqual([], d.calls)
d._group_members = old_members
with self.assertRaises(RuntimeError):
d.ensure_service_groups(request)
self.assertEqual("groups_ensured", d.ensure_service_groups(req(idempotency_key="another-valid-operation-key")).status)
def test_current_correlations_are_bound_to_original_ensure(self):
d = Directory()
d.ensure_service_groups(req())
d.calls.clear()
with self.assertRaises(ValueError):
d.ensure_service_groups(req(correlation_id="other-case"))
payload = rollback()
payload["ensure_correlation_id"] = "other-case"
with self.assertRaises(ValueError):
d.revoke_service_groups(payload)
self.assertEqual([], d.calls)
def test_readiness_separates_directory_health_from_group_capability(self):
provider = LLDAPProvisioner(base_url="https://invalid", admin_password="synthetic")
provider._login = lambda **_: "synthetic"
provider._gql = lambda *_args, **_kw: {"groups": []}
self.assertEqual("disabled", provider.preflight()["service_groups"])
d = Directory()
provider.group_provenance = d.group_provenance
self.assertEqual("available", provider.preflight()["service_groups"])
os.chmod(d.group_provenance.path, 0o644)
with self.assertRaisesRegex(RuntimeError, "group_provenance_unavailable"):
provider.preflight()
def test_concurrent_sqlite_reservation_allows_one_original_attempt(self):
d = Directory()
path = d.group_provenance.path
def reserve(_):
return GroupProvenance(path).reserve("synthetic-concurrent-key", "same-binding")[1]
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as pool:
fresh = list(pool.map(reserve, range(24)))
self.assertEqual(1, sum(fresh))
def test_bounded_records_never_age_or_evict_into_authority(self):
d = Directory()
store = GroupProvenance(d.group_provenance.path, capacity=1)
receipt, _ = store.reserve("one", "binding")
receipt.update(state="ensured", requested_groups=[], ensure_correlation_id="test")
store.save(receipt)
with self.assertRaisesRegex(RuntimeError, "capacity"):
store.reserve("two", "binding")
self.assertFalse(store.reserve("one", "binding")[1])
def test_rollback_replay_and_key_reuse_do_not_recreate(self):
d = Directory()
d.ensure_service_groups(req())
first = d.revoke_service_groups(rollback(delete_if_empty=True))
d.calls.clear()
result = d.revoke_service_groups(rollback(delete_if_empty=True))
self.assertEqual(first.status, result.status)
self.assertTrue(result.resumed)
with self.assertRaises(RuntimeError):
d.ensure_service_groups(req())
with self.assertRaises(ValueError):
d.revoke_service_groups(rollback(delete_if_empty=False))
self.assertEqual([], d.calls)
def test_real_http_handler_reinitialized_from_config_retains_receipt(self):
d = Directory()
path = d.group_provenance.path
def boot():
with patch.dict(os.environ, {"GROUP_PROVENANCE_DB": path}):
provider = LLDAPProvisioner(base_url="https://synthetic.invalid", admin_password="unused", group_provenance=configured_group_provenance())
# Only directory transport is synthetic; actual initialized adapter,
# dispatch, HTTP authentication, configuration and SQLite are used.
for name in ("_login", "_directory", "_group_members", "_ensure_group", "_gql", "_remove_group"):
setattr(provider, name, getattr(d, name))
class LocalHandler(Handler):
provisioner = provider
service_token = "synthetic-service-token"
server = ThreadingHTTPServer(("127.0.0.1", 0), LocalHandler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
return server, thread
def stop(server, thread):
server.shutdown(); server.server_close(); thread.join()
def post(server, endpoint, payload):
request = Request(f"http://127.0.0.1:{server.server_port}/v1/groups/{endpoint}", data=json.dumps(payload).encode(),
headers={"Authorization": "Bearer synthetic-service-token"})
with urlopen(request) as response:
return json.load(response)
server, thread = boot()
try:
created = post(server, "ensure", req())
finally:
stop(server, thread)
server, thread = boot()
try:
d.calls.clear()
replay = post(server, "ensure", req())
self.assertTrue(replay["resumed"])
self.assertEqual(created["created_memberships"], replay["created_memberships"])
self.assertEqual([], d.calls)
revoked = post(server, "revoke", rollback(delete_if_empty=True))
self.assertEqual("groups_revoked_and_deleted", revoked["status"])
self.assertIn("forge-admins", d.groups)
with self.assertRaises(HTTPError) as error:
post(server, "revoke", req())
self.assertEqual(400, error.exception.code)
d.calls.clear()
os.chmod(path, 0o644)
with self.assertRaises(HTTPError) as error:
post(server, "ensure", req(idempotency_key="different-invocation-key"))
self.assertEqual(503, error.exception.code)
self.assertEqual({"error": "dependency_unavailable", "dependency": "group_provenance"}, json.load(error.exception))
self.assertEqual([], d.calls)
finally:
stop(server, thread)