Coverage for node / src / stigmem_node / federation / peer_policy.py: 100%
34 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-18 05:34 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-18 05:34 +0000
1"""Per-peer federation tenant policy resolution (Phase 1, fail-closed).
3A federated fact's local tenant is determined ENTIRELY by the receiving node's
4per-peer policy (no wire-carried tenant in Phase 1). Non-default tenancy is only
5real when the multi-tenant plugin is active; otherwise tenant_resolve collapses
6everything to 'default' (see multi_tenant_gate). We fail closed rather than
7silently label-without-isolate.
8"""
10from __future__ import annotations
12from typing import Any
14DEFAULT_TENANT_ID = "default"
17class PeerPolicyError(ValueError):
18 """Raised when a peer's tenant policy cannot be safely honored (fail-closed)."""
21def resolve_ingest_tenant(
22 peer: dict[str, Any] | Any,
23 *,
24 plugin_active: bool,
25 node_is_multitenant: bool = False,
26) -> str:
27 """Return the local tenant inbound facts from this peer are stamped into.
29 Fail-closed rules:
30 - A non-default ``ingest_tenant`` requires the multi-tenant plugin (else the
31 label is not actually isolated) -> PeerPolicyError.
32 - An unpinned peer (no ``ingest_tenant``) on a node that hosts non-default
33 tenants is ambiguous -> PeerPolicyError (configure the peer explicitly).
34 - An explicit ``default`` (or a single-tenant node) is always fine.
35 """
36 raw = _get(peer, "ingest_tenant")
37 pinned: str | None = str(raw) if raw else None
38 if pinned is None:
39 if node_is_multitenant:
40 raise PeerPolicyError(
41 "peer has no ingest_tenant but the node hosts non-default tenants; "
42 "set the peer's ingest_tenant explicitly"
43 )
44 return DEFAULT_TENANT_ID
45 if pinned != DEFAULT_TENANT_ID and not plugin_active:
46 raise PeerPolicyError(
47 f"ingest_tenant={pinned!r} requires the multi-tenant plugin "
48 "(stigmem-plugin-multi-tenant); without it tenants are not isolated"
49 )
50 return pinned
53def _node_is_multitenant(conn: Any) -> bool:
54 """True when this node hosts at least one non-default API-key tenant.
56 A node-capability probe (not an ingest concern, review M3): used to fail
57 closed (PeerPolicyError) when an unpinned peer would otherwise be ambiguous
58 on a multi-tenant node.
59 """
60 return bool(
61 conn.execute(
62 "SELECT 1 FROM api_keys WHERE tenant_id != 'default' LIMIT 1"
63 ).fetchone()
64 )
67def resolve_ingest_tenant_for_peer(peer: dict[str, Any] | Any, conn: Any) -> str:
68 """Resolve a peer's ingest tenant, wiring the plugin + node-multitenancy probes.
70 Single shared call site for all three federation ingest paths (pull loop +
71 both push paths) so the fail-closed policy can only be evaluated against a
72 peer row that actually carries the policy columns. Raises PeerPolicyError
73 (fail-closed). ``peer`` MUST carry the ``ingest_tenant`` column.
74 """
75 from ..multi_tenant_gate import multi_tenant_plugin_registered
77 return resolve_ingest_tenant(
78 peer,
79 plugin_active=multi_tenant_plugin_registered(),
80 node_is_multitenant=_node_is_multitenant(conn),
81 )
84def resolve_origin_tenant_for_peer(
85 peer: dict[str, Any] | Any, origin_tenant: str, conn: Any
86) -> str:
87 """Resolve a wire-carried ``origin_tenant`` from a peer to a local tenant (default-deny).
89 Phase 2b per-origin mapping. Resolution order for ``(peer, origin_tenant)``:
91 1. An explicit ``peer_tenant_map`` row for ``(peer["id"], origin_tenant)`` ->
92 its ``local_tenant``.
93 2. NO map rows exist for this peer AND ``origin_tenant == "default"`` AND the
94 node is genuinely single-tenant (multi-tenant plugin NOT registered) ->
95 fall back to the Phase-1 single-scalar pin
96 (:func:`resolve_ingest_tenant_for_peer`) for backward compatibility.
97 3. Everything else -> :class:`PeerPolicyError` (fail-closed / default-deny).
99 The branch-2 guard (single-tenant only) is the F-4 tightening: on a
100 multi-tenant node an unmapped origin tenant -- including ``"default"`` -- is
101 denied rather than silently collapsed.
102 """
103 from ..multi_tenant_gate import multi_tenant_plugin_registered
105 peer_id = _get(peer, "id")
106 row = conn.execute(
107 "SELECT local_tenant FROM peer_tenant_map WHERE peer_id = ? AND origin_tenant = ?",
108 (peer_id, origin_tenant),
109 ).fetchone()
110 if row is not None:
111 return str(row["local_tenant"])
113 has_any_map = conn.execute(
114 "SELECT 1 FROM peer_tenant_map WHERE peer_id = ? LIMIT 1", (peer_id,)
115 ).fetchone()
116 if (
117 has_any_map is None
118 and origin_tenant == DEFAULT_TENANT_ID
119 and not multi_tenant_plugin_registered()
120 ):
121 return resolve_ingest_tenant_for_peer(peer, conn)
123 raise PeerPolicyError(
124 f"no peer_tenant_map entry for peer {peer_id!r} origin_tenant={origin_tenant!r}; "
125 "configure the mapping explicitly (fail-closed)"
126 )
129def _get(peer: Any, key: str) -> Any:
130 """Read a key from a dict or a sqlite3.Row-like object, returning None if absent."""
131 try:
132 return peer[key]
133 except (KeyError, IndexError, TypeError):
134 return None