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.
279 lines
9.8 KiB
Python
279 lines
9.8 KiB
Python
"""Target Revenue Control Plane backend (WP-0009-T03).
|
|
|
|
A client layer over the already-finished hosted Trust Service
|
|
(`registry.py`, `ledger.py`) — not a replacement for it, per
|
|
`specs/TargetRevenueControlPlaneConcept.md`. Every write-capable Control
|
|
Plane action funnels through this module so it can (a) check the acting
|
|
credential's rights tier before doing anything, and (b) record who did
|
|
what in `control_plane_audit_log`, independent of the Trust Service's own
|
|
signed records, which only ever attest "the tenant did this," never the
|
|
individual human (concept §2; `migrations/0005_licensor_credentials.sql`).
|
|
|
|
Rights enforcement lives here, not in `registry.py`/`ledger.py` — those
|
|
modules only ever needed to answer "which tenant is this," and per
|
|
WP-0009-T01's accepted decision, per-human rights tiers
|
|
(Viewer/Contributor/Operator/Admin) are a Control Plane concern layered on
|
|
top, not a Trust Service one.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from . import ledger, registry
|
|
from .registry import Licensor
|
|
|
|
if TYPE_CHECKING:
|
|
from psycopg import Connection
|
|
|
|
|
|
class ControlPlaneError(ValueError):
|
|
"""Raised when a Control Plane action is rejected (insufficient
|
|
rights, an unknown proposal id, etc.)."""
|
|
|
|
|
|
def _require_right(licensor: Licensor, minimum: str) -> None:
|
|
if not registry.has_right(licensor.rights, minimum):
|
|
raise ControlPlaneError(
|
|
f"insufficient rights: {minimum!r} required, this credential has "
|
|
f"{licensor.rights!r}"
|
|
)
|
|
|
|
|
|
def record_audit_event(
|
|
conn: "Connection",
|
|
actor: Licensor,
|
|
action: str,
|
|
phase_id: str | None = None,
|
|
trust_service_record_id: str | None = None,
|
|
detail: dict[str, Any] | None = None,
|
|
) -> None:
|
|
"""Append one row to the Control Plane's own audit log. Called by
|
|
every function below after (never instead of, and never before
|
|
confirming) the underlying Trust Service action actually succeeded."""
|
|
from psycopg.types.json import Jsonb
|
|
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO control_plane_audit_log
|
|
(actor_token, actor_licensor_id, actor_credential_label, action,
|
|
phase_id, trust_service_record_id, detail)
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
|
""",
|
|
(
|
|
actor.token,
|
|
actor.licensor_id,
|
|
actor.credential_label,
|
|
action,
|
|
phase_id,
|
|
trust_service_record_id,
|
|
Jsonb(detail) if detail is not None else None,
|
|
),
|
|
)
|
|
|
|
|
|
def get_audit_log(conn: "Connection", phase_id: str | None = None) -> list[dict[str, Any]]:
|
|
"""Read the audit log, optionally scoped to one Phase, in append order."""
|
|
if phase_id:
|
|
rows = conn.execute(
|
|
"""
|
|
SELECT id, actor_licensor_id, actor_credential_label, action, phase_id,
|
|
trust_service_record_id, detail, created_at
|
|
FROM control_plane_audit_log
|
|
WHERE phase_id = %s
|
|
ORDER BY id ASC
|
|
""",
|
|
(phase_id,),
|
|
).fetchall()
|
|
else:
|
|
rows = conn.execute(
|
|
"""
|
|
SELECT id, actor_licensor_id, actor_credential_label, action, phase_id,
|
|
trust_service_record_id, detail, created_at
|
|
FROM control_plane_audit_log
|
|
ORDER BY id ASC
|
|
"""
|
|
).fetchall()
|
|
columns = (
|
|
"id", "actor_licensor_id", "actor_credential_label", "action", "phase_id",
|
|
"trust_service_record_id", "detail", "created_at",
|
|
)
|
|
return [dict(zip(columns, row)) for row in rows]
|
|
|
|
|
|
# --- Human-user (credential) management: Admin tier -------------------------
|
|
|
|
|
|
def issue_user_credential(
|
|
conn: "Connection", admin: Licensor, licensor_id: str, credential_label: str, rights: str
|
|
) -> Licensor:
|
|
"""Admin-only: issue a new credential for a human user acting as the
|
|
given Licensor tenant. Wraps `registry.issue_sub_credential` with the
|
|
rights check and audit log this module exists to add."""
|
|
_require_right(admin, "admin")
|
|
new_credential = registry.issue_sub_credential(
|
|
conn,
|
|
licensor_id=licensor_id,
|
|
credential_label=credential_label,
|
|
rights=rights,
|
|
issued_by=admin.credential_label or admin.token,
|
|
)
|
|
record_audit_event(
|
|
conn, admin, action="issue_user_credential",
|
|
detail={"credential_label": credential_label, "rights": rights},
|
|
)
|
|
return new_credential
|
|
|
|
|
|
def revoke_user_credential(conn: "Connection", admin: Licensor, token: str) -> None:
|
|
"""Admin-only: revoke a credential. Logs only the revoked token's
|
|
suffix, not the full secret, in the audit detail."""
|
|
_require_right(admin, "admin")
|
|
registry.revoke_sub_credential(conn, token)
|
|
record_audit_event(
|
|
conn, admin, action="revoke_user_credential",
|
|
detail={"revoked_token_suffix": token[-6:]},
|
|
)
|
|
|
|
|
|
# --- Phase / Ledger actions --------------------------------------------------
|
|
|
|
|
|
def register_phase(conn: "Connection", licensor: Licensor, manifest: dict[str, Any]) -> None:
|
|
"""Operator+ only, per concept §2's rights table."""
|
|
_require_right(licensor, "operator")
|
|
registry.register_phase_manifest(conn, licensor, manifest)
|
|
record_audit_event(
|
|
conn, licensor, action="register_phase", phase_id=manifest["phase"]["id"],
|
|
detail={"milestone_release": manifest["phase"]["milestone_release"]["name"]},
|
|
)
|
|
|
|
|
|
def append_development_credit(
|
|
conn: "Connection",
|
|
licensor: Licensor,
|
|
phase_id: str,
|
|
entry_input: dict[str, Any],
|
|
signing_key: Any,
|
|
) -> dict[str, Any]:
|
|
"""Operator+ only — appends directly. A Contributor must use
|
|
`propose_ledger_entry` instead (concept §2: "not directly appended")."""
|
|
_require_right(licensor, "operator")
|
|
stored = ledger.append_entry(conn, licensor, phase_id, entry_input, signing_key)
|
|
record_audit_event(
|
|
conn, licensor, action="append_ledger_entry", phase_id=phase_id,
|
|
trust_service_record_id=stored["id"],
|
|
detail={"type": stored["type"], "amount": stored["amount"]},
|
|
)
|
|
return stored
|
|
|
|
|
|
def propose_ledger_entry(
|
|
conn: "Connection", licensor: Licensor, phase_id: str, entry_input: dict[str, Any]
|
|
) -> int:
|
|
"""Contributor+ — submit a proposed entry for an Operator/Admin to
|
|
review. Not appended to the real Ledger until approved."""
|
|
from psycopg.types.json import Jsonb
|
|
|
|
_require_right(licensor, "contributor")
|
|
row = conn.execute(
|
|
"""
|
|
INSERT INTO control_plane_proposed_entries (phase_id, entry, proposed_by_token)
|
|
VALUES (%s, %s, %s)
|
|
RETURNING id
|
|
""",
|
|
(phase_id, Jsonb(entry_input), licensor.token),
|
|
).fetchone()
|
|
proposal_id = row[0]
|
|
record_audit_event(
|
|
conn, licensor, action="propose_ledger_entry", phase_id=phase_id,
|
|
detail={"proposal_id": proposal_id, "type": entry_input.get("type")},
|
|
)
|
|
return proposal_id
|
|
|
|
|
|
def list_proposed_entries(
|
|
conn: "Connection", phase_id: str | None = None, status: str = "pending"
|
|
) -> list[dict[str, Any]]:
|
|
if phase_id:
|
|
rows = conn.execute(
|
|
"""
|
|
SELECT id, phase_id, entry, proposed_by_token, status, created_at
|
|
FROM control_plane_proposed_entries
|
|
WHERE phase_id = %s AND status = %s
|
|
ORDER BY id ASC
|
|
""",
|
|
(phase_id, status),
|
|
).fetchall()
|
|
else:
|
|
rows = conn.execute(
|
|
"""
|
|
SELECT id, phase_id, entry, proposed_by_token, status, created_at
|
|
FROM control_plane_proposed_entries
|
|
WHERE status = %s
|
|
ORDER BY id ASC
|
|
""",
|
|
(status,),
|
|
).fetchall()
|
|
columns = ("id", "phase_id", "entry", "proposed_by_token", "status", "created_at")
|
|
return [dict(zip(columns, row)) for row in rows]
|
|
|
|
|
|
def approve_proposed_entry(
|
|
conn: "Connection",
|
|
reviewer: Licensor,
|
|
proposal_id: int,
|
|
signing_key: Any,
|
|
review_note: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Operator+ approves: actually appends the entry, under the
|
|
*reviewer's* own credential/attribution — the reviewer is the one
|
|
authorizing it into the real Ledger. The original proposer remains on
|
|
record in the proposal row and the audit log, not lost or overwritten.
|
|
"""
|
|
_require_right(reviewer, "operator")
|
|
row = conn.execute(
|
|
"SELECT phase_id, entry FROM control_plane_proposed_entries "
|
|
"WHERE id = %s AND status = 'pending'",
|
|
(proposal_id,),
|
|
).fetchone()
|
|
if row is None:
|
|
raise ControlPlaneError(f"no pending proposal with id {proposal_id!r}")
|
|
phase_id, entry_input = row
|
|
|
|
stored = ledger.append_entry(conn, reviewer, phase_id, entry_input, signing_key)
|
|
|
|
conn.execute(
|
|
"SELECT review_proposed_entry(%s, %s, %s, %s, %s)",
|
|
(proposal_id, "approved", reviewer.token, review_note, stored["id"]),
|
|
)
|
|
record_audit_event(
|
|
conn, reviewer, action="approve_proposed_entry", phase_id=phase_id,
|
|
trust_service_record_id=stored["id"], detail={"proposal_id": proposal_id},
|
|
)
|
|
return stored
|
|
|
|
|
|
def reject_proposed_entry(
|
|
conn: "Connection", reviewer: Licensor, proposal_id: int, review_note: str | None = None
|
|
) -> None:
|
|
"""Operator+ rejects: the proposal is marked rejected, nothing is
|
|
appended to the real Ledger."""
|
|
_require_right(reviewer, "operator")
|
|
row = conn.execute(
|
|
"SELECT phase_id FROM control_plane_proposed_entries "
|
|
"WHERE id = %s AND status = 'pending'",
|
|
(proposal_id,),
|
|
).fetchone()
|
|
if row is None:
|
|
raise ControlPlaneError(f"no pending proposal with id {proposal_id!r}")
|
|
phase_id = row[0]
|
|
|
|
conn.execute(
|
|
"SELECT review_proposed_entry(%s, %s, %s, %s, %s)",
|
|
(proposal_id, "rejected", reviewer.token, review_note, None),
|
|
)
|
|
record_audit_event(
|
|
conn, reviewer, action="reject_proposed_entry", phase_id=phase_id,
|
|
detail={"proposal_id": proposal_id},
|
|
)
|