liminal-server 0.28.7

Standalone server for the liminal messaging bus
Documentation
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;
        // The services fixture holds no link grants.
        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(())
}