use std::sync::Arc;
use lys_pass::membership::{
CONTRACT_VERSION, GrantLog, MembershipDecision, MembershipRequest, Verdict,
};
use lys_pass::membership_admission::{AdmissionDecision, AdmissionRequest};
use lys_pass::membership_pages::{MembershipPage, RecipientPageRequest, ResourcePageRequest};
use lys_pass::rights::Mode;
use super::fixture::{
FixtureAuthority, FixtureSession, NOT_HELD, SeededProvider, config, resource, subject,
};
use super::{
AuthorityState, ConnectionMembership, Decided, MembershipAction, MembershipChange,
MembershipGate, MembershipListener, MembershipProvider, MembershipRefusal, MembershipSubject,
ProviderError, judged,
};
type Failure = Box<dyn std::error::Error>;
const WORKSPACE_KIND: &str = "liminal.workspace";
const CHANNEL_KIND: &str = "liminal.channel";
fn log() -> GrantLog {
GrantLog {
identity: "grant-log-a".to_owned(),
epoch: 0,
}
}
fn ward() -> Result<Arc<SeededProvider>, Failure> {
let workspace = resource(WORKSPACE_KIND, "ward");
let world = SeededProvider::new(&log())
.place(resource(CHANNEL_KIND, "ward-a"), workspace.clone(), false)
.place(resource(CHANNEL_KIND, "ward-b"), workspace.clone(), false)
.place(resource(CHANNEL_KIND, "ward-c"), workspace.clone(), false)
.place(resource(CHANNEL_KIND, "ward-secret"), workspace, true);
world.grant(
"reader-grant",
subject("reader", "person"),
resource(CHANNEL_KIND, "ward-a"),
&["read"],
Mode::Outright,
)?;
world.grant(
"poster-grant",
subject("poster", "person"),
resource(CHANNEL_KIND, "ward-a"),
&["post"],
Mode::Outright,
)?;
world.grant(
"responsible-grant",
subject("responsible", "person"),
resource(CHANNEL_KIND, "ward-c"),
&["read", "post"],
Mode::Outright,
)?;
world.grant(
"ai-grant",
subject("ai", "agent"),
resource(CHANNEL_KIND, "ward-c"),
&["read"],
Mode::Outright,
)?;
Ok(Arc::new(world))
}
fn gate(world: &Arc<SeededProvider>, max_decisions: usize) -> Result<MembershipGate, Failure> {
let config = config(
&log(),
(WORKSPACE_KIND, CHANNEL_KIND),
&[
("ward.a", "ward", "ward-a"),
("ward.b", "ward", "ward-b"),
("ward.c", "ward", "ward-c"),
("ward.secret", "ward", "ward-secret"),
],
&[(42, "ward.a")],
(16, 65_536, max_decisions),
);
Ok(MembershipGate::new(
&config,
Arc::clone(world) as Arc<dyn MembershipProvider>,
)?)
}
fn ready(gate: &MembershipGate, revision: u64) -> Result<Arc<FixtureAuthority>, Failure> {
let authority = Arc::new(FixtureAuthority::ready(&log(), revision, 1));
gate.install_authority(Arc::clone(&authority) as Arc<dyn super::MembershipAuthority>)
.map_err(|_| "an authority was already installed")?;
Ok(authority)
}
fn holder(id: &str, kind: &str) -> Result<MembershipSubject, Failure> {
Ok(MembershipSubject::from_session(&FixtureSession::new(
id,
Some(kind),
))?)
}
fn allowed(decided: &Result<Decided, MembershipRefusal>) -> bool {
matches!(decided, Ok(Decided::Allowed { .. }))
}
fn refused_as(decided: &Result<Decided, MembershipRefusal>, name: &str) -> bool {
decided
.as_ref()
.is_err_and(|refusal| refusal.name() == name)
}
#[test]
fn a_reader_reads_only_its_channel_and_cannot_post() -> Result<(), Failure> {
let world = ward()?;
let gate = gate(&world, 64)?;
ready(&gate, world.revision())?;
let reader = holder("reader", "person")?;
assert!(allowed(&gate.check(
1,
Some(&reader),
"ward.a",
MembershipAction::Read
)));
assert!(refused_as(
&gate.check(1, Some(&reader), "ward.a", MembershipAction::Post),
NOT_HELD
));
assert!(refused_as(
&gate.check(1, Some(&reader), "ward.b", MembershipAction::Read),
NOT_HELD
));
let poster = holder("poster", "person")?;
assert!(allowed(&gate.check(
2,
Some(&poster),
"ward.a",
MembershipAction::Post
)));
assert!(refused_as(
&gate.check(2, Some(&poster), "ward.b", MembershipAction::Post),
NOT_HELD
));
assert!(refused_as(
&gate.check(2, Some(&poster), "ward.a", MembershipAction::Read),
NOT_HELD
));
Ok(())
}
#[test]
fn an_unmapped_channel_is_not_protected_and_asks_nothing() -> Result<(), Failure> {
let world = ward()?;
let gate = gate(&world, 64)?;
ready(&gate, world.revision())?;
let reader = holder("reader", "person")?;
for channel in ["ward.z", "ward", "ward.a.child", "ward-a"] {
assert_eq!(
gate.check(1, Some(&reader), channel, MembershipAction::Read)?,
Decided::NotProtected
);
}
assert_eq!(world.asked(), 0);
assert_eq!(gate.counts().decisions_asked, 0);
Ok(())
}
#[test]
fn no_holder_and_no_kind_each_refuse_by_name_with_no_lookup() -> Result<(), Failure> {
let world = ward()?;
let gate = Arc::new(gate(&world, 64)?);
ready(&gate, world.revision())?;
let unverified = ConnectionMembership::bind(Some(Arc::clone(&gate)), 3, None);
assert!(refused_as(
&unverified.check("ward.a", MembershipAction::Read),
"membership_holder_unverified"
));
let kindless = FixtureSession::new("reader", None);
let unnamed = ConnectionMembership::bind(Some(Arc::clone(&gate)), 4, Some(&kindless));
assert!(refused_as(
&unnamed.check("ward.a", MembershipAction::Read),
"membership_holder_kind_unknown"
));
assert_eq!(world.asked(), 0);
Ok(())
}
#[test]
fn an_ai_is_asked_as_itself_never_as_its_responsible_person() -> Result<(), Failure> {
let world = ward()?;
let gate = gate(&world, 64)?;
ready(&gate, world.revision())?;
let ai = holder("ai", "agent")?;
assert!(allowed(&gate.check(
1,
Some(&ai),
"ward.c",
MembershipAction::Read
)));
assert!(refused_as(
&gate.check(1, Some(&ai), "ward.c", MembershipAction::Post),
NOT_HELD
));
let forged = holder("ai", "person")?;
assert!(
gate.check(2, Some(&forged), "ward.c", MembershipAction::Read)
.is_err()
);
Ok(())
}
#[test]
fn a_restricted_child_is_not_reached_through_its_workspace() -> Result<(), Failure> {
let world = ward()?;
world.grant(
"steward-grant",
subject("steward", "person"),
resource(WORKSPACE_KIND, "ward"),
&["read"],
Mode::Outright,
)?;
let gate = gate(&world, 64)?;
ready(&gate, world.revision())?;
let steward = holder("steward", "person")?;
assert!(allowed(&gate.check(
1,
Some(&steward),
"ward.b",
MembershipAction::Read
)));
assert!(refused_as(
&gate.check(1, Some(&steward), "ward.secret", MembershipAction::Read),
NOT_HELD
));
Ok(())
}
fn decision(
request: &MembershipRequest,
verdict: Verdict,
revision: Option<u64>,
) -> MembershipDecision {
MembershipDecision {
contract: CONTRACT_VERSION,
log: log(),
request: request.clone(),
revision,
verdict,
}
}
fn asked(at_least: u64) -> MembershipRequest {
MembershipRequest {
contract: CONTRACT_VERSION,
log: log(),
workspace: resource(WORKSPACE_KIND, "ward"),
subject: subject("reader", "person"),
resource: resource(CHANNEL_KIND, "ward-a"),
action: "read".to_owned(),
at_least,
}
}
#[test]
fn an_answer_that_is_not_the_one_asked_refuses_before_any_effect() -> Result<(), Failure> {
let world = ward()?;
let gate = gate(&world, 64)?;
let mapped = gate.mapped("ward.a").ok_or("ward.a is mapped")?;
let request = asked(3);
let allow = Verdict::Allowed {
grant: "reader-grant".to_owned(),
path: Vec::new(),
scope: resource(CHANNEL_KIND, "ward-a"),
};
let mut other_contract = decision(&request, allow.clone(), Some(3));
other_contract.contract = CONTRACT_VERSION.saturating_add(1);
assert!(matches!(
judged(&request, &log(), mapped, &other_contract),
Err(MembershipRefusal::ContractUnsupported { .. })
));
let mut other_log = decision(&request, allow.clone(), Some(3));
other_log.log.epoch = 1;
assert_eq!(
judged(&request, &log(), mapped, &other_log),
Err(MembershipRefusal::LogMismatch)
);
let mut substituted = decision(&request, allow.clone(), Some(3));
substituted.request.subject = subject("poster", "person");
assert_eq!(
judged(&request, &log(), mapped, &substituted),
Err(MembershipRefusal::AnswerSubstituted)
);
assert!(matches!(
judged(
&request,
&log(),
mapped,
&decision(&request, allow, Some(2))
),
Err(MembershipRefusal::RevisionStale {
required: 3,
decided: Some(2)
})
));
let sibling_scope = Verdict::Allowed {
grant: "reader-grant".to_owned(),
path: Vec::new(),
scope: resource(CHANNEL_KIND, "ward-b"),
};
assert_eq!(
judged(
&request,
&log(),
mapped,
&decision(&request, sibling_scope, Some(3))
),
Err(MembershipRefusal::ScopeOutsideMapping)
);
let undecided = Verdict::Refused {
refusal: "StaleDecision".to_owned(),
reason: String::new(),
};
assert!(matches!(
judged(
&request,
&log(),
mapped,
&decision(&request, undecided, None)
),
Err(MembershipRefusal::Refused { .. })
));
Ok(())
}
#[test]
fn a_substituting_provider_releases_nothing() -> Result<(), Failure> {
let world = ward()?;
let gate = gate(&world, 64)?;
ready(&gate, world.revision())?;
world.set_substitute(true);
let reader = holder("reader", "person")?;
assert!(refused_as(
&gate.check(1, Some(&reader), "ward.a", MembershipAction::Read),
"membership_answer_substituted"
));
assert_eq!(gate.retained()?, 0);
Ok(())
}
#[test]
fn a_hot_check_after_the_first_is_local() -> Result<(), Failure> {
let world = ward()?;
let gate = gate(&world, 64)?;
ready(&gate, world.revision())?;
let reader = holder("reader", "person")?;
for _ in 0..100 {
assert!(allowed(&gate.check(
1,
Some(&reader),
"ward.a",
MembershipAction::Read
)));
}
assert_eq!(world.asked(), 1);
let counts = gate.counts();
assert_eq!(counts.decisions_asked, 1);
assert_eq!(counts.local_lookups, 99);
Ok(())
}
#[test]
fn every_authority_state_but_ready_refuses_with_its_cause() -> Result<(), Failure> {
let world = ward()?;
let gate = gate(&world, 64)?;
let reader = holder("reader", "person")?;
assert!(refused_as(
&gate.check(1, Some(&reader), "ward.a", MembershipAction::Read),
"membership_authority_missing"
));
let authority = ready(&gate, world.revision())?;
assert!(allowed(&gate.check(
1,
Some(&reader),
"ward.a",
MembershipAction::Read
)));
for (state, name) in [
(AuthorityState::NotReady, "membership_not_ready"),
(
AuthorityState::Unavailable {
cause: "stream_lost".to_owned(),
},
"membership_unavailable",
),
(
AuthorityState::Gap {
after: 4,
missing: 2,
},
"membership_gap",
),
(AuthorityState::Reset, "membership_log_reset"),
(
AuthorityState::Ready {
log: GrantLog {
identity: "grant-log-b".to_owned(),
epoch: 0,
},
revision: 9,
generation: 1,
},
"membership_log_mismatch",
),
] {
authority.set(state)?;
assert!(refused_as(
&gate.check(1, Some(&reader), "ward.a", MembershipAction::Read),
name
));
}
assert_eq!(world.asked(), 1);
Ok(())
}
#[test]
fn a_revocation_re_decides_only_the_bindings_it_reaches() -> Result<(), Failure> {
let world = ward()?;
let gate = gate(&world, 64)?;
ready(&gate, world.revision())?;
let reader = holder("reader", "person")?;
let ai = holder("ai", "agent")?;
assert!(allowed(&gate.check(
1,
Some(&reader),
"ward.a",
MembershipAction::Read
)));
assert!(allowed(&gate.check(
2,
Some(&ai),
"ward.c",
MembershipAction::Read
)));
let revision = world.revoke("reader-grant")?;
let applied = gate.changed(&MembershipChange {
log: log(),
revision,
subject: Some(subject("reader", "person")),
resource: resource(CHANNEL_KIND, "ward-a"),
})?;
assert_eq!(applied.affected, 1);
assert_eq!(applied.redecided, 1);
assert_eq!(applied.failed, 0);
assert_eq!(gate.counts().affected_visited, 1);
assert!(refused_as(
&gate.check(1, Some(&reader), "ward.a", MembershipAction::Read),
"Revoked"
));
assert!(allowed(&gate.check(
2,
Some(&ai),
"ward.c",
MembershipAction::Read
)));
let stale = gate.changed(&MembershipChange {
log: log(),
revision: revision.saturating_sub(1),
subject: None,
resource: resource(CHANNEL_KIND, "ward-a"),
})?;
assert!(stale.duplicate);
assert!(refused_as(
&gate.check(1, Some(&reader), "ward.a", MembershipAction::Read),
"Revoked"
));
Ok(())
}
#[test]
fn a_workspace_change_reaches_every_channel_placed_in_it() -> Result<(), Failure> {
let world = ward()?;
let gate = gate(&world, 64)?;
ready(&gate, world.revision())?;
let reader = holder("reader", "person")?;
let ai = holder("ai", "agent")?;
assert!(allowed(&gate.check(
1,
Some(&reader),
"ward.a",
MembershipAction::Read
)));
assert!(allowed(&gate.check(
2,
Some(&ai),
"ward.c",
MembershipAction::Read
)));
let applied = gate.changed(&MembershipChange {
log: log(),
revision: world.revision().saturating_add(1),
subject: None,
resource: resource(WORKSPACE_KIND, "ward"),
});
let applied = applied?;
assert_eq!(applied.affected, 2);
assert_eq!(applied.failed, 2);
assert_eq!(gate.retained()?, 0);
Ok(())
}
#[test]
fn a_change_on_another_log_is_refused() -> Result<(), Failure> {
let world = ward()?;
let gate = gate(&world, 64)?;
ready(&gate, world.revision())?;
let refused = gate.changed(&MembershipChange {
log: GrantLog {
identity: "grant-log-a".to_owned(),
epoch: 1,
},
revision: 99,
subject: None,
resource: resource(WORKSPACE_KIND, "ward"),
});
assert_eq!(refused, Err(MembershipRefusal::LogMismatch));
Ok(())
}
#[test]
fn a_resume_rechecks_every_binding_once_and_only_then_adopts() -> Result<(), Failure> {
let world = ward()?;
let gate = gate(&world, 64)?;
let authority = ready(&gate, world.revision())?;
let reader = holder("reader", "person")?;
let ai = holder("ai", "agent")?;
assert!(allowed(&gate.check(
1,
Some(&reader),
"ward.a",
MembershipAction::Read
)));
assert!(allowed(&gate.check(
2,
Some(&ai),
"ward.c",
MembershipAction::Read
)));
authority.set(AuthorityState::Unavailable {
cause: "stream_lost".to_owned(),
})?;
world.revoke("reader-grant")?;
authority.set(AuthorityState::Ready {
log: log(),
revision: world.revision(),
generation: 2,
})?;
assert!(refused_as(
&gate.check(1, Some(&reader), "ward.a", MembershipAction::Read),
"membership_resume_pending"
));
let report = gate.resumed(2, world.revision())?;
assert_eq!(report.rechecked, 2);
assert_eq!(report.failed, 0);
assert!(report.settled);
assert_eq!(gate.counts().resume_rechecks, 2);
assert!(refused_as(
&gate.check(1, Some(&reader), "ward.a", MembershipAction::Read),
"Revoked"
));
assert!(allowed(&gate.check(
2,
Some(&ai),
"ward.c",
MembershipAction::Read
)));
Ok(())
}
#[test]
fn a_resume_that_cannot_ask_does_not_settle() -> Result<(), Failure> {
let world = ward()?;
let gate = gate(&world, 64)?;
ready(&gate, world.revision())?;
let reader = holder("reader", "person")?;
assert!(allowed(&gate.check(
1,
Some(&reader),
"ward.a",
MembershipAction::Read
)));
world.set_unavailable(true);
let report = gate.resumed(2, world.revision())?;
assert!(!report.settled);
assert_eq!(report.failed, 1);
assert_eq!(gate.retained()?, 0);
Ok(())
}
#[test]
fn the_index_admits_exactly_its_bound_and_refuses_one_more() -> Result<(), Failure> {
let world = ward()?;
let gate = gate(&world, 2)?;
ready(&gate, world.revision())?;
let reader = holder("reader", "person")?;
assert!(allowed(&gate.check(
1,
Some(&reader),
"ward.a",
MembershipAction::Read
)));
assert!(
gate.check(1, Some(&reader), "ward.a", MembershipAction::Post)
.is_err()
);
assert_eq!(gate.retained()?, 2);
let asked_before = world.asked();
assert_eq!(
gate.check(1, Some(&reader), "ward.b", MembershipAction::Read),
Err(MembershipRefusal::CapacityExceeded { limit: 2 })
);
assert_eq!(world.asked(), asked_before);
assert_eq!(gate.retained()?, 2);
Ok(())
}
#[test]
fn a_closed_connection_releases_only_the_bindings_no_one_else_holds() -> Result<(), Failure> {
let world = ward()?;
let gate = Arc::new(gate(&world, 64)?);
ready(&gate, world.revision())?;
let session = FixtureSession::new("reader", Some("person"));
let first = ConnectionMembership::bind(Some(Arc::clone(&gate)), 1, Some(&session));
let second = ConnectionMembership::bind(Some(Arc::clone(&gate)), 2, Some(&session));
assert!(allowed(&first.check("ward.a", MembershipAction::Read)));
assert!(allowed(&second.check("ward.a", MembershipAction::Read)));
assert!(first.check("ward.b", MembershipAction::Read).is_err());
assert_eq!(gate.retained()?, 2);
drop(first);
assert_eq!(gate.retained()?, 1);
assert_eq!(gate.counts().released, 1);
drop(second);
assert_eq!(gate.retained()?, 0);
Ok(())
}
#[test]
fn a_mapped_conversation_is_released_on_its_channel_and_an_unmapped_one_is_not_protected()
-> Result<(), Failure> {
let world = ward()?;
let gate = Arc::new(gate(&world, 64)?);
ready(&gate, world.revision())?;
assert_eq!(gate.channel_for_conversation(42), Some("ward.a"));
assert_eq!(gate.channel_for_conversation(43), None);
let reader = FixtureSession::new("reader", Some("person"));
let poster = FixtureSession::new("poster", Some("person"));
let reading = ConnectionMembership::bind(Some(Arc::clone(&gate)), 1, Some(&reader));
let posting = ConnectionMembership::bind(Some(Arc::clone(&gate)), 2, Some(&poster));
assert!(allowed(&reading.release_conversation(42)));
assert!(posting.release_conversation(42).is_err());
assert_eq!(posting.release_conversation(43)?, Decided::NotProtected);
assert_eq!(gate.counts().release_checks, 2);
Ok(())
}
#[test]
fn an_invalid_mapping_is_refused_at_start_by_member() -> Result<(), Failure> {
let world = ward()?;
let provider = Arc::clone(&world) as Arc<dyn MembershipProvider>;
let base = |channels: &[(&str, &str, &str)], conversations: &[(u64, &str)]| {
config(
&log(),
(WORKSPACE_KIND, CHANNEL_KIND),
channels,
conversations,
(16, 65_536, 64),
)
};
assert!(MembershipGate::new(&base(&[], &[]), Arc::clone(&provider)).is_err());
assert!(
MembershipGate::new(
&base(&[("a", "ward", "ward-a"), ("a", "ward", "ward-b")], &[]),
Arc::clone(&provider)
)
.is_err()
);
assert!(
MembershipGate::new(
&base(&[("a", "ward", "ward-a"), ("b", "ward", "ward-a")], &[]),
Arc::clone(&provider)
)
.is_err()
);
assert!(
MembershipGate::new(
&base(&[("a", "ward", "ward-a")], &[(1, "b")]),
Arc::clone(&provider)
)
.is_err()
);
assert!(
MembershipGate::new(&base(&[("a", "", "ward-a")], &[]), Arc::clone(&provider)).is_err()
);
let mut zero = base(&[("a", "ward", "ward-a")], &[]);
zero.page_rows = 0;
assert!(MembershipGate::new(&zero, Arc::clone(&provider)).is_err());
let mut contract = base(&[("a", "ward", "ward-a")], &[]);
contract.contract = CONTRACT_VERSION.saturating_add(1);
assert!(MembershipGate::new(&contract, Arc::clone(&provider)).is_err());
let mut elsewhere = base(&[("a", "ward", "ward-a")], &[]);
elsewhere.connect_workspace = Some("elsewhere".to_owned());
assert!(MembershipGate::new(&elsewhere, provider).is_err());
Ok(())
}
#[test]
fn scoped_guest_admission_needs_a_current_grant_in_the_workspace() -> Result<(), Failure> {
let world = ward()?;
let mut scoped = config(
&log(),
(WORKSPACE_KIND, CHANNEL_KIND),
&[("ward.a", "ward", "ward-a"), ("ward.b", "ward", "ward-b")],
&[],
(16, 65_536, 64),
);
scoped.connect_workspace = Some("ward".to_owned());
let gate = MembershipGate::new(&scoped, Arc::clone(&world) as Arc<dyn MembershipProvider>)?;
ready(&gate, world.revision())?;
let reader = holder("reader", "person")?;
assert!(gate.admit_connect(&reader)?.is_some());
assert!(allowed(&gate.check(
1,
Some(&reader),
"ward.a",
MembershipAction::Read
)));
assert!(
gate.check(1, Some(&reader), "ward.b", MembershipAction::Read)
.is_err()
);
let stranger = holder("stranger", "person")?;
assert!(matches!(
gate.admit_connect(&stranger),
Err(MembershipRefusal::Refused { refusal }) if refusal == "membership_no_current_grant"
));
Ok(())
}
#[derive(Debug)]
struct Down;
fn down() -> ProviderError {
ProviderError {
cause: "lys_could_not_be_asked".to_owned(),
}
}
impl MembershipProvider for Down {
fn decide(&self, _request: &MembershipRequest) -> Result<MembershipDecision, ProviderError> {
Err(down())
}
fn admission(&self, _request: &AdmissionRequest) -> Result<AdmissionDecision, ProviderError> {
Err(down())
}
fn resources(&self, _request: &ResourcePageRequest) -> Result<MembershipPage, ProviderError> {
Err(down())
}
fn recipients(&self, _request: &RecipientPageRequest) -> Result<MembershipPage, ProviderError> {
Err(down())
}
}
#[test]
fn an_unavailable_provider_is_named_never_an_answer() -> Result<(), Failure> {
let config = config(
&log(),
(WORKSPACE_KIND, CHANNEL_KIND),
&[("ward.a", "ward", "ward-a")],
&[],
(16, 65_536, 64),
);
let gate = MembershipGate::new(&config, Arc::new(Down))?;
ready(&gate, 0)?;
let reader = holder("reader", "person")?;
assert_eq!(
gate.check(1, Some(&reader), "ward.a", MembershipAction::Read),
Err(MembershipRefusal::Unavailable {
cause: "lys_could_not_be_asked".to_owned()
})
);
assert_eq!(gate.retained()?, 0);
Ok(())
}
#[test]
fn no_refusal_carries_a_resource_or_holder_in_its_words() -> Result<(), Failure> {
let world = ward()?;
let gate = gate(&world, 64)?;
ready(&gate, world.revision())?;
let reader = holder("reader", "person")?;
let refusal = gate
.check(1, Some(&reader), "ward.b", MembershipAction::Read)
.err()
.ok_or("ward.b is refused")?;
let words = refusal.to_string();
for secret in ["ward", "reader", "ward-b"] {
assert!(!words.contains(secret), "{words} names {secret}");
}
let debug = format!("{reader:?}");
assert!(!debug.contains("reader"));
Ok(())
}