use std::io::Write;
use std::sync::{Arc, Mutex, PoisonError};
use super::advisory::{ADVISED_GRANT, OWN_STATION_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())
}
#[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(())
}
fn subject_named(name: &str) -> Result<SubjectBinding, Box<dyn std::error::Error>> {
Ok(SubjectBinding::new(
"network".to_owned(),
name.to_owned(),
name.to_owned(),
)?)
}
#[test]
fn access013_a_seat_s_own_inbox_is_admitted_on_identity_under_enforce() -> TestResult {
let enforce = adapter(Decisions::Enforce, None)?;
let pass = pass(&enforce, &no_rights(), NOW + 600)?;
for own in [
format!("agents.{HOLDER}.inbox"),
format!("console.tap.agents.{HOLDER}.inbox"),
] {
let (decided, log) = captured(|| {
enforce
.decide_hot(&pass, &subject_named(&own)?, HotAction::Subscribe, NOW)
.map_err(|error| -> Box<dyn std::error::Error> { error.cause.to_string().into() })
});
assert_eq!(decided?.grant, OWN_STATION_GRANT, "{own}");
assert!(!log.contains("advised:"), "{log}");
}
let own = subject_named(&format!("agents.{HOLDER}.inbox"))?;
let publish = enforce
.decide_hot(&pass, &own, HotAction::Publish, NOW)
.err()
.ok_or("identity admitted more than a subscribe")?;
assert_eq!(publish.cause, RefusalCause::MissingGrant);
for foreign in [
"agents.agent-other.inbox",
"console.tap.agents.agent-other.inbox",
"agents.agent-advised.state",
] {
let refused = enforce
.decide_hot(&pass, &subject_named(foreign)?, HotAction::Subscribe, NOW)
.err()
.ok_or("identity admitted a subject that is not its own inbox")?;
assert_eq!(refused.cause, RefusalCause::MissingGrant, "{foreign}");
}
Ok(())
}