use serde::{Deserialize, Serialize};
pub mod live;
pub mod lys_pass_adapter;
pub mod routes;
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, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NeededGrant {
pub app: String,
pub kind: String,
pub id: String,
pub action: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RequiredGrant {
pub resource: String,
pub action: String,
pub needed: NeededGrant,
pub request: String,
}
impl RequiredGrant {
#[must_use]
pub fn resource_id(&self) -> &str {
self.resource
.split_once(':')
.map_or(self.resource.as_str(), |(_, id)| id)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AccessRefusal {
pub refused: Box<RequiredGrant>,
}
impl AccessRefusal {
#[must_use]
pub fn new(resource: AccessResource, action: AccessAction, request: String) -> Self {
let AccessResource { kind, id } = resource;
Self::for_target(LYS_APP, kind.qualified(), &id, action.name(), request)
}
#[must_use]
pub fn for_target(app: &str, kind: &str, id: &str, action: &str, request: String) -> Self {
Self {
refused: Box::new(RequiredGrant {
resource: format!("{kind}:{id}"),
action: action.to_owned(),
needed: NeededGrant {
app: app.to_owned(),
kind: kind.to_owned(),
id: id.to_owned(),
action: action.to_owned(),
},
request,
}),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum RefusalCause {
#[error("required grant is missing")]
MissingGrant,
#[error("pass lifetime has expired")]
Expired,
#[error("pass audience is not liminal")]
WrongAudience,
#[error("pass signature was refused")]
Signature,
#[error("subject placement is restricted")]
RestrictedPlacement,
#[error("grant mode cannot authorize a hot action")]
HotMode,
#[error("lys_could_not_be_asked")]
DeliberateUnavailable,
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("{cause}")]
pub struct AccessDecisionError {
pub cause: RefusalCause,
pub refusal: AccessRefusal,
}
#[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 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>;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "class", rename_all = "snake_case")]
pub enum AccessRouteClass {
Publish,
Subscribe,
Call,
Serve,
Deliberate {
mode: GrantMode,
},
}
impl AccessRouteClass {
#[must_use]
pub const fn hot_action(self) -> Option<HotAction> {
match self {
Self::Publish => Some(HotAction::Publish),
Self::Subscribe => Some(HotAction::Subscribe),
Self::Call => Some(HotAction::Call),
Self::Serve => Some(HotAction::Serve),
Self::Deliberate { .. } => None,
}
}
}