1use crate::{Fact, SearchReplayReportV1, SearchResult, StoredGraphEdge};
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6
7pub const ORIGIN_AUTHORITY_LABEL_V1: &str = "origin_authority_label_v1";
8pub const ORIGIN_AUTHORITY_DECISION_V1: &str = "origin_authority_decision_v1";
9pub const GOVERNED_ACCESS_POLICY_V1: &str = "governed_access_policy_v1";
10
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
13#[serde(transparent)]
14pub struct SubjectPrincipalV1(pub String);
15
16impl SubjectPrincipalV1 {
17 pub fn new(value: impl Into<String>) -> Result<Self, String> {
18 let value = value.into();
19 if value.trim().is_empty() {
20 Err("subject principal must be non-empty".into())
21 } else {
22 Ok(Self(value))
23 }
24 }
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
29#[serde(transparent)]
30pub struct CallerPrincipalV1(pub String);
31
32impl CallerPrincipalV1 {
33 pub fn new(value: impl Into<String>) -> Result<Self, String> {
34 let value = value.into();
35 if value.trim().is_empty() {
36 Err("caller principal must be non-empty".into())
37 } else {
38 Ok(Self(value))
39 }
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
45pub struct AudienceV1(pub Vec<String>);
46
47impl AudienceV1 {
48 pub fn new(mut values: Vec<String>) -> Self {
49 values.retain(|value| !value.trim().is_empty());
50 values.sort();
51 values.dedup();
52 Self(values)
53 }
54
55 fn intersects(&self, other: &Self) -> bool {
56 self.0.iter().any(|value| other.0.contains(value))
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
62pub struct NamespaceScopeV1 {
63 pub namespace: String,
64 pub domain: Option<String>,
65 pub workspace_id: Option<String>,
66 pub repo_id: Option<String>,
67}
68
69impl NamespaceScopeV1 {
70 pub fn exact(namespace: impl Into<String>) -> Self {
71 Self {
72 namespace: namespace.into(),
73 ..Self::default()
74 }
75 }
76
77 pub fn is_bound(&self) -> bool {
78 !self.namespace.trim().is_empty()
79 }
80
81 fn permits_namespace(&self, namespace: &str) -> bool {
82 self.is_bound() && self.namespace == namespace
83 }
84
85 fn permits_scope(&self, requested: &Self) -> bool {
86 self.is_bound()
87 && requested.is_bound()
88 && self.namespace == requested.namespace
89 && optional_scope_matches(&self.domain, &requested.domain)
90 && optional_scope_matches(&self.workspace_id, &requested.workspace_id)
91 && optional_scope_matches(&self.repo_id, &requested.repo_id)
92 }
93}
94
95fn optional_scope_matches(resource: &Option<String>, requested: &Option<String>) -> bool {
96 match (resource, requested) {
97 (Some(resource), Some(requested)) => resource == requested,
98 (None, None) => true,
101 _ => false,
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110pub struct DelegationElevationLeaseV1 {
111 pub lease_id: String,
112 pub delegator: SubjectPrincipalV1,
113 pub delegatee: CallerPrincipalV1,
114 pub purposes: Vec<GovernedAccessPurposeV1>,
115 pub scope: NamespaceScopeV1,
116 pub audience: AudienceV1,
117 pub expires_at: String,
118 #[serde(default)]
119 pub revoked: bool,
120 pub elevation: bool,
121}
122
123impl DelegationElevationLeaseV1 {
124 pub fn delegation(
125 lease_id: impl Into<String>,
126 delegator: impl Into<String>,
127 delegatee: impl Into<String>,
128 purposes: Vec<GovernedAccessPurposeV1>,
129 scope: NamespaceScopeV1,
130 audience: Vec<String>,
131 expires_at: impl Into<String>,
132 ) -> Self {
133 Self {
134 lease_id: lease_id.into(),
135 delegator: SubjectPrincipalV1(delegator.into()),
136 delegatee: CallerPrincipalV1(delegatee.into()),
137 purposes,
138 scope,
139 audience: AudienceV1::new(audience),
140 expires_at: expires_at.into(),
141 revoked: false,
142 elevation: false,
143 }
144 }
145
146 pub fn elevation(
147 lease_id: impl Into<String>,
148 subject: impl Into<String>,
149 caller: impl Into<String>,
150 scope: NamespaceScopeV1,
151 expires_at: impl Into<String>,
152 ) -> Self {
153 Self {
154 lease_id: lease_id.into(),
155 delegator: SubjectPrincipalV1(subject.into()),
156 delegatee: CallerPrincipalV1(caller.into()),
157 purposes: vec![GovernedAccessPurposeV1::Admin],
158 scope,
159 audience: AudienceV1::default(),
160 expires_at: expires_at.into(),
161 revoked: false,
162 elevation: true,
163 }
164 }
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
168#[serde(rename_all = "snake_case")]
169pub enum OriginRiskV1 {
170 Low,
171 Medium,
172 High,
173 Critical,
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
177#[serde(rename_all = "snake_case")]
178pub enum AuthorityScopeV1 {
179 Denied,
180 PrincipalOnly,
181 Audience,
182 Universal,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186pub struct AuthorityScopesV1 {
187 pub recall: AuthorityScopeV1,
188 pub assertion: AuthorityScopeV1,
189 pub action: AuthorityScopeV1,
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
193#[serde(rename_all = "snake_case")]
194pub enum OriginClassV1 {
195 UserStatement,
196 ExternalEvidence,
197 ToolOutput,
198 OperatorSystem,
199 Derived,
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
203#[serde(rename_all = "snake_case")]
204pub enum ElevationRequirementV1 {
205 Never,
206 ExplicitOperatorApproval,
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
210#[serde(rename_all = "snake_case")]
211pub enum RevocationStatusV1 {
212 Active,
213 PendingReview,
214 Revoked,
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
218#[serde(rename_all = "snake_case")]
219pub enum OriginDerivationKindV1 {
220 Summary,
221 Rephrase,
222 TrustedToolEcho,
223 Corroboration,
224 Other,
225}
226
227#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
229pub struct OriginAuthorityLabelV1 {
230 pub schema_version: String,
231 pub origin_class: OriginClassV1,
232 pub origin_principal: String,
233 pub origin_channel: String,
234 pub origin_digest: String,
235 pub risk: OriginRiskV1,
236 pub scopes: AuthorityScopesV1,
237 pub elevation: ElevationRequirementV1,
238 pub revocation_reference: Option<String>,
239 pub revocation_status: RevocationStatusV1,
240 pub audience: Vec<String>,
241 pub ancestor_digests: Vec<String>,
242 pub derivation_kind: Option<OriginDerivationKindV1>,
243 #[serde(default)]
246 pub subject_principal: Option<SubjectPrincipalV1>,
247 #[serde(default)]
249 pub audience_at_write: AudienceV1,
250 #[serde(default)]
252 pub resource_scope: NamespaceScopeV1,
253 #[serde(default = "default_policy_version")]
254 pub policy_version: String,
255 #[serde(default)]
256 pub policy_digest: String,
257}
258
259fn default_policy_version() -> String {
260 GOVERNED_ACCESS_POLICY_V1.into()
261}
262
263impl OriginAuthorityLabelV1 {
264 #[allow(clippy::too_many_arguments)]
265 pub fn new(
266 origin_class: OriginClassV1,
267 origin_principal: impl Into<String>,
268 origin_channel: impl Into<String>,
269 origin_digest: impl Into<String>,
270 risk: OriginRiskV1,
271 scopes: AuthorityScopesV1,
272 elevation: ElevationRequirementV1,
273 revocation_reference: Option<String>,
274 revocation_status: RevocationStatusV1,
275 mut audience: Vec<String>,
276 ) -> Result<Self, String> {
277 let origin_principal = origin_principal.into();
278 let origin_channel = origin_channel.into();
279 let origin_digest = origin_digest.into();
280 if origin_principal.trim().is_empty()
281 || origin_channel.trim().is_empty()
282 || origin_digest.trim().is_empty()
283 {
284 return Err("origin principal, channel, and digest must be non-empty".into());
285 }
286 if revocation_status != RevocationStatusV1::Active
287 && revocation_reference.as_deref().map_or(true, str::is_empty)
288 {
289 return Err("non-active origin labels require a revocation reference".into());
290 }
291 audience.retain(|value| !value.trim().is_empty());
292 audience.sort();
293 audience.dedup();
294 let mut label = Self {
295 schema_version: ORIGIN_AUTHORITY_LABEL_V1.into(),
296 origin_class,
297 origin_principal,
298 origin_channel,
299 origin_digest,
300 risk,
301 scopes,
302 elevation,
303 revocation_reference,
304 revocation_status,
305 audience: audience.clone(),
306 ancestor_digests: Vec::new(),
307 derivation_kind: None,
308 subject_principal: None,
309 audience_at_write: AudienceV1::new(audience.clone()),
310 resource_scope: NamespaceScopeV1::default(),
311 policy_version: default_policy_version(),
312 policy_digest: String::new(),
313 };
314 label.refresh_policy_digest()?;
315 Ok(label)
316 }
317
318 pub fn with_subject_principal(mut self, subject: SubjectPrincipalV1) -> Self {
319 self.subject_principal = Some(subject);
320 self.refresh_policy_digest()
321 .expect("origin policy label serialization is infallible");
322 self
323 }
324
325 pub fn with_resource_scope(mut self, scope: NamespaceScopeV1) -> Self {
326 self.resource_scope = scope;
327 self.refresh_policy_digest()
328 .expect("origin policy label serialization is infallible");
329 self
330 }
331
332 pub(crate) fn bind_resource_scope(mut self, scope: NamespaceScopeV1) -> Result<Self, String> {
333 if !self.resource_scope.is_bound() {
334 self.resource_scope = scope;
335 }
336 self.refresh_policy_digest()?;
337 Ok(self)
338 }
339
340 fn effective_audience(&self) -> AudienceV1 {
341 if self.audience_at_write.0.is_empty() {
342 AudienceV1::new(self.audience.clone())
343 } else {
344 self.audience_at_write.clone()
345 }
346 }
347
348 fn effective_subject(&self) -> SubjectPrincipalV1 {
349 self.subject_principal
350 .clone()
351 .unwrap_or_else(|| SubjectPrincipalV1(self.origin_principal.clone()))
352 }
353
354 fn refresh_policy_digest(&mut self) -> Result<(), String> {
355 self.policy_digest = self.computed_policy_digest()?;
356 Ok(())
357 }
358
359 fn computed_policy_digest(&self) -> Result<String, String> {
360 let bytes = serde_json::to_vec(&(
361 &self.policy_version,
362 &self.origin_principal,
363 &self.subject_principal,
364 self.effective_audience(),
365 &self.resource_scope,
366 &self.scopes,
367 self.elevation,
368 ))
369 .map_err(|error| format!("serialize access policy label: {error}"))?;
370 Ok(format!("blake3:{}", blake3::hash(&bytes).to_hex()))
371 }
372
373 pub fn derive(
375 ancestors: &[Self],
376 kind: OriginDerivationKindV1,
377 derived_content_digest: impl Into<String>,
378 ) -> Result<Self, String> {
379 if ancestors.is_empty() {
380 return Err("derived origin requires at least one ancestor".into());
381 }
382 let derived_content_digest = derived_content_digest.into();
383 if derived_content_digest.trim().is_empty() {
384 return Err("derived content digest must be non-empty".into());
385 }
386 let risk = ancestors.iter().map(|label| label.risk).max().unwrap();
387 let scopes = AuthorityScopesV1 {
388 recall: ancestors
389 .iter()
390 .map(|label| label.scopes.recall)
391 .min()
392 .unwrap(),
393 assertion: ancestors
394 .iter()
395 .map(|label| label.scopes.assertion)
396 .min()
397 .unwrap(),
398 action: ancestors
399 .iter()
400 .map(|label| label.scopes.action)
401 .min()
402 .unwrap(),
403 };
404 let principal = if ancestors
405 .iter()
406 .all(|label| label.origin_principal == ancestors[0].origin_principal)
407 {
408 ancestors[0].origin_principal.clone()
409 } else {
410 "multiple-origins".into()
411 };
412 let mut audience = ancestors[0].audience.clone();
413 for ancestor in &ancestors[1..] {
414 audience.retain(|entry| ancestor.audience.contains(entry));
415 }
416 let mut ancestor_digests = ancestors
417 .iter()
418 .map(label_digest)
419 .collect::<Result<Vec<_>, _>>()?;
420 ancestor_digests.sort();
421 let digest_input = serde_json::to_vec(&(
422 "origin-derivation-v1",
423 kind,
424 &derived_content_digest,
425 &ancestor_digests,
426 risk,
427 &scopes,
428 ))
429 .map_err(|error| format!("serialize origin derivation: {error}"))?;
430 let subject_principal = if ancestors
431 .iter()
432 .all(|label| label.effective_subject() == ancestors[0].effective_subject())
433 {
434 Some(ancestors[0].effective_subject())
435 } else {
436 None
437 };
438 let resource_scope = if ancestors
439 .iter()
440 .all(|label| label.resource_scope == ancestors[0].resource_scope)
441 {
442 ancestors[0].resource_scope.clone()
443 } else {
444 NamespaceScopeV1::default()
445 };
446 let mut label = Self {
447 schema_version: ORIGIN_AUTHORITY_LABEL_V1.into(),
448 origin_class: OriginClassV1::Derived,
449 origin_principal: principal,
450 origin_channel: format!("derived:{kind:?}").to_lowercase(),
451 origin_digest: format!("blake3:{}", blake3::hash(&digest_input).to_hex()),
452 risk,
453 scopes,
454 elevation: ElevationRequirementV1::Never,
457 revocation_reference: ancestors
458 .iter()
459 .find_map(|label| label.revocation_reference.clone()),
460 revocation_status: ancestors
461 .iter()
462 .map(|label| label.revocation_status)
463 .max_by_key(|status| match status {
464 RevocationStatusV1::Active => 0,
465 RevocationStatusV1::PendingReview => 1,
466 RevocationStatusV1::Revoked => 2,
467 })
468 .unwrap(),
469 audience: audience.clone(),
470 ancestor_digests,
471 derivation_kind: Some(kind),
472 subject_principal,
473 audience_at_write: AudienceV1::new(audience.clone()),
474 resource_scope,
475 policy_version: default_policy_version(),
476 policy_digest: String::new(),
477 };
478 label.refresh_policy_digest()?;
479 Ok(label)
480 }
481
482 pub fn operator_system(principal: &str, channel: &str) -> Self {
483 Self::new(
484 OriginClassV1::OperatorSystem,
485 principal,
486 channel,
487 format!(
488 "blake3:{}",
489 blake3::hash(format!("operator:{principal}:{channel}").as_bytes()).to_hex()
490 ),
491 OriginRiskV1::Low,
492 AuthorityScopesV1 {
493 recall: AuthorityScopeV1::Universal,
494 assertion: AuthorityScopeV1::Universal,
495 action: AuthorityScopeV1::Universal,
496 },
497 ElevationRequirementV1::ExplicitOperatorApproval,
498 None,
499 RevocationStatusV1::Active,
500 vec![principal.to_string()],
501 )
502 .expect("operator origin constants are valid")
503 }
504}
505
506pub fn label_digest(label: &OriginAuthorityLabelV1) -> Result<String, String> {
507 let bytes =
508 serde_json::to_vec(label).map_err(|error| format!("serialize origin label: {error}"))?;
509 Ok(format!("blake3:{}", blake3::hash(&bytes).to_hex()))
510}
511
512#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
513pub struct OriginAuthorityRecordV1 {
514 pub fact_id: String,
515 pub label: OriginAuthorityLabelV1,
516 pub label_digest: String,
517 pub recorded_at: String,
518}
519
520#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
521#[serde(rename_all = "snake_case")]
522pub enum GovernedAccessPurposeV1 {
523 Recall,
524 Assertion,
525 Action,
526 Export,
527 Replay,
528 Admin,
529}
530
531#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
532pub struct GovernedAccessRequestV1 {
533 pub principal: String,
535 pub audience: String,
537 pub purpose: GovernedAccessPurposeV1,
538 pub namespace: String,
540 pub caller: CallerPrincipalV1,
541 pub subject: SubjectPrincipalV1,
542 pub audiences: AudienceV1,
543 pub scope: NamespaceScopeV1,
544 pub delegation_or_elevation: Option<DelegationElevationLeaseV1>,
545 pub policy_version: String,
546 pub policy_digest: String,
547}
548
549impl GovernedAccessRequestV1 {
550 pub fn new(
551 principal: impl Into<String>,
552 audience: impl Into<String>,
553 purpose: GovernedAccessPurposeV1,
554 namespace: impl Into<String>,
555 ) -> Self {
556 let principal = principal.into();
557 let audience = audience.into();
558 let namespace = namespace.into();
559 Self::for_principals(
560 CallerPrincipalV1(principal.clone()),
561 SubjectPrincipalV1(principal),
562 vec![audience],
563 purpose,
564 NamespaceScopeV1::exact(namespace),
565 )
566 }
567
568 pub fn for_principals(
569 caller: CallerPrincipalV1,
570 subject: SubjectPrincipalV1,
571 audience: Vec<String>,
572 purpose: GovernedAccessPurposeV1,
573 scope: NamespaceScopeV1,
574 ) -> Self {
575 let audiences = AudienceV1::new(audience);
576 let policy_version = GOVERNED_ACCESS_POLICY_V1.to_string();
577 let policy_digest = access_request_digest(
578 &caller,
579 &subject,
580 &audiences,
581 purpose,
582 &scope,
583 None,
584 &policy_version,
585 );
586 Self {
587 principal: caller.0.clone(),
588 audience: audiences.0.first().cloned().unwrap_or_default(),
589 purpose,
590 namespace: scope.namespace.clone(),
591 caller,
592 subject,
593 audiences,
594 scope,
595 delegation_or_elevation: None,
596 policy_version,
597 policy_digest,
598 }
599 }
600
601 pub fn with_delegation_or_elevation(mut self, lease: DelegationElevationLeaseV1) -> Self {
602 self.delegation_or_elevation = Some(lease);
603 self.policy_digest = access_request_digest(
604 &self.caller,
605 &self.subject,
606 &self.audiences,
607 self.purpose,
608 &self.scope,
609 self.delegation_or_elevation.as_ref(),
610 &self.policy_version,
611 );
612 self
613 }
614
615 pub fn with_purpose(mut self, purpose: GovernedAccessPurposeV1) -> Self {
616 self.purpose = purpose;
617 self.policy_digest = access_request_digest(
618 &self.caller,
619 &self.subject,
620 &self.audiences,
621 self.purpose,
622 &self.scope,
623 self.delegation_or_elevation.as_ref(),
624 &self.policy_version,
625 );
626 self
627 }
628
629 pub fn with_audiences(mut self, audience: Vec<String>) -> Self {
630 self.audiences = AudienceV1::new(audience);
631 self.audience = self.audiences.0.first().cloned().unwrap_or_default();
632 self.policy_digest = access_request_digest(
633 &self.caller,
634 &self.subject,
635 &self.audiences,
636 self.purpose,
637 &self.scope,
638 self.delegation_or_elevation.as_ref(),
639 &self.policy_version,
640 );
641 self
642 }
643
644 pub fn with_lease_revoked(mut self) -> Self {
645 if let Some(lease) = &mut self.delegation_or_elevation {
646 lease.revoked = true;
647 }
648 self.policy_digest = access_request_digest(
649 &self.caller,
650 &self.subject,
651 &self.audiences,
652 self.purpose,
653 &self.scope,
654 self.delegation_or_elevation.as_ref(),
655 &self.policy_version,
656 );
657 self
658 }
659}
660
661#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
662pub struct OriginAuthorityDecisionV1 {
663 pub schema_version: String,
664 pub fact_id: String,
665 pub principal: String,
666 pub audience_compat: String,
667 pub purpose: GovernedAccessPurposeV1,
668 pub allowed: bool,
669 pub reasons: Vec<String>,
670 pub origin_label_digest: Option<String>,
671 pub revocation_reference: Option<String>,
672 pub decision_digest: String,
673 pub caller: CallerPrincipalV1,
674 pub subject: SubjectPrincipalV1,
675 pub audience: AudienceV1,
676 pub scope: NamespaceScopeV1,
677 pub policy_version: String,
678 pub policy_digest: String,
679 pub outcome: PolicyDecisionV1,
680 pub lease_id: Option<String>,
681}
682
683#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
685#[serde(rename_all = "snake_case")]
686pub enum PolicyDecisionV1 {
687 Allow,
688 Deny,
689}
690
691#[derive(Debug, Clone, Serialize, Deserialize)]
692pub struct GovernedFactAccessV1 {
693 pub fact: Option<Fact>,
694 pub decision: OriginAuthorityDecisionV1,
695 pub origin: Option<OriginAuthorityRecordV1>,
696}
697
698#[derive(Debug, Clone, Serialize, Deserialize)]
699pub struct GovernedSearchResponseV1 {
700 pub results: Vec<SearchResult>,
701 pub decisions: Vec<OriginAuthorityDecisionV1>,
702}
703
704#[derive(Debug, Clone, Serialize, Deserialize)]
705pub struct GovernedFactListResponseV1 {
706 pub facts: Vec<Fact>,
707 pub decisions: Vec<OriginAuthorityDecisionV1>,
708}
709
710#[derive(Debug, Clone, Serialize, Deserialize)]
711pub struct GovernedGraphResponseV1 {
712 pub edges: Vec<StoredGraphEdge>,
713 pub decisions: Vec<OriginAuthorityDecisionV1>,
714}
715
716#[derive(Debug, Clone, Serialize, Deserialize)]
717pub struct GovernedReplayResponseV1 {
718 pub replay: SearchReplayReportV1,
719 pub allowed_result_ids: Vec<String>,
720 pub decisions: Vec<OriginAuthorityDecisionV1>,
721}
722
723#[derive(Debug, Clone, Serialize, Deserialize)]
724pub struct GovernedStateResolutionResponseV1 {
725 pub response: crate::ResolvedMemoryAnswerV1,
726 pub decisions: Vec<OriginAuthorityDecisionV1>,
727}
728
729#[derive(Debug, Clone, Serialize, Deserialize)]
732pub struct GovernedProjectionResponseV1<T> {
733 pub items: Vec<T>,
734 pub decisions: Vec<OriginAuthorityDecisionV1>,
735}
736
737pub(crate) fn decide(
738 fact_id: &str,
739 fact_namespace: Option<&str>,
740 origin: Option<&OriginAuthorityRecordV1>,
741 dynamic_revocation: Option<&str>,
742 request: &GovernedAccessRequestV1,
743) -> OriginAuthorityDecisionV1 {
744 let Some(record) = origin else {
745 return evaluate_governed_access_v1(
746 fact_id,
747 fact_namespace,
748 None,
749 dynamic_revocation,
750 request,
751 );
752 };
753 if label_digest(&record.label).ok().as_deref() != Some(record.label_digest.as_str()) {
754 let mut inconsistent = record.label.clone();
757 inconsistent.policy_digest.clear();
758 return evaluate_governed_access_v1(
759 fact_id,
760 fact_namespace,
761 Some(&inconsistent),
762 dynamic_revocation,
763 request,
764 );
765 }
766 evaluate_governed_access_v1(
767 fact_id,
768 fact_namespace,
769 Some(&record.label),
770 dynamic_revocation,
771 request,
772 )
773}
774
775pub fn evaluate_governed_access_v1(
779 resource_id: &str,
780 resource_namespace: Option<&str>,
781 label: Option<&OriginAuthorityLabelV1>,
782 dynamic_revocation: Option<&str>,
783 request: &GovernedAccessRequestV1,
784) -> OriginAuthorityDecisionV1 {
785 let mut reasons = Vec::new();
786 let mut allowed = true;
787 if request.policy_version != GOVERNED_ACCESS_POLICY_V1
788 || request.policy_digest
789 != access_request_digest(
790 &request.caller,
791 &request.subject,
792 &request.audiences,
793 request.purpose,
794 &request.scope,
795 request.delegation_or_elevation.as_ref(),
796 &request.policy_version,
797 )
798 || request.principal != request.caller.0
799 || request.namespace != request.scope.namespace
800 || request.audience != request.audiences.0.first().cloned().unwrap_or_default()
801 || request.caller.0.trim().is_empty()
802 || request.subject.0.trim().is_empty()
803 || request.audiences.0.is_empty()
804 || !request.scope.is_bound()
805 {
806 allowed = false;
807 reasons.push("invalid_access_request".into());
808 }
809 let Some(label) = label else {
810 allowed = false;
811 reasons.push("origin_absent".into());
812 return finish_decision(
813 resource_id,
814 request,
815 allowed,
816 reasons,
817 None,
818 dynamic_revocation,
819 );
820 };
821 if label.policy_version != GOVERNED_ACCESS_POLICY_V1
822 || label.computed_policy_digest().ok().as_deref() != Some(label.policy_digest.as_str())
823 {
824 allowed = false;
825 reasons.push("policy_label_inconsistent".into());
826 }
827 if resource_namespace != Some(request.scope.namespace.as_str())
828 || !label
829 .resource_scope
830 .permits_namespace(request.scope.namespace.as_str())
831 || !label.resource_scope.permits_scope(&request.scope)
832 {
833 allowed = false;
834 reasons.push("namespace_scope_mismatch".into());
835 }
836 let revocation_reference = dynamic_revocation
837 .map(str::to_string)
838 .or_else(|| label.revocation_reference.clone());
839 if dynamic_revocation.is_some() || label.revocation_status != RevocationStatusV1::Active {
840 allowed = false;
841 reasons.push("origin_revoked_or_pending".into());
842 }
843 let lease = request.delegation_or_elevation.as_ref();
844 let valid_delegation = validate_lease(lease, request, false, &mut reasons);
845 let valid_elevation = validate_lease(lease, request, true, &mut reasons);
846 if request.caller.0 != request.subject.0 && !valid_delegation && !valid_elevation {
847 allowed = false;
848 reasons.push("caller_subject_delegation_required".into());
849 }
850 let label_audience = label.effective_audience();
851 if !request.audiences.intersects(&label_audience) {
852 allowed = false;
853 reasons.push("audience_intersection_empty".into());
854 }
855 if label.origin_class == OriginClassV1::Derived
856 && label.subject_principal.is_none()
857 && label.origin_principal == "multiple-origins"
858 {
859 allowed = false;
860 reasons.push("cross_principal_derived_subject_ambiguous".into());
861 }
862 if request.purpose == GovernedAccessPurposeV1::Admin {
863 if !valid_elevation {
864 allowed = false;
865 reasons.push("admin_elevation_required".into());
866 }
867 } else {
868 let scope = match request.purpose {
869 GovernedAccessPurposeV1::Recall => label.scopes.recall,
870 GovernedAccessPurposeV1::Assertion => label.scopes.assertion,
871 GovernedAccessPurposeV1::Action => label.scopes.action,
872 GovernedAccessPurposeV1::Export | GovernedAccessPurposeV1::Replay => {
873 label.scopes.recall
874 }
875 GovernedAccessPurposeV1::Admin => unreachable!("admin is handled above"),
876 };
877 let in_scope = match scope {
878 AuthorityScopeV1::Denied => false,
879 AuthorityScopeV1::PrincipalOnly => request.caller.0 == label.origin_principal,
880 AuthorityScopeV1::Audience => request.audiences.intersects(&label_audience),
881 AuthorityScopeV1::Universal => true,
882 };
883 if !in_scope {
884 allowed = false;
885 reasons.push("scope_or_principal_denied".into());
886 }
887 }
888 if reasons.is_empty() {
889 reasons.push("origin_authority_satisfied".into());
890 }
891 finish_decision(
892 resource_id,
893 request,
894 allowed,
895 reasons,
896 label_digest(label).ok(),
897 revocation_reference.as_deref(),
898 )
899}
900
901fn validate_lease(
902 lease: Option<&DelegationElevationLeaseV1>,
903 request: &GovernedAccessRequestV1,
904 require_elevation: bool,
905 reasons: &mut Vec<String>,
906) -> bool {
907 let Some(lease) = lease else {
908 return false;
909 };
910 if lease.elevation != require_elevation {
911 return false;
912 }
913 if lease.revoked {
914 reasons.push("delegation_revoked".into());
915 return false;
916 }
917 let expiry = DateTime::parse_from_rfc3339(&lease.expires_at)
918 .ok()
919 .map(|value| value.with_timezone(&Utc));
920 if expiry.map_or(true, |expiry| expiry <= Utc::now()) {
921 reasons.push("delegation_expired".into());
922 return false;
923 }
924 if lease.lease_id.trim().is_empty()
925 || lease.delegator != request.subject
926 || lease.delegatee != request.caller
927 || !lease.scope.permits_scope(&request.scope)
928 || (!require_elevation && !lease.purposes.contains(&request.purpose))
929 || (!lease.audience.0.is_empty() && !request.audiences.intersects(&lease.audience))
930 {
931 reasons.push("delegation_scope_denied".into());
932 return false;
933 }
934 reasons.push("untrusted_caller_carried_lease".into());
935 false
936}
937
938fn access_request_digest(
939 caller: &CallerPrincipalV1,
940 subject: &SubjectPrincipalV1,
941 audiences: &AudienceV1,
942 purpose: GovernedAccessPurposeV1,
943 scope: &NamespaceScopeV1,
944 lease: Option<&DelegationElevationLeaseV1>,
945 policy_version: &str,
946) -> String {
947 let bytes = serde_json::to_vec(&(
948 policy_version,
949 caller,
950 subject,
951 audiences,
952 purpose,
953 scope,
954 lease,
955 ))
956 .expect("access policy request serialization cannot fail");
957 format!("blake3:{}", blake3::hash(&bytes).to_hex())
958}
959
960fn finish_decision(
961 fact_id: &str,
962 request: &GovernedAccessRequestV1,
963 allowed: bool,
964 reasons: Vec<String>,
965 origin_label_digest: Option<String>,
966 revocation_reference: Option<&str>,
967) -> OriginAuthorityDecisionV1 {
968 let digest_bytes = serde_json::to_vec(&(
969 ORIGIN_AUTHORITY_DECISION_V1,
970 fact_id,
971 request,
972 allowed,
973 &reasons,
974 &origin_label_digest,
975 revocation_reference,
976 ))
977 .expect("decision contract serialization cannot fail");
978 OriginAuthorityDecisionV1 {
979 schema_version: ORIGIN_AUTHORITY_DECISION_V1.into(),
980 fact_id: fact_id.into(),
981 principal: request.principal.clone(),
982 audience_compat: request.audience.clone(),
983 purpose: request.purpose,
984 allowed,
985 reasons,
986 origin_label_digest,
987 revocation_reference: revocation_reference.map(str::to_string),
988 decision_digest: format!("blake3:{}", blake3::hash(&digest_bytes).to_hex()),
989 caller: request.caller.clone(),
990 subject: request.subject.clone(),
991 audience: request.audiences.clone(),
992 scope: request.scope.clone(),
993 policy_version: request.policy_version.clone(),
994 policy_digest: request.policy_digest.clone(),
995 outcome: if allowed {
996 PolicyDecisionV1::Allow
997 } else {
998 PolicyDecisionV1::Deny
999 },
1000 lease_id: request
1001 .delegation_or_elevation
1002 .as_ref()
1003 .map(|lease| lease.lease_id.clone()),
1004 }
1005}