use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use cheers_core::{
Error, Lease, PrincipalId, ResourceRevocationKey, Revoked, SchemaRegistry, SetSnapshot, SnapshotMember,
StoreError, Subject,
};
use crate::codec::PasetoV4SecretMinter;
use crate::ownership::{OwnershipRow, OwnershipStore, OwnershipTuples};
use crate::revocation::RevocationWriter;
pub async fn revoke_ownership<O, W>(store: &O, revocations: &W, id: &str, now: i64) -> Result<u64, StoreError>
where
O: OwnershipStore + ?Sized,
W: RevocationWriter + ?Sized,
{
let row = store.get(id).await?.ok_or(StoreError::NotFound)?;
let version = store.revoke_by_id(id, now).await?;
let Subject::Principal(principal) = &row.subject else {
return Ok(version);
};
if !principal.kind.is_member_kind() {
return Ok(version);
}
let still_direct = store
.list_for_resource(&row.resource_kind, &row.resource_id)
.await?
.iter()
.any(|r| !r.is_revoked() && r.subject.principal() == Some(principal));
if !still_direct {
let entry = Revoked::membership(&row.resource_kind, &row.resource_id, principal.clone(), version);
revocations.revoke(&entry).await?;
}
Ok(version)
}
pub async fn revoke_principal_ownership<O, W>(
store: &O,
revocations: &W,
principal: &PrincipalId,
now: i64,
) -> Result<u64, StoreError>
where
O: OwnershipStore + ?Sized,
W: RevocationWriter + ?Sized,
{
let version = store.revoke_by_principal(principal, now).await?;
if !principal.kind.is_member_kind() {
return Ok(version);
}
let resource = |r: OwnershipRow| (r.resource_kind, r.resource_id);
let live: BTreeSet<_> = store.list_for_principal(principal).await?.into_iter().map(resource).collect();
let held: BTreeSet<_> = store.list_history_for_principal(principal).await?.into_iter().map(resource).collect();
for (kind, id) in held.difference(&live) {
revocations.revoke(&Revoked::membership(kind.as_str(), id.as_str(), principal.clone(), version)).await?;
}
Ok(version)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignedSetSnapshot {
pub snapshot: SetSnapshot,
pub token: String,
}
pub struct SnapshotIssuer {
store: Arc<dyn OwnershipStore>,
schema: Arc<SchemaRegistry>,
minter: PasetoV4SecretMinter,
issuer: String,
kid: String,
refresh_after_seconds: i64,
}
impl std::fmt::Debug for SnapshotIssuer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SnapshotIssuer")
.field("issuer", &self.issuer)
.field("kid", &self.kid)
.field("refresh_after_seconds", &self.refresh_after_seconds)
.finish_non_exhaustive()
}
}
impl SnapshotIssuer {
pub const DEFAULT_REFRESH_AFTER_SECONDS: i64 = 7 * 24 * 60 * 60;
pub const MINT_ATTEMPTS: usize = 5;
pub fn new(
store: impl OwnershipStore + 'static,
schema: Arc<SchemaRegistry>,
minter: PasetoV4SecretMinter,
issuer: impl Into<String>,
kid: impl Into<String>,
) -> Self {
Self {
store: Arc::new(store),
schema,
minter,
issuer: issuer.into(),
kid: kid.into(),
refresh_after_seconds: Self::DEFAULT_REFRESH_AFTER_SECONDS,
}
}
pub fn with_refresh_after(mut self, seconds: i64) -> Self {
self.refresh_after_seconds = seconds;
self
}
pub fn issuer(&self) -> &str {
&self.issuer
}
async fn member_leases(&self, kind: &str, id: &str, now: i64) -> Result<BTreeMap<PrincipalId, Option<Lease>>, Error> {
let mut out: BTreeMap<PrincipalId, Option<Lease>> = BTreeMap::new();
for row in self.store.list_for_resource(kind, id).await? {
let Some(p) = row.subject.principal().cloned() else { continue };
if !row.is_live_at(now) {
continue;
}
let lease = row.lease.map(|l| l.lease);
out.entry(p)
.and_modify(|held| {
*held = match (*held, lease) {
(Some(a), Some(b)) => Some(if b.exp() > a.exp() { b } else { a }),
_ => None,
}
})
.or_insert(lease);
}
Ok(out)
}
pub async fn mint(&self, kind: &str, id: &str, now: i64) -> Result<SignedSetSnapshot, Error> {
for _ in 0..Self::MINT_ATTEMPTS {
let before = self.store.current_version().await?;
let members = self.schema.members(&OwnershipTuples::at(&*self.store, now), kind, id).await?;
let policy = self.store.admission_policy(kind, id).await?;
let policy = policy.filter(|p| self.schema.validate_admission_policy(kind, p).is_ok());
if self.store.current_version().await? != before {
continue;
}
let leases = self.member_leases(kind, id, now).await?;
let here = [(kind.to_owned(), id.to_owned())];
let members: Vec<SnapshotMember> = members
.into_iter()
.filter(|m| m.principal.kind.is_member_kind())
.filter(|m| self.schema.is_membership(kind, &m.relation))
.map(|m| {
let via: Vec<(String, String)> = m.via.into_iter().collect();
let lease = if via[..] == here[..] { leases.get(&m.principal).copied().flatten() } else { None };
SnapshotMember { principal: m.principal, relation: m.relation, via, lease }
})
.collect();
let mut resources: BTreeSet<(String, String)> = members.iter().flat_map(|m| m.via.iter().cloned()).collect();
resources.insert((kind.to_owned(), id.to_owned()));
let mut keys = Vec::with_capacity(resources.len());
for (k, i) in resources {
let key = self.store.revocation_key(&k, &i).await?;
keys.push(ResourceRevocationKey { kind: k, id: i, key });
}
let lease = Lease::new(now, now.saturating_add(self.refresh_after_seconds), None)?;
let snapshot = SetSnapshot::new(&self.issuer, kind, id, before, members, keys, now, lease).with_policy(policy);
let token = self.minter.mint_artifact(&snapshot, &self.kid)?;
return Ok(SignedSetSnapshot { snapshot, token });
}
Err(Error::SnapshotContended {
kind: kind.to_owned(),
id: id.to_owned(),
attempts: Self::MINT_ATTEMPTS,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use cheers_core::LeaseState;
use crate::ownership::{Inserted, MemoryOwnershipStore, NewOwnership};
use crate::revocation::{MemoryRevocationStore, RevocationPublisher, RevocationSnapshot};
use async_trait::async_trait;
use cheers_core::{RelationDef, ResourceSchema, ScopeRegistry};
use cheers_verify::{IssuerTrust, ReplicatedRevocations, SnapshotVerifier, VerifiedSnapshot};
use std::sync::atomic::{AtomicUsize, Ordering};
const ISS: &str = "https://c.test";
const NAMESPACE: ResourceSchema = ResourceSchema {
kind: "namespace",
relations: &[
RelationDef {
name: "owner",
membership: true,
implies: &["admin"],
scopes: &[],
grants: &["owner", "admin", "publisher", "member", "guest"],
},
RelationDef {
name: "admin",
membership: true,
implies: &["publisher"],
scopes: &[],
grants: &["admin", "publisher", "member", "guest"],
},
RelationDef { name: "publisher", membership: true, implies: &["member"], scopes: &[], grants: &[] },
RelationDef { name: "member", membership: true, implies: &["guest"], scopes: &[], grants: &[] },
RelationDef { name: "guest", membership: true, implies: &[], scopes: &[], grants: &[] },
RelationDef { name: "allow", membership: false, implies: &[], scopes: &[], grants: &[] },
RelationDef { name: "steward", membership: false, implies: &["guest"], scopes: &[], grants: &[] },
],
kind_relations: &[],
};
fn schema() -> Arc<SchemaRegistry> {
let scopes = ScopeRegistry::builder().build().unwrap();
Arc::new(SchemaRegistry::build(&[NAMESPACE], &scopes).unwrap())
}
fn ns(who: impl Into<Subject>, id: &str, rel: &str) -> NewOwnership {
NewOwnership::new(who, "namespace", id, rel, PrincipalId::service("noisetable-account"), None).unwrap()
}
fn user(id: &str) -> PrincipalId {
PrincipalId::user(id)
}
async fn institution(store: &MemoryOwnershipStore) {
store.insert(&ns(Subject::set("namespace", "museum", "admin"), "ed", "admin"), 10).await.unwrap();
store.insert(&ns(Subject::set("namespace", "museum", "member"), "ed", "guest"), 10).await.unwrap();
}
struct Rig {
store: MemoryOwnershipStore,
revocations: MemoryRevocationStore,
issuer: SnapshotIssuer,
publisher: RevocationPublisher<MemoryRevocationStore>,
replica: Arc<ReplicatedRevocations>,
edge: SnapshotVerifier<Arc<ReplicatedRevocations>>,
}
impl Rig {
async fn gossip(&self, now: i64) {
let set = self.publisher.current(now).await.unwrap();
self.replica.adopt(&set.token).await.unwrap();
}
}
fn rig() -> Rig {
let (minter, verifier) = PasetoV4SecretMinter::generate().unwrap();
let store = MemoryOwnershipStore::new();
let revocations = MemoryRevocationStore::new();
let key: &[u8; 64] = minter.secret_key_bytes().try_into().unwrap();
let same_key = PasetoV4SecretMinter::from_secret_key(key).unwrap();
let issuer = SnapshotIssuer::new(store.clone(), schema(), minter, ISS, "k1").with_refresh_after(60);
let publisher = RevocationPublisher::new(revocations.clone(), Arc::new(store.clone()), same_key, ISS, "k1");
let trust = IssuerTrust::pinned(ISS, verifier);
let replica = Arc::new(ReplicatedRevocations::new(trust.clone()));
Rig {
edge: SnapshotVerifier::new(trust, replica.clone()),
store,
revocations,
issuer,
publisher,
replica,
}
}
fn admitted(v: &VerifiedSnapshot) -> Vec<String> {
v.admitted.iter().map(|m| format!("{}#{}", m.principal.id, m.relation)).collect()
}
#[test]
fn mint_lists_users_closure_expanded_with_via_at_the_store_version() {
pollster::block_on(async {
let r = rig();
institution(&r.store).await;
r.store.insert(&ns(user("alice"), "museum", "member"), 100).await.unwrap();
r.store.insert(&ns(user("carol"), "ed", "admin"), 100).await.unwrap();
r.store.insert(&ns(PrincipalId::service("bot"), "ed", "member"), 100).await.unwrap();
let version = r.store.current_version().await.unwrap();
let s = r.issuer.mint("namespace", "ed", 1_000).await.unwrap();
assert_eq!(s.snapshot.epoch, version);
assert_eq!((s.snapshot.iat, s.snapshot.lease.refresh_after()), (1_000, 1_060));
let listed: Vec<_> = s
.snapshot
.members
.iter()
.map(|m| (m.principal.id.as_str(), m.relation.as_str(), m.via.clone()))
.collect();
let ed = || vec![("namespace".to_owned(), "ed".to_owned())];
let museum = || vec![("namespace".to_owned(), "museum".to_owned())];
assert_eq!(
listed,
vec![
("alice", "guest", museum()),
("carol", "admin", ed()),
("carol", "guest", ed()),
("carol", "member", ed()),
("carol", "publisher", ed()),
]
);
let carried: Vec<_> =
s.snapshot.revocation_keys.iter().map(|k| (k.kind.as_str(), k.id.as_str(), k.key.clone())).collect();
assert_eq!(
carried,
vec![
("namespace", "ed", r.store.revocation_key("namespace", "ed").await.unwrap()),
("namespace", "museum", r.store.revocation_key("namespace", "museum").await.unwrap()),
]
);
let ok = r.edge.verify_snapshot_at(&s.token, 1_000).await.unwrap();
assert!(ok.holds(&PrincipalId::user("alice"), "guest"));
assert!(!ok.holds(&PrincipalId::user("alice"), "member"));
assert_eq!(ok.lease, LeaseState::Current);
});
}
fn listed(s: &SignedSetSnapshot) -> Vec<String> {
s.snapshot.members.iter().map(|m| format!("{}#{}", m.principal.id, m.relation)).collect()
}
#[test]
fn a_non_membership_row_is_never_minted() {
pollster::block_on(async {
let r = rig();
r.store.insert(&ns(user("alice"), "ed", "member"), 100).await.unwrap();
r.store.insert(&ns(user("bob"), "ed", "allow"), 100).await.unwrap();
let s = r.issuer.mint("namespace", "ed", 1_000).await.unwrap();
assert_eq!(listed(&s), vec!["alice#guest", "alice#member"]);
});
}
#[test]
fn a_non_membership_relation_still_mints_the_membership_one_it_implies() {
pollster::block_on(async {
let r = rig();
r.store.insert(&ns(user("sam"), "ed", "steward"), 100).await.unwrap();
let s = r.issuer.mint("namespace", "ed", 1_000).await.unwrap();
assert_eq!(listed(&s), vec!["sam#guest"]);
});
}
#[test]
fn a_subject_set_member_at_a_membership_relation_is_minted() {
pollster::block_on(async {
let r = rig();
institution(&r.store).await;
r.store.insert(&ns(user("alice"), "museum", "member"), 100).await.unwrap();
r.store.insert(&ns(user("bob"), "museum", "allow"), 100).await.unwrap();
let s = r.issuer.mint("namespace", "ed", 1_000).await.unwrap();
assert_eq!(listed(&s), vec!["alice#guest"]);
});
}
#[test]
fn a_policy_naming_a_non_membership_relation_is_minted_as_none() {
use cheers_core::{AdmissionMode, AdmissionPolicy, PolicyError};
let reg = schema();
let ok = AdmissionPolicy::new(AdmissionMode::Knock, "guest").with_admitters(["admin"]);
assert_eq!(reg.validate_admission_policy("namespace", &ok), Ok(()));
let bad_floor = AdmissionPolicy::new(AdmissionMode::Open, "allow");
let bad_admitter = AdmissionPolicy::new(AdmissionMode::Knock, "guest").with_admitters(["admin", "steward"]);
assert_eq!(
reg.validate_admission_policy("namespace", &bad_floor),
Err(PolicyError::NotMembership { kind: "namespace".into(), relation: "allow".into() })
);
assert_eq!(
reg.validate_admission_policy("namespace", &bad_admitter),
Err(PolicyError::NotMembership { kind: "namespace".into(), relation: "steward".into() })
);
pollster::block_on(async {
let r = rig();
for (p, want) in [(&bad_floor, None), (&bad_admitter, None), (&ok, Some(&ok))] {
r.store.set_admission_policy("namespace", "ed", Some(p), 1_000).await.unwrap();
let s = r.issuer.mint("namespace", "ed", 1_000).await.unwrap();
assert_eq!(s.snapshot.policy.as_ref(), want);
}
});
}
#[test]
fn the_policy_row_is_minted_and_its_change_advances_the_epoch() {
use cheers_core::{AdmissionMode, AdmissionPolicy};
pollster::block_on(async {
let r = rig();
institution(&r.store).await;
let bare = r.issuer.mint("namespace", "ed", 1_000).await.unwrap();
assert_eq!(bare.snapshot.policy, None);
let open = AdmissionPolicy::new(AdmissionMode::Open, "guest");
let v = r.store.set_admission_policy("namespace", "ed", Some(&open), 1_000).await.unwrap();
assert!(v > bare.snapshot.epoch);
let s = r.issuer.mint("namespace", "ed", 1_000).await.unwrap();
assert_eq!((s.snapshot.epoch, s.snapshot.policy.as_ref()), (v, Some(&open)));
let ok = r.edge.verify_snapshot_at(&s.token, 1_000).await.unwrap();
assert_eq!(ok.admission_policy(), Some(&open));
let v2 = r.store.set_admission_policy("namespace", "ed", None, 1_000).await.unwrap();
let s = r.issuer.mint("namespace", "ed", 1_000).await.unwrap();
assert_eq!((s.snapshot.epoch, s.snapshot.policy), (v2, None));
assert!(v2 > v);
});
}
#[test]
fn a_key_principal_is_minted_held_and_revoked_like_a_user() {
pollster::block_on(async {
let r = rig();
institution(&r.store).await;
let k = PrincipalId::from_public_key(&[0x4b; 32]);
let tuple = r.store.insert(&ns(k.clone(), "ed", "member"), 100).await.unwrap();
let held = r.issuer.mint("namespace", "ed", 1_000).await.unwrap();
assert!(held.snapshot.lists(&k, "member"));
let ok = r.edge.verify_snapshot_at(&held.token, 1_000).await.unwrap();
assert!(ok.holds(&k, "member") && ok.holds(&k, "guest"));
assert!(!ok.holds(&PrincipalId::user(k.id.clone()), "member"));
let v = revoke_ownership(&r.store, &r.revocations, &tuple.row.id, 2_000).await.unwrap();
assert!(r.revocations.snapshot().await.unwrap().revoked.contains(&Revoked::membership("namespace", "ed", k.clone(), v)));
r.gossip(2_000).await;
let ok = r.edge.verify_snapshot_at(&held.token, 2_000).await.unwrap();
assert!(!ok.holds(&k, "member"));
});
}
#[test]
fn removal_from_the_parent_drops_an_inherited_member_and_keeps_a_direct_one() {
pollster::block_on(async {
let r = rig();
institution(&r.store).await;
let alice = r.store.insert(&ns(user("alice"), "museum", "member"), 100).await.unwrap();
let bob = r.store.insert(&ns(user("bob"), "museum", "member"), 100).await.unwrap();
r.store.insert(&ns(user("bob"), "ed", "member"), 100).await.unwrap();
let held = r.issuer.mint("namespace", "ed", 1_000).await.unwrap();
let ok = r.edge.verify_snapshot_at(&held.token, 1_000).await.unwrap();
assert_eq!(admitted(&ok), ["alice#guest", "bob#guest", "bob#member"]);
let va = revoke_ownership(&r.store, &r.revocations, &alice.row.id, 2_000).await.unwrap();
let vb = revoke_ownership(&r.store, &r.revocations, &bob.row.id, 2_000).await.unwrap();
assert!(va > held.snapshot.epoch && vb > va);
r.gossip(2_000).await;
let ok = r.edge.verify_snapshot_at(&held.token, 2_000).await.unwrap();
assert_eq!(admitted(&ok), ["bob#guest", "bob#member"]);
assert_eq!(ok.snapshot.members.len(), 3, "the signed list itself is untouched");
let fresh = r.issuer.mint("namespace", "ed", 2_000).await.unwrap();
assert_eq!(fresh.snapshot.epoch, vb);
assert_eq!(admitted(&r.edge.verify_snapshot_at(&fresh.token, 2_000).await.unwrap()), [
"bob#guest",
"bob#member"
]);
assert!(r.edge.verify_snapshot_at(&held.token, 2_000).await.is_err());
r.store.insert(&ns(user("alice"), "museum", "member"), 3_000).await.unwrap();
let readded = r.issuer.mint("namespace", "ed", 3_000).await.unwrap();
let ok = r.edge.verify_snapshot_at(&readded.token, 3_000).await.unwrap();
assert!(ok.holds(&PrincipalId::user("alice"), "guest"));
});
}
#[test]
fn recording_fires_only_when_the_last_direct_tuple_goes() {
pollster::block_on(async {
let r = rig();
institution(&r.store).await;
let owner = r.store.insert(&ns(user("carol"), "ed", "owner"), 100).await.unwrap();
let member = r.store.insert(&ns(user("carol"), "ed", "member"), 100).await.unwrap();
let bot = r.store.insert(&ns(PrincipalId::service("bot"), "ed", "member"), 100).await.unwrap();
let set_rows = r.store.list_for_subject_set("namespace", "museum").await.unwrap();
let epoch = || async { r.revocations.snapshot().await.unwrap() };
revoke_ownership(&r.store, &r.revocations, &owner.row.id, 200).await.unwrap();
assert!(epoch().await.revoked.is_empty());
revoke_ownership(&r.store, &r.revocations, &set_rows[0].id, 200).await.unwrap();
revoke_ownership(&r.store, &r.revocations, &bot.row.id, 200).await.unwrap();
assert!(epoch().await.revoked.is_empty());
let v = revoke_ownership(&r.store, &r.revocations, &member.row.id, 200).await.unwrap();
assert_eq!(epoch().await.revoked, vec![Revoked::membership("namespace", "ed", PrincipalId::user("carol"), v)]);
assert_eq!(revoke_ownership(&r.store, &r.revocations, &member.row.id, 300).await.unwrap(), v);
assert!(matches!(
revoke_ownership(&r.store, &r.revocations, "ghost", 300).await,
Err(StoreError::NotFound)
));
});
}
#[test]
fn deleting_a_user_drops_them_from_every_held_snapshot() {
pollster::block_on(async {
let r = rig();
institution(&r.store).await;
r.store.insert(&ns(user("dana"), "band", "member"), 100).await.unwrap();
r.store.insert(&ns(user("dana"), "museum", "member"), 100).await.unwrap();
r.store.insert(&ns(user("erin"), "band", "member"), 100).await.unwrap();
r.store.insert(&ns(user("erin"), "museum", "member"), 100).await.unwrap();
let band = r.issuer.mint("namespace", "band", 1_000).await.unwrap();
let ed = r.issuer.mint("namespace", "ed", 1_000).await.unwrap();
for held in [&band, &ed] {
let ok = r.edge.verify_snapshot_at(&held.token, 1_000).await.unwrap();
assert!(admitted(&ok).iter().any(|m| m.starts_with("dana#")));
}
let v = revoke_principal_ownership(&r.store, &r.revocations, &user("dana"), 2_000).await.unwrap();
assert!(r.store.list_for_principal(&user("dana")).await.unwrap().is_empty());
let mut recorded = r.revocations.snapshot().await.unwrap().revoked;
recorded.sort_by(|a, b| a.identity().cmp(&b.identity()));
assert_eq!(
recorded,
vec![
Revoked::membership("namespace", "band", PrincipalId::user("dana"), v),
Revoked::membership("namespace", "museum", PrincipalId::user("dana"), v),
]
);
r.gossip(2_000).await;
assert_eq!(admitted(&r.edge.verify_snapshot_at(&band.token, 2_000).await.unwrap()), ["erin#guest", "erin#member"]);
assert_eq!(admitted(&r.edge.verify_snapshot_at(&ed.token, 2_000).await.unwrap()), ["erin#guest"]);
r.store.insert(&ns(PrincipalId::service("bot"), "band", "admin"), 3_000).await.unwrap();
revoke_principal_ownership(&r.store, &r.revocations, &PrincipalId::service("bot"), 3_000).await.unwrap();
assert_eq!(r.revocations.snapshot().await.unwrap().revoked.len(), 2);
});
}
struct Flaky {
inner: MemoryRevocationStore,
failures: AtomicUsize,
}
#[async_trait]
impl RevocationWriter for Flaky {
async fn revoke(&self, entry: &Revoked) -> Result<(), StoreError> {
if self.failures.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |n| n.checked_sub(1)).is_ok() {
return Err(StoreError::Backend("revocation log unavailable".into()));
}
self.inner.revoke(entry).await
}
async fn snapshot(&self) -> Result<RevocationSnapshot, StoreError> {
self.inner.snapshot().await
}
}
#[test]
fn a_failed_cascade_recording_is_healed_by_re_running_it() {
pollster::block_on(async {
let store = MemoryOwnershipStore::new();
store.insert(&ns(user("dana"), "band", "member"), 100).await.unwrap();
store.insert(&ns(user("dana"), "ed", "admin"), 100).await.unwrap();
let log = Flaky { inner: MemoryRevocationStore::new(), failures: AtomicUsize::new(1) };
assert!(revoke_principal_ownership(&store, &log, &user("dana"), 2_000).await.is_err());
assert!(store.list_for_principal(&user("dana")).await.unwrap().is_empty());
assert!(log.snapshot().await.unwrap().revoked.is_empty());
let v = revoke_principal_ownership(&store, &log, &user("dana"), 3_000).await.unwrap();
assert_eq!(v, store.current_version().await.unwrap());
let mut recorded = log.snapshot().await.unwrap().revoked;
recorded.sort_by(|a, b| a.identity().cmp(&b.identity()));
assert_eq!(
recorded,
vec![
Revoked::membership("namespace", "band", PrincipalId::user("dana"), v),
Revoked::membership("namespace", "ed", PrincipalId::user("dana"), v),
]
);
});
}
struct Racing {
inner: MemoryOwnershipStore,
writes: AtomicUsize,
}
#[async_trait]
impl OwnershipStore for Racing {
async fn insert(&self, o: &NewOwnership, now: i64) -> Result<Inserted, StoreError> {
self.inner.insert(o, now).await
}
async fn get(&self, id: &str) -> Result<Option<OwnershipRow>, StoreError> {
self.inner.get(id).await
}
async fn revoke_by_id(&self, id: &str, now: i64) -> Result<u64, StoreError> {
self.inner.revoke_by_id(id, now).await
}
async fn revoke_by_principal(&self, p: &PrincipalId, now: i64) -> Result<u64, StoreError> {
self.inner.revoke_by_principal(p, now).await
}
async fn list_history_for_principal(&self, p: &PrincipalId) -> Result<Vec<OwnershipRow>, StoreError> {
self.inner.list_history_for_principal(p).await
}
async fn current_version(&self) -> Result<u64, StoreError> {
self.inner.current_version().await
}
async fn list_for_principal(&self, p: &PrincipalId) -> Result<Vec<OwnershipRow>, StoreError> {
self.inner.list_for_principal(p).await
}
async fn list_for_resource(&self, kind: &str, id: &str) -> Result<Vec<OwnershipRow>, StoreError> {
let rows = self.inner.list_for_resource(kind, id).await;
if self.writes.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |n| n.checked_sub(1)).is_ok() {
let n = self.writes.load(Ordering::SeqCst);
self.inner.insert(&ns(user(&format!("late-{n}")), "ed", "member"), 500).await?;
}
rows
}
async fn list_for_subject_set(&self, kind: &str, id: &str) -> Result<Vec<OwnershipRow>, StoreError> {
self.inner.list_for_subject_set(kind, id).await
}
async fn list_for_kind(&self, kind: &str) -> Result<Vec<OwnershipRow>, StoreError> {
self.inner.list_for_kind(kind).await
}
async fn revocation_key(&self, kind: &str, id: &str) -> Result<cheers_core::RevocationKey, StoreError> {
self.inner.revocation_key(kind, id).await
}
async fn admission_policy(&self, kind: &str, id: &str) -> Result<Option<cheers_core::AdmissionPolicy>, StoreError> {
self.inner.admission_policy(kind, id).await
}
async fn set_admission_policy(
&self,
kind: &str,
id: &str,
policy: Option<&cheers_core::AdmissionPolicy>,
now: i64,
) -> Result<u64, StoreError> {
self.inner.set_admission_policy(kind, id, policy, now).await
}
}
fn racing_issuer(writes: usize) -> (MemoryOwnershipStore, SnapshotIssuer) {
let inner = MemoryOwnershipStore::new();
let (minter, _) = PasetoV4SecretMinter::generate().unwrap();
let store = Racing { inner: inner.clone(), writes: AtomicUsize::new(writes) };
(inner, SnapshotIssuer::new(store, schema(), minter, ISS, "k1"))
}
#[test]
fn a_write_under_the_walk_retries_and_mints_the_later_state() {
pollster::block_on(async {
let (store, issuer) = racing_issuer(2);
store.insert(&ns(user("alice"), "ed", "member"), 100).await.unwrap();
let s = issuer.mint("namespace", "ed", 1_000).await.unwrap();
assert_eq!(s.snapshot.epoch, store.current_version().await.unwrap());
let users: Vec<_> = s.snapshot.members.iter().map(|m| m.principal.id.as_str()).collect();
assert!(users.contains(&"late-0") && users.contains(&"late-1"), "{users:?}");
});
}
#[test]
fn a_walk_that_never_settles_is_refused() {
pollster::block_on(async {
let (store, issuer) = racing_issuer(usize::MAX);
store.insert(&ns(user("alice"), "ed", "member"), 100).await.unwrap();
match issuer.mint("namespace", "ed", 1_000).await {
Err(Error::SnapshotContended { attempts, .. }) => assert_eq!(attempts, SnapshotIssuer::MINT_ATTEMPTS),
other => panic!("expected SnapshotContended, got {other:?}"),
}
});
}
}