Skip to main content

made_core/entities/
authorization_policy.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use time::OffsetDateTime;
4
5use super::authorization_policy_helpers::{owner_permits, separation_rule_map, validity_contains};
6use super::AuthorizationPolicyEvent;
7use crate::value_objects::{
8    AuthenticatedPrincipal, AuthorizationAction, AuthorizationDecision, AuthorizationDecisionId,
9    AuthorizationDecisionKind, AuthorizationDecisionPlan, AuthorizationDecisionTtl,
10    AuthorizationDenialReason, AuthorizationGrant, AuthorizationGrantId, AuthorizationPolicyId,
11    AuthorizationPolicyVersion, AuthorizationRequest, AuthorizationRequestId,
12    AuthorizationRevocation, AuthorizationRevocationReason, AuthorizationScope, PrincipalKind,
13    SeparationRule,
14};
15use crate::DomainError;
16
17#[path = "authorization_policy/accepted_work.rs"]
18mod accepted_work;
19#[path = "authorization_policy/separation.rs"]
20mod separation;
21
22#[derive(Debug, Clone, Default, PartialEq, Eq)]
23pub struct AuthorizationPolicy {
24    id: Option<AuthorizationPolicyId>,
25    owner: Option<AuthenticatedPrincipal>,
26    version: AuthorizationPolicyVersion,
27    grants: BTreeMap<AuthorizationGrantId, AuthorizationGrant>,
28    revoked: BTreeSet<AuthorizationGrantId>,
29    separation_rules: BTreeMap<AuthorizationAction, SeparationRule>,
30    decisions_by_request: BTreeMap<AuthorizationRequestId, AuthorizationDecision>,
31    decisions_by_id: BTreeMap<AuthorizationDecisionId, AuthorizationDecision>,
32}
33
34impl AuthorizationPolicy {
35    #[must_use]
36    pub fn empty() -> Self {
37        Self::default()
38    }
39    pub fn rehydrate(events: &[AuthorizationPolicyEvent]) -> Result<Self, DomainError> {
40        let mut policy = Self::empty();
41        for event in events {
42            policy.apply(event.clone())?;
43        }
44        Ok(policy)
45    }
46    #[must_use]
47    pub const fn version(&self) -> AuthorizationPolicyVersion {
48        self.version
49    }
50    #[must_use]
51    pub fn id(&self) -> Option<&AuthorizationPolicyId> {
52        self.id.as_ref()
53    }
54    #[must_use]
55    pub fn owner(&self) -> Option<&AuthenticatedPrincipal> {
56        self.owner.as_ref()
57    }
58    pub fn decisions(&self) -> impl Iterator<Item = &AuthorizationDecision> {
59        self.decisions_by_id.values()
60    }
61    pub fn grants(&self) -> impl Iterator<Item = &AuthorizationGrant> {
62        self.grants.values()
63    }
64    pub fn revoked_grant_ids(&self) -> impl Iterator<Item = &AuthorizationGrantId> {
65        self.revoked.iter()
66    }
67    pub fn separation_rules(&self) -> impl Iterator<Item = SeparationRule> + '_ {
68        self.separation_rules.values().copied()
69    }
70
71    pub fn decide_open(
72        &self,
73        policy_id: AuthorizationPolicyId,
74        owner: AuthenticatedPrincipal,
75        separation_rules: Vec<SeparationRule>,
76        opened_at: OffsetDateTime,
77    ) -> Result<Option<AuthorizationPolicyEvent>, DomainError> {
78        owner.validate()?;
79        if owner.kind() != PrincipalKind::TrustedHost {
80            return Err(DomainError::InvariantViolated {
81                reason: "authorization policy owner must be an explicit trusted host",
82            });
83        }
84        let proposed_rules = separation_rule_map(separation_rules.clone())?;
85        match (&self.id, &self.owner) {
86            (None, None) => Ok(Some(AuthorizationPolicyEvent::Opened {
87                policy_id,
88                owner,
89                separation_rules,
90                opened_at,
91            })),
92            (Some(id), Some(stored_owner))
93                if id == &policy_id
94                    && stored_owner == &owner
95                    && self.separation_rules == proposed_rules =>
96            {
97                Ok(None)
98            }
99            _ => Err(DomainError::Conflict {
100                what: "authorization_policy",
101            }),
102        }
103    }
104
105    pub fn decide_authorize(
106        &self,
107        request: AuthorizationRequest,
108        now: OffsetDateTime,
109        ttl: AuthorizationDecisionTtl,
110    ) -> Result<AuthorizationDecisionPlan, DomainError> {
111        let existing = self.decisions_by_request.get(request.id());
112        let approval = request
113            .approval_decision_id()
114            .and_then(|id| self.decisions_by_id.get(id));
115        self.decide_authorize_with_decisions(request, existing, approval, now, ttl)
116    }
117
118    pub fn decide_authorize_with_decisions(
119        &self,
120        request: AuthorizationRequest,
121        existing: Option<&AuthorizationDecision>,
122        approval: Option<&AuthorizationDecision>,
123        now: OffsetDateTime,
124        ttl: AuthorizationDecisionTtl,
125    ) -> Result<AuthorizationDecisionPlan, DomainError> {
126        let policy_id = self.id.clone().ok_or(DomainError::NotFound {
127            what: "authorization_policy",
128        })?;
129        if let Some(existing) = existing {
130            if existing.request() == &request {
131                return Ok(AuthorizationDecisionPlan::new(existing.clone(), None));
132            }
133            return Err(DomainError::Conflict {
134                what: "authorization_request",
135            });
136        }
137
138        let grant = self.matching_grant(&request, now);
139        let owner = self.is_owner(request.principal()) && owner_permits(request.action());
140        let denial = if !owner && grant.is_none() {
141            Some(AuthorizationDenialReason::NoMatchingGrant)
142        } else {
143            self.approval_intent_denial(&request)
144                .or_else(|| self.separation_denial(&request, approval, now))
145        };
146        let requested_until =
147            now.checked_add(ttl.duration())
148                .ok_or(DomainError::InvariantViolated {
149                    reason: "authorization decision expiry exceeds supported time range",
150                })?;
151        let valid_until = grant
152            .and_then(AuthorizationGrant::valid_until)
153            .map_or(requested_until, |grant_until| {
154                grant_until.min(requested_until)
155            });
156        let decision = if let Some(reason) = denial {
157            AuthorizationDecision::deny(request, self.version.next(), reason, now, requested_until)?
158        } else {
159            AuthorizationDecision::allow(
160                request,
161                self.version.next(),
162                grant.map(|value| value.id().clone()),
163                now,
164                valid_until,
165            )?
166        };
167        let event = AuthorizationPolicyEvent::DecisionRecorded {
168            policy_id,
169            decision: decision.clone(),
170        };
171        Ok(AuthorizationDecisionPlan::new(decision, Some(event)))
172    }
173
174    pub fn decide_issue(
175        &self,
176        issuer: &AuthenticatedPrincipal,
177        grant: AuthorizationGrant,
178        now: OffsetDateTime,
179    ) -> Result<Option<AuthorizationPolicyEvent>, DomainError> {
180        grant.validate()?;
181        let policy_id = self.id.clone().ok_or(DomainError::NotFound {
182            what: "authorization_policy",
183        })?;
184        issuer.validate()?;
185        if grant.issued_by() != issuer {
186            return Err(DomainError::InvariantViolated {
187                reason: "authorization grant issuer must be the authenticated principal",
188            });
189        }
190        if let Some(existing) = self.grants.get(grant.id()) {
191            return if existing == &grant {
192                Ok(None)
193            } else {
194                Err(DomainError::Conflict {
195                    what: "authorization_grant",
196                })
197            };
198        }
199        let authority_is_valid = if self.is_owner(issuer) {
200            grant.parent_grant_id().is_none()
201        } else {
202            self.may_delegate(issuer, &grant, now)
203        };
204        if !authority_is_valid {
205            return Err(DomainError::InvariantViolated {
206                reason: "principal cannot delegate the requested authorization grant",
207            });
208        }
209        Ok(Some(AuthorizationPolicyEvent::GrantIssued {
210            policy_id,
211            grant,
212            issued_at: now,
213        }))
214    }
215
216    pub fn decide_revoke(
217        &self,
218        issuer: &AuthenticatedPrincipal,
219        grant_id: &AuthorizationGrantId,
220        reason: AuthorizationRevocationReason,
221        now: OffsetDateTime,
222    ) -> Result<Option<AuthorizationPolicyEvent>, DomainError> {
223        let policy_id = self.id.clone().ok_or(DomainError::NotFound {
224            what: "authorization_policy",
225        })?;
226        let grant = self.grants.get(grant_id).ok_or(DomainError::NotFound {
227            what: "authorization_grant",
228        })?;
229        if self.revoked.contains(grant_id) {
230            return Ok(None);
231        }
232        issuer.validate()?;
233        let allowed = self.is_owner(issuer)
234            || grant.issued_by() == issuer
235            || self.permits(
236                issuer,
237                AuthorizationAction::RevokeAuthorizationGrant,
238                grant.scope(),
239                now,
240            );
241        if !allowed {
242            return Err(DomainError::InvariantViolated {
243                reason: "principal cannot revoke the authorization grant",
244            });
245        }
246        Ok(Some(AuthorizationPolicyEvent::GrantRevoked {
247            policy_id,
248            revocation: AuthorizationRevocation::new(grant_id.clone(), issuer.clone(), reason, now),
249        }))
250    }
251
252    #[must_use]
253    pub fn permits(
254        &self,
255        principal: &AuthenticatedPrincipal,
256        action: AuthorizationAction,
257        scope: &AuthorizationScope,
258        now: OffsetDateTime,
259    ) -> bool {
260        (self.is_owner(principal) && owner_permits(action))
261            || self.grants.values().any(|grant| {
262                grant.grantee() == principal.id()
263                    && grant.permits(action, scope, now)
264                    && self.grant_chain_is_live(grant, now)
265            })
266    }
267
268    pub fn apply(&mut self, event: AuthorizationPolicyEvent) -> Result<(), DomainError> {
269        if let Some(id) = &self.id {
270            if event.policy_id() != id {
271                return Err(DomainError::InvariantViolated {
272                    reason: "authorization event belongs to another policy",
273                });
274            }
275        }
276        match event {
277            AuthorizationPolicyEvent::Opened {
278                policy_id,
279                owner,
280                separation_rules,
281                ..
282            } => {
283                if self.id.is_some() {
284                    return Err(DomainError::AlreadyExists {
285                        what: "authorization_policy",
286                    });
287                }
288                owner.validate()?;
289                if owner.kind() != PrincipalKind::TrustedHost {
290                    return Err(DomainError::InvariantViolated {
291                        reason: "authorization policy owner must be an explicit trusted host",
292                    });
293                }
294                let mapped_rules = separation_rule_map(separation_rules)?;
295                self.id = Some(policy_id);
296                self.owner = Some(owner);
297                self.separation_rules = mapped_rules;
298            }
299            AuthorizationPolicyEvent::GrantIssued {
300                grant, issued_at, ..
301            } => self.apply_grant(grant, issued_at)?,
302            AuthorizationPolicyEvent::GrantRevoked { revocation, .. } => {
303                self.apply_revocation(&revocation)?;
304            }
305            AuthorizationPolicyEvent::DecisionRecorded { decision, .. } => {
306                self.apply_decision(decision)?;
307            }
308        }
309        self.version = self.version.next();
310        Ok(())
311    }
312
313    fn apply_grant(
314        &mut self,
315        grant: AuthorizationGrant,
316        issued_at: OffsetDateTime,
317    ) -> Result<(), DomainError> {
318        if self.id.is_none() {
319            return Err(DomainError::NotFound {
320                what: "authorization_policy",
321            });
322        }
323        if self.grants.contains_key(grant.id()) {
324            return Err(DomainError::AlreadyExists {
325                what: "authorization_grant",
326            });
327        }
328        grant.validate()?;
329        let authority_is_valid = if self.is_owner(grant.issued_by()) {
330            grant.parent_grant_id().is_none()
331        } else {
332            self.may_delegate(grant.issued_by(), &grant, issued_at)
333        };
334        if !authority_is_valid {
335            return Err(DomainError::InvariantViolated {
336                reason: "stored authorization grant has invalid delegation authority",
337            });
338        }
339        self.grants.insert(grant.id().clone(), grant);
340        Ok(())
341    }
342
343    fn apply_revocation(
344        &mut self,
345        revocation: &AuthorizationRevocation,
346    ) -> Result<(), DomainError> {
347        revocation.revoked_by().validate()?;
348        let grant = self
349            .grants
350            .get(revocation.grant_id())
351            .ok_or(DomainError::NotFound {
352                what: "authorization_grant",
353            })?;
354        if self.revoked.contains(revocation.grant_id()) {
355            return Err(DomainError::AlreadyExists {
356                what: "authorization_revocation",
357            });
358        }
359        let allowed = self.is_owner(revocation.revoked_by())
360            || grant.issued_by() == revocation.revoked_by()
361            || self.permits(
362                revocation.revoked_by(),
363                AuthorizationAction::RevokeAuthorizationGrant,
364                grant.scope(),
365                revocation.revoked_at(),
366            );
367        if !allowed {
368            return Err(DomainError::InvariantViolated {
369                reason: "stored authorization revocation has invalid authority",
370            });
371        }
372        self.revoked.insert(revocation.grant_id().clone());
373        Ok(())
374    }
375
376    fn apply_decision(&mut self, decision: AuthorizationDecision) -> Result<(), DomainError> {
377        decision.validate()?;
378        if decision.policy_version() != self.version.next() {
379            return Err(DomainError::InvariantViolated {
380                reason: "authorization decision carries the wrong policy version",
381            });
382        }
383        if self
384            .decisions_by_request
385            .contains_key(decision.request().id())
386            || self.decisions_by_id.contains_key(decision.id())
387        {
388            return Err(DomainError::AlreadyExists {
389                what: "authorization_decision",
390            });
391        }
392        if !self.decision_authority_is_valid(&decision) {
393            return Err(DomainError::InvariantViolated {
394                reason: "stored authorization decision contradicts the active policy",
395            });
396        }
397        self.decisions_by_request
398            .insert(decision.request().id().clone(), decision.clone());
399        self.decisions_by_id.insert(decision.id().clone(), decision);
400        Ok(())
401    }
402
403    pub fn apply_projected(
404        &mut self,
405        event: AuthorizationPolicyEvent,
406        approval: Option<&AuthorizationDecision>,
407    ) -> Result<(), DomainError> {
408        self.apply_projected_with_authorities(event, approval, None)
409    }
410
411    pub fn apply_projected_with_authorities(
412        &mut self,
413        event: AuthorizationPolicyEvent,
414        approval: Option<&AuthorizationDecision>,
415        accepted: Option<&AuthorizationDecision>,
416    ) -> Result<(), DomainError> {
417        let AuthorizationPolicyEvent::DecisionRecorded { decision, .. } = &event else {
418            return self.apply(event);
419        };
420        if self.id.as_ref() != Some(event.policy_id()) {
421            return Err(DomainError::InvariantViolated {
422                reason: "authorization event belongs to another policy",
423            });
424        }
425        decision.validate()?;
426        if decision.policy_version() != self.version.next() {
427            return Err(DomainError::InvariantViolated {
428                reason: "authorization decision carries the wrong policy version",
429            });
430        }
431        if !self.decision_authority_is_valid_with_authorities(decision, approval, accepted) {
432            return Err(DomainError::InvariantViolated {
433                reason: "stored authorization decision contradicts the active policy",
434            });
435        }
436        self.version = self.version.next();
437        Ok(())
438    }
439
440    pub fn restore_version(
441        &mut self,
442        version: AuthorizationPolicyVersion,
443    ) -> Result<(), DomainError> {
444        if version < self.version {
445            return Err(DomainError::InvariantViolated {
446                reason: "authorization projection version precedes its state",
447            });
448        }
449        self.version = version;
450        Ok(())
451    }
452
453    fn may_delegate(
454        &self,
455        issuer: &AuthenticatedPrincipal,
456        child: &AuthorizationGrant,
457        now: OffsetDateTime,
458    ) -> bool {
459        let Some(parent_id) = child.parent_grant_id() else {
460            return false;
461        };
462        self.grants.get(parent_id).is_some_and(|parent| {
463            parent.grantee() == issuer.id()
464                && self.grant_chain_is_live(parent, now)
465                && parent
466                    .actions()
467                    .contains(&AuthorizationAction::IssueAuthorizationGrant)
468                && child.actions().is_subset(parent.actions())
469                && parent.scope().covers(child.scope())
470                && parent.valid_from() <= child.valid_from()
471                && validity_contains(parent.valid_until(), child.valid_until())
472                && parent.delegation_depth().value() > child.delegation_depth().value()
473        })
474    }
475
476    fn matching_grant(
477        &self,
478        request: &AuthorizationRequest,
479        now: OffsetDateTime,
480    ) -> Option<&AuthorizationGrant> {
481        self.grants.values().find(|grant| {
482            grant.grantee() == request.principal().id()
483                && grant.permits(request.action(), request.scope(), now)
484                && self.grant_chain_is_live(grant, now)
485        })
486    }
487
488    fn grant_chain_is_live(&self, grant: &AuthorizationGrant, now: OffsetDateTime) -> bool {
489        if self.revoked.contains(grant.id()) || !grant.is_active_at(now) {
490            return false;
491        }
492        let Some(parent_id) = grant.parent_grant_id() else {
493            return self.is_owner(grant.issued_by());
494        };
495        self.grants.get(parent_id).is_some_and(|parent| {
496            parent.grantee() == grant.issued_by().id() && self.grant_chain_is_live(parent, now)
497        })
498    }
499
500    fn is_owner(&self, principal: &AuthenticatedPrincipal) -> bool {
501        self.owner.as_ref() == Some(principal)
502    }
503
504    fn decision_authority_is_valid(&self, decision: &AuthorizationDecision) -> bool {
505        let approval = decision
506            .request()
507            .approval_decision_id()
508            .and_then(|id| self.decisions_by_id.get(id));
509        let accepted = decision
510            .request()
511            .accepted_work_decision_id()
512            .and_then(|id| self.decisions_by_id.get(id));
513        self.decision_authority_is_valid_with_authorities(decision, approval, accepted)
514    }
515
516    fn decision_authority_is_valid_with_authorities(
517        &self,
518        decision: &AuthorizationDecision,
519        approval: Option<&AuthorizationDecision>,
520        accepted: Option<&AuthorizationDecision>,
521    ) -> bool {
522        let request = decision.request();
523        let now = decision.decided_at();
524        if decision.valid_until() - now > time::Duration::seconds(300) {
525            return false;
526        }
527        let continuation =
528            accepted.is_some_and(|accepted| accepted_work::authority_is_valid(request, accepted));
529        let owner = self.is_owner(request.principal()) && owner_permits(request.action());
530        let grant = decision
531            .grant_id()
532            .and_then(|grant_id| self.grants.get(grant_id));
533        let granted = owner && decision.grant_id().is_none()
534            || grant.is_some_and(|grant| {
535                grant.grantee() == request.principal().id()
536                    && grant.permits(request.action(), request.scope(), now)
537                    && self.grant_chain_is_live(grant, now)
538                    && grant
539                        .valid_until()
540                        .is_none_or(|until| decision.valid_until() <= until)
541            });
542        let separation_denial = self
543            .approval_intent_denial(request)
544            .or_else(|| self.separation_denial(request, approval, now));
545        match decision.kind() {
546            AuthorizationDecisionKind::Allow => {
547                continuation || granted && separation_denial.is_none()
548            }
549            AuthorizationDecisionKind::Deny => {
550                let expected = if !owner && self.matching_grant(request, now).is_none() {
551                    Some(AuthorizationDenialReason::NoMatchingGrant)
552                } else {
553                    separation_denial
554                };
555                decision.grant_id().is_none() && decision.denial_reason() == expected
556            }
557        }
558    }
559
560    pub(super) fn decision_grant_is_live(
561        &self,
562        decision: &AuthorizationDecision,
563        now: OffsetDateTime,
564    ) -> bool {
565        match decision.grant_id() {
566            Some(grant_id) => self
567                .grants
568                .get(grant_id)
569                .is_some_and(|grant| self.grant_chain_is_live(grant, now)),
570            None => {
571                self.is_owner(decision.request().principal())
572                    && owner_permits(decision.request().action())
573            }
574        }
575    }
576}