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 {
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> {
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(())
}