1use std::collections::BTreeMap;
51use std::path::Path;
52use std::sync::OnceLock;
53
54use serde::{Deserialize, Serialize};
55use zeroize::Zeroizing;
56
57use super::register::{self, Register};
58
59pub(crate) const VALIDITY_SECONDS: i64 = 10 * 60;
61
62pub(crate) const MAX_CAPABILITY_TTL_SECONDS: i64 = 24 * 60 * 60;
64
65const CAPABILITY_PREFIX: &str = "lcp1_";
67const REFERENCE_PREFIX: &str = "presenter:";
69const MAX_CAPABILITY_FILE_BYTES: u64 = 128;
71
72const MAX_PRESENTERS_PER_SOURCE: usize = 16;
75const MAX_PROCESS_PRESENTERS_PER_SOURCE: usize = 16;
78const MAX_IDENTITIES_PER_BINDING: usize = 8;
80
81#[cfg(not(unix))]
84const LOCAL_OPERATOR: &str = "local-operator";
85
86#[must_use]
92pub(crate) fn verified_principal() -> String {
93 #[cfg(unix)]
94 {
95 let uid = unsafe { libc::geteuid() };
97 format!("os-user:{uid}")
98 }
99 #[cfg(not(unix))]
100 {
101 LOCAL_OPERATOR.to_owned()
102 }
103}
104
105#[derive(Clone)]
114pub struct PresenterCapability(Zeroizing<[u8; 32]>);
115
116impl std::fmt::Debug for PresenterCapability {
117 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 formatter.write_str("PresenterCapability(<redacted>)")
119 }
120}
121
122fn hex_nibble(digit: u8) -> Option<u8> {
123 match digit {
124 b'0'..=b'9' => Some(digit - b'0'),
125 b'a'..=b'f' => Some(digit - b'a' + 10),
126 _ => None,
127 }
128}
129
130impl PresenterCapability {
131 fn mint() -> Option<Self> {
132 let mut secret = Zeroizing::new([0u8; 32]);
133 getrandom::fill(&mut *secret).ok()?;
134 Some(Self(secret))
135 }
136
137 #[must_use]
140 pub fn parse(text: &str) -> Option<Self> {
141 let digits = text.strip_prefix(CAPABILITY_PREFIX)?.as_bytes();
142 if digits.len() != 64 {
143 return None;
144 }
145 let mut secret = Zeroizing::new([0u8; 32]);
146 for (index, pair) in digits.as_chunks::<2>().0.iter().enumerate() {
147 secret[index] = (hex_nibble(pair[0])? << 4) | hex_nibble(pair[1])?;
148 }
149 Some(Self(secret))
150 }
151
152 #[must_use]
154 pub fn expose(&self) -> Zeroizing<String> {
155 const DIGITS: &[u8; 16] = b"0123456789abcdef";
156 let mut text = Zeroizing::new(String::with_capacity(CAPABILITY_PREFIX.len() + 64));
157 text.push_str(CAPABILITY_PREFIX);
158 for byte in self.0.iter() {
159 text.push(char::from(DIGITS[usize::from(byte >> 4)]));
160 text.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
161 }
162 text
163 }
164
165 fn root_verifier(&self) -> Zeroizing<[u8; 32]> {
168 Zeroizing::new(blake3::derive_key(
169 "leanctx 2026-10 presenter root verifier v1",
170 &*self.0,
171 ))
172 }
173
174 fn id(&self) -> String {
176 let mut hash = blake3::Hasher::new();
177 hash.update(b"leanctx-presenter-id-v1\0");
178 hash.update(&*self.root_verifier());
179 hash.finalize().to_hex().to_string()
180 }
181
182 #[must_use]
186 pub fn reference(&self) -> String {
187 format!("{REFERENCE_PREFIX}{}", &self.id()[..32])
188 }
189
190 fn source_verifier(&self, binding: &PresentedBinding, id: &str) -> blake3::Hash {
194 let mut hash = blake3::Hasher::new_keyed(&self.0);
195 hash.update(b"leanctx-presenter-source-v1\0");
196 binding.absorb_source(&mut hash);
197 hash.update(id.as_bytes());
198 hash.finalize()
199 }
200}
201
202static PROCESS_SESSION: OnceLock<Option<PresenterCapability>> = OnceLock::new();
206
207fn process_session() -> Option<&'static PresenterCapability> {
208 PROCESS_SESSION
209 .get_or_init(PresenterCapability::mint)
210 .as_ref()
211}
212
213fn is_process_session(id: &str) -> bool {
214 PROCESS_SESSION
215 .get()
216 .and_then(Option::as_ref)
217 .is_some_and(|own| own.id() == id)
218}
219
220tokio::task_local! {
221 static REQUEST_PRESENTER: Option<PresenterCapability>;
224}
225
226pub fn with_presenter<T>(
229 capability: Option<PresenterCapability>,
230 operation: impl FnOnce() -> T,
231) -> T {
232 REQUEST_PRESENTER.sync_scope(capability, operation)
233}
234
235pub async fn presenter_scope<F: std::future::Future>(
237 capability: Option<PresenterCapability>,
238 request: F,
239) -> F::Output {
240 REQUEST_PRESENTER.scope(capability, request).await
241}
242
243#[must_use]
248pub fn request_presenter() -> Option<PresenterCapability> {
249 REQUEST_PRESENTER.try_with(Clone::clone).ok().flatten()
250}
251
252fn with_current<T>(use_capability: impl FnOnce(&PresenterCapability) -> T) -> Option<T> {
255 let shown = REQUEST_PRESENTER.try_with(Clone::clone).ok().flatten();
256 match shown {
257 Some(capability) => Some(use_capability(&capability)),
258 None => process_session().map(use_capability),
259 }
260}
261
262#[must_use]
265pub(crate) fn current_presenter_reference() -> Option<String> {
266 with_current(PresenterCapability::reference)
267}
268
269#[must_use]
275pub fn shown_presenter_reference() -> Option<String> {
276 let shown = REQUEST_PRESENTER.try_with(Clone::clone).ok().flatten()?;
277 let id = shown.id();
278 (!is_process_session(&id) && lifetime_end(&id, now()).is_some()).then(|| shown.reference())
279}
280
281#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
283#[serde(deny_unknown_fields)]
284struct PresenterRecord {
285 expires_at: i64,
287 revoked: bool,
288}
289
290const PRESENTERS: Register = Register {
291 name: "presenters",
292 shard_entries: 128,
293 max_age_seconds: MAX_CAPABILITY_TTL_SECONDS,
294};
295
296pub const REGISTER_DIRECTORY: &str = "presented-v2";
304
305const REGISTER: Register = Register {
306 name: REGISTER_DIRECTORY,
307 shard_entries: 128,
308 max_age_seconds: MAX_CAPABILITY_TTL_SECONDS,
309};
310
311fn lifetime_end(id: &str, now: i64) -> Option<i64> {
315 match PRESENTERS.read::<PresenterRecord>(id, now) {
316 Some(record) if record.revoked || record.expires_at < now => None,
317 Some(record) => Some(record.expires_at),
318 None => is_process_session(id).then(|| now.saturating_add(MAX_CAPABILITY_TTL_SECONDS)),
319 }
320}
321
322pub struct IssuedCapability {
324 pub capability: PresenterCapability,
325 pub(crate) expires_at: i64,
327}
328
329#[must_use]
334pub fn issue(ttl_seconds: u32) -> Option<IssuedCapability> {
335 issue_at(ttl_seconds, now())
336}
337
338fn issue_at(ttl_seconds: u32, now: i64) -> Option<IssuedCapability> {
340 let ttl = i64::from(ttl_seconds);
341 if !(1..=MAX_CAPABILITY_TTL_SECONDS).contains(&ttl) {
342 return None;
343 }
344 let capability = PresenterCapability::mint()?;
345 let expires_at = now.checked_add(ttl)?;
346 PRESENTERS
347 .update::<PresenterRecord>(&capability.id(), now, |known| {
348 known.is_none().then_some(PresenterRecord {
349 expires_at,
350 revoked: false,
351 })
352 })
353 .then_some(IssuedCapability {
354 capability,
355 expires_at,
356 })
357}
358
359#[must_use]
362pub fn issued_lifetime_end(capability: &PresenterCapability) -> Option<i64> {
363 let id = capability.id();
364 if is_process_session(&id) {
365 return None;
366 }
367 lifetime_end(&id, now())
368}
369
370pub fn revoke(capability: &PresenterCapability) -> bool {
374 let now = now();
375 PRESENTERS.update::<PresenterRecord>(&capability.id(), now, |known| {
376 known.map(|record| PresenterRecord {
377 revoked: true,
378 ..record
379 })
380 })
381}
382
383#[must_use]
392pub fn read_capability_file(path: &Path) -> Option<PresenterCapability> {
393 use std::io::Read as _;
394
395 if !std::fs::symlink_metadata(path).ok()?.file_type().is_file() {
396 return None;
397 }
398 let mut options = std::fs::OpenOptions::new();
399 options.read(true);
400 #[cfg(unix)]
401 {
402 use std::os::unix::fs::OpenOptionsExt;
403 options.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK);
404 }
405 let file = options.open(path).ok()?;
406 let metadata = file.metadata().ok()?;
407 if !metadata.is_file() || metadata.len() > MAX_CAPABILITY_FILE_BYTES {
408 return None;
409 }
410 #[cfg(unix)]
411 {
412 use std::os::unix::fs::MetadataExt;
413 let own = unsafe { libc::geteuid() };
415 if metadata.uid() != own || metadata.mode() & 0o077 != 0 {
416 return None;
417 }
418 }
419 let mut text = Zeroizing::new(String::new());
420 let mut bounded = file.take(MAX_CAPABILITY_FILE_BYTES);
421 bounded.read_to_string(&mut text).ok()?;
422 PresenterCapability::parse(text.trim_end_matches(['\r', '\n']))
423}
424
425#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
429#[serde(deny_unknown_fields)]
430pub struct PresentedBinding {
431 project: String,
433 caller: String,
435 role: String,
437 object_ref: String,
438 source_id: String,
439 content_digest: String,
442 policy_identity: String,
445}
446
447#[derive(Debug, Clone, Copy, PartialEq, Eq)]
450pub(super) enum Presentation {
451 Current,
453 OtherPolicy,
456 None,
460}
461
462#[derive(Debug, Default, Serialize, Deserialize)]
464#[serde(deny_unknown_fields)]
465struct SourcePresenters {
466 presenters: BTreeMap<String, PresenterEntry>,
468}
469
470#[derive(Debug, Clone, Serialize, Deserialize)]
471#[serde(deny_unknown_fields)]
472struct PresenterEntry {
473 verifier: String,
475 admitted: BTreeMap<String, i64>,
477 expires_at: i64,
479 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
481 process_only: bool,
482}
483
484fn in_time(presented_at: i64, now: i64) -> bool {
485 now.checked_sub(presented_at)
486 .is_some_and(|age| (0..=VALIDITY_SECONDS).contains(&age))
487}
488
489impl PresenterEntry {
490 fn live(&self, now: i64) -> bool {
493 self.expires_at >= now && self.admitted.values().any(|at| in_time(*at, now))
494 }
495
496 fn last_presented(&self) -> i64 {
497 self.admitted.values().copied().max().unwrap_or(i64::MIN)
498 }
499}
500
501pub(crate) fn policy_identity(project: &Path) -> Option<String> {
506 let gateway = crate::core::context_admission::stores::StoreAdmission::current();
507 let pack = crate::core::policy::runtime::for_project(project).ok()?;
508 let pack = serde_json::to_vec(&pack.as_ref().map(|active| &active.resolved)).ok()?;
509 let kernel =
510 crate::core::engine_interface::context_plan::sources::kernel_policy_identity(project)?;
511 let mut hash = blake3::Hasher::new();
512 hash.update(b"leanctx-presented-policy-v1\0");
513 hash.update(gateway.digest().as_bytes());
514 hash.update(b"\0");
515 hash.update(&pack);
516 hash.update(b"\0");
517 hash.update(kernel.as_bytes());
518 Some(hash.finalize().to_hex().to_string())
519}
520
521fn canonical_project(project: &Path) -> Option<String> {
522 crate::core::pathutil::canonicalize_secure(project)
523 .ok()?
524 .to_str()
525 .map(str::to_owned)
526}
527
528impl PresentedBinding {
529 pub fn admitted_now(
532 project: &Path,
533 object_ref: &str,
534 source_id: &str,
535 content_digest: &str,
536 ) -> Option<Self> {
537 Self::admitted_by(
538 &verified_principal(),
539 project,
540 object_ref,
541 source_id,
542 content_digest,
543 )
544 }
545
546 fn admitted_by(
548 principal: &str,
549 project: &Path,
550 object_ref: &str,
551 source_id: &str,
552 content_digest: &str,
553 ) -> Option<Self> {
554 Some(Self {
555 project: canonical_project(project)?,
556 caller: principal.to_owned(),
557 role: crate::core::roles::active_role_name(),
558 object_ref: object_ref.to_owned(),
559 source_id: source_id.to_owned(),
560 content_digest: content_digest.to_owned(),
561 policy_identity: policy_identity(project)?,
562 })
563 }
564
565 pub(super) fn project(&self) -> &str {
566 &self.project
567 }
568
569 pub(super) fn source_id(&self) -> &str {
571 &self.source_id
572 }
573
574 pub(super) fn same_source(&self, other: &Self) -> bool {
579 self.key() == other.key()
580 }
581
582 pub(super) fn identity(&self) -> String {
585 format!("{}:{}", self.key(), self.policy_identity)
586 }
587
588 fn absorb_source(&self, hash: &mut blake3::Hasher) {
591 for field in [
592 &self.project,
593 &self.caller,
594 &self.role,
595 &self.object_ref,
596 &self.source_id,
597 &self.content_digest,
598 ] {
599 hash.update(&(field.len() as u64).to_be_bytes());
600 hash.update(field.as_bytes());
601 }
602 }
603
604 fn key(&self) -> String {
606 let mut hash = blake3::Hasher::new();
607 hash.update(b"leanctx-presented-binding-v2\0");
608 self.absorb_source(&mut hash);
609 hash.finalize().to_hex().to_string()
610 }
611
612 pub(super) fn matches_request(&self) -> bool {
615 let project = crate::core::policy::runtime::REQUEST_PROJECT
616 .try_with(|slot| slot.borrow().clone())
617 .ok()
618 .flatten()
619 .and_then(|path| canonical_project(&path));
620 project.as_deref() == Some(self.project.as_str())
621 && self.caller == verified_principal()
622 && self.role == crate::core::roles::active_role_name()
623 }
624
625 fn own_entry(&self, now: i64) -> Option<PresenterEntry> {
630 with_current(|capability| {
631 let id = capability.id();
632 let mut set = REGISTER.read::<SourcePresenters>(&self.key(), now)?;
633 let entry = set.presenters.remove(&id)?;
634 let stored = blake3::Hash::from_hex(entry.verifier.as_bytes()).ok()?;
635 (stored == capability.source_verifier(self, &id)
636 && entry.expires_at >= now
637 && lifetime_end(&id, now).is_some())
638 .then_some(entry)
639 })
640 .flatten()
641 }
642
643 pub(super) fn presentation(&self) -> Presentation {
658 let now = now();
659 if self.own_entry(now).is_none() {
660 return Presentation::None;
661 }
662 let Some(current) = policy_identity(Path::new(&self.project)) else {
663 return Presentation::OtherPolicy;
664 };
665 if self.policy_identity != current {
666 return Presentation::OtherPolicy;
667 }
668 self.presentation_at(¤t, now)
669 }
670
671 fn presentation_at(&self, current_identity: &str, now: i64) -> Presentation {
672 let Some(entry) = self.own_entry(now) else {
673 return Presentation::None;
674 };
675 if entry
676 .admitted
677 .get(current_identity)
678 .is_some_and(|at| in_time(*at, now))
679 {
680 Presentation::Current
681 } else if entry.admitted.values().any(|at| in_time(*at, now)) {
682 Presentation::OtherPolicy
683 } else {
684 Presentation::None
685 }
686 }
687}
688
689pub fn record_presentation(binding: &PresentedBinding) -> bool {
696 record_presentation_at(binding, now())
697}
698
699#[cfg(not(any(test, feature = "test-support")))]
702fn now() -> i64 {
703 register::now()
704}
705
706#[cfg(any(test, feature = "test-support"))]
707fn now() -> i64 {
708 register::now().saturating_add(TEST_CLOCK_AHEAD.load(std::sync::atomic::Ordering::SeqCst))
709}
710
711#[cfg(any(test, feature = "test-support"))]
713static TEST_CLOCK_AHEAD: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(0);
714
715#[cfg(any(test, feature = "test-support"))]
719pub struct TestClock;
720
721#[cfg(any(test, feature = "test-support"))]
722impl TestClock {
723 #[must_use]
725 pub fn past_validity() -> Self {
726 TEST_CLOCK_AHEAD.fetch_add(VALIDITY_SECONDS + 1, std::sync::atomic::Ordering::SeqCst);
727 Self
728 }
729
730 #[must_use]
733 pub fn past_capability_lifetime() -> Self {
734 TEST_CLOCK_AHEAD.fetch_add(
735 MAX_CAPABILITY_TTL_SECONDS + 1,
736 std::sync::atomic::Ordering::SeqCst,
737 );
738 Self
739 }
740}
741
742#[cfg(any(test, feature = "test-support"))]
743impl Drop for TestClock {
744 fn drop(&mut self) {
745 TEST_CLOCK_AHEAD.store(0, std::sync::atomic::Ordering::SeqCst);
746 }
747}
748
749fn record_presentation_at(binding: &PresentedBinding, now: i64) -> bool {
750 let Some((id, verifier, expires_at, process_only)) = with_current(|capability| {
751 let id = capability.id();
752 let expires_at = lifetime_end(&id, now)?;
753 let verifier = capability
754 .source_verifier(binding, &id)
755 .to_hex()
756 .to_string();
757 let process_only = is_process_session(&id);
758 Some((id, verifier, expires_at, process_only))
759 })
760 .flatten() else {
761 return false;
762 };
763 REGISTER.update::<SourcePresenters>(&binding.key(), now, |known| {
764 let mut set = known.unwrap_or_default();
765 set.presenters.retain(|_, entry| entry.live(now));
767 let mut entry = set
768 .presenters
769 .remove(&id)
770 .filter(|known| known.verifier == verifier)
771 .unwrap_or_else(|| PresenterEntry {
772 verifier: verifier.clone(),
773 admitted: BTreeMap::new(),
774 expires_at,
775 process_only,
776 });
777 entry.expires_at = expires_at;
778 entry.admitted.retain(|_, at| in_time(*at, now));
779 entry.admitted.insert(binding.policy_identity.clone(), now);
780 while entry.admitted.len() > MAX_IDENTITIES_PER_BINDING {
781 let oldest = entry
782 .admitted
783 .iter()
784 .min_by_key(|(identity, presented_at)| (**presented_at, (*identity).clone()))
785 .map(|(identity, _)| identity.clone())?;
786 entry.admitted.remove(&oldest);
787 }
788 let of_kind = |set: &SourcePresenters| {
789 set.presenters
790 .values()
791 .filter(|known| known.process_only == process_only)
792 .count()
793 };
794 if process_only {
795 while of_kind(&set) >= MAX_PROCESS_PRESENTERS_PER_SOURCE {
798 let oldest = set
799 .presenters
800 .iter()
801 .filter(|(_, known)| known.process_only)
802 .min_by_key(|(known_id, known)| (known.last_presented(), (*known_id).clone()))
803 .map(|(known_id, _)| known_id.clone())?;
804 set.presenters.remove(&oldest);
805 }
806 } else if of_kind(&set) >= MAX_PRESENTERS_PER_SOURCE {
807 return None;
810 }
811 set.presenters.insert(id.clone(), entry);
812 Some(set)
813 })
814}
815
816#[cfg(test)]
817mod tests {
818 use super::*;
819
820 fn binding(project: &Path, object_ref: &str, digest: &str, identity: &str) -> PresentedBinding {
821 PresentedBinding {
822 project: canonical_project(project).expect("project"),
823 caller: verified_principal(),
824 role: crate::core::roles::active_role_name(),
825 object_ref: object_ref.to_owned(),
826 source_id: "source-one".to_owned(),
827 content_digest: digest.to_owned(),
828 policy_identity: identity.to_owned(),
829 }
830 }
831
832 #[test]
837 fn a_presentation_by_one_principal_does_not_satisfy_another() {
838 let _data = crate::core::data_dir::isolated_data_dir();
839 let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
840 let directory = tempfile::tempdir().expect("project");
841 let root = directory.path().canonicalize().expect("canonical root");
842 let by = |principal: &str| {
843 PresentedBinding::admitted_by(principal, &root, "ref-quartz", "source-one", "sha256:aa")
844 .expect("policy identity available")
845 };
846 let mine = PresentedBinding::admitted_now(&root, "ref-quartz", "source-one", "sha256:aa")
847 .expect("policy identity available");
848 assert_eq!(mine, by(&verified_principal()));
849 assert_ne!(verified_principal(), "another-verified-principal");
850 let theirs = by("another-verified-principal");
851 assert!(!mine.same_source(&theirs));
852
853 assert!(record_presentation(&theirs));
854 assert_eq!(theirs.presentation(), Presentation::Current);
855 assert_eq!(
856 mine.presentation(),
857 Presentation::None,
858 "their presentation is not mine"
859 );
860 crate::core::policy::runtime::REQUEST_PROJECT.sync_scope(
861 std::cell::RefCell::new(Some(root.clone())),
862 || {
863 assert!(mine.matches_request());
864 assert!(
865 !theirs.matches_request(),
866 "a binding of another principal is refused for this request"
867 );
868 },
869 );
870
871 let issued = issue(60).expect("issued");
874 with_presenter(Some(issued.capability.clone()), || {
875 assert!(record_presentation(&theirs));
876 });
877 let raw = std::fs::read_to_string(REGISTER.shard_path(&theirs.key())).expect("shard");
878 assert!(!raw.contains("another-verified-principal"), "{raw}");
879 let wire = issued.capability.expose();
880 let records = std::fs::read_to_string(PRESENTERS.shard_path(&issued.capability.id()))
881 .expect("presenter record");
882 for stored in [&raw, &records] {
883 assert!(!stored.contains(&wire[CAPABILITY_PREFIX.len()..]));
884 }
885 assert_eq!(
886 format!("{:?}", issued.capability),
887 "PresenterCapability(<redacted>)"
888 );
889 assert!(PresenterCapability::parse(&wire).is_some());
890 let upper = wire.to_uppercase();
891 for malformed in ["", "lcp1_", &wire[..wire.len() - 1], upper.as_str()] {
892 assert!(PresenterCapability::parse(malformed).is_none());
893 }
894 }
895
896 #[test]
899 fn a_presentation_holds_for_its_exact_binding_policy_and_validity_only() {
900 let _data = crate::core::data_dir::isolated_data_dir();
901 let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
902 let directory = tempfile::tempdir().expect("project");
903 let root = directory.path().canonicalize().expect("canonical root");
904 let start = register::now();
905 let one = binding(&root, "ref-quartz", "sha256:aa", "policy-one");
906
907 assert_eq!(one.presentation_at("policy-one", start), Presentation::None);
908 assert!(record_presentation_at(&one, start));
909 assert_eq!(
910 one.presentation_at("policy-one", start),
911 Presentation::Current
912 );
913 assert_eq!(
914 one.presentation_at("policy-one", start + VALIDITY_SECONDS),
915 Presentation::Current
916 );
917 assert_eq!(
918 one.presentation_at("policy-one", start + VALIDITY_SECONDS + 1),
919 Presentation::None,
920 "not presented again within the validity"
921 );
922 assert_eq!(
923 one.presentation_at("policy-two", start),
924 Presentation::OtherPolicy,
925 "presented, but not admitted under the current policy"
926 );
927
928 let other_content = binding(&root, "ref-quartz", "sha256:bb", "policy-one");
931 let other_ref = binding(&root, "ref-basalt", "sha256:aa", "policy-one");
932 let mut other_caller = one.clone();
933 other_caller.caller = "another-principal".to_owned();
934 let mut other_role = one.clone();
935 other_role.role = "another-role".to_owned();
936 let elsewhere = tempfile::tempdir().expect("project");
937 let other_project = binding(elsewhere.path(), "ref-quartz", "sha256:aa", "policy-one");
938 for different in [
939 &other_content,
940 &other_ref,
941 &other_caller,
942 &other_role,
943 &other_project,
944 ] {
945 assert!(!one.same_source(different));
946 assert_eq!(
947 different.presentation_at("policy-one", start),
948 Presentation::None
949 );
950 }
951
952 let again = binding(&root, "ref-quartz", "sha256:aa", "policy-two");
954 assert!(one.same_source(&again));
955 assert!(record_presentation_at(&again, start + 5));
956 assert_eq!(
957 one.presentation_at("policy-two", start + 5),
958 Presentation::Current
959 );
960
961 let shard = REGISTER.shard_path(&one.key());
963 let raw = std::fs::read_to_string(&shard).expect("persisted shard");
964 let principal = verified_principal();
965 for identifying in ["ref-quartz", "source-one", "sha256:aa", principal.as_str()] {
966 assert!(!raw.contains(identifying), "{identifying}");
967 }
968 #[cfg(unix)]
969 {
970 use std::os::unix::fs::PermissionsExt;
971 let mode = std::fs::metadata(&shard)
972 .expect("shard")
973 .permissions()
974 .mode();
975 assert_eq!(mode & 0o777, 0o600, "owner-only");
976 }
977
978 for index in 0..MAX_IDENTITIES_PER_BINDING + 2 {
980 let identity = format!("policy-bound-{index}");
981 let presented = binding(&root, "ref-quartz", "sha256:aa", &identity);
982 assert!(record_presentation_at(&presented, start + 10));
983 }
984 let kept = REGISTER
985 .read::<SourcePresenters>(&one.key(), start + 10)
986 .expect("entry");
987 assert_eq!(kept.presenters.len(), 1, "one presenter: this process");
988 let own = kept.presenters.values().next().expect("own entry");
989 assert_eq!(own.admitted.len(), MAX_IDENTITIES_PER_BINDING);
990
991 let issued: Vec<IssuedCapability> = (0..=MAX_PRESENTERS_PER_SOURCE)
994 .map(|_| issue_at(60, start + 10).expect("issued"))
999 .collect();
1000 let recorded: Vec<bool> = issued
1001 .iter()
1002 .map(|presenter| {
1003 with_presenter(Some(presenter.capability.clone()), || {
1004 record_presentation_at(&one, start + 10)
1005 })
1006 })
1007 .collect();
1008 assert_eq!(
1009 recorded.iter().filter(|recorded| **recorded).count(),
1010 MAX_PRESENTERS_PER_SOURCE
1011 );
1012 assert_eq!(recorded.last(), Some(&false), "the one too many");
1013 with_presenter(Some(issued[0].capability.clone()), || {
1014 assert_eq!(
1015 one.presentation_at("policy-one", start + 10),
1016 Presentation::Current,
1017 "the first presenter kept its entry"
1018 );
1019 });
1020 }
1021}