liminal-server 0.27.0

Standalone server for the liminal messaging bus
Documentation
//! ACCESS-012 R2: an open exchange (no `[auth]`) records who a connection
//! says it is, `open:<name>` from the record or `open:anonymous`, and serves
//! the service lifecycle under that holder with no Lys session; the verbs
//! that ask a person or a draft are refused by name; a gated exchange refuses
//! the `open:` record with today's words.

use super::*;
use crate::server::connection::declared_holder::DeclaredHolder;

const TEMPLATE: &str = "echo";

/// A runtime that gates nothing, with one template's lifecycle under the
/// network `open` and no placer, as the open boot installs it (R1).
fn open_services_runtime(
    door: &Arc<Door>,
    dir: &std::path::Path,
) -> Result<ConnectionRuntime, Failure> {
    let runtime = ConnectionRuntime::for_tests(Arc::clone(door) as Arc<dyn ConnectionServices>);
    runtime.register_for_tests(PID)?;
    let lifecycle = crate::services::lifecycle::Lifecycle::open(
        &[crate::config::types::ServiceTemplateConfig {
            name: TEMPLATE.to_owned(),
            command: std::path::PathBuf::from("/bin/sh"),
            args: vec!["-c".to_owned(), "exec sleep 30".to_owned()],
            subjects: vec![TEMPLATE.to_owned()],
            env: Vec::new(),
            stop_timeout_ms: 2_000,
        }],
        dir.to_path_buf(),
        crate::services::lifecycle::Lifecycle::OPEN_NETWORK.to_owned(),
        None,
    )
    .map_err(|error| format!("the registry opens: {error:?}"))?;
    lifecycle.listening("127.0.0.1:1".to_owned());
    assert!(runtime.install_lifecycle_for_tests(lifecycle));
    Ok(runtime)
}

fn connect(
    runtime: &ConnectionRuntime,
    state: &mut ConnectionProcessState,
    credential: &[u8],
) -> FrameAction {
    let version = ProtocolVersion::new(1, 0);
    apply_frame(
        PID,
        runtime,
        state,
        Frame::Connect {
            flags: 0,
            min_version: version,
            max_version: version,
            auth_token: credential.to_vec(),
        },
    )
}

/// The lifecycle verb `verb` with `payload`, asked on its own conversation
/// (opened on `services.<verb>`, id `conversation`) with a reply wanted,
/// and its answer as JSON, or the refusal's words.
fn lifecycle_verb(
    runtime: &ConnectionRuntime,
    state: &mut ConnectionProcessState,
    conversation: u64,
    verb: &str,
    operation: &str,
    payload: &serde_json::Value,
) -> Result<serde_json::Value, String> {
    let opened = apply_frame(
        PID,
        runtime,
        state,
        Frame::ConversationOpen {
            flags: 0,
            stream_id: STREAM,
            conversation_id: conversation,
            subject: format!("services.{verb}"),
        },
    );
    if !matches!(opened, FrameAction::NoResponse) {
        return Err(format!("services.{verb} did not open: {opened:?}"));
    }
    let request = serde_json::json!({"operation": operation, "payload": payload});
    let action = apply_frame(
        PID,
        runtime,
        state,
        Frame::ConversationMessage {
            flags: liminal::protocol::CONVERSATION_REPLY_REQUESTED_FLAG,
            stream_id: STREAM,
            conversation_id: conversation,
            envelope: MessageEnvelope::new(
                SchemaId::new([1; SchemaId::WIRE_LEN]),
                CausalContext::independent(),
                request.to_string().into_bytes(),
            ),
        },
    );
    match action {
        FrameAction::Respond(Frame::ConversationMessage { envelope, .. }) => {
            serde_json::from_slice(&envelope.payload).map_err(|error| error.to_string())
        }
        FrameAction::Respond(Frame::ConversationError {
            message: Some(message),
            ..
        }) => Err(message),
        other => Err(format!("unexpected answer {other:?}")),
    }
}

#[test]
fn access012_an_open_exchange_records_the_holder_the_record_declares() -> Result<(), Failure> {
    let door = Arc::new(Door::default());
    let runtime = ConnectionRuntime::for_tests(Arc::clone(&door) as Arc<dyn ConnectionServices>);
    runtime.register_for_tests(PID)?;
    let mut state = ConnectionProcessState::default();
    assert!(matches!(
        connect(&runtime, &mut state, b"open:hermes"),
        FrameAction::Respond(Frame::ConnectAck { .. })
    ));
    assert_eq!(
        state.declared_holder,
        Some(DeclaredHolder::Open("hermes".to_owned()))
    );
    assert_eq!(state.holder_record(), Some("open:hermes".to_owned()));
    assert!(state.verified_holder.is_none() && state.lys_session.is_none());
    Ok(())
}

#[test]
fn access012_every_other_record_on_an_open_exchange_is_anonymous() -> Result<(), Failure> {
    let door = Arc::new(Door::default());
    for credential in [
        Vec::new(),
        WireCredential::lys_record(b"not-a-pass"),
        vec![9, 9],
        b"open:Not Valid!".to_vec(),
        b"open:".to_vec(),
    ] {
        let runtime =
            ConnectionRuntime::for_tests(Arc::clone(&door) as Arc<dyn ConnectionServices>);
        runtime.register_for_tests(PID)?;
        let mut state = ConnectionProcessState::default();
        assert!(matches!(
            connect(&runtime, &mut state, &credential),
            FrameAction::Respond(Frame::ConnectAck { .. })
        ));
        assert_eq!(
            state.holder_record(),
            Some("open:anonymous".to_owned()),
            "{credential:?}"
        );
    }
    Ok(())
}

#[test]
fn access012_a_gated_exchange_refuses_an_open_record_as_an_unknown_credential()
-> Result<(), Failure> {
    let door = Arc::new(Door::default());
    let runtime = ConnectionRuntime::for_tests_with_auth_token(
        Arc::clone(&door) as Arc<dyn ConnectionServices>,
        b"bearer-under-test".to_vec(),
    );
    runtime.register_for_tests(PID)?;
    let mut state = ConnectionProcessState::default();
    let action = connect(&runtime, &mut state, b"open:hermes");
    let FrameAction::RespondThenClose(Frame::ConnectError {
        message: Some(words),
        ..
    }) = action
    else {
        return Err(format!("the open record was not refused: {action:?}").into());
    };
    assert_eq!(words, "connection authentication token rejected");
    assert!(state.declared_holder.is_none() && !state.authenticated);
    Ok(())
}

#[test]
fn access012_the_lifecycle_serves_an_open_holder_with_no_session() -> Result<(), Failure> {
    let door = Arc::new(Door::default());
    let dir = tempfile::tempdir()?;
    let runtime = open_services_runtime(&door, dir.path())?;
    let mut state = ConnectionProcessState::default();
    connect(&runtime, &mut state, b"open:hermes");

    let made = lifecycle_verb(
        &runtime,
        &mut state,
        1,
        "create",
        "op-1",
        &serde_json::json!({"template": TEMPLATE, "name": "one"}),
    )?;
    assert_eq!(made["outcome"], "acted", "{made}");
    assert_eq!(made["holder"], "open:hermes", "{made}");
    assert_eq!(made["service"]["created_by"], "open:hermes", "{made}");
    assert_eq!(made["service"]["state"], "asleep", "{made}");
    let id = made["service"]["id"]
        .as_str()
        .ok_or("the record has an id")?
        .to_owned();

    let listed = lifecycle_verb(
        &runtime,
        &mut state,
        2,
        "list",
        "op-2",
        &serde_json::Value::Null,
    )?;
    let services = listed["services"]
        .as_array()
        .ok_or("list answers services")?;
    assert!(
        services.iter().any(|service| service["id"] == id),
        "{listed}"
    );

    let started = lifecycle_verb(
        &runtime,
        &mut state,
        3,
        "deploy",
        "op-3",
        &serde_json::json!({"id": id}),
    )?;
    assert_eq!(started["outcome"], "acted", "{started}");
    let held = lifecycle_verb(
        &runtime,
        &mut state,
        4,
        "hold",
        "op-4",
        &serde_json::json!({"id": id}),
    )?;
    assert_eq!(held["outcome"], "acted", "{held}");
    let retired = lifecycle_verb(
        &runtime,
        &mut state,
        5,
        "retire",
        "op-5",
        &serde_json::json!({"id": id}),
    )?;
    assert_eq!(retired["outcome"], "acted", "{retired}");
    assert_eq!(retired["service"]["state"], "retired", "{retired}");
    Ok(())
}

#[test]
fn access012_an_anonymous_open_connection_is_served_too() -> Result<(), Failure> {
    let door = Arc::new(Door::default());
    let dir = tempfile::tempdir()?;
    let runtime = open_services_runtime(&door, dir.path())?;
    let mut state = ConnectionProcessState::default();
    connect(&runtime, &mut state, b"");
    let made = lifecycle_verb(
        &runtime,
        &mut state,
        1,
        "create",
        "op-1",
        &serde_json::json!({"template": TEMPLATE, "name": "anon"}),
    )?;
    assert_eq!(made["service"]["created_by"], "open:anonymous", "{made}");
    Ok(())
}

#[test]
fn access012_the_verbs_that_ask_a_person_are_refused_on_an_open_exchange() -> Result<(), Failure> {
    let door = Arc::new(Door::default());
    let dir = tempfile::tempdir()?;
    let runtime = open_services_runtime(&door, dir.path())?;
    let mut state = ConnectionProcessState::default();
    connect(&runtime, &mut state, b"open:hermes");
    for verb in [
        "create_site",
        "bind_builder",
        "unbind_builder",
        "plan_set",
        "plan_step",
        "propose_template",
        "complete",
    ] {
        let refused =
            conversation_refusal(open(&runtime, &mut state, &format!("services.{verb}")))?;
        assert_eq!(
            refused,
            format!(
                "needs_a_lys_exchange: {verb} asks a person or a draft; this exchange has no [auth]"
            )
        );
    }
    for verb in ["update", "retire"] {
        let refused =
            conversation_refusal(open(&runtime, &mut state, &format!("templates.{verb}")))?;
        assert_eq!(
            refused,
            format!(
                "needs_a_lys_exchange: templates.{verb} asks a person or a draft; this exchange has no [auth]"
            )
        );
    }
    Ok(())
}