Skip to main content

gatehouse/
policy.rs

1use crate::{EvaluationSession, FactProvenance, PolicyEvalResult, SecurityRuleMetadata};
2use async_trait::async_trait;
3use std::borrow::Cow;
4use std::sync::Arc;
5
6/// Names the four Rust types that make up one authorization domain.
7///
8/// A domain is usually one resource family in an application: documents,
9/// invoices, projects, packages. The marker type keeps policy APIs anchored to
10/// a business domain instead of repeating `<Subject, Action, Resource, Context>`
11/// on every checker, policy, and builder.
12pub trait PolicyDomain: Send + Sync + 'static {
13    /// Entity requesting access.
14    type Subject: Send + Sync;
15    /// Operation being attempted.
16    type Action: Send + Sync;
17    /// Target resource or scope resource.
18    type Resource: Send + Sync;
19    /// Request-scoped evaluation inputs.
20    type Context: Send + Sync;
21}
22
23/// The declared effect of a policy: whether it can grant, forbid, or both.
24///
25/// `Allow` (the default everywhere) means the policy grants access when it
26/// matches. `Forbid` means the policy **forbids** access when it matches: a
27/// matched forbid produces [`PolicyEvalResult::Forbidden`], which
28/// [`crate::PermissionChecker`] honors over any grant from sibling policies.
29/// `AllowOrForbid` is for composed or custom policies that can produce either
30/// result depending on their inputs.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32#[non_exhaustive]
33pub enum Effect {
34    /// The policy may grant access, but must not actively forbid.
35    Allow,
36    /// The policy may actively forbid access, but must not grant.
37    Forbid,
38    /// The policy may either grant or actively forbid access.
39    AllowOrForbid,
40}
41
42impl Effect {
43    /// Whether this effect can produce a grant.
44    pub fn can_grant(self) -> bool {
45        matches!(self, Self::Allow | Self::AllowOrForbid)
46    }
47
48    /// Whether this effect can produce an active forbid.
49    pub fn can_forbid(self) -> bool {
50        matches!(self, Self::Forbid | Self::AllowOrForbid)
51    }
52
53    pub(crate) fn from_capabilities(can_grant: bool, can_forbid: bool) -> Self {
54        match (can_grant, can_forbid) {
55            (true, true) => Self::AllowOrForbid,
56            (false, true) => Self::Forbid,
57            _ => Self::Allow,
58        }
59    }
60
61    pub(crate) fn telemetry_label(self) -> &'static str {
62        match self {
63            Self::Allow => "allow",
64            Self::Forbid => "deny",
65            Self::AllowOrForbid => "allow_or_forbid",
66        }
67    }
68}
69
70/// A borrowed resource passed to batch policy evaluators.
71///
72/// Values are borrowed from caller-owned batch items, so policy implementations
73/// can evaluate a batch without forcing resources to be cloned.
74pub struct PolicyBatchItem<'a, D: PolicyDomain> {
75    /// The target resource for this item.
76    pub resource: &'a D::Resource,
77}
78
79/// Per-item policy evaluation context.
80pub struct EvalCtx<'a, D: PolicyDomain> {
81    /// Request-scoped fact session.
82    pub session: &'a EvaluationSession,
83    /// Entity requesting access.
84    pub subject: &'a D::Subject,
85    /// Action being performed.
86    pub action: &'a D::Action,
87    /// Target resource.
88    pub resource: &'a D::Resource,
89    /// Additional per-request evaluation context.
90    ///
91    /// Carries request-scoped inputs that are not properties of the subject or
92    /// resource: current time, MFA freshness, network zone, tenant-level
93    /// overrides. Relationship data belongs behind a [`crate::FactSource`] and
94    /// loads through [`EvaluationSession`].
95    pub context: &'a D::Context,
96    /// The current policy's [`Policy::policy_type`], captured by the checker
97    /// before dispatch and used by [`Self::grant`],
98    /// [`Self::not_applicable`], and [`Self::forbid`].
99    pub policy_type: Cow<'static, str>,
100}
101
102impl<'a, D: PolicyDomain> EvalCtx<'a, D> {
103    /// Shorthand for `PolicyEvalResult::granted(ctx.policy_type, Some(reason))`.
104    pub fn grant(&self, reason: impl Into<String>) -> PolicyEvalResult {
105        PolicyEvalResult::granted(self.policy_type.clone(), Some(reason.into()))
106    }
107
108    /// Shorthand for `PolicyEvalResult::not_applicable(ctx.policy_type, reason)`.
109    pub fn not_applicable(&self, reason: impl Into<String>) -> PolicyEvalResult {
110        PolicyEvalResult::not_applicable(self.policy_type.clone(), reason)
111    }
112
113    /// Shorthand for `PolicyEvalResult::forbidden(ctx.policy_type, reason)`.
114    ///
115    /// Use this for an active veto. A hand-written policy that can only veto
116    /// should override [`Policy::effect`] to return [`Effect::Forbid`]. A policy
117    /// that can grant or veto should return [`Effect::AllowOrForbid`]. Both
118    /// make [`crate::PermissionChecker`] evaluate the policy before allow-only
119    /// policies so grant short-circuiting cannot skip the veto.
120    pub fn forbid(&self, reason: impl Into<String>) -> PolicyEvalResult {
121        PolicyEvalResult::forbidden(self.policy_type.clone(), reason)
122    }
123
124    /// Shorthand for [`PolicyEvalResult::granted_with_facts`] tagged with
125    /// `ctx.policy_type`.
126    pub fn grant_with_facts(
127        &self,
128        reason: impl Into<String>,
129        provenance: Vec<FactProvenance>,
130    ) -> PolicyEvalResult {
131        PolicyEvalResult::granted_with_facts(
132            self.policy_type.clone(),
133            Some(reason.into()),
134            provenance,
135        )
136    }
137
138    /// Shorthand for [`PolicyEvalResult::not_applicable_with_facts`] tagged
139    /// with `ctx.policy_type`.
140    pub fn not_applicable_with_facts(
141        &self,
142        reason: impl Into<String>,
143        provenance: Vec<FactProvenance>,
144    ) -> PolicyEvalResult {
145        PolicyEvalResult::not_applicable_with_facts(self.policy_type.clone(), reason, provenance)
146    }
147
148    /// Shorthand for [`PolicyEvalResult::forbidden_with_facts`] tagged with
149    /// `ctx.policy_type`.
150    pub fn forbid_with_facts(
151        &self,
152        reason: impl Into<String>,
153        provenance: Vec<FactProvenance>,
154    ) -> PolicyEvalResult {
155        PolicyEvalResult::forbidden_with_facts(self.policy_type.clone(), reason, provenance)
156    }
157}
158
159/// Batch policy evaluation context.
160///
161/// A batch holds one subject, one action, and one request context evaluated
162/// against many resources.
163pub struct BatchEvalCtx<'a, D: PolicyDomain> {
164    /// Request-scoped fact session.
165    pub session: &'a EvaluationSession,
166    /// Entity requesting access.
167    pub subject: &'a D::Subject,
168    /// Action being performed, shared across every item in the batch.
169    pub action: &'a D::Action,
170    /// Request-scoped context, shared across every item in the batch.
171    pub context: &'a D::Context,
172    /// Borrowed resources.
173    pub items: &'a [PolicyBatchItem<'a, D>],
174    /// The current policy's [`Policy::policy_type`].
175    pub policy_type: Cow<'static, str>,
176}
177
178/// A generic async trait representing a single authorization policy for one
179/// [`PolicyDomain`].
180#[async_trait]
181pub trait Policy<D: PolicyDomain>: Send + Sync {
182    /// Evaluates whether access should be granted.
183    async fn evaluate(&self, ctx: &EvalCtx<'_, D>) -> PolicyEvalResult;
184
185    /// Evaluates access for a batch of resources.
186    ///
187    /// The default implementation preserves single-item semantics by evaluating
188    /// each item sequentially. Policies with set-oriented backends can override
189    /// this method to reduce round trips while returning one result per input
190    /// item in the same order.
191    async fn evaluate_batch<'item>(&self, ctx: &BatchEvalCtx<'item, D>) -> Vec<PolicyEvalResult> {
192        let mut results = Vec::with_capacity(ctx.items.len());
193        for item in ctx.items {
194            let item_ctx = EvalCtx {
195                session: ctx.session,
196                subject: ctx.subject,
197                action: ctx.action,
198                resource: item.resource,
199                context: ctx.context,
200                policy_type: ctx.policy_type.clone(),
201            };
202            results.push(self.evaluate(&item_ctx).await);
203        }
204        results
205    }
206
207    /// Policy name for debugging, trace trees, and telemetry fallbacks.
208    fn policy_type(&self) -> Cow<'static, str>;
209
210    /// The declared effect of this policy. Defaults to [`Effect::Allow`].
211    ///
212    /// [`crate::PermissionChecker`] reads this declaration when the policy is
213    /// added. Policies declaring [`Effect::Forbid`] or
214    /// [`Effect::AllowOrForbid`] run before allow-only policies, so a matched
215    /// forbid is observed before a grant can short-circuit.
216    ///
217    /// A policy that returns [`PolicyEvalResult::Forbidden`] while leaving this
218    /// at the default [`Effect::Allow`] still vetoes wherever it is observed,
219    /// but the checker emits a contract-violation `WARN`: the veto is not
220    /// scheduled ahead of grants and an earlier grant can short-circuit before
221    /// it is reached. Declare [`Effect::Forbid`] or [`Effect::AllowOrForbid`]
222    /// for an order-independent veto.
223    fn effect(&self) -> Effect {
224        Effect::Allow
225    }
226
227    /// Metadata describing the security rule that backs this policy.
228    fn security_rule(&self) -> SecurityRuleMetadata {
229        SecurityRuleMetadata::default()
230    }
231}
232
233#[async_trait]
234impl<D> Policy<D> for Box<dyn Policy<D>>
235where
236    D: PolicyDomain,
237{
238    async fn evaluate(&self, ctx: &EvalCtx<'_, D>) -> PolicyEvalResult {
239        (**self).evaluate(ctx).await
240    }
241
242    async fn evaluate_batch<'item>(&self, ctx: &BatchEvalCtx<'item, D>) -> Vec<PolicyEvalResult> {
243        (**self).evaluate_batch(ctx).await
244    }
245
246    fn policy_type(&self) -> Cow<'static, str> {
247        (**self).policy_type()
248    }
249
250    fn effect(&self) -> Effect {
251        (**self).effect()
252    }
253
254    fn security_rule(&self) -> SecurityRuleMetadata {
255        (**self).security_rule()
256    }
257}
258
259#[async_trait]
260impl<D> Policy<D> for Arc<dyn Policy<D>>
261where
262    D: PolicyDomain,
263{
264    async fn evaluate(&self, ctx: &EvalCtx<'_, D>) -> PolicyEvalResult {
265        (**self).evaluate(ctx).await
266    }
267
268    async fn evaluate_batch<'item>(&self, ctx: &BatchEvalCtx<'item, D>) -> Vec<PolicyEvalResult> {
269        (**self).evaluate_batch(ctx).await
270    }
271
272    fn policy_type(&self) -> Cow<'static, str> {
273        (**self).policy_type()
274    }
275
276    fn effect(&self) -> Effect {
277        (**self).effect()
278    }
279
280    fn security_rule(&self) -> SecurityRuleMetadata {
281        (**self).security_rule()
282    }
283}