use serde::{Deserialize, Serialize};
pub mod live;
pub mod lys_pass_adapter;
pub mod prepared;
mod refusal;
pub mod root_presence;
pub mod root_presence_start;
pub mod routes;
pub use refusal::{AccessDecisionError, AccessRefusal, NeededGrant, RefusalCause, RequiredGrant};
pub mod session;
pub const LIMINAL_PASS_AUDIENCE: &str = "liminal";
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DecisionClass {
Hot,
Deliberate,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AccessAction {
Publish,
Subscribe,
Call,
Serve,
LinkExport,
LinkImport,
DeployService,
Admin,
}
impl AccessAction {
#[must_use]
pub const fn class(self) -> DecisionClass {
match self {
Self::Publish | Self::Subscribe | Self::Call | Self::Serve => DecisionClass::Hot,
Self::LinkExport | Self::LinkImport | Self::DeployService | Self::Admin => {
DecisionClass::Deliberate
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HotAction {
Publish,
Subscribe,
Call,
Serve,
}
impl From<HotAction> for AccessAction {
fn from(value: HotAction) -> Self {
match value {
HotAction::Publish => Self::Publish,
HotAction::Subscribe => Self::Subscribe,
HotAction::Call => Self::Call,
HotAction::Serve => Self::Serve,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
#[error("action {0:?} requires a live Lys decision")]
pub struct DeliberateAction(pub AccessAction);
impl TryFrom<AccessAction> for HotAction {
type Error = DeliberateAction;
fn try_from(value: AccessAction) -> Result<Self, Self::Error> {
match value {
AccessAction::Publish => Ok(Self::Publish),
AccessAction::Subscribe => Ok(Self::Subscribe),
AccessAction::Call => Ok(Self::Call),
AccessAction::Serve => Ok(Self::Serve),
AccessAction::LinkExport
| AccessAction::LinkImport
| AccessAction::DeployService
| AccessAction::Admin => Err(DeliberateAction(value)),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResourceKind {
Network,
Subject,
Link,
Service,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AccessResource {
pub kind: ResourceKind,
pub id: String,
}
#[derive(Debug, PartialEq, Eq)]
pub struct SubjectBinding {
network_id: String,
resource: AccessResource,
subject: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
#[error("invalid placed subject binding: {0}")]
pub struct SubjectBindingError(pub &'static str);
impl SubjectBinding {
pub fn new(
network_id: String,
resource_id: String,
subject: String,
) -> Result<Self, SubjectBindingError> {
for (name, value) in [
("network", &network_id),
("resource", &resource_id),
("subject", &subject),
] {
if value.is_empty()
|| value.chars().any(char::is_whitespace)
|| value.chars().any(char::is_control)
{
return Err(SubjectBindingError(name));
}
}
if subject.contains(['*', '>']) {
return Err(SubjectBindingError("subject pattern"));
}
Ok(Self {
network_id,
resource: AccessResource {
kind: ResourceKind::Subject,
id: resource_id,
},
subject,
})
}
#[must_use]
pub fn network_id(&self) -> &str {
self.network_id.as_str()
}
#[must_use]
pub const fn resource(&self) -> &AccessResource {
&self.resource
}
#[must_use]
pub fn subject(&self) -> &str {
self.subject.as_str()
}
}
pub const LYS_APP: &str = "liminal";
impl ResourceKind {
#[must_use]
pub const fn qualified(self) -> &'static str {
match self {
Self::Network => "liminal.network",
Self::Subject => "liminal.subject",
Self::Link => "liminal.link",
Self::Service => "liminal.service",
}
}
}
impl AccessAction {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Publish => "publish",
Self::Subscribe => "subscribe",
Self::Call => "call",
Self::Serve => "serve",
Self::LinkExport => "link_export",
Self::LinkImport => "link_import",
Self::DeployService => "deploy_service",
Self::Admin => "admin",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GrantReceipt {
pub grant: String,
}
pub trait LysPassConsumer {
type Pass;
type Holder: ?Sized;
type AdmissionError: std::error::Error;
fn verify_offline(
&self,
credential: &[u8],
audience: &str,
now: u64,
) -> Result<Self::Pass, Self::AdmissionError>;
fn verified_holder<'p>(&self, pass: &'p Self::Pass) -> &'p Self::Holder;
fn verified_holder_kind<'p>(&self, _pass: &'p Self::Pass) -> Option<&'p str> {
None
}
fn verified_issued_at(&self, _pass: &Self::Pass) -> Option<u64> {
None
}
fn decide_hot(
&self,
pass: &Self::Pass,
subject: &SubjectBinding,
action: HotAction,
now: u64,
) -> Result<GrantReceipt, AccessDecisionError>;
fn decide_link(
&self,
pass: &Self::Pass,
subject: &SubjectBinding,
action: crate::link::LinkAction,
now: u64,
) -> Result<GrantReceipt, AccessDecisionError>;
}
pub struct AdmittedLysPass<P> {
pass: P,
}
impl<P> std::fmt::Debug for AdmittedLysPass<P> {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("AdmittedLysPass")
.finish_non_exhaustive()
}
}
pub fn admit_lys_pass<C: LysPassConsumer>(
consumer: &C,
credential: &[u8],
now: u64,
) -> Result<AdmittedLysPass<C::Pass>, C::AdmissionError> {
consumer
.verify_offline(credential, LIMINAL_PASS_AUDIENCE, now)
.map(|pass| AdmittedLysPass { pass })
}
pub fn decide_message<C: LysPassConsumer>(
consumer: &C,
pass: &AdmittedLysPass<C::Pass>,
subject: &SubjectBinding,
action: HotAction,
now: u64,
) -> Result<GrantReceipt, AccessDecisionError> {
consumer.decide_hot(&pass.pass, subject, action, now)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GrantMode {
Outright,
ByDraft,
ByTwo,
}
#[derive(Debug, PartialEq, Eq)]
pub enum DeliberateDecision {
Outright(GrantReceipt),
ByDraft {
grant: String,
draft: String,
},
ByTwo {
grant: String,
draft: String,
},
}
pub trait LysDeliberateConsumer: LysPassConsumer {
fn hold_permission(
&self,
_pass: &Self::Pass,
_request: prepared::PermissionRequest<'_>,
) -> Result<serde_json::Value, lys_pass::Error> {
Err(lys_pass::Error::CannotAsk(
"prepared permission unavailable",
))
}
fn hold_site(
&self,
_pass: &Self::Pass,
_request: prepared::SiteRequest<'_>,
) -> Result<serde_json::Value, lys_pass::Error> {
Err(lys_pass::Error::CannotAsk(
"prepared construction unavailable",
))
}
fn decide_live(
&self,
pass: &Self::Pass,
resource: &AccessResource,
action: AccessAction,
now: u64,
) -> Result<DeliberateDecision, AccessDecisionError>;
}
#[must_use]
pub fn verified_lys_holder<'p, C: LysPassConsumer>(
consumer: &C,
pass: &'p AdmittedLysPass<C::Pass>,
) -> &'p C::Holder {
consumer.verified_holder(&pass.pass)
}
pub fn decide_deliberate<C: LysDeliberateConsumer>(
consumer: &C,
pass: &AdmittedLysPass<C::Pass>,
resource: &AccessResource,
action: AccessAction,
now: u64,
) -> Result<DeliberateDecision, AccessDecisionError> {
consumer.decide_live(&pass.pass, resource, action, now)
}
pub trait LysPolicySource {
type Snapshot;
type Error: std::error::Error;
fn read_policy(&self, resource: &AccessResource) -> Result<Self::Snapshot, Self::Error>;
}
#[path = "auth_lys/route_class.rs"]
mod route_class;
#[cfg(test)]
#[path = "auth_lys/root_presence_start_tests.rs"]
mod root_presence_start_tests;
#[cfg(test)]
#[path = "auth_lys/root_presence_tests.rs"]
mod root_presence_tests;
pub use route_class::AccessRouteClass;
pub use live::drafts;