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}