Skip to main content

macp_runtime/
policy_engine.rs

1//! Pluggable ingress policy engine (E3, master plan §4.6).
2//!
3//! This is the identity-aware, async authorization surface for external
4//! engines (OPA, Cedar, org-specific services). It is deliberately distinct
5//! from [`macp_core::policy::PolicyEvaluator`]:
6//!
7//! - `PolicyEvaluator` governs **commitment evaluation** and must be a pure,
8//!   deterministic function of bound rules + accepted history (RFC-MACP-0012
9//!   §6.3) — it replays.
10//! - `PolicyEngine` governs **ingress**: whether an authenticated identity may
11//!   start a session, send a message, or observe a session. Rejected traffic
12//!   never enters accepted history, so replay only ever sees engine-approved
13//!   envelopes — an async, non-deterministic external engine here cannot
14//!   diverge replay, by the same reasoning that keeps authentication outside
15//!   the replay boundary (RFC-MACP-0003).
16//!
17//! Failure semantics are **deny-on-error**: an engine that cannot answer is a
18//! denial, never an allow.
19
20use crate::security::AuthIdentity;
21use macp_core::policy::PolicyDecision;
22use macp_core::session::Session;
23use macp_pb::pb::Envelope;
24
25/// Decision points an external engine may govern at ingress.
26#[async_trait::async_trait]
27pub trait PolicyEngine: Send + Sync {
28    /// May `identity` start a session in `mode`? Runs after authentication
29    /// and the security layer's own checks, before the kernel accepts the
30    /// SessionStart.
31    async fn evaluate_session_start(
32        &self,
33        identity: &AuthIdentity,
34        mode: &str,
35        env: &Envelope,
36    ) -> PolicyDecision;
37
38    /// May `identity` send this session-scoped envelope? Runs after mode
39    /// binding is known, before kernel acceptance.
40    async fn evaluate_message(
41        &self,
42        identity: &AuthIdentity,
43        session: &Session,
44        env: &Envelope,
45    ) -> PolicyDecision;
46
47    /// May `identity` observe this session (GetSession / StreamSession
48    /// subscribe)? Purely a read gate; never replayed.
49    async fn evaluate_session_access(
50        &self,
51        identity: &AuthIdentity,
52        session: &Session,
53    ) -> PolicyDecision;
54}
55
56/// Convert an engine decision into a transport error, fail closed.
57pub fn require_allow(decision: PolicyDecision, what: &str) -> Result<(), tonic::Status> {
58    match decision {
59        PolicyDecision::Allow { .. } => Ok(()),
60        PolicyDecision::Deny { reasons } => Err(tonic::Status::permission_denied(format!(
61            "policy engine denied {what}: {}",
62            reasons.join("; ")
63        ))),
64        other => Err(tonic::Status::permission_denied(format!(
65            "policy engine returned unrecognized decision for {what} (fail closed): {other:?}"
66        ))),
67    }
68}