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

1"""Per-peer federation tenant policy resolution (Phase 1, fail-closed). 

2 

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""" 

9 

10from __future__ import annotations 

11 

12from typing import Any 

13 

14DEFAULT_TENANT_ID = "default" 

15 

16 

17class PeerPolicyError(ValueError): 

18 """Raised when a peer's tenant policy cannot be safely honored (fail-closed).""" 

19 

20 

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. 

28 

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 

51 

52 

53def _node_is_multitenant(conn: Any) -> bool: 

54 """True when this node hosts at least one non-default API-key tenant. 

55 

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 ) 

65 

66 

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. 

69 

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 

76 

77 return resolve_ingest_tenant( 

78 peer, 

79 plugin_active=multi_tenant_plugin_registered(), 

80 node_is_multitenant=_node_is_multitenant(conn), 

81 ) 

82 

83 

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). 

88 

89 Phase 2b per-origin mapping. Resolution order for ``(peer, origin_tenant)``: 

90 

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). 

98 

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 

104 

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"]) 

112 

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) 

122 

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 ) 

127 

128 

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