use liminal_server::auth_lys::{
AccessAction, AccessDecisionError, AccessRefusal, AccessResource, AccessRouteClass,
DeliberateDecision, GrantMode, GrantReceipt, HotAction, LysDeliberateConsumer, LysPassConsumer,
RefusalCause, ResourceKind, SubjectBinding, admit_lys_pass, decide_deliberate, decide_message,
};
use std::cell::Cell;
type TestResult = Result<(), Box<dyn std::error::Error>>;
#[derive(Clone, Copy)]
enum LiveAnswer {
Outright,
ByDraft,
ByTwo,
Unavailable,
}
struct ConsumerFixture {
answer: LiveAnswer,
hot_calls: Cell<usize>,
live_calls: Cell<usize>,
}
impl ConsumerFixture {
const fn new(answer: LiveAnswer) -> Self {
Self {
answer,
hot_calls: Cell::new(0),
live_calls: Cell::new(0),
}
}
}
fn keys_resource() -> AccessResource {
AccessResource {
kind: ResourceKind::Subject,
id: "session-keys-resource".to_owned(),
}
}
fn refusal(
resource: &AccessResource,
action: AccessAction,
cause: RefusalCause,
) -> AccessDecisionError {
AccessDecisionError {
cause,
refusal: AccessRefusal::new(
resource.clone(),
action,
"https://lys.invalid/requests/access".to_owned(),
),
}
}
impl LysPassConsumer for ConsumerFixture {
type Pass = ();
type Holder = str;
type AdmissionError = std::io::Error;
fn verify_offline(
&self,
credential: &[u8],
audience: &str,
now: u64,
) -> Result<Self::Pass, Self::AdmissionError> {
if credential != b"service-fixture-pass" || audience != "liminal" || now != 10 {
return Err(std::io::Error::other("fixture admission refused"));
}
Ok(())
}
fn verified_holder<'p>(&self, pass: &'p Self::Pass) -> &'p Self::Holder {
let () = pass;
"service-fixture-holder"
}
fn decide_hot(
&self,
pass: &Self::Pass,
subject: &SubjectBinding,
action: HotAction,
now: u64,
) -> Result<GrantReceipt, AccessDecisionError> {
let () = pass;
assert_eq!(now, 11);
self.hot_calls.set(self.hot_calls.get() + 1);
match self.answer {
LiveAnswer::Outright => Ok(GrantReceipt {
grant: "outright-grant".to_owned(),
}),
LiveAnswer::ByDraft | LiveAnswer::ByTwo => Err(refusal(
subject.resource(),
action.into(),
RefusalCause::HotMode,
)),
LiveAnswer::Unavailable => Err(refusal(
subject.resource(),
action.into(),
RefusalCause::MissingGrant,
)),
}
}
fn decide_link(
&self,
pass: &Self::Pass,
subject: &SubjectBinding,
action: liminal_server::link::LinkAction,
_now: u64,
) -> Result<GrantReceipt, AccessDecisionError> {
let () = pass;
Err(refusal(
subject.resource(),
action.access_action(),
RefusalCause::MissingGrant,
))
}
}
impl LysDeliberateConsumer for ConsumerFixture {
fn decide_live(
&self,
pass: &Self::Pass,
resource: &AccessResource,
action: AccessAction,
now: u64,
) -> Result<DeliberateDecision, AccessDecisionError> {
let () = pass;
assert_eq!(resource, &keys_resource());
assert_eq!(action, AccessAction::Call);
assert_eq!(now, 11);
self.live_calls.set(self.live_calls.get() + 1);
match self.answer {
LiveAnswer::Outright => Ok(DeliberateDecision::Outright(GrantReceipt {
grant: "administrator-outright-grant".to_owned(),
})),
LiveAnswer::ByDraft => Ok(DeliberateDecision::ByDraft {
grant: "keys-draft-grant".to_owned(),
draft: "keys-held-draft".to_owned(),
}),
LiveAnswer::ByTwo => Ok(DeliberateDecision::ByTwo {
grant: "keys-two-grant".to_owned(),
draft: "keys-held-two".to_owned(),
}),
LiveAnswer::Unavailable => Err(refusal(
resource,
action,
RefusalCause::DeliberateUnavailable,
)),
}
}
}
#[test]
fn session_keys_schema_is_deliberate_by_draft_and_has_no_hot_call_action() -> TestResult {
let vectors: serde_json::Value = serde_json::from_str(include_str!(
"../../../services/session-keys/acceptance.json"
))?;
let schema: AccessRouteClass =
serde_json::from_value(vectors["cases"][0]["expected_schema"].clone())?;
assert_eq!(
schema,
AccessRouteClass::Deliberate {
mode: GrantMode::ByDraft,
}
);
assert_eq!(schema.hot_action(), None);
assert_eq!(AccessRouteClass::Call.hot_action(), Some(HotAction::Call));
Ok(())
}
#[test]
fn session_keys_live_draft_and_two_decisions_remain_held_without_hot_fallback() -> TestResult {
for (answer, expected) in [
(
LiveAnswer::ByDraft,
DeliberateDecision::ByDraft {
grant: "keys-draft-grant".to_owned(),
draft: "keys-held-draft".to_owned(),
},
),
(
LiveAnswer::ByTwo,
DeliberateDecision::ByTwo {
grant: "keys-two-grant".to_owned(),
draft: "keys-held-two".to_owned(),
},
),
] {
let consumer = ConsumerFixture::new(answer);
let pass = admit_lys_pass(&consumer, b"service-fixture-pass", 10)?;
assert_eq!(
decide_deliberate(&consumer, &pass, &keys_resource(), AccessAction::Call, 11)?,
expected
);
assert_eq!(consumer.live_calls.get(), 1);
assert_eq!(consumer.hot_calls.get(), 0);
}
Ok(())
}
#[test]
fn session_keys_outright_is_the_live_grant_and_never_a_call_override() -> TestResult {
let consumer = ConsumerFixture::new(LiveAnswer::Outright);
let pass = admit_lys_pass(&consumer, b"service-fixture-pass", 10)?;
assert_eq!(
decide_deliberate(&consumer, &pass, &keys_resource(), AccessAction::Call, 11)?,
DeliberateDecision::Outright(GrantReceipt {
grant: "administrator-outright-grant".to_owned(),
})
);
assert_eq!(consumer.live_calls.get(), 1);
assert_eq!(consumer.hot_calls.get(), 0);
Ok(())
}
#[test]
fn session_keys_unavailable_preserves_named_refusal_without_hot_fallback() -> TestResult {
let consumer = ConsumerFixture::new(LiveAnswer::Unavailable);
let pass = admit_lys_pass(&consumer, b"service-fixture-pass", 10)?;
let error = decide_deliberate(&consumer, &pass, &keys_resource(), AccessAction::Call, 11)
.err()
.ok_or("unavailable deliberate consumer allowed the act")?;
assert_eq!(error.cause, RefusalCause::DeliberateUnavailable);
assert!(error.to_string().contains("lys_could_not_be_asked"));
assert_eq!(
serde_json::to_value(&error.refusal)?,
serde_json::json!({
"refused": {
"resource": "liminal.subject:session-keys-resource",
"action": "call",
"needed": {"app": "liminal", "kind": "liminal.subject", "id": "session-keys-resource", "action": "call"},
"request": "https://lys.invalid/requests/access"
}
})
);
assert_eq!(consumer.live_calls.get(), 1);
assert_eq!(consumer.hot_calls.get(), 0);
Ok(())
}
#[test]
fn session_keys_draft_and_two_cannot_authorize_the_hot_call_seam() -> TestResult {
let binding = SubjectBinding::new(
"network-fixture".to_owned(),
"session-keys-resource".to_owned(),
"sessions.x.keys".to_owned(),
)?;
for answer in [LiveAnswer::ByDraft, LiveAnswer::ByTwo] {
let consumer = ConsumerFixture::new(answer);
let pass = admit_lys_pass(&consumer, b"service-fixture-pass", 10)?;
let error = decide_message(&consumer, &pass, &binding, HotAction::Call, 11)
.err()
.ok_or("held mode authorized a hot call")?;
assert_eq!(error.cause, RefusalCause::HotMode);
assert_eq!(error.refusal.refused.resource_id(), binding.resource().id);
assert_eq!(error.refusal.refused.action, AccessAction::Call.name());
assert_eq!(consumer.hot_calls.get(), 1);
assert_eq!(consumer.live_calls.get(), 0);
}
Ok(())
}