1use std::{collections::BTreeMap, 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>(
149 &self,
150 policy_id: PolicyId,
151 policy: &Policy<O>,
152 context: Context,
153 ) -> Result<Authorized<O>, GatekeepRejection<R::Error, A::Error>>
154 where
155 O: Lattice + Serialize + Send + Sync,
156 {
157 let anchor = PolicyAnchor {
158 policy_id,
159 policy_hash: policy
160 .hash()
161 .map_err(GatekeepAxumError::PolicyHash)
162 .map_err(GatekeepRejection::from_error)?,
163 };
164 let required = required_facts(policy).into_iter().collect::<Vec<_>>();
165 let facts = self
166 .resolver
167 .resolve_for_decision(&required, &context)
168 .await
169 .map_err(GatekeepAxumError::Resolve)
170 .map_err(GatekeepRejection::from_error)?;
171 let decision = evaluate(policy, &facts);
172
173 self.observe_and_audit(&anchor, &decision, &context)
174 .await
175 .map_err(GatekeepRejection::from_error)?;
176
177 match decision.effect.clone() {
178 Effect::Permit(outcome) => Ok(Authorized { outcome, decision }),
179 Effect::Deny => {
180 let reason = decision
181 .denial_reason()
182 .map_err(GatekeepAxumError::Trace)
183 .map_err(GatekeepRejection::from_error)?;
184 let response = self.denial_response.denied(
185 denial_shape(&decision),
186 reason.as_ref(),
187 &context.locale,
188 self.reason_catalog.as_ref(),
189 );
190 Err(response.into())
191 }
192 }
193 }
194
195 async fn observe_and_audit<O>(
196 &self,
197 anchor: &PolicyAnchor,
198 decision: &Decision<O>,
199 context: &Context,
200 ) -> Result<(), GatekeepAxumError<R::Error, A::Error>>
201 where
202 O: Serialize + Clone + Sync,
203 {
204 let (tenant, principal) = match self.audit_subjects {
205 AuditSubjects::Omit => (None, None),
206 AuditSubjects::Record => (
207 Some(context.tenant.clone()),
208 Some(context.principal.clone()),
209 ),
210 };
211 let trace = decision.to_trace().map_err(GatekeepAxumError::Trace)?;
212 let entry = AuditEntry {
213 request_id: context.request_id.clone(),
214 anchor: anchor.clone(),
215 effect: EffectKind::from(decision),
216 obligations: decision.obligations.clone(),
217 consulted: trace.consulted.clone(),
218 decisive: trace.decisive.clone(),
219 denial_reason: decision.denial_reason().map_err(GatekeepAxumError::Trace)?,
220 trace,
221 tenant,
222 principal,
223 subjects: match self.audit_subjects {
224 AuditSubjects::Omit => BTreeMap::default(),
225 AuditSubjects::Record => context.subjects.clone(),
226 },
227 };
228 let summary = DecisionSummary {
229 anchor: anchor.clone(),
230 effect: EffectKind::from(decision),
231 obligations: decision.obligations.clone(),
232 consulted: decision.trace.consulted.clone(),
233 };
234 self.audit_sink
235 .record(&entry)
236 .await
237 .map_err(GatekeepAxumError::Audit)?;
238
239 self.observer.observe(&summary);
240 Ok(())
241 }
242}
243
244const fn denial_shape<O>(decision: &Decision<O>) -> DenyShape {
245 match &decision.trace.decisive {
246 DecisiveClause::Deny { shape, .. } => *shape,
247 DecisiveClause::Permit { .. } => DenyShape::Forbidden,
248 }
249}