use crate::auth_lys::session::LysSession;
use crate::auth_lys::{
AccessAction, AccessDecisionError, AccessResource, AccessRouteClass, AdmittedLysPass,
DeliberateDecision, GrantMode, GrantReceipt, HotAction, LysDeliberateConsumer, RefusalCause,
SubjectBinding, SubjectBindingError, decide_deliberate, decide_message,
};
use serde::{Deserialize, Serialize};
pub mod actions;
pub mod broker;
pub mod budgets;
pub mod context_watch;
pub mod keys;
pub mod lifecycle;
pub mod lys_http;
pub mod production;
pub mod relay;
pub mod seats;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ServiceKind {
ContextWatch,
Seats,
Budgets,
Alarms,
Launches,
SessionKeys,
}
impl ServiceKind {
pub const ALL: [Self; 6] = [
Self::ContextWatch,
Self::Seats,
Self::Budgets,
Self::Alarms,
Self::Launches,
Self::SessionKeys,
];
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::ContextWatch => "context-watch",
Self::Seats => "seats",
Self::Budgets => "budgets",
Self::Alarms => "alarms",
Self::Launches => "launches",
Self::SessionKeys => "session-keys",
}
}
#[must_use]
pub const fn subject_shape(self) -> &'static str {
match self {
Self::ContextWatch => "context-watch",
Self::Seats => "seats.<name>",
Self::Budgets => "budgets.<name>",
Self::Alarms => "alarms",
Self::Launches => "launches",
Self::SessionKeys => "sessions.<id>.keys",
}
}
#[must_use]
pub const fn default_class(self) -> AccessRouteClass {
match self {
Self::SessionKeys => AccessRouteClass::Deliberate {
mode: GrantMode::ByDraft,
},
Self::ContextWatch | Self::Seats | Self::Budgets | Self::Alarms | Self::Launches => {
AccessRouteClass::Call
}
}
}
#[must_use]
pub fn manifest(self) -> ServiceManifest {
ServiceManifest {
service: self,
subject: self.subject_shape().to_owned(),
placed_under: "network".to_owned(),
class: self.default_class(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServiceManifest {
pub service: ServiceKind,
pub subject: String,
pub placed_under: String,
#[serde(flatten)]
pub class: AccessRouteClass,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ServiceSubject<'a> {
pub service: ServiceKind,
pub instance: Option<&'a str>,
}
impl ServiceSubject<'_> {
#[must_use]
pub const fn class(&self) -> AccessRouteClass {
self.service.default_class()
}
}
fn segment(value: &str) -> Option<&str> {
(!value.is_empty()
&& !value.contains(['.', '*', '>'])
&& !value.chars().any(char::is_whitespace))
.then_some(value)
}
#[must_use]
pub fn service_of(subject: &str) -> Option<ServiceSubject<'_>> {
match subject {
"context-watch" => Some(ServiceSubject {
service: ServiceKind::ContextWatch,
instance: None,
}),
"alarms" => Some(ServiceSubject {
service: ServiceKind::Alarms,
instance: None,
}),
"launches" => Some(ServiceSubject {
service: ServiceKind::Launches,
instance: None,
}),
_ => {
if let Some(name) = subject.strip_prefix("seats.") {
return segment(name).map(|name| ServiceSubject {
service: ServiceKind::Seats,
instance: Some(name),
});
}
if let Some(name) = subject.strip_prefix("budgets.") {
return segment(name).map(|name| ServiceSubject {
service: ServiceKind::Budgets,
instance: Some(name),
});
}
let id = subject.strip_prefix("sessions.")?.strip_suffix(".keys")?;
segment(id).map(|id| ServiceSubject {
service: ServiceKind::SessionKeys,
instance: Some(id),
})
}
}
}
#[must_use]
pub fn classify_subject(subject: &str) -> Option<AccessRouteClass> {
service_of(subject).map(|resolved| resolved.class())
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ServiceEffectError(pub String);
impl std::fmt::Display for ServiceEffectError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "service effect failed: {}", self.0)
}
}
impl std::error::Error for ServiceEffectError {}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BudgetPolicy {
pub holder: String,
pub budgets: serde_json::Value,
pub stops: serde_json::Value,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ServiceRequest<'a> {
pub operation: &'a str,
pub payload: &'a [u8],
}
pub trait SeatStarter {
fn start(&self, seat: &str, operation: &str) -> Result<(), ServiceEffectError>;
}
pub trait KeySender {
fn send(&self, session: &str, keys: &[u8], operation: &str) -> Result<(), ServiceEffectError>;
}
pub trait BudgetSource {
fn read(&self, name: &str) -> Result<Option<BudgetPolicy>, ServiceEffectError>;
}
pub trait ServiceAction {
fn act(
&self,
service: ServiceKind,
payload: &[u8],
operation: &str,
) -> Result<(), ServiceEffectError>;
}
pub struct Services<'a> {
pub seats: &'a dyn SeatStarter,
pub keys: &'a dyn KeySender,
pub budgets: &'a dyn BudgetSource,
pub actions: &'a dyn ServiceAction,
}
impl std::fmt::Debug for Services<'_> {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.debug_struct("Services").finish_non_exhaustive()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ServiceOutcome {
Acted {
receipt: GrantReceipt,
},
Held {
mode: GrantMode,
grant: String,
draft: String,
},
Policy(BudgetPolicy),
}
#[derive(Debug, PartialEq, Eq)]
pub enum ServiceRefusal {
NotAService,
Binding(SubjectBindingError),
Decision(AccessDecisionError),
PolicyUnavailable,
NoOperation,
Effect(ServiceEffectError),
}
impl std::fmt::Display for ServiceRefusal {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotAService => formatter.write_str("no service serves this subject"),
Self::Binding(SubjectBindingError(member)) => {
write!(formatter, "subject binding refused: {member}")
}
Self::Decision(error) => write!(formatter, "{error}"),
Self::PolicyUnavailable => {
formatter.write_str("budget policy unavailable from Lys; liminal sets none")
}
Self::Effect(error) => write!(formatter, "{error}"),
Self::NoOperation => {
formatter.write_str("service request names no operation id; nothing was done")
}
}
}
}
impl std::error::Error for ServiceRefusal {}
pub fn serve<C: LysDeliberateConsumer>(
services: &Services<'_>,
consumer: Option<&C>,
pass: &AdmittedLysPass<C::Pass>,
network_id: &str,
subject: &str,
request: &ServiceRequest<'_>,
now: u64,
) -> Result<ServiceOutcome, ServiceRefusal> {
let decider = consumer.map(|consumer| ConsumerDecider { consumer, pass });
serve_with(
services,
decider
.as_ref()
.map(|decider| -> &dyn ServiceDecider { decider }),
network_id,
subject,
request,
now,
)
}
pub fn serve_session(
services: &Services<'_>,
session: Option<&dyn LysSession>,
network_id: &str,
subject: &str,
request: &ServiceRequest<'_>,
now: u64,
) -> Result<ServiceOutcome, ServiceRefusal> {
let decider = session.map(SessionDecider);
serve_with(
services,
decider
.as_ref()
.map(|decider| -> &dyn ServiceDecider { decider }),
network_id,
subject,
request,
now,
)
}
pub trait ServiceDecider {
fn hot(
&self,
subject: &SubjectBinding,
action: HotAction,
now: u64,
) -> Result<GrantReceipt, AccessDecisionError>;
fn live(
&self,
resource: &AccessResource,
action: AccessAction,
now: u64,
) -> Result<DeliberateDecision, AccessDecisionError>;
}
struct ConsumerDecider<'a, C: LysDeliberateConsumer> {
consumer: &'a C,
pass: &'a AdmittedLysPass<C::Pass>,
}
impl<C: LysDeliberateConsumer> ServiceDecider for ConsumerDecider<'_, C> {
fn hot(
&self,
subject: &SubjectBinding,
action: HotAction,
now: u64,
) -> Result<GrantReceipt, AccessDecisionError> {
decide_message(self.consumer, self.pass, subject, action, now)
}
fn live(
&self,
resource: &AccessResource,
action: AccessAction,
now: u64,
) -> Result<DeliberateDecision, AccessDecisionError> {
decide_deliberate(self.consumer, self.pass, resource, action, now)
}
}
struct SessionDecider<'a>(&'a dyn LysSession);
impl ServiceDecider for SessionDecider<'_> {
fn hot(
&self,
subject: &SubjectBinding,
action: HotAction,
now: u64,
) -> Result<GrantReceipt, AccessDecisionError> {
self.0.decide(subject, action, now)
}
fn live(
&self,
resource: &AccessResource,
action: AccessAction,
now: u64,
) -> Result<DeliberateDecision, AccessDecisionError> {
self.0.decide_live(resource, action, now)
}
}
fn serve_with(
services: &Services<'_>,
decider: Option<&dyn ServiceDecider>,
network_id: &str,
subject: &str,
request: &ServiceRequest<'_>,
now: u64,
) -> Result<ServiceOutcome, ServiceRefusal> {
let resolved = service_of(subject).ok_or(ServiceRefusal::NotAService)?;
if request.operation.trim().is_empty() {
return Err(ServiceRefusal::NoOperation);
}
let binding = SubjectBinding::new(
network_id.to_owned(),
subject.to_owned(),
subject.to_owned(),
)
.map_err(ServiceRefusal::Binding)?;
let Some(decider) = decider else {
return Err(ServiceRefusal::Decision(unavailable(&binding, &resolved)));
};
match resolved.class() {
AccessRouteClass::Deliberate { .. } => {
let decision = decider
.live(binding.resource(), HotAction::Call.into(), now)
.map_err(ServiceRefusal::Decision)?;
match decision {
DeliberateDecision::Outright(receipt) => {
effect(services, &resolved, request)?;
Ok(ServiceOutcome::Acted { receipt })
}
DeliberateDecision::ByDraft { grant, draft } => Ok(ServiceOutcome::Held {
mode: GrantMode::ByDraft,
grant,
draft,
}),
DeliberateDecision::ByTwo { grant, draft } => Ok(ServiceOutcome::Held {
mode: GrantMode::ByTwo,
grant,
draft,
}),
}
}
AccessRouteClass::Call
| AccessRouteClass::Publish
| AccessRouteClass::Subscribe
| AccessRouteClass::Serve => {
let receipt = decider
.hot(&binding, HotAction::Call, now)
.map_err(ServiceRefusal::Decision)?;
if resolved.service == ServiceKind::Budgets {
let name = resolved.instance.unwrap_or_default();
let policy = services
.budgets
.read(name)
.map_err(ServiceRefusal::Effect)?;
return policy.map_or(Err(ServiceRefusal::PolicyUnavailable), |policy| {
Ok(ServiceOutcome::Policy(policy))
});
}
effect(services, &resolved, request)?;
Ok(ServiceOutcome::Acted { receipt })
}
}
}
fn effect(
services: &Services<'_>,
resolved: &ServiceSubject<'_>,
request: &ServiceRequest<'_>,
) -> Result<(), ServiceRefusal> {
let instance = resolved.instance.unwrap_or_default();
match resolved.service {
ServiceKind::Seats => services.seats.start(instance, request.operation),
ServiceKind::SessionKeys => {
services
.keys
.send(instance, request.payload, request.operation)
}
ServiceKind::Budgets => Ok(()),
ServiceKind::ContextWatch | ServiceKind::Alarms | ServiceKind::Launches => services
.actions
.act(resolved.service, request.payload, request.operation),
}
.map_err(ServiceRefusal::Effect)
}
fn unavailable(binding: &SubjectBinding, resolved: &ServiceSubject<'_>) -> AccessDecisionError {
let action = match resolved.class() {
AccessRouteClass::Deliberate { .. } => crate::auth_lys::AccessAction::Admin,
_ => HotAction::Call.into(),
};
AccessDecisionError {
cause: RefusalCause::DeliberateUnavailable,
refusal: crate::auth_lys::AccessRefusal::new(
binding.resource().clone(),
action,
format!("lys://ask/{}", binding.subject()),
),
}
}