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}