target-revenue/tests/test_control_plane.py
tegwick 885da0a1cb Implement Control Plane backend: rights enforcement + audit log (WP-0009-T03)
migrations/0006_control_plane.sql: control_plane_audit_log (append-only,
no UPDATE/DELETE for trf_app) and control_plane_proposed_entries (the
Contributor tier's "propose, don't append" workflow from concept §2) -
review decisions go through a review_proposed_entry() SECURITY DEFINER
function, same governance-action pattern as
set_extension_status/revoke_credential, not a direct UPDATE.

src/target_revenue/control_plane.py is the enforcement layer concept
§2 called for: register_phase/append_development_credit require
Operator+; propose_ledger_entry requires Contributor+ and stores a
pending proposal without touching the real Ledger; approve_proposed_entry
(Operator+) appends it under the *reviewer's own* credential/attribution
(not the original proposer's - the reviewer is who's authorizing it into
the real Ledger, while the proposer stays on record in the proposal row
and audit log); reject_proposed_entry (Operator+) discards it. issue_/
revoke_user_credential (Admin+) wrap registry.py's T02 functions with
the same rights check and audit logging. Every action funnels through
record_audit_event, independent of the Trust Service's own signed
records.

tests/test_control_plane.py (12 tests): rights enforcement at each
tier boundary, the full propose -> approve -> appended-under-reviewer
flow, propose -> reject -> nothing appended, double-review rejection,
audit log content/attribution, DB-level UPDATE rejection on both new
tables. Full suite: 84 offline (unchanged), 53 with Docker (up from
41); no stray containers left running.
2026-07-30 15:08:59 +02:00

343 lines
12 KiB
Python

"""Integration tests for WP-0009-T03 (Control Plane backend: rights
enforcement + audit log + proposed-entry review).
Same ephemeral, disposable Postgres-via-Docker pattern as the other
hosted test modules (never the shared state-hub instance).
"""
from __future__ import annotations
import shutil
import subprocess
import time
import uuid
from pathlib import Path
import pytest
psycopg = pytest.importorskip("psycopg")
from conftest import golden_manifest # noqa: E402
REPO_ROOT = Path(__file__).resolve().parents[1]
MIGRATIONS = [
REPO_ROOT / "migrations" / "0001_registries.sql",
REPO_ROOT / "migrations" / "0002_ledger.sql",
REPO_ROOT / "migrations" / "0003_attestations.sql",
REPO_ROOT / "migrations" / "0004_breach_records.sql",
REPO_ROOT / "migrations" / "0005_licensor_credentials.sql",
REPO_ROOT / "migrations" / "0006_control_plane.sql",
]
pytestmark = pytest.mark.skipif(
shutil.which("docker") is None, reason="docker not available"
)
@pytest.fixture(scope="module")
def pg_container():
name = f"trf-test-pg-cp-{uuid.uuid4().hex[:8]}"
subprocess.run(
[
"docker", "run", "--rm", "-d",
"--name", name,
"-e", "POSTGRES_PASSWORD=postgres",
"-e", "POSTGRES_DB=target_revenue_test",
"-p", "127.0.0.1::5432",
"postgres:16-alpine",
],
check=True, capture_output=True,
)
try:
port_out = subprocess.run(
["docker", "port", name, "5432/tcp"], check=True, capture_output=True, text=True
).stdout.strip()
host_port = port_out.split(":")[-1]
dsn = f"host=127.0.0.1 port={host_port} dbname=target_revenue_test user=postgres password=postgres"
for _ in range(60):
try:
with psycopg.connect(dsn, connect_timeout=1):
break
except psycopg.OperationalError:
time.sleep(0.5)
else:
raise RuntimeError("postgres container did not become ready in time")
with psycopg.connect(dsn) as admin_conn:
for migration in MIGRATIONS:
admin_conn.execute(migration.read_text(encoding="utf-8"))
admin_conn.commit()
admin_conn.execute(
"INSERT INTO licensors (token, licensor_id, credential_label, rights, issued_by) "
"VALUES (%s, %s, %s, %s, %s)",
("founding-admin-token", "binky", "founder", "admin", "bootstrap"),
)
admin_conn.commit()
app_dsn = (
f"host=127.0.0.1 port={host_port} dbname=target_revenue_test "
f"user=trf_app password=changeme-in-deployment"
)
yield {"admin_dsn": dsn, "app_dsn": app_dsn, "admin_token": "founding-admin-token"}
finally:
subprocess.run(["docker", "stop", name], capture_output=True)
@pytest.fixture()
def conn(pg_container):
with psycopg.connect(pg_container["app_dsn"]) as connection:
yield connection
@pytest.fixture()
def signing_key():
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
return Ed25519PrivateKey.generate()
def _entry(phase_id: str, entry_id: str) -> dict:
return {
"id": entry_id,
"phase": phase_id,
"type": "development-credit",
"amount": 100,
"currency": "USD",
"recognized_at": "2026-08-01T00:00:00Z",
"evidence_reference": f"confidential:evidence:{entry_id}",
"extension": {"id": "trsl:extension:development-license", "version": "1.0"},
}
@pytest.fixture()
def credentials(conn, pg_container):
"""One credential per rights tier, all under the `binky` tenant.
Labels are uniquified per test invocation — `pg_container` is
module-scoped (one Postgres for the whole file), so reusing fixed
labels across tests would collide with the active-label uniqueness
constraint from a previous test's still-active credential."""
from target_revenue import registry
admin = registry.authenticate(conn, pg_container["admin_token"])
suffix = uuid.uuid4().hex[:8]
tiers = {}
for label, rights in [
("viewer-user", "viewer"),
("contributor-user", "contributor"),
("operator-user", "operator"),
]:
cred = registry.issue_sub_credential(
conn, licensor_id="binky", credential_label=f"{label}-{suffix}", rights=rights,
issued_by="founder",
)
tiers[rights] = cred
conn.commit()
tiers["admin"] = admin
return tiers
@pytest.fixture()
def registered_phase(conn, credentials):
from target_revenue import control_plane
manifest = golden_manifest()
manifest["phase"]["id"] = manifest["phase"]["id"] + "-cp-test-" + uuid.uuid4().hex[:6]
control_plane.register_phase(conn, credentials["operator"], manifest)
conn.commit()
return manifest
# --- rights enforcement -----------------------------------------------------
def test_viewer_cannot_register_phase(conn, credentials):
from target_revenue import control_plane
manifest = golden_manifest()
manifest["phase"]["id"] = manifest["phase"]["id"] + "-viewer-" + uuid.uuid4().hex[:6]
with pytest.raises(control_plane.ControlPlaneError, match="insufficient rights"):
control_plane.register_phase(conn, credentials["viewer"], manifest)
def test_contributor_cannot_append_directly(conn, credentials, registered_phase, signing_key):
from target_revenue import control_plane
phase_id = registered_phase["phase"]["id"]
with pytest.raises(control_plane.ControlPlaneError, match="insufficient rights"):
control_plane.append_development_credit(
conn, credentials["contributor"], phase_id, _entry(phase_id, "trsl:entry:cpdirect0001"), signing_key
)
def test_operator_can_register_and_append(conn, credentials, signing_key):
from target_revenue import control_plane
manifest = golden_manifest()
manifest["phase"]["id"] = manifest["phase"]["id"] + "-opappend-" + uuid.uuid4().hex[:6]
control_plane.register_phase(conn, credentials["operator"], manifest)
conn.commit()
stored = control_plane.append_development_credit(
conn, credentials["operator"], manifest["phase"]["id"],
_entry(manifest["phase"]["id"], "trsl:entry:cpop0001"), signing_key,
)
conn.commit()
assert stored["id"] == "trsl:entry:cpop0001"
# --- proposed-entry review workflow -----------------------------------------
def test_contributor_can_propose_but_not_review(conn, credentials, registered_phase):
from target_revenue import control_plane
phase_id = registered_phase["phase"]["id"]
proposal_id = control_plane.propose_ledger_entry(
conn, credentials["contributor"], phase_id, _entry(phase_id, "trsl:entry:cppropose0001")
)
conn.commit()
assert isinstance(proposal_id, int)
pending = control_plane.list_proposed_entries(conn, phase_id=phase_id, status="pending")
assert len(pending) == 1
assert pending[0]["id"] == proposal_id
with pytest.raises(control_plane.ControlPlaneError, match="insufficient rights"):
control_plane.approve_proposed_entry(
conn, credentials["contributor"], proposal_id, signing_key=None
)
def test_operator_approves_proposal_appends_under_own_credential(
conn, credentials, registered_phase, signing_key
):
from target_revenue import control_plane
phase_id = registered_phase["phase"]["id"]
proposal_id = control_plane.propose_ledger_entry(
conn, credentials["contributor"], phase_id, _entry(phase_id, "trsl:entry:cpapprove0001")
)
conn.commit()
stored = control_plane.approve_proposed_entry(
conn, credentials["operator"], proposal_id, signing_key, review_note="looks right"
)
conn.commit()
assert stored["id"] == "trsl:entry:cpapprove0001"
remaining_pending = control_plane.list_proposed_entries(conn, phase_id=phase_id, status="pending")
assert remaining_pending == []
approved = control_plane.list_proposed_entries(conn, phase_id=phase_id, status="approved")
assert len(approved) == 1
# Attribution: proposer and approver are both independently visible.
from target_revenue import ledger
attribution = ledger.get_ledger_attribution(conn, phase_id)
assert attribution[0]["credential_label"] == credentials["operator"].credential_label
def test_operator_rejects_proposal_nothing_appended(conn, credentials, registered_phase):
from target_revenue import control_plane, ledger
phase_id = registered_phase["phase"]["id"]
proposal_id = control_plane.propose_ledger_entry(
conn, credentials["contributor"], phase_id, _entry(phase_id, "trsl:entry:cpreject0001")
)
conn.commit()
control_plane.reject_proposed_entry(conn, credentials["operator"], proposal_id, review_note="not this one")
conn.commit()
rejected = control_plane.list_proposed_entries(conn, phase_id=phase_id, status="rejected")
assert len(rejected) == 1
assert ledger.get_ledger(conn, phase_id) == []
def test_double_review_of_same_proposal_rejected(conn, credentials, registered_phase, signing_key):
from target_revenue import control_plane
phase_id = registered_phase["phase"]["id"]
proposal_id = control_plane.propose_ledger_entry(
conn, credentials["contributor"], phase_id, _entry(phase_id, "trsl:entry:cpdouble0001")
)
conn.commit()
control_plane.approve_proposed_entry(conn, credentials["operator"], proposal_id, signing_key)
conn.commit()
with pytest.raises(control_plane.ControlPlaneError, match="no pending proposal"):
control_plane.reject_proposed_entry(conn, credentials["operator"], proposal_id)
# --- audit log ---------------------------------------------------------------
def test_audit_log_records_actor_and_action(conn, credentials, registered_phase, signing_key):
from target_revenue import control_plane
phase_id = registered_phase["phase"]["id"]
control_plane.append_development_credit(
conn, credentials["operator"], phase_id, _entry(phase_id, "trsl:entry:cpaudit0001"), signing_key
)
conn.commit()
log = control_plane.get_audit_log(conn, phase_id=phase_id)
actions = [row["action"] for row in log]
assert "register_phase" in actions
assert "append_ledger_entry" in actions
append_row = next(row for row in log if row["action"] == "append_ledger_entry")
assert append_row["actor_credential_label"] == credentials["operator"].credential_label
assert append_row["trust_service_record_id"] == "trsl:entry:cpaudit0001"
# --- admin credential management ---------------------------------------------
def test_admin_can_issue_and_revoke_credential(conn, credentials):
from target_revenue import control_plane, registry
new_cred = control_plane.issue_user_credential(
conn, credentials["admin"], licensor_id="binky", credential_label="dana", rights="viewer"
)
conn.commit()
assert registry.authenticate(conn, new_cred.token).credential_label == "dana"
control_plane.revoke_user_credential(conn, credentials["admin"], new_cred.token)
conn.commit()
with pytest.raises(registry.RegistrationError):
registry.authenticate(conn, new_cred.token)
def test_non_admin_cannot_issue_credential(conn, credentials):
from target_revenue import control_plane
with pytest.raises(control_plane.ControlPlaneError, match="insufficient rights"):
control_plane.issue_user_credential(
conn, credentials["operator"], licensor_id="binky", credential_label="eve", rights="viewer"
)
# --- DB-level append-only enforcement ----------------------------------------
def test_application_role_cannot_update_audit_log(pg_container):
with psycopg.connect(pg_container["app_dsn"]) as app_conn:
with pytest.raises(psycopg.errors.InsufficientPrivilege):
app_conn.execute(
"UPDATE control_plane_audit_log SET action = 'tampered' WHERE id = 1"
)
app_conn.rollback()
with pytest.raises(psycopg.errors.InsufficientPrivilege):
app_conn.execute("DELETE FROM control_plane_audit_log WHERE id = 1")
app_conn.rollback()
def test_application_role_cannot_update_proposed_entries_directly(pg_container):
with psycopg.connect(pg_container["app_dsn"]) as app_conn:
with pytest.raises(psycopg.errors.InsufficientPrivilege):
app_conn.execute(
"UPDATE control_plane_proposed_entries SET status = 'approved' WHERE id = 1"
)
app_conn.rollback()