liminal-server 0.24.9

Standalone server for the liminal messaging bus
Documentation
//! Signed agent passes with a supplied clock and no listener.
use super::{Authority, Current, MachineConfig, MachinePass, PassRefusal, Renewal, renewal_at};
use crate::auth_lys::lys_pass_adapter::{
    LiveAnswer, LiveLysAsk, LysPassAdapter, MachineLinkRights,
};
use crate::auth_lys::{LysPassConsumer, ResourceKind};
use lys_core::keys::Ed25519Identity;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, mpsc};
use zeroize::Zeroizing;

pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
pub const ISSUED_AT: u64 = 1_700_000_000;
pub const LIFETIME: u64 = 600;
pub const NOW: u64 = ISSUED_AT + 3601;
pub const LINK: &str = "seats-to-peer";

fn encoded(bytes: &[u8]) -> String {
    const ABC: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
    let mut output = String::new();
    for chunk in bytes.chunks(3) {
        let a = chunk[0];
        let b = chunk.get(1).copied().unwrap_or(0);
        let c = chunk.get(2).copied().unwrap_or(0);
        output.push(char::from(ABC[usize::from(a >> 2)]));
        output.push(char::from(ABC[usize::from((a & 3) << 4 | b >> 4)]));
        if chunk.len() > 1 {
            output.push(char::from(ABC[usize::from((b & 15) << 2 | c >> 6)]));
        }
        if chunk.len() > 2 {
            output.push(char::from(ABC[usize::from(c & 63)]));
        }
    }
    output
}

pub fn token(holder: &str, iat: u64, exp: u64, grant: bool) -> Result<Zeroizing<String>> {
    let signer = Ed25519Identity::from_seed(&Zeroizing::new([9; 32]));
    let header = encoded(br#"{"alg":"EdDSA","typ":"JWT","kid":"fixture"}"#);
    let rights = if grant {
        serde_json::json!([{"resource":{"kind":"liminal.link","id":LINK},
            "actions":["link_import","link_export"],"mode":"outright","grant":"link-grant"}])
    } else {
        serde_json::json!([])
    };
    let claims = serde_json::json!({"iss":"https://lys.ablative.com.au","aud":"liminal",
        "sub":holder,"iat":iat,"exp":exp,
        "holder":{"kind":"agent","id":holder,"responsible":"owner"},"rights":rights});
    let signing_input = format!("{header}.{}", encoded(&serde_json::to_vec(&claims)?));
    Ok(Zeroizing::new(format!(
        "{signing_input}.{}",
        encoded(&signer.sign(signing_input.as_bytes()))
    )))
}

#[derive(Debug)]
struct Ask;
impl LiveLysAsk for Ask {
    fn ask(
        &self,
        _: &str,
        _: &str,
        _: &lys_pass::Target,
        _: u64,
    ) -> std::result::Result<LiveAnswer, lys_pass::Error> {
        Ok(LiveAnswer::Outright {
            grant: "live-grant".to_owned(),
        })
    }
}
pub fn adapter() -> Result<Arc<LysPassAdapter>> {
    let signer = Ed25519Identity::from_seed(&Zeroizing::new([9; 32]));
    let keys = serde_json::json!({"keys":[{"kty":"OKP","crv":"Ed25519","kid":"fixture",
        "x":encoded(&signer.public_key_bytes()),"alg":"EdDSA","use":"sig"}]})
    .to_string();
    Ok(Arc::new(
        LysPassAdapter::from_keys_json(&keys, "https://lys.ablative.com.au".to_owned())?
            .with_live(Arc::new(Ask)),
    ))
}
fn current(
    adapter: &LysPassAdapter,
    token: &[u8],
    now: u64,
) -> std::result::Result<Current, PassRefusal> {
    let pass = adapter
        .verify_offline(token, "liminal", now)
        .map_err(|error| super::named(error.name()))?;
    let expiry = pass.verified().claims().exp;
    Ok(Current {
        rights: MachineLinkRights::new(adapter, pass),
        expiry,
        renew_at: renewal_at(expiry, now),
        renewed: false,
    })
}
struct Source {
    token: Zeroizing<String>,
    calls: Arc<AtomicUsize>,
    refusal: bool,
    /// Holds an ask inside Lys: says it has entered, then waits for release.
    hold: Option<Gate>,
}
/// The ask's side of a hold: it says it entered, then waits for release.
type Gate = Mutex<(mpsc::Sender<()>, mpsc::Receiver<()>)>;
/// The test's side of a held ask: `entered` fires once the ask is inside
/// Lys (the authority's lock held); `release` lets it answer.
pub struct Hold {
    pub entered: mpsc::Receiver<()>,
    pub release: mpsc::Sender<()>,
}
impl std::fmt::Debug for Source {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Source").finish_non_exhaustive()
    }
}
impl Renewal for Source {
    fn renew(
        &self,
        adapter: &LysPassAdapter,
        now: u64,
    ) -> std::result::Result<(Zeroizing<String>, Current), PassRefusal> {
        self.calls.fetch_add(1, Ordering::Relaxed);
        if let Some(gate) = &self.hold {
            let gate = gate
                .lock()
                .map_err(|_| PassRefusal("pass_holder_refused"))?;
            gate.0
                .send(())
                .map_err(|_| PassRefusal("lys_could_not_be_asked"))?;
            gate.1
                .recv()
                .map_err(|_| PassRefusal("lys_could_not_be_asked"))?;
            drop(gate);
        }
        if self.refusal {
            return Err(PassRefusal("ProviderUnavailable"));
        }
        Ok((
            self.token.clone(),
            current(adapter, self.token.as_bytes(), now)?,
        ))
    }
}
pub struct Fixture {
    pub(super) authority: Arc<Authority>,
    pub(crate) calls: Arc<AtomicUsize>,
    pub(crate) renewed: Zeroizing<String>,
    root: tempfile::TempDir,
}
impl Fixture {
    pub(crate) fn session(&self) -> Result<Box<dyn super::LysSession>> {
        let pass = token("exporter", NOW, NOW + LIFETIME, true)?;
        let admitter = super::ConsumerAdmitter::new(self.authority.adapter.clone());
        Ok(Box::new(super::Session {
            inner: super::LysAdmitter::admit(&admitter, pass.as_bytes(), NOW)?,
            authority: self.authority.clone(),
        }))
    }
    pub(crate) fn new(grant: bool, refusal: bool) -> Result<Self> {
        let root = tempfile::tempdir()?;
        let machine: MachineConfig = toml::from_str(&format!(
            "identity_path = '{}'\nagent_id = 'importer'\naudience = 'liminal'",
            root.path().join("identity").display()
        ))?;
        let adapter = adapter()?;
        let first = token("importer", ISSUED_AT, ISSUED_AT + LIFETIME, true)?;
        let renewed = token("importer", NOW, NOW + LIFETIME, grant)?;
        let calls = Arc::new(AtomicUsize::new(0));
        let authority = Arc::new(Authority {
            current: Mutex::new(Some(current(&adapter, first.as_bytes(), ISSUED_AT)?)),
            pass: MachinePass::hold(&machine, Some(first))?,
            dial: Some(Box::new(Source {
                token: renewed.clone(),
                calls: calls.clone(),
                refusal,
                hold: None,
            })),
            adapter,
            boot_pending: std::sync::atomic::AtomicBool::new(false),
        });
        authority.pass.attach(&authority)?;
        Ok(Self {
            authority,
            calls,
            renewed,
            root,
        })
    }
    /// An authority as a refused boot ask leaves it (ACCESS-009 R1): no
    /// pass held, the dial ready to ask at the first open; `refusal` makes
    /// every ask refuse.
    pub(crate) fn unissued(refusal: bool) -> Result<Self> {
        Self::unissued_with(refusal, None)
    }
    /// [`Self::unissued`] whose every ask is held until the test releases it.
    pub(crate) fn unissued_held() -> Result<(Self, Hold)> {
        let (entered_send, entered) = mpsc::channel();
        let (release, release_receive) = mpsc::channel();
        let fixture =
            Self::unissued_with(false, Some(Mutex::new((entered_send, release_receive))))?;
        Ok((fixture, Hold { entered, release }))
    }
    fn unissued_with(refusal: bool, hold: Option<Gate>) -> Result<Self> {
        let root = tempfile::tempdir()?;
        let machine: MachineConfig = toml::from_str(&format!(
            "identity_path = '{}'\nagent_id = 'importer'\naudience = 'liminal'",
            root.path().join("identity").display()
        ))?;
        let adapter = adapter()?;
        let renewed = token("importer", NOW, NOW + LIFETIME, true)?;
        let calls = Arc::new(AtomicUsize::new(0));
        let authority = Arc::new(Authority {
            current: Mutex::new(None),
            pass: MachinePass::hold(&machine, None)?,
            dial: Some(Box::new(Source {
                token: renewed.clone(),
                calls: calls.clone(),
                refusal,
                hold,
            })),
            adapter,
            boot_pending: std::sync::atomic::AtomicBool::new(false),
        });
        authority.pass.attach(&authority)?;
        Ok(Self {
            authority,
            calls,
            renewed,
            root,
        })
    }
    pub(crate) fn link() -> crate::auth_lys::AccessResource {
        crate::auth_lys::AccessResource {
            kind: ResourceKind::Link,
            id: LINK.to_owned(),
        }
    }
    pub(crate) fn finish(self) -> Result {
        drop(self.authority);
        self.root.close()?;
        Ok(())
    }
}