Skip to main content

gatekeep_axum/
authorizer.rs

1use std::sync::Arc;
2
3use gatekeep::{
4    AuditEntry, AuditSink, Context, Decision, DecisionSummary, DecisiveClause, DenyShape, Effect,
5    EffectKind, FactResolver, IdentityReasonCatalog, Lattice, NoopAuditSink, NoopPolicyObserver,
6    Policy, PolicyAnchor, PolicyId, PolicyObserver, ReasonCatalog, evaluate, required_facts,
7};
8use serde::Serialize;
9
10use crate::{DenialResponseConfig, GatekeepAxumError, GatekeepRejection};
11
12/// Whether audit entries should include request subject identifiers.
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
14pub enum AuditSubjects {
15    /// Leave tenant and principal identifiers out of audit entries.
16    #[default]
17    Omit,
18    /// Copy tenant and principal identifiers from the request context.
19    Record,
20}
21
22/// Successful authorization result returned to handlers.
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct Authorized<O> {
25    /// Granted outcome.
26    pub outcome: O,
27    /// Full decision returned by the pure evaluator.
28    pub decision: Decision<O>,
29}
30
31/// Axum-friendly authorization boundary.
32pub struct Gatekeeper<R, A = NoopAuditSink, C = IdentityReasonCatalog, W = NoopPolicyObserver> {
33    resolver: Arc<R>,
34    audit_sink: Arc<A>,
35    reason_catalog: Arc<C>,
36    observer: Arc<W>,
37    denial_response: DenialResponseConfig,
38    audit_subjects: AuditSubjects,
39}
40
41impl<R, A, C, W> Clone for Gatekeeper<R, A, C, W> {
42    fn clone(&self) -> Self {
43        Self {
44            resolver: Arc::clone(&self.resolver),
45            audit_sink: Arc::clone(&self.audit_sink),
46            reason_catalog: Arc::clone(&self.reason_catalog),
47            observer: Arc::clone(&self.observer),
48            denial_response: self.denial_response.clone(),
49            audit_subjects: self.audit_subjects,
50        }
51    }
52}
53
54impl<R> Gatekeeper<R> {
55    /// Creates a gatekeeper with no-op audit and identity reason rendering.
56    #[must_use]
57    pub fn new(resolver: R) -> Self {
58        Self {
59            resolver: Arc::new(resolver),
60            audit_sink: Arc::new(NoopAuditSink),
61            reason_catalog: Arc::new(IdentityReasonCatalog),
62            observer: Arc::new(NoopPolicyObserver),
63            denial_response: DenialResponseConfig::default(),
64            audit_subjects: AuditSubjects::default(),
65        }
66    }
67}
68
69impl<R, A, C, W> Gatekeeper<R, A, C, W> {
70    /// Replaces the audit sink.
71    #[must_use]
72    pub fn with_audit_sink<NextAudit>(
73        self,
74        audit_sink: NextAudit,
75    ) -> Gatekeeper<R, NextAudit, C, W> {
76        Gatekeeper {
77            resolver: self.resolver,
78            audit_sink: Arc::new(audit_sink),
79            reason_catalog: self.reason_catalog,
80            observer: self.observer,
81            denial_response: self.denial_response,
82            audit_subjects: self.audit_subjects,
83        }
84    }
85
86    /// Replaces the reason catalog used for forbidden denials.
87    #[must_use]
88    pub fn with_reason_catalog<NextCatalog>(
89        self,
90        reason_catalog: NextCatalog,
91    ) -> Gatekeeper<R, A, NextCatalog, W> {
92        Gatekeeper {
93            resolver: self.resolver,
94            audit_sink: self.audit_sink,
95            reason_catalog: Arc::new(reason_catalog),
96            observer: self.observer,
97            denial_response: self.denial_response,
98            audit_subjects: self.audit_subjects,
99        }
100    }
101
102    /// Replaces the side-channel decision observer.
103    #[must_use]
104    pub fn with_observer<NextObserver>(
105        self,
106        observer: NextObserver,
107    ) -> Gatekeeper<R, A, C, NextObserver> {
108        Gatekeeper {
109            resolver: self.resolver,
110            audit_sink: self.audit_sink,
111            reason_catalog: self.reason_catalog,
112            observer: Arc::new(observer),
113            denial_response: self.denial_response,
114            audit_subjects: self.audit_subjects,
115        }
116    }
117
118    /// Replaces denial presentation settings.
119    #[must_use]
120    pub fn with_denial_response(mut self, denial_response: DenialResponseConfig) -> Self {
121        self.denial_response = denial_response;
122        self
123    }
124
125    /// Controls whether audit entries include tenant and principal identifiers.
126    #[must_use]
127    pub const fn with_audit_subjects(mut self, audit_subjects: AuditSubjects) -> Self {
128        self.audit_subjects = audit_subjects;
129        self
130    }
131}
132
133impl<R, A, C, W> Gatekeeper<R, A, C, W>
134where
135    R: FactResolver,
136    A: AuditSink,
137    C: ReasonCatalog + Send + Sync,
138    W: PolicyObserver,
139{
140    /// Resolves facts, evaluates the policy, observes and audits the decision,
141    /// and returns an axum rejection for denied requests.
142    pub async fn authorize<O>(
143        &self,
144        policy_id: PolicyId,
145        policy: &Policy<O>,
146        context: Context,
147    ) -> Result<Authorized<O>, GatekeepRejection<R::Error, A::Error>>
148    where
149        O: Lattice + Serialize + Send + Sync,
150    {
151        let anchor = PolicyAnchor {
152            policy_id,
153            policy_hash: policy
154                .hash()
155                .map_err(GatekeepAxumError::PolicyHash)
156                .map_err(GatekeepRejection::from_error)?,
157        };
158        let required = required_facts(policy).into_iter().collect::<Vec<_>>();
159        let facts = self
160            .resolver
161            .resolve_for_decision(&required, &context)
162            .await
163            .map_err(GatekeepAxumError::Resolve)
164            .map_err(GatekeepRejection::from_error)?;
165        let decision = evaluate(policy, &facts);
166
167        self.observe_and_audit(&anchor, &decision, &context)
168            .map_err(GatekeepRejection::from_error)?;
169
170        match decision.effect.clone() {
171            Effect::Permit(outcome) => Ok(Authorized { outcome, decision }),
172            Effect::Deny => {
173                let reason = decision
174                    .denial_reason()
175                    .map_err(GatekeepAxumError::Trace)
176                    .map_err(GatekeepRejection::from_error)?;
177                let response = self.denial_response.denied(
178                    denial_shape(&decision),
179                    reason.as_ref(),
180                    &context.locale,
181                    self.reason_catalog.as_ref(),
182                );
183                Err(response.into())
184            }
185        }
186    }
187
188    fn observe_and_audit<O>(
189        &self,
190        anchor: &PolicyAnchor,
191        decision: &Decision<O>,
192        context: &Context,
193    ) -> Result<(), GatekeepAxumError<R::Error, A::Error>>
194    where
195        O: Serialize + Clone,
196    {
197        let (tenant, principal) = match self.audit_subjects {
198            AuditSubjects::Omit => (None, None),
199            AuditSubjects::Record => (
200                Some(context.tenant.clone()),
201                Some(context.principal.clone()),
202            ),
203        };
204        let entry = AuditEntry {
205            anchor: anchor.clone(),
206            trace: decision.to_trace().map_err(GatekeepAxumError::Trace)?,
207            effect: EffectKind::from(decision),
208            obligations: decision.obligations.clone(),
209            tenant,
210            principal,
211        };
212        self.audit_sink
213            .record(&entry)
214            .map_err(GatekeepAxumError::Audit)?;
215
216        let summary = DecisionSummary {
217            anchor: anchor.clone(),
218            effect: EffectKind::from(decision),
219            obligations: decision.obligations.clone(),
220            consulted: decision.trace.consulted.clone(),
221        };
222        self.observer.observe(&summary);
223        Ok(())
224    }
225}
226
227const fn denial_shape<O>(decision: &Decision<O>) -> DenyShape {
228    match &decision.trace.decisive {
229        DecisiveClause::Deny { shape, .. } => *shape,
230        DecisiveClause::Permit { .. } => DenyShape::Forbidden,
231    }
232}