liminal-server 0.28.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; a site route
//! says construction is not configured until `[open.construction]` installs
//! it (ACCESS-015); 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> {
    ask(
        runtime,
        state,
        conversation,
        &format!("services.{verb}"),
        operation,
        payload,
    )
}

/// `subject` asked on its own conversation `conversation` with a reply
/// wanted: its answer as JSON, or the refusal's words.
fn ask(
    runtime: &ConnectionRuntime,
    state: &mut ConnectionProcessState,
    conversation: u64,
    subject: &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: subject.to_owned(),
        },
    );
    if !matches!(opened, FrameAction::NoResponse) {
        return Err(format!("{subject} 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 access015_an_open_exchange_without_construction_says_so_on_every_site_route()
-> 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 routes = [
        "services.create_site",
        "services.bind_builder",
        "services.unbind_builder",
        "services.plan_set",
        "services.plan_step",
        "services.propose_template",
        "services.complete",
        "templates.update",
        "templates.retire",
    ];
    for (conversation, subject) in (1..).zip(routes) {
        let refused = ask(
            &runtime,
            &mut state,
            conversation,
            subject,
            "op-1",
            &serde_json::json!({}),
        );
        assert_eq!(
            refused,
            Err(format!(
                "site_invalid: {}",
                crate::server::connection::services::NOT_CONFIGURED
            )),
            "{subject}"
        );
    }
    Ok(())
}