use super::{
AccessAction, AccessDecisionError, AccessRefusal, AccessResource, DeliberateDecision,
GrantMode, GrantReceipt, HotAction, LysDeliberateConsumer, LysPassConsumer, RefusalCause,
ResourceKind, SubjectBinding,
};
use crate::link::LinkAction;
use lys_pass::{Decision, Error, KeySet, Target, VerifiedPass};
use std::sync::Arc;
pub const REQUEST_PATH: &str = "/#/requests";
pub struct LysPass {
verified: Arc<VerifiedPass>,
token: String,
}
impl std::fmt::Debug for LysPass {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("LysPass")
.field("holder", &self.holder())
.finish_non_exhaustive()
}
}
impl LysPass {
pub(super) fn shared_verified(&self) -> Arc<VerifiedPass> {
self.verified.clone()
}
pub(super) fn raw_token(&self) -> &str {
&self.token
}
#[must_use]
pub fn verified(&self) -> &VerifiedPass {
&self.verified
}
#[must_use]
pub fn holder(&self) -> &str {
&self.verified.claims().holder.id
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LiveAnswer {
Outright {
grant: String,
},
Held {
mode: GrantMode,
grant: String,
draft: String,
},
Refused {
refusal: String,
},
}
pub trait LiveLysAsk: Send + Sync + std::fmt::Debug {
fn ask(&self, pass: &str, holder: &str, target: &Target, now: u64)
-> Result<LiveAnswer, Error>;
}
pub struct LysPassAdapter {
keys: KeySet,
issuer: String,
live: Option<Arc<dyn LiveLysAsk>>,
}
impl std::fmt::Debug for LysPassAdapter {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("LysPassAdapter")
.field("issuer", &self.issuer)
.field("live", &self.live.is_some())
.finish_non_exhaustive()
}
}
impl LysPassAdapter {
#[must_use]
pub const fn new(keys: KeySet, issuer: String) -> Self {
Self {
keys,
issuer,
live: None,
}
}
pub fn from_keys_json(keys_json: &str, issuer: String) -> Result<Self, Error> {
Ok(Self::new(KeySet::from_json(keys_json)?, issuer))
}
#[must_use]
pub fn with_live(mut self, live: Arc<dyn LiveLysAsk>) -> Self {
self.live = Some(live);
self
}
#[must_use]
pub fn issuer(&self) -> &str {
&self.issuer
}
#[must_use]
pub fn asks_live(&self) -> bool {
self.live.is_some()
}
#[must_use]
pub fn request_url(&self) -> String {
format!("{}{REQUEST_PATH}", self.issuer.trim_end_matches('/'))
}
fn evaluate(
&self,
pass: &VerifiedPass,
subject: &SubjectBinding,
action: AccessAction,
now: u64,
) -> Result<GrantReceipt, AccessDecisionError> {
evaluate_grant(pass, subject.resource(), action, now, &self.request_url())
}
}
fn evaluate_grant(
pass: &VerifiedPass,
resource: &AccessResource,
action: AccessAction,
now: u64,
request_url: &str,
) -> Result<GrantReceipt, AccessDecisionError> {
let refused = |cause| {
crate::server::connection::services::plan_subject(&resource.id)
.filter(|_| resource.kind == ResourceKind::Subject && action == AccessAction::Publish)
.map_or_else(
|| crate::agents::binding::refusal(resource, action, request_url, cause),
|name| AccessDecisionError {
cause,
refusal: AccessRefusal::for_target(
"liminal",
"liminal.service",
name,
"plan",
request_url.to_owned(),
),
},
)
};
match crate::server::connection::services::evaluate_site_subject(pass, resource, action, now) {
Ok(Decision::Allowed { grant }) => Ok(GrantReceipt {
grant: grant.to_owned(),
}),
Ok(Decision::Held { .. }) => Err(refused(RefusalCause::HotMode)),
Ok(Decision::Refused) => Err(refused(RefusalCause::MissingGrant)),
Err(error) => Err(refused(cause_of(&error))),
}
}
#[must_use]
pub const fn action_name(action: HotAction) -> &'static str {
match action {
HotAction::Publish => "publish",
HotAction::Subscribe => "subscribe",
HotAction::Call => "call",
HotAction::Serve => "serve",
}
}
#[must_use]
pub const fn cause_of(error: &Error) -> RefusalCause {
match error {
Error::Expired | Error::NotYetValid { .. } => RefusalCause::Expired,
Error::WrongAudience | Error::WrongIssuer | Error::RightsOutsideAudience => {
RefusalCause::WrongAudience
}
_ => RefusalCause::Signature,
}
}
#[must_use]
pub const fn live_cause_of(error: &Error) -> RefusalCause {
match error {
Error::Transport(_) | Error::CannotAsk(_) | Error::Url(_) => {
RefusalCause::DeliberateUnavailable
}
other => cause_of(other),
}
}
impl LysPassConsumer for LysPassAdapter {
type Pass = LysPass;
type Holder = str;
type AdmissionError = Error;
fn verify_offline(
&self,
credential: &[u8],
audience: &str,
now: u64,
) -> Result<LysPass, Error> {
let token =
std::str::from_utf8(credential).map_err(|_| Error::Invalid("pass is not UTF-8"))?;
let token = token.trim();
let verified = VerifiedPass::verify(token, &self.keys, &self.issuer, audience, now)?;
Ok(LysPass {
verified: Arc::new(verified),
token: token.to_owned(),
})
}
fn verified_holder<'p>(&self, pass: &'p LysPass) -> &'p str {
pass.holder()
}
fn verified_holder_kind<'p>(&self, pass: &'p LysPass) -> Option<&'p str> {
Some(pass.verified.claims().holder.kind.as_str())
}
fn verified_issued_at(&self, pass: &LysPass) -> Option<u64> {
Some(pass.verified.claims().iat)
}
fn decide_hot(
&self,
pass: &LysPass,
subject: &SubjectBinding,
action: HotAction,
now: u64,
) -> Result<GrantReceipt, AccessDecisionError> {
self.evaluate(&pass.verified, subject, AccessAction::from(action), now)
}
fn decide_link(
&self,
pass: &LysPass,
subject: &SubjectBinding,
action: LinkAction,
now: u64,
) -> Result<GrantReceipt, AccessDecisionError> {
self.evaluate(&pass.verified, subject, action.access_action(), now)
}
}
impl LysDeliberateConsumer for LysPassAdapter {
fn hold_permission(
&self,
pass: &LysPass,
request: super::prepared::PermissionRequest<'_>,
) -> Result<serde_json::Value, Error> {
request.execute(pass)
}
fn hold_site(
&self,
pass: &LysPass,
request: super::prepared::SiteRequest<'_>,
) -> Result<serde_json::Value, Error> {
request.execute(pass)
}
fn decide_live(
&self,
pass: &LysPass,
resource: &AccessResource,
action: AccessAction,
now: u64,
) -> Result<DeliberateDecision, AccessDecisionError> {
let refused = |cause| AccessDecisionError {
cause,
refusal: AccessRefusal::new(resource.clone(), action, self.request_url()),
};
let Some(live) = self.live.as_ref() else {
return Err(refused(RefusalCause::DeliberateUnavailable));
};
if pass.verified.claims().exp <= now {
return Err(refused(RefusalCause::Expired));
}
let target = Target::new(resource.kind.qualified(), &resource.id, action.name())
.map_err(|error| refused(cause_of(&error)))?;
match live.ask(&pass.token, pass.holder(), &target, now) {
Ok(LiveAnswer::Outright { grant }) => {
Ok(DeliberateDecision::Outright(GrantReceipt { grant }))
}
Ok(LiveAnswer::Held {
mode: GrantMode::ByDraft,
grant,
draft,
}) => Ok(DeliberateDecision::ByDraft { grant, draft }),
Ok(LiveAnswer::Held {
mode: GrantMode::ByTwo,
grant,
draft,
}) => Ok(DeliberateDecision::ByTwo { grant, draft }),
Ok(LiveAnswer::Held {
mode: GrantMode::Outright,
..
}) => Err(refused(RefusalCause::DeliberateUnavailable)),
Ok(LiveAnswer::Refused { .. }) => Err(refused(RefusalCause::MissingGrant)),
Err(error) => {
tracing::warn!(resource = %resource.id, action = action.name(), %error, "a live Lys decision could not be made");
Err(refused(live_cause_of(&error)))
}
}
}
}
pub struct MachineLinkRights {
pass: LysPass,
request_url: String,
}
impl std::fmt::Debug for MachineLinkRights {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("MachineLinkRights")
.field("holder", &self.pass.holder())
.field("request_url", &self.request_url)
.finish_non_exhaustive()
}
}
impl MachineLinkRights {
#[must_use]
pub fn new(adapter: &LysPassAdapter, pass: LysPass) -> Self {
Self {
pass,
request_url: adapter.request_url(),
}
}
#[must_use]
pub fn holder(&self) -> &str {
self.pass.holder()
}
pub(crate) fn holds_resource(
&self,
action: LinkAction,
resource: &AccessResource,
now: u64,
) -> Result<GrantReceipt, AccessDecisionError> {
evaluate_grant(
&self.pass.verified,
resource,
action.access_action(),
now,
&self.request_url,
)
}
}
impl crate::link::LinkRights for MachineLinkRights {
fn holds(
&self,
action: LinkAction,
subject: &SubjectBinding,
now: u64,
) -> Result<GrantReceipt, AccessDecisionError> {
self.holds_resource(action, subject.resource(), now)
}
}