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