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 let ttl = i64::from(ttl_seconds);
336 if !(1..=MAX_CAPABILITY_TTL_SECONDS).contains(&ttl) {
337 return None;
338 }
339 let capability = PresenterCapability::mint()?;
340 let now = now();
341 let expires_at = now.checked_add(ttl)?;
342 PRESENTERS
343 .update::<PresenterRecord>(&capability.id(), now, |known| {
344 known.is_none().then_some(PresenterRecord {
345 expires_at,
346 revoked: false,
347 })
348 })
349 .then_some(IssuedCapability {
350 capability,
351 expires_at,
352 })
353}
354
355#[must_use]
358pub fn issued_lifetime_end(capability: &PresenterCapability) -> Option<i64> {
359 let id = capability.id();
360 if is_process_session(&id) {
361 return None;
362 }
363 lifetime_end(&id, now())
364}
365
366pub fn revoke(capability: &PresenterCapability) -> bool {
370 let now = now();
371 PRESENTERS.update::<PresenterRecord>(&capability.id(), now, |known| {
372 known.map(|record| PresenterRecord {
373 revoked: true,
374 ..record
375 })
376 })
377}
378
379#[must_use]
388pub fn read_capability_file(path: &Path) -> Option<PresenterCapability> {
389 use std::io::Read as _;
390
391 if !std::fs::symlink_metadata(path).ok()?.file_type().is_file() {
392 return None;
393 }
394 let mut options = std::fs::OpenOptions::new();
395 options.read(true);
396 #[cfg(unix)]
397 {
398 use std::os::unix::fs::OpenOptionsExt;
399 options.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK);
400 }
401 let file = options.open(path).ok()?;
402 let metadata = file.metadata().ok()?;
403 if !metadata.is_file() || metadata.len() > MAX_CAPABILITY_FILE_BYTES {
404 return None;
405 }
406 #[cfg(unix)]
407 {
408 use std::os::unix::fs::MetadataExt;
409 let own = unsafe { libc::geteuid() };
411 if metadata.uid() != own || metadata.mode() & 0o077 != 0 {
412 return None;
413 }
414 }
415 let mut text = Zeroizing::new(String::new());
416 let mut bounded = file.take(MAX_CAPABILITY_FILE_BYTES);
417 bounded.read_to_string(&mut text).ok()?;
418 PresenterCapability::parse(text.trim_end_matches(['\r', '\n']))
419}
420
421#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
425#[serde(deny_unknown_fields)]
426pub struct PresentedBinding {
427 project: String,
429 caller: String,
431 role: String,
433 object_ref: String,
434 source_id: String,
435 content_digest: String,
438 policy_identity: String,
441}
442
443#[derive(Debug, Clone, Copy, PartialEq, Eq)]
446pub(super) enum Presentation {
447 Current,
449 OtherPolicy,
452 None,
456}
457
458#[derive(Debug, Default, Serialize, Deserialize)]
460#[serde(deny_unknown_fields)]
461struct SourcePresenters {
462 presenters: BTreeMap<String, PresenterEntry>,
464}
465
466#[derive(Debug, Clone, Serialize, Deserialize)]
467#[serde(deny_unknown_fields)]
468struct PresenterEntry {
469 verifier: String,
471 admitted: BTreeMap<String, i64>,
473 expires_at: i64,
475 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
477 process_only: bool,
478}
479
480fn in_time(presented_at: i64, now: i64) -> bool {
481 now.checked_sub(presented_at)
482 .is_some_and(|age| (0..=VALIDITY_SECONDS).contains(&age))
483}
484
485impl PresenterEntry {
486 fn live(&self, now: i64) -> bool {
489 self.expires_at >= now && self.admitted.values().any(|at| in_time(*at, now))
490 }
491
492 fn last_presented(&self) -> i64 {
493 self.admitted.values().copied().max().unwrap_or(i64::MIN)
494 }
495}
496
497pub(crate) fn policy_identity(project: &Path) -> Option<String> {
502 let gateway = crate::core::context_admission::stores::StoreAdmission::current();
503 let pack = crate::core::policy::runtime::for_project(project).ok()?;
504 let pack = serde_json::to_vec(&pack.as_ref().map(|active| &active.resolved)).ok()?;
505 let kernel =
506 crate::core::engine_interface::context_plan::sources::kernel_policy_identity(project)?;
507 let mut hash = blake3::Hasher::new();
508 hash.update(b"leanctx-presented-policy-v1\0");
509 hash.update(gateway.digest().as_bytes());
510 hash.update(b"\0");
511 hash.update(&pack);
512 hash.update(b"\0");
513 hash.update(kernel.as_bytes());
514 Some(hash.finalize().to_hex().to_string())
515}
516
517fn canonical_project(project: &Path) -> Option<String> {
518 crate::core::pathutil::canonicalize_secure(project)
519 .ok()?
520 .to_str()
521 .map(str::to_owned)
522}
523
524impl PresentedBinding {
525 pub fn admitted_now(
528 project: &Path,
529 object_ref: &str,
530 source_id: &str,
531 content_digest: &str,
532 ) -> Option<Self> {
533 Self::admitted_by(
534 &verified_principal(),
535 project,
536 object_ref,
537 source_id,
538 content_digest,
539 )
540 }
541
542 fn admitted_by(
544 principal: &str,
545 project: &Path,
546 object_ref: &str,
547 source_id: &str,
548 content_digest: &str,
549 ) -> Option<Self> {
550 Some(Self {
551 project: canonical_project(project)?,
552 caller: principal.to_owned(),
553 role: crate::core::roles::active_role_name(),
554 object_ref: object_ref.to_owned(),
555 source_id: source_id.to_owned(),
556 content_digest: content_digest.to_owned(),
557 policy_identity: policy_identity(project)?,
558 })
559 }
560
561 pub(super) fn project(&self) -> &str {
562 &self.project
563 }
564
565 pub(super) fn source_id(&self) -> &str {
567 &self.source_id
568 }
569
570 pub(super) fn same_source(&self, other: &Self) -> bool {
575 self.key() == other.key()
576 }
577
578 pub(super) fn identity(&self) -> String {
581 format!("{}:{}", self.key(), self.policy_identity)
582 }
583
584 fn absorb_source(&self, hash: &mut blake3::Hasher) {
587 for field in [
588 &self.project,
589 &self.caller,
590 &self.role,
591 &self.object_ref,
592 &self.source_id,
593 &self.content_digest,
594 ] {
595 hash.update(&(field.len() as u64).to_be_bytes());
596 hash.update(field.as_bytes());
597 }
598 }
599
600 fn key(&self) -> String {
602 let mut hash = blake3::Hasher::new();
603 hash.update(b"leanctx-presented-binding-v2\0");
604 self.absorb_source(&mut hash);
605 hash.finalize().to_hex().to_string()
606 }
607
608 pub(super) fn matches_request(&self) -> bool {
611 let project = crate::core::policy::runtime::REQUEST_PROJECT
612 .try_with(|slot| slot.borrow().clone())
613 .ok()
614 .flatten()
615 .and_then(|path| canonical_project(&path));
616 project.as_deref() == Some(self.project.as_str())
617 && self.caller == verified_principal()
618 && self.role == crate::core::roles::active_role_name()
619 }
620
621 fn own_entry(&self, now: i64) -> Option<PresenterEntry> {
626 with_current(|capability| {
627 let id = capability.id();
628 let mut set = REGISTER.read::<SourcePresenters>(&self.key(), now)?;
629 let entry = set.presenters.remove(&id)?;
630 let stored = blake3::Hash::from_hex(entry.verifier.as_bytes()).ok()?;
631 (stored == capability.source_verifier(self, &id)
632 && entry.expires_at >= now
633 && lifetime_end(&id, now).is_some())
634 .then_some(entry)
635 })
636 .flatten()
637 }
638
639 pub(super) fn presentation(&self) -> Presentation {
654 let now = now();
655 if self.own_entry(now).is_none() {
656 return Presentation::None;
657 }
658 let Some(current) = policy_identity(Path::new(&self.project)) else {
659 return Presentation::OtherPolicy;
660 };
661 if self.policy_identity != current {
662 return Presentation::OtherPolicy;
663 }
664 self.presentation_at(¤t, now)
665 }
666
667 fn presentation_at(&self, current_identity: &str, now: i64) -> Presentation {
668 let Some(entry) = self.own_entry(now) else {
669 return Presentation::None;
670 };
671 if entry
672 .admitted
673 .get(current_identity)
674 .is_some_and(|at| in_time(*at, now))
675 {
676 Presentation::Current
677 } else if entry.admitted.values().any(|at| in_time(*at, now)) {
678 Presentation::OtherPolicy
679 } else {
680 Presentation::None
681 }
682 }
683}
684
685pub fn record_presentation(binding: &PresentedBinding) -> bool {
692 record_presentation_at(binding, now())
693}
694
695#[cfg(not(any(test, feature = "test-support")))]
698fn now() -> i64 {
699 register::now()
700}
701
702#[cfg(any(test, feature = "test-support"))]
703fn now() -> i64 {
704 register::now().saturating_add(TEST_CLOCK_AHEAD.load(std::sync::atomic::Ordering::SeqCst))
705}
706
707#[cfg(any(test, feature = "test-support"))]
709static TEST_CLOCK_AHEAD: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(0);
710
711#[cfg(any(test, feature = "test-support"))]
715pub struct TestClock;
716
717#[cfg(any(test, feature = "test-support"))]
718impl TestClock {
719 #[must_use]
721 pub fn past_validity() -> Self {
722 TEST_CLOCK_AHEAD.fetch_add(VALIDITY_SECONDS + 1, std::sync::atomic::Ordering::SeqCst);
723 Self
724 }
725
726 #[must_use]
729 pub fn past_capability_lifetime() -> Self {
730 TEST_CLOCK_AHEAD.fetch_add(
731 MAX_CAPABILITY_TTL_SECONDS + 1,
732 std::sync::atomic::Ordering::SeqCst,
733 );
734 Self
735 }
736}
737
738#[cfg(any(test, feature = "test-support"))]
739impl Drop for TestClock {
740 fn drop(&mut self) {
741 TEST_CLOCK_AHEAD.store(0, std::sync::atomic::Ordering::SeqCst);
742 }
743}
744
745fn record_presentation_at(binding: &PresentedBinding, now: i64) -> bool {
746 let Some((id, verifier, expires_at, process_only)) = with_current(|capability| {
747 let id = capability.id();
748 let expires_at = lifetime_end(&id, now)?;
749 let verifier = capability
750 .source_verifier(binding, &id)
751 .to_hex()
752 .to_string();
753 let process_only = is_process_session(&id);
754 Some((id, verifier, expires_at, process_only))
755 })
756 .flatten() else {
757 return false;
758 };
759 REGISTER.update::<SourcePresenters>(&binding.key(), now, |known| {
760 let mut set = known.unwrap_or_default();
761 set.presenters.retain(|_, entry| entry.live(now));
763 let mut entry = set
764 .presenters
765 .remove(&id)
766 .filter(|known| known.verifier == verifier)
767 .unwrap_or_else(|| PresenterEntry {
768 verifier: verifier.clone(),
769 admitted: BTreeMap::new(),
770 expires_at,
771 process_only,
772 });
773 entry.expires_at = expires_at;
774 entry.admitted.retain(|_, at| in_time(*at, now));
775 entry.admitted.insert(binding.policy_identity.clone(), now);
776 while entry.admitted.len() > MAX_IDENTITIES_PER_BINDING {
777 let oldest = entry
778 .admitted
779 .iter()
780 .min_by_key(|(identity, presented_at)| (**presented_at, (*identity).clone()))
781 .map(|(identity, _)| identity.clone())?;
782 entry.admitted.remove(&oldest);
783 }
784 let of_kind = |set: &SourcePresenters| {
785 set.presenters
786 .values()
787 .filter(|known| known.process_only == process_only)
788 .count()
789 };
790 if process_only {
791 while of_kind(&set) >= MAX_PROCESS_PRESENTERS_PER_SOURCE {
794 let oldest = set
795 .presenters
796 .iter()
797 .filter(|(_, known)| known.process_only)
798 .min_by_key(|(known_id, known)| (known.last_presented(), (*known_id).clone()))
799 .map(|(known_id, _)| known_id.clone())?;
800 set.presenters.remove(&oldest);
801 }
802 } else if of_kind(&set) >= MAX_PRESENTERS_PER_SOURCE {
803 return None;
806 }
807 set.presenters.insert(id.clone(), entry);
808 Some(set)
809 })
810}
811
812#[cfg(test)]
813mod tests {
814 use super::*;
815
816 fn binding(project: &Path, object_ref: &str, digest: &str, identity: &str) -> PresentedBinding {
817 PresentedBinding {
818 project: canonical_project(project).expect("project"),
819 caller: verified_principal(),
820 role: crate::core::roles::active_role_name(),
821 object_ref: object_ref.to_owned(),
822 source_id: "source-one".to_owned(),
823 content_digest: digest.to_owned(),
824 policy_identity: identity.to_owned(),
825 }
826 }
827
828 #[test]
833 fn a_presentation_by_one_principal_does_not_satisfy_another() {
834 let _data = crate::core::data_dir::isolated_data_dir();
835 let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
836 let directory = tempfile::tempdir().expect("project");
837 let root = directory.path().canonicalize().expect("canonical root");
838 let by = |principal: &str| {
839 PresentedBinding::admitted_by(principal, &root, "ref-quartz", "source-one", "sha256:aa")
840 .expect("policy identity available")
841 };
842 let mine = PresentedBinding::admitted_now(&root, "ref-quartz", "source-one", "sha256:aa")
843 .expect("policy identity available");
844 assert_eq!(mine, by(&verified_principal()));
845 assert_ne!(verified_principal(), "another-verified-principal");
846 let theirs = by("another-verified-principal");
847 assert!(!mine.same_source(&theirs));
848
849 assert!(record_presentation(&theirs));
850 assert_eq!(theirs.presentation(), Presentation::Current);
851 assert_eq!(
852 mine.presentation(),
853 Presentation::None,
854 "their presentation is not mine"
855 );
856 crate::core::policy::runtime::REQUEST_PROJECT.sync_scope(
857 std::cell::RefCell::new(Some(root.clone())),
858 || {
859 assert!(mine.matches_request());
860 assert!(
861 !theirs.matches_request(),
862 "a binding of another principal is refused for this request"
863 );
864 },
865 );
866
867 let issued = issue(60).expect("issued");
870 with_presenter(Some(issued.capability.clone()), || {
871 assert!(record_presentation(&theirs));
872 });
873 let raw = std::fs::read_to_string(REGISTER.shard_path(&theirs.key())).expect("shard");
874 assert!(!raw.contains("another-verified-principal"), "{raw}");
875 let wire = issued.capability.expose();
876 let records = std::fs::read_to_string(PRESENTERS.shard_path(&issued.capability.id()))
877 .expect("presenter record");
878 for stored in [&raw, &records] {
879 assert!(!stored.contains(&wire[CAPABILITY_PREFIX.len()..]));
880 }
881 assert_eq!(
882 format!("{:?}", issued.capability),
883 "PresenterCapability(<redacted>)"
884 );
885 assert!(PresenterCapability::parse(&wire).is_some());
886 let upper = wire.to_uppercase();
887 for malformed in ["", "lcp1_", &wire[..wire.len() - 1], upper.as_str()] {
888 assert!(PresenterCapability::parse(malformed).is_none());
889 }
890 }
891
892 #[test]
895 fn a_presentation_holds_for_its_exact_binding_policy_and_validity_only() {
896 let _data = crate::core::data_dir::isolated_data_dir();
897 let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
898 let directory = tempfile::tempdir().expect("project");
899 let root = directory.path().canonicalize().expect("canonical root");
900 let start = register::now();
901 let one = binding(&root, "ref-quartz", "sha256:aa", "policy-one");
902
903 assert_eq!(one.presentation_at("policy-one", start), Presentation::None);
904 assert!(record_presentation_at(&one, start));
905 assert_eq!(
906 one.presentation_at("policy-one", start),
907 Presentation::Current
908 );
909 assert_eq!(
910 one.presentation_at("policy-one", start + VALIDITY_SECONDS),
911 Presentation::Current
912 );
913 assert_eq!(
914 one.presentation_at("policy-one", start + VALIDITY_SECONDS + 1),
915 Presentation::None,
916 "not presented again within the validity"
917 );
918 assert_eq!(
919 one.presentation_at("policy-two", start),
920 Presentation::OtherPolicy,
921 "presented, but not admitted under the current policy"
922 );
923
924 let other_content = binding(&root, "ref-quartz", "sha256:bb", "policy-one");
927 let other_ref = binding(&root, "ref-basalt", "sha256:aa", "policy-one");
928 let mut other_caller = one.clone();
929 other_caller.caller = "another-principal".to_owned();
930 let mut other_role = one.clone();
931 other_role.role = "another-role".to_owned();
932 let elsewhere = tempfile::tempdir().expect("project");
933 let other_project = binding(elsewhere.path(), "ref-quartz", "sha256:aa", "policy-one");
934 for different in [
935 &other_content,
936 &other_ref,
937 &other_caller,
938 &other_role,
939 &other_project,
940 ] {
941 assert!(!one.same_source(different));
942 assert_eq!(
943 different.presentation_at("policy-one", start),
944 Presentation::None
945 );
946 }
947
948 let again = binding(&root, "ref-quartz", "sha256:aa", "policy-two");
950 assert!(one.same_source(&again));
951 assert!(record_presentation_at(&again, start + 5));
952 assert_eq!(
953 one.presentation_at("policy-two", start + 5),
954 Presentation::Current
955 );
956
957 let shard = REGISTER.shard_path(&one.key());
959 let raw = std::fs::read_to_string(&shard).expect("persisted shard");
960 let principal = verified_principal();
961 for identifying in ["ref-quartz", "source-one", "sha256:aa", principal.as_str()] {
962 assert!(!raw.contains(identifying), "{identifying}");
963 }
964 #[cfg(unix)]
965 {
966 use std::os::unix::fs::PermissionsExt;
967 let mode = std::fs::metadata(&shard)
968 .expect("shard")
969 .permissions()
970 .mode();
971 assert_eq!(mode & 0o777, 0o600, "owner-only");
972 }
973
974 for index in 0..MAX_IDENTITIES_PER_BINDING + 2 {
976 let identity = format!("policy-bound-{index}");
977 let presented = binding(&root, "ref-quartz", "sha256:aa", &identity);
978 assert!(record_presentation_at(&presented, start + 10));
979 }
980 let kept = REGISTER
981 .read::<SourcePresenters>(&one.key(), start + 10)
982 .expect("entry");
983 assert_eq!(kept.presenters.len(), 1, "one presenter: this process");
984 let own = kept.presenters.values().next().expect("own entry");
985 assert_eq!(own.admitted.len(), MAX_IDENTITIES_PER_BINDING);
986
987 let issued: Vec<IssuedCapability> = (0..=MAX_PRESENTERS_PER_SOURCE)
990 .map(|_| issue(60).expect("issued"))
991 .collect();
992 let recorded: Vec<bool> = issued
993 .iter()
994 .map(|presenter| {
995 with_presenter(Some(presenter.capability.clone()), || {
996 record_presentation_at(&one, start + 10)
997 })
998 })
999 .collect();
1000 assert_eq!(
1001 recorded.iter().filter(|recorded| **recorded).count(),
1002 MAX_PRESENTERS_PER_SOURCE
1003 );
1004 assert_eq!(recorded.last(), Some(&false), "the one too many");
1005 with_presenter(Some(issued[0].capability.clone()), || {
1006 assert_eq!(
1007 one.presentation_at("policy-one", start + 10),
1008 Presentation::Current,
1009 "the first presenter kept its entry"
1010 );
1011 });
1012 }
1013}