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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
14pub enum AuditSubjects {
15 #[default]
17 Omit,
18 Record,
20}
21
22#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct Authorized<O> {
25 pub outcome: O,
27 pub decision: Decision<O>,
29}
30
31pub 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 #[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 #[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 #[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 #[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 #[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 #[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 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}