liminal-server 0.28.2

Standalone server for the liminal messaging bus
Documentation
use liminal::protocol::FrameType;
use liminal_protocol::wire::{ClientDiscriminant, PARTICIPANT_FRAME_TYPE};
use liminal_server::auth_lys::routes::{
    RouteClassification, classify_participant_route, classify_wire_route,
};
use liminal_server::auth_lys::{
    AccessAction, AccessDecisionError, AccessRefusal, AccessResource, DecisionClass, GrantReceipt,
    HotAction, LysPassConsumer, RefusalCause, ResourceKind, SubjectBinding, admit_lys_pass,
    decide_message,
};
use std::cell::Cell;

type TestResult = Result<(), Box<dyn std::error::Error>>;

struct PassFixture {
    verifies: Cell<usize>,
    decisions: Cell<usize>,
}

struct FixturePass {
    expires_at: u64,
}

impl LysPassConsumer for PassFixture {
    type Pass = FixturePass;
    type Holder = str;
    type AdmissionError = std::io::Error;

    fn verify_offline(
        &self,
        bytes: &[u8],
        audience: &str,
        now: u64,
    ) -> Result<Self::Pass, Self::AdmissionError> {
        self.verifies.set(self.verifies.get() + 1);
        if bytes != b"fixture-pass" || audience != "liminal" || now >= 20 {
            return Err(std::io::Error::other("fixture admission refused"));
        }
        Ok(FixturePass { expires_at: 20 })
    }

    fn verified_holder<'p>(&self, pass: &'p Self::Pass) -> &'p Self::Holder {
        if pass.expires_at == 20 {
            "fixture-holder"
        } else {
            "other-fixture-holder"
        }
    }

    fn decide_hot(
        &self,
        pass: &Self::Pass,
        subject: &SubjectBinding,
        action: HotAction,
        now: u64,
    ) -> Result<GrantReceipt, AccessDecisionError> {
        self.decisions.set(self.decisions.get() + 1);
        let cause = if now >= pass.expires_at {
            Some(RefusalCause::Expired)
        } else if subject.network_id() != "network-a" {
            Some(RefusalCause::MissingGrant)
        } else {
            // This fixture's explicit placement edges are independent of spelling.
            let allowed = matches!(
                (subject.resource().id.as_str(), action),
                ("orders" | "created-resource", HotAction::Publish)
                    | ("secret-explicit" | "invoice-read", HotAction::Subscribe)
                    | ("call-service", HotAction::Call)
                    | ("serve-service", HotAction::Serve)
            );
            if allowed {
                None
            } else {
                Some(RefusalCause::MissingGrant)
            }
        };
        if let Some(cause) = cause {
            return Err(AccessDecisionError {
                cause,
                refusal: AccessRefusal::new(
                    subject.resource().clone(),
                    action.into(),
                    "https://lys.invalid/requests/access".to_owned(),
                ),
            });
        }
        Ok(GrantReceipt {
            grant: "fixture-grant".to_owned(),
        })
    }

    fn decide_link(
        &self,
        _pass: &Self::Pass,
        subject: &SubjectBinding,
        action: liminal_server::link::LinkAction,
        _now: u64,
    ) -> Result<GrantReceipt, AccessDecisionError> {
        // This fixture holds no link grants.
        Err(AccessDecisionError {
            cause: RefusalCause::MissingGrant,
            refusal: AccessRefusal::new(
                subject.resource().clone(),
                action.access_action(),
                "https://lys.invalid/requests/access".to_owned(),
            ),
        })
    }
}

const fn consumer() -> PassFixture {
    PassFixture {
        verifies: Cell::new(0),
        decisions: Cell::new(0),
    }
}

fn subject(
    network: &str,
    resource: &str,
    name: &str,
) -> Result<SubjectBinding, Box<dyn std::error::Error>> {
    Ok(SubjectBinding::new(
        network.to_owned(),
        resource.to_owned(),
        name.to_owned(),
    )?)
}

#[test]
fn access_subject_action_matrix_is_separate_and_placement_based() -> TestResult {
    let provider = consumer();
    let pass = admit_lys_pass(&provider, b"fixture-pass", 10)?;
    let cases = [
        ("orders", "orders", [true, false, false, false]),
        (
            "created-resource",
            "orders.created",
            [true, false, false, false],
        ),
        ("invoice-read", "invoices", [false, true, false, false]),
        ("call-service", "service", [false, false, true, false]),
        ("serve-service", "service", [false, false, false, true]),
        ("restricted", "orders.private", [false, false, false, false]),
        (
            "secret-explicit",
            "orders.private",
            [false, true, false, false],
        ),
        ("unplaced", "orders.guessed", [false, false, false, false]),
        ("lookalike", "orders2.created", [false, false, false, false]),
    ];
    let actions = [
        HotAction::Publish,
        HotAction::Subscribe,
        HotAction::Call,
        HotAction::Serve,
    ];
    for (resource, name, expected) in cases {
        let binding = subject("network-a", resource, name)?;
        for (action, allowed) in actions.into_iter().zip(expected) {
            assert_eq!(
                decide_message(&provider, &pass, &binding, action, 11).is_ok(),
                allowed,
                "{name} {action:?}"
            );
        }
    }
    assert!(
        decide_message(
            &provider,
            &pass,
            &subject("network-b", "orders", "orders")?,
            HotAction::Publish,
            11
        )
        .is_err()
    );
    assert_eq!(provider.verifies.get(), 1);
    assert_eq!(provider.decisions.get(), 37);
    Ok(())
}

#[test]
fn access_pass_is_decided_again_for_each_message_and_expires_at_boundary() -> TestResult {
    let provider = consumer();
    let pass = admit_lys_pass(&provider, b"fixture-pass", 10)?;
    let binding = subject("network-a", "orders", "orders.created")?;
    assert!(decide_message(&provider, &pass, &binding, HotAction::Publish, 19).is_ok());
    let refusal = decide_message(&provider, &pass, &binding, HotAction::Publish, 20)
        .err()
        .ok_or("expired message was admitted")?;
    assert_eq!(refusal.cause, RefusalCause::Expired);
    assert_eq!(refusal.refusal.refused.resource_id(), binding.resource().id);
    assert_eq!(refusal.refusal.refused.action, AccessAction::Publish.name());
    assert_eq!(provider.verifies.get(), 1);
    assert_eq!(provider.decisions.get(), 2);
    Ok(())
}

#[test]
fn access_refusal_has_the_shared_four_field_wire_shape() -> TestResult {
    let refusal = AccessRefusal::new(
        AccessResource {
            kind: ResourceKind::Subject,
            id: "orders".to_owned(),
        },
        AccessAction::Publish,
        "https://lys.invalid/requests/access".to_owned(),
    );
    assert_eq!(
        serde_json::to_string(&refusal)?,
        r#"{"refused":{"resource":"liminal.subject:orders","action":"publish","needed":{"app":"liminal","kind":"liminal.subject","id":"orders","action":"publish"},"request":"https://lys.invalid/requests/access"}}"#
    );
    Ok(())
}

#[test]
fn access_binding_is_explicit_and_rejects_patterns_and_empty_identifiers() {
    for (network, resource, name) in [
        ("", "orders", "orders"),
        ("network", "", "orders"),
        ("network", "orders", ""),
        ("network", "orders", "orders.*"),
        ("network", "orders", "orders.>"),
    ] {
        assert!(
            SubjectBinding::new(network.to_owned(), resource.to_owned(), name.to_owned()).is_err()
        );
    }
}

#[test]
fn access_deliberate_actions_cannot_enter_the_hot_message_seam() {
    for action in [
        AccessAction::LinkExport,
        AccessAction::LinkImport,
        AccessAction::DeployService,
        AccessAction::Admin,
    ] {
        assert_eq!(action.class(), DecisionClass::Deliberate);
        assert!(HotAction::try_from(action).is_err());
    }
}

#[test]
fn access_every_wire_and_participant_route_matches_the_committed_matrix() -> TestResult {
    let matrix: serde_json::Value = serde_json::from_str(include_str!(
        "../../../docs/design/access/route_matrix.json"
    ))?;
    let rows = matrix.as_array().ok_or("matrix is not a table")?;
    assert_eq!(rows.len(), 40);
    for row in rows {
        let family = row["family"].as_str().ok_or("matrix family")?;
        let tag = row["tag"].as_u64().ok_or("matrix tag")?;
        let actual = match family {
            "wire" => classify_wire_route(FrameType::from(u8::try_from(tag)?)),
            "participant" => {
                classify_participant_route(ClientDiscriminant::try_from(u16::try_from(tag)?)?)
            }
            _ => return Err("unknown matrix family".into()),
        };
        assert_eq!(
            serde_json::to_value(actual)?,
            row["policy"],
            "{}",
            row["route"]
        );
    }
    assert_eq!(
        classify_wire_route(FrameType::Unknown(PARTICIPANT_FRAME_TYPE)),
        RouteClassification::ParticipantEnvelope
    );
    assert_eq!(
        classify_wire_route(FrameType::Unknown(0xFE)),
        RouteClassification::Unknown
    );
    Ok(())
}