liminal-server 0.27.1

Standalone server for the liminal messaging bus
Documentation
//! The adapter's live (deliberate) decisions: Lys asked with the holder's
//! own pass, and, under ACCESS-011 R2, a refused, held or unanswered ask
//! advised through.

use super::{
    AccessAction, AccessDecisionError, AccessRefusal, AccessResource, DeliberateDecision,
    GrantMode, GrantReceipt, LiveAnswer, LysDeliberateConsumer, LysPass, LysPassAdapter,
    RefusalCause, Target, cause_of, live_cause_of,
};
use lys_pass::Error;

impl LysDeliberateConsumer for LysPassAdapter {
    fn hold_permission(
        &self,
        pass: &LysPass,
        request: super::super::prepared::PermissionRequest<'_>,
    ) -> Result<serde_json::Value, Error> {
        request.execute(pass)
    }

    fn hold_site(
        &self,
        pass: &LysPass,
        request: super::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));
        };
        // The live route is asked with this pass, so the pass must still be
        // current; a stale pass is refused here by its own cause.
        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)))?;
        // ACCESS-011 R2: a refused, held or unanswered ask is advised through
        // under `decisions = "advise"`; under enforce every arm is as before.
        let advised = |lacking: &str| {
            super::super::advisory::advised_deliberate(
                self.decisions,
                &pass.warned,
                pass.holder(),
                resource,
                action,
                lacking,
            )
        };
        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(advised(&format!("held under draft {draft}"))
                .unwrap_or(DeliberateDecision::ByDraft { grant, draft })),
            Ok(LiveAnswer::Held {
                mode: GrantMode::ByTwo,
                grant,
                draft,
            }) => Ok(advised(&format!("held under draft {draft}"))
                .unwrap_or(DeliberateDecision::ByTwo { grant, draft })),
            // An outright grant is never "held": the answer is inconsistent,
            // so Lys was not usefully asked.
            Ok(LiveAnswer::Held {
                mode: GrantMode::Outright,
                ..
            }) => Err(refused(RefusalCause::DeliberateUnavailable)),
            Ok(LiveAnswer::Refused { refusal }) => {
                advised(&refusal).ok_or_else(|| refused(RefusalCause::MissingGrant))
            }
            Err(error) => {
                // The person sees only the cause; the operator's log names what failed.
                tracing::warn!(resource = %resource.id, action = action.name(), %error, "a live Lys decision could not be made");
                let cause = live_cause_of(&error);
                if cause == RefusalCause::DeliberateUnavailable {
                    advised(error.name()).ok_or_else(|| refused(cause))
                } else {
                    Err(refused(cause))
                }
            }
        }
    }
}