liminal-server 0.26.0

Standalone server for the liminal messaging bus
Documentation
//! ACCESS-011 R2: under `decisions = "advise"` a missing or held grant is
//! said by name and the act proceeds with the receipt `advised`; under
//! `enforce` the same pass is refused as before, and an expired pass is
//! refused under both.

use std::io::Write;
use std::sync::{Arc, Mutex, PoisonError};

use super::advisory::{ADVISED_GRANT, advised_total};
use super::lys_pass_adapter::{LiveAnswer, LiveLysAsk, LysPass, LysPassAdapter};
use super::{
    AccessAction, AccessResource, DeliberateDecision, GrantMode, HotAction, LysDeliberateConsumer,
    LysPassConsumer, RefusalCause, ResourceKind, SubjectBinding,
};
use crate::config::types::Decisions;
use crate::link_pass::fixture::{ISSUER, NOW, keys_json, token_with_rights};

type TestResult = Result<(), Box<dyn std::error::Error>>;

const HOLDER: &str = "agent-advised";
const SUBJECT: &str = "console.topology";

#[derive(Clone)]
struct CaptureWriter(Arc<Mutex<Vec<u8>>>);

impl Write for CaptureWriter {
    fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
        self.0
            .lock()
            .unwrap_or_else(PoisonError::into_inner)
            .extend_from_slice(bytes);
        Ok(bytes.len())
    }

    fn flush(&mut self) -> std::io::Result<()> {
        Ok(())
    }
}

fn captured<T>(body: impl FnOnce() -> T) -> (T, String) {
    let buffer = Arc::new(Mutex::new(Vec::new()));
    let writer = CaptureWriter(Arc::clone(&buffer));
    let subscriber = tracing_subscriber::fmt()
        .with_writer(move || writer.clone())
        .with_ansi(false)
        .with_max_level(tracing::Level::DEBUG)
        .finish();
    let result = tracing::subscriber::with_default(subscriber, body);
    let bytes = buffer.lock().unwrap_or_else(PoisonError::into_inner);
    (result, String::from_utf8_lossy(&bytes).into_owned())
}

/// What the fake live route answers every ask with.
#[derive(Debug, Clone, Copy)]
enum Fake {
    Refused,
    Held,
    Down,
}

impl LiveLysAsk for Fake {
    fn ask(
        &self,
        _: &str,
        _: &str,
        _: &lys_pass::Target,
        _: u64,
    ) -> Result<LiveAnswer, lys_pass::Error> {
        match self {
            Self::Refused => Ok(LiveAnswer::Refused {
                refusal: "grant_missing".to_owned(),
            }),
            Self::Held => Ok(LiveAnswer::Held {
                mode: GrantMode::ByDraft,
                grant: "held-grant".to_owned(),
                draft: "draft-7".to_owned(),
            }),
            Self::Down => Err(lys_pass::Error::CannotAsk("fixture is down")),
        }
    }
}

fn adapter(decisions: Decisions, ask: Option<Fake>) -> Result<LysPassAdapter, lys_pass::Error> {
    let adapter =
        LysPassAdapter::from_keys_json(&keys_json(), ISSUER.to_owned())?.with_decisions(decisions);
    Ok(match ask {
        Some(ask) => adapter.with_live(Arc::new(ask)),
        None => adapter,
    })
}

fn no_rights() -> serde_json::Value {
    serde_json::json!([])
}

fn held_right() -> serde_json::Value {
    serde_json::json!([{"resource":{"kind":"liminal.subject","id":SUBJECT},
        "actions":["subscribe"],"mode":"by_draft","grant":"held-grant"}])
}

fn pass(
    adapter: &LysPassAdapter,
    rights: &serde_json::Value,
    exp: u64,
) -> Result<LysPass, Box<dyn std::error::Error>> {
    let token = token_with_rights(HOLDER, NOW - 60, exp, rights)?;
    Ok(adapter.verify_offline(token.as_bytes(), "liminal", NOW)?)
}

fn subject() -> Result<SubjectBinding, Box<dyn std::error::Error>> {
    Ok(SubjectBinding::new(
        "network".to_owned(),
        SUBJECT.to_owned(),
        SUBJECT.to_owned(),
    )?)
}

fn resource() -> AccessResource {
    AccessResource {
        kind: ResourceKind::Subject,
        id: SUBJECT.to_owned(),
    }
}

#[test]
fn a_missing_grant_is_advised_through_by_name_and_refused_under_enforce() -> TestResult {
    let subject = subject()?;
    let advise = adapter(Decisions::Advise, None)?;
    let pass_advise = pass(&advise, &no_rights(), NOW + 600)?;
    let (decided, log) =
        captured(|| advise.decide_hot(&pass_advise, &subject, HotAction::Subscribe, NOW));
    assert_eq!(decided?.grant, ADVISED_GRANT);
    let expected = format!(
        "advised: {HOLDER} subscribe on liminal.subject:{SUBJECT}, grant lacking: liminal.subject:{SUBJECT} subscribe"
    );
    assert!(
        log.lines()
            .any(|line| line.contains("WARN") && line.contains(&expected)),
        "missing {expected:?} in {log}"
    );

    let enforce = adapter(Decisions::Enforce, None)?;
    let pass_enforce = pass(&enforce, &no_rights(), NOW + 600)?;
    let (refused, log) =
        captured(|| enforce.decide_hot(&pass_enforce, &subject, HotAction::Subscribe, NOW));
    let refused = refused.err().ok_or("enforce let a missing grant through")?;
    assert_eq!(refused.cause, RefusalCause::MissingGrant);
    assert!(!log.contains("advised:"), "{log}");
    Ok(())
}

#[test]
fn a_held_grant_is_advised_through_and_refused_hot_mode_under_enforce() -> TestResult {
    let subject = subject()?;
    let advise = adapter(Decisions::Advise, None)?;
    let pass_advise = pass(&advise, &held_right(), NOW + 600)?;
    let (decided, log) =
        captured(|| advise.decide_hot(&pass_advise, &subject, HotAction::Subscribe, NOW));
    assert_eq!(decided?.grant, ADVISED_GRANT);
    assert!(
        log.contains(&format!(
            "advised: {HOLDER} subscribe on liminal.subject:{SUBJECT}"
        )),
        "{log}"
    );

    let enforce = adapter(Decisions::Enforce, None)?;
    let pass_enforce = pass(&enforce, &held_right(), NOW + 600)?;
    let refused = enforce
        .decide_hot(&pass_enforce, &subject, HotAction::Subscribe, NOW)
        .err()
        .ok_or("enforce let a held grant through")?;
    assert_eq!(refused.cause, RefusalCause::HotMode);
    Ok(())
}

#[test]
fn an_expired_pass_is_refused_under_both_modes() -> TestResult {
    for decisions in [Decisions::Advise, Decisions::Enforce] {
        let adapter = adapter(decisions, None)?;
        let refusal = pass(&adapter, &no_rights(), NOW - 1)
            .err()
            .map(|error| error.to_string())
            .ok_or("an expired pass was admitted")?;
        assert!(refusal.contains("expired"), "{decisions}: {refusal}");
    }
    Ok(())
}

#[test]
fn the_live_ask_refused_held_and_failed_are_outright_advised_and_keep_their_enforce_answers()
-> TestResult {
    for fake in [Fake::Refused, Fake::Held, Fake::Down] {
        let advise = adapter(Decisions::Advise, Some(fake))?;
        let pass_advise = pass(&advise, &no_rights(), NOW + 600)?;
        let (decided, log) = captured(|| {
            advise.decide_live(&pass_advise, &resource(), AccessAction::Subscribe, NOW)
        });
        match decided? {
            DeliberateDecision::Outright(receipt) => assert_eq!(receipt.grant, ADVISED_GRANT),
            other => return Err(format!("{fake:?}: {other:?} is not outright").into()),
        }
        assert!(log.contains("advised:"), "{fake:?}: {log}");

        let enforce = adapter(Decisions::Enforce, Some(fake))?;
        let pass_enforce = pass(&enforce, &no_rights(), NOW + 600)?;
        let decided = enforce.decide_live(&pass_enforce, &resource(), AccessAction::Subscribe, NOW);
        match (fake, decided) {
            (Fake::Refused, Err(error)) => assert_eq!(error.cause, RefusalCause::MissingGrant),
            (Fake::Held, Ok(DeliberateDecision::ByDraft { draft, .. })) => {
                assert_eq!(draft, "draft-7");
            }
            (Fake::Down, Err(error)) => {
                assert_eq!(error.cause, RefusalCause::DeliberateUnavailable);
            }
            (fake, other) => return Err(format!("{fake:?} under enforce: {other:?}").into()),
        }
    }
    Ok(())
}

#[test]
fn the_warning_is_once_per_connection_and_every_act_is_counted() -> TestResult {
    let subject = subject()?;
    let advise = adapter(Decisions::Advise, None)?;
    let first = pass(&advise, &no_rights(), NOW + 600)?;
    let second = pass(&advise, &no_rights(), NOW + 600)?;
    let before = advised_total();
    let (result, log) = captured(|| -> TestResult {
        for _ in 0..10 {
            advise.decide_hot(&first, &subject, HotAction::Subscribe, NOW)?;
        }
        advise.decide_hot(&second, &subject, HotAction::Subscribe, NOW)?;
        Ok(())
    });
    result?;
    assert_eq!(advised_total() - before, 11);
    let warnings = log.lines().filter(|line| line.contains("advised:")).count();
    assert_eq!(warnings, 2, "{log}");
    Ok(())
}