Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions rust/alera-cli/src/terminal_host/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ mod orchestration_board_requests;
#[cfg(test)]
mod orchestration_board_tests;
mod orchestration_completion;
#[cfg(test)]
mod orchestration_mobile_access_tests;
mod orchestration_owned_spawn;
mod orchestration_policy_requests;
mod orchestration_profile_spawn;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
use std::collections::HashMap;

use serde_json::{json, Value};
use tokio::sync::mpsc::UnboundedReceiver;

use super::actor_test_harness::{local_client, mobile_client, test_actor};
use super::ServerActor;
use crate::terminal_host::client::{ClientFrame, ClientHandle};

const LOCAL: u64 = 1;
const PHONE: u64 = 2;
const RELAY: u64 = 3;
const ANONYMOUS: u64 = 4;

struct Fixture {
_dir: tempfile::TempDir,
actor: ServerActor,
receivers: HashMap<u64, UnboundedReceiver<ClientFrame>>,
}

async fn fixture() -> Fixture {
let dir = tempfile::tempdir().unwrap();
let mut clients = HashMap::new();
let mut receivers = HashMap::new();
for id in [LOCAL, PHONE, RELAY, ANONYMOUS] {
let (handle, events) = ClientHandle::test_channels();
let client = match id {
LOCAL => local_client(handle),
RELAY => {
let mut client = mobile_client(handle, "relay");
client.relay_client_id = Some("relay-client".to_string());
client
}
ANONYMOUS => {
let mut client = mobile_client(handle, "anonymous");
client.authenticated = false;
client
}
_ => mobile_client(handle, "phone"),
};
clients.insert(id, client);
receivers.insert(id, events);
}
let actor = test_actor(&dir, clients, HashMap::new()).await;
Fixture {
_dir: dir,
actor,
receivers,
}
}

impl Fixture {
async fn request(&mut self, client: u64, request_type: &str, payload: Value) -> Value {
let line = json!({"id": 7, "type": request_type, "payload": payload}).to_string();
self.actor.handle_line(client, line).await;
let events = self.receivers.get_mut(&client).unwrap();
loop {
let frame = events.try_recv().expect("the request was answered");
let frame = frame.as_json().unwrap();
if frame.get("id") == Some(&json!(7)) {
return frame;
}
}
}
}

#[tokio::test]
async fn orchestration_requests_follow_the_mobile_allowlist() {
let mut fixture = fixture().await;
let payload = json!({"terminal": "worker", "limit": 5});

let local = fixture
.request(LOCAL, "orchestration.inbox", payload.clone())
.await;
assert_eq!(local["ok"], true, "{local}");

for client in [PHONE, RELAY] {
let refused = fixture
.request(client, "orchestration.inbox", payload.clone())
.await;
assert_eq!(refused["ok"], false, "{refused}");
assert_eq!(
refused["error"],
"Mobile clients cannot call terminal host request: orchestration.inbox"
);
}

let anonymous = fixture
.request(ANONYMOUS, "orchestration.inbox", payload)
.await;
assert_eq!(anonymous["ok"], false, "{anonymous}");
assert_eq!(
anonymous["error"],
"Terminal host client is not authenticated."
);
}

#[tokio::test]
async fn mobile_clients_cannot_mutate_orchestration_state() {
let mut fixture = fixture().await;
fixture
.request(
LOCAL,
"orchestration.send",
json!({"from": "coordinator", "to": "worker", "subject": "Keep"}),
)
.await;

let reset = fixture
.request(PHONE, "orchestration.reset", json!({}))
.await;
assert_eq!(reset["ok"], false, "{reset}");

let inbox = fixture
.request(LOCAL, "orchestration.inbox", json!({"terminal": "worker"}))
.await;
assert_eq!(inbox["payload"]["items"].as_array().unwrap().len(), 1);
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ impl ServerActor {
request_type: &str,
payload: &Value,
) -> HostResult<Option<Value>> {
self.require_auth(client_id)?;
// This router runs before `handle_request`, so it must apply the
// mobile allowlist itself; no orchestration verb is on it.
self.require_authenticated_local_request(client_id, request_type)?;
match request_type {
"orchestration.agentSpawn" => self.orchestration_agent_spawn(payload).await.map(Some),
"orchestration.agentSpawnTimeout" => self
Expand Down
Loading