Skip to main content

gatekeep_axum/
authorizer.rs

1use std::sync::Arc;
2
3use gatekeep::{
4    AuditSink, Authorizer, Context, Decision, DecisionAuditOccurrence, DecisiveClause, DenyShape,
5    Effect, FactResolver, IdentityReasonCatalog, Lattice, NoopAuditSink, NoopPolicyObserver,
6    Policy, PolicyId, PolicyObserver, PreparedPolicy, ReasonCatalog,
7};
8use serde::Serialize;
9
10use crate::{DenialResponseConfig, GatekeepAxumError, GatekeepRejection};
11
12/// Successful authorization result returned to handlers.
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct Authorized<O> {
15    /// Granted outcome.
16    pub outcome: O,
17    /// Full decision returned by the pure evaluator.
18    pub decision: Decision<O>,
19    /// Stable identity and occurrence time used by the durable audit record.
20    /// Retain this value when an owning operation may need to retry.
21    pub audit_occurrence: DecisionAuditOccurrence,
22}
23
24/// Axum-friendly authorization boundary.
25pub struct Gatekeeper<R, A = NoopAuditSink, C = IdentityReasonCatalog, W = NoopPolicyObserver> {
26    authorizer: Authorizer<R, A, W>,
27    reason_catalog: Arc<C>,
28    denial_response: DenialResponseConfig,
29}
30
31impl<R, A, C, W> Clone for Gatekeeper<R, A, C, W> {
32    fn clone(&self) -> Self {
33        Self {
34            authorizer: self.authorizer.clone(),
35            reason_catalog: Arc::clone(&self.reason_catalog),
36            denial_response: self.denial_response.clone(),
37        }
38    }
39}
40
41impl<R> Gatekeeper<R> {
42    /// Creates an explicitly unaudited gatekeeper with identity reason
43    /// rendering.
44    ///
45    /// The name is intentionally explicit: use [`Self::new`] for production
46    /// authorization where every decision must reach a durable audit sink.
47    #[must_use]
48    pub fn unaudited(resolver: R) -> Self {
49        Self {
50            authorizer: Authorizer::unaudited(resolver),
51            reason_catalog: Arc::new(IdentityReasonCatalog),
52            denial_response: DenialResponseConfig::default(),
53        }
54    }
55}
56
57impl<R, A> Gatekeeper<R, A> {
58    /// Creates a gatekeeper with an explicit audit sink.
59    #[must_use]
60    pub fn new(resolver: R, audit_sink: A) -> Self {
61        Self {
62            authorizer: Authorizer::new(resolver, audit_sink),
63            reason_catalog: Arc::new(IdentityReasonCatalog),
64            denial_response: DenialResponseConfig::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            authorizer: self.authorizer.with_audit_sink(audit_sink),
78            reason_catalog: self.reason_catalog,
79            denial_response: self.denial_response,
80        }
81    }
82
83    /// Replaces the reason catalog used for forbidden denials.
84    #[must_use]
85    pub fn with_reason_catalog<NextCatalog>(
86        self,
87        reason_catalog: NextCatalog,
88    ) -> Gatekeeper<R, A, NextCatalog, W> {
89        Gatekeeper {
90            authorizer: self.authorizer,
91            reason_catalog: Arc::new(reason_catalog),
92            denial_response: self.denial_response,
93        }
94    }
95
96    /// Replaces the side-channel decision observer.
97    #[must_use]
98    pub fn with_observer<NextObserver>(
99        self,
100        observer: NextObserver,
101    ) -> Gatekeeper<R, A, C, NextObserver> {
102        Gatekeeper {
103            authorizer: self.authorizer.with_observer(observer),
104            reason_catalog: self.reason_catalog,
105            denial_response: self.denial_response,
106        }
107    }
108
109    /// Replaces denial presentation settings.
110    #[must_use]
111    pub fn with_denial_response(mut self, denial_response: DenialResponseConfig) -> Self {
112        self.denial_response = denial_response;
113        self
114    }
115
116    /// Replaces the clock used by tenant validation, fact resolution, and
117    /// audit occurrence capture.
118    #[must_use]
119    pub fn with_clock<F>(mut self, clock: F) -> Self
120    where
121        F: gatekeep::Clock + 'static,
122    {
123        self.authorizer = self.authorizer.with_clock(clock);
124        self
125    }
126}
127
128impl<R, A, C, W> Gatekeeper<R, A, C, W>
129where
130    R: FactResolver,
131    A: AuditSink,
132    C: ReasonCatalog + Send + Sync,
133    W: PolicyObserver,
134{
135    /// Resolves facts, evaluates the policy, observes and audits the decision,
136    /// and returns an axum rejection for denied requests.
137    ///
138    /// # Errors
139    ///
140    /// Returns [`GatekeepRejection`] when policy hashing, fact resolution,
141    /// trace conversion, or audit persistence fails, or when the policy denies
142    /// the request.
143    pub async fn authorize<O>(
144        &self,
145        policy_id: PolicyId,
146        policy: &Policy<O>,
147        context: Context,
148    ) -> Result<Authorized<O>, GatekeepRejection<R::Error, A::Error>>
149    where
150        O: Lattice + Serialize + Send + Sync,
151    {
152        let result = self
153            .authorizer
154            .authorize_policy(policy_id, policy, &context)
155            .await
156            .map_err(GatekeepRejection::from_error)?;
157        self.present(result, &context)
158    }
159
160    /// Authorizes a prepared policy with explicit completeness checks.
161    ///
162    /// # Errors
163    /// Returns a rejection on denial, omitted facts, invalid context or failed audit.
164    pub async fn authorize_prepared<O>(
165        &self,
166        policy: &PreparedPolicy<O>,
167        context: Context,
168    ) -> Result<Authorized<O>, GatekeepRejection<R::Error, A::Error>>
169    where
170        O: Lattice + Serialize + Send + Sync,
171    {
172        let result = self
173            .authorizer
174            .authorize(policy, &context)
175            .await
176            .map_err(GatekeepRejection::from_error)?;
177        self.present(result, &context)
178    }
179}
180
181impl<R, A, C: ReasonCatalog, W> Gatekeeper<R, A, C, W> {
182    fn present<O: Serialize + Clone, Resolve, Audit>(
183        &self,
184        result: gatekeep::AuthorizationDecision<O>,
185        context: &Context,
186    ) -> Result<Authorized<O>, GatekeepRejection<Resolve, Audit>> {
187        let gatekeep::AuthorizationDecision {
188            decision,
189            audit_occurrence,
190        } = result;
191        match decision.effect.clone() {
192            Effect::Permit(outcome) => Ok(Authorized {
193                outcome,
194                decision,
195                audit_occurrence,
196            }),
197            Effect::Deny => {
198                let reason = decision
199                    .denial_reason()
200                    .map_err(GatekeepAxumError::Trace)
201                    .map_err(GatekeepRejection::from_error)?;
202                let response = self.denial_response.denied(
203                    denial_shape(&decision),
204                    reason.as_ref(),
205                    context.locale(),
206                    self.reason_catalog.as_ref(),
207                );
208                Err(response.into())
209            }
210        }
211    }
212}
213
214const fn denial_shape<O>(decision: &Decision<O>) -> DenyShape {
215    match &decision.trace.decisive {
216        DecisiveClause::Deny { shape, .. } => *shape,
217        DecisiveClause::Permit { .. } => DenyShape::Forbidden,
218    }
219}
220
221impl<R, A, C, W> Gatekeeper<R, A, C, W>
222where
223    R: gatekeep::ResourcePolicy,
224    A: AuditSink,
225    C: ReasonCatalog + Send + Sync,
226    W: PolicyObserver,
227{
228    /// Authorizes an application-typed operation and applies HTTP denial presentation.
229    ///
230    /// # Errors
231    /// Returns hidden/forbidden denial or typed source, evidence and audit failures.
232    pub async fn authorize_resource(
233        &self,
234        action: &R::Action,
235        principal: &R::Principal,
236        resource: &R::Resource,
237        context: Context,
238    ) -> Result<Authorized<R::Outcome>, GatekeepRejection<R::Error, A::Error>> {
239        let result = self
240            .authorizer
241            .authorize_resource(action, principal, resource, &context)
242            .await
243            .map_err(GatekeepRejection::from_error)?;
244        self.present(result, &context)
245    }
246}