fix: preserve auth and response bounds for session reads
This commit is contained in:
@@ -278,6 +278,7 @@ impl BackendRuntimeListTarget {
|
|||||||
pub enum BackendRuntimeClientError {
|
pub enum BackendRuntimeClientError {
|
||||||
InvalidTarget(String),
|
InvalidTarget(String),
|
||||||
Api(BackendApiClientError),
|
Api(BackendApiClientError),
|
||||||
|
SessionApi(server_api::ServerApiClientError),
|
||||||
Http(reqwest::Error),
|
Http(reqwest::Error),
|
||||||
Protocol(String),
|
Protocol(String),
|
||||||
}
|
}
|
||||||
@@ -287,6 +288,7 @@ impl fmt::Display for BackendRuntimeClientError {
|
|||||||
match self {
|
match self {
|
||||||
Self::InvalidTarget(message) => f.write_str(message),
|
Self::InvalidTarget(message) => f.write_str(message),
|
||||||
Self::Api(error) => write!(f, "{error}"),
|
Self::Api(error) => write!(f, "{error}"),
|
||||||
|
Self::SessionApi(error) => write!(f, "{error}"),
|
||||||
Self::Http(error) => write!(f, "{error}"),
|
Self::Http(error) => write!(f, "{error}"),
|
||||||
Self::Protocol(message) => f.write_str(message),
|
Self::Protocol(message) => f.write_str(message),
|
||||||
}
|
}
|
||||||
@@ -295,6 +297,12 @@ impl fmt::Display for BackendRuntimeClientError {
|
|||||||
|
|
||||||
impl std::error::Error for BackendRuntimeClientError {}
|
impl std::error::Error for BackendRuntimeClientError {}
|
||||||
|
|
||||||
|
impl From<server_api::ServerApiClientError> for BackendRuntimeClientError {
|
||||||
|
fn from(value: server_api::ServerApiClientError) -> Self {
|
||||||
|
Self::SessionApi(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<BackendApiClientError> for BackendRuntimeClientError {
|
impl From<BackendApiClientError> for BackendRuntimeClientError {
|
||||||
fn from(error: BackendApiClientError) -> Self {
|
fn from(error: BackendApiClientError) -> Self {
|
||||||
Self::Api(error)
|
Self::Api(error)
|
||||||
@@ -307,6 +315,59 @@ impl From<reqwest::Error> for BackendRuntimeClientError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const WORKER_SESSION_RESPONSE_LIMIT: usize = 16 * 1024 * 1024;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct StoredBearerAuthorizer {
|
||||||
|
authorization: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl server_api::client_support::RequestAuthorizer for StoredBearerAuthorizer {
|
||||||
|
fn authorize(
|
||||||
|
&self,
|
||||||
|
_request: server_api::client_support::AuthorizerRequest<'_>,
|
||||||
|
) -> Result<reqwest::header::HeaderMap, server_api::client_support::AuthorizationError> {
|
||||||
|
let mut headers = reqwest::header::HeaderMap::new();
|
||||||
|
headers.insert(
|
||||||
|
reqwest::header::AUTHORIZATION,
|
||||||
|
self.authorization
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| server_api::client_support::AuthorizationError::new())?,
|
||||||
|
);
|
||||||
|
Ok(headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn observe_backend_worker_session(
|
||||||
|
target: &BackendRuntimeTarget,
|
||||||
|
) -> Result<server_api::WorkspaceWorkerSessionResponse, BackendRuntimeClientError> {
|
||||||
|
validate_target(target)?;
|
||||||
|
let backend = BackendApiClient::from_stored_token(&target.base_url)?;
|
||||||
|
observe_backend_worker_session_with_client(target, &backend).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn observe_backend_worker_session_with_client(
|
||||||
|
target: &BackendRuntimeTarget,
|
||||||
|
backend: &BackendApiClient,
|
||||||
|
) -> Result<server_api::WorkspaceWorkerSessionResponse, BackendRuntimeClientError> {
|
||||||
|
let client = server_api::ServerApiClient::builder(&target.base_url)
|
||||||
|
.map_err(|error| BackendRuntimeClientError::Protocol(error.to_string()))?
|
||||||
|
.authorizer(StoredBearerAuthorizer {
|
||||||
|
authorization: backend.authorization_header_value(),
|
||||||
|
})
|
||||||
|
.response_body_limit(WORKER_SESSION_RESPONSE_LIMIT)
|
||||||
|
.build()
|
||||||
|
.map_err(|error| BackendRuntimeClientError::Protocol(error.to_string()))?;
|
||||||
|
client
|
||||||
|
.worker_session(
|
||||||
|
target.workspace_id.clone(),
|
||||||
|
target.runtime_id.clone(),
|
||||||
|
target.worker_id.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_backend_worker_launch_options(
|
pub async fn get_backend_worker_launch_options(
|
||||||
target: &BackendWorkerLaunchTarget,
|
target: &BackendWorkerLaunchTarget,
|
||||||
) -> Result<BackendWorkerLaunchOptions, BackendRuntimeClientError> {
|
) -> Result<BackendWorkerLaunchOptions, BackendRuntimeClientError> {
|
||||||
@@ -765,6 +826,43 @@ mod tests {
|
|||||||
(base_url, task)
|
(base_url, task)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn worker_session_request_uses_bearer_and_sixteen_mib_response_limit() {
|
||||||
|
let large_message = "x".repeat(2 * 1024 * 1024);
|
||||||
|
let (base_url, server) = serve_json_once(serde_json::json!({
|
||||||
|
"subject": {
|
||||||
|
"kind": "runtime_worker",
|
||||||
|
"runtime_id": "runtime-a",
|
||||||
|
"worker_id": "worker-a"
|
||||||
|
},
|
||||||
|
"availability": "unavailable",
|
||||||
|
"reason": "storage_unavailable",
|
||||||
|
"message": large_message
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
|
let target = BackendRuntimeTarget::new(&base_url, "workspace-a", "runtime-a", "worker-a");
|
||||||
|
let api =
|
||||||
|
BackendApiClient::from_access_token_for_test(&base_url, "session-secret").unwrap();
|
||||||
|
|
||||||
|
let response = observe_backend_worker_session_with_client(&target, &api)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
response.observation,
|
||||||
|
runtime_api::WorkerSessionAvailability::Unavailable { ref message, .. }
|
||||||
|
if message.len() == 2 * 1024 * 1024
|
||||||
|
));
|
||||||
|
let request = server.await.unwrap();
|
||||||
|
assert!(request.starts_with(
|
||||||
|
"GET /api/w/workspace-a/runtimes/runtime-a/workers/worker-a/session HTTP/1.1\r\n"
|
||||||
|
));
|
||||||
|
assert!(
|
||||||
|
request
|
||||||
|
.to_ascii_lowercase()
|
||||||
|
.contains("authorization: bearer session-secret\r\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn launch_options_request_uses_workspace_path_and_bearer_auth() {
|
async fn launch_options_request_uses_workspace_path_and_bearer_auth() {
|
||||||
let (base_url, server) = serve_json_once(serde_json::json!({
|
let (base_url, server) = serve_json_once(serde_json::json!({
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ pub use backend_runtime::{
|
|||||||
BackendWorkerRestoreResult, BackendWorkerRestoreState, BackendWorkerSummary,
|
BackendWorkerRestoreResult, BackendWorkerRestoreState, BackendWorkerSummary,
|
||||||
BackendWorkerWorkspaceSummary, BackendWorkingDirectorySummary, connect_backend_runtime,
|
BackendWorkerWorkspaceSummary, BackendWorkingDirectorySummary, connect_backend_runtime,
|
||||||
create_backend_worker, get_backend_worker_launch_options, list_backend_stopped_workers,
|
create_backend_worker, get_backend_worker_launch_options, list_backend_stopped_workers,
|
||||||
list_backend_workers, restore_backend_worker,
|
list_backend_workers, observe_backend_worker_session, restore_backend_worker,
|
||||||
};
|
};
|
||||||
pub use backend_workspace::{
|
pub use backend_workspace::{
|
||||||
BackendWorkspace, BackendWorkspaceCatalogTarget, BackendWorkspaceClientError,
|
BackendWorkspace, BackendWorkspaceCatalogTarget, BackendWorkspaceClientError,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use workspace_api::WorkspaceWorkerSubject;
|
use workspace_api::WorkspaceWorkerSubject;
|
||||||
|
|
||||||
pub type ServerApiError = runtime_api::RuntimeApiError;
|
pub type ServerApiError = runtime_api::RuntimeApiError;
|
||||||
|
pub type ServerApiClientError = client_support::ClientError<ServerApiError>;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct WorkspaceWorkerSessionResponse {
|
pub struct WorkspaceWorkerSessionResponse {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ use std::io;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use client::{
|
use client::{
|
||||||
BackendRuntimeListTarget, BackendWorkerSummary, ServerApiClient, WorkerSessionAvailability,
|
BackendRuntimeListTarget, BackendWorkerSummary, WorkerSessionAvailability,
|
||||||
list_backend_stopped_workers, list_backend_workers,
|
list_backend_stopped_workers, list_backend_workers, observe_backend_worker_session,
|
||||||
};
|
};
|
||||||
use crossterm::event::{self, Event as TermEvent, KeyCode, KeyEventKind, KeyModifiers};
|
use crossterm::event::{self, Event as TermEvent, KeyCode, KeyEventKind, KeyModifiers};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
@@ -83,16 +83,7 @@ pub(crate) async fn run(
|
|||||||
let mut attach_target = target
|
let mut attach_target = target
|
||||||
.runtime_target(selected.runtime_id.clone(), selected.worker_id.clone())
|
.runtime_target(selected.runtime_id.clone(), selected.worker_id.clone())
|
||||||
.map_err(|error| io::Error::other(error.to_string()))?;
|
.map_err(|error| io::Error::other(error.to_string()))?;
|
||||||
let api = ServerApiClient::builder(&target.base_url)
|
let response = observe_backend_worker_session(&attach_target)
|
||||||
.map_err(|error| io::Error::other(error.to_string()))?
|
|
||||||
.build()
|
|
||||||
.map_err(|error| io::Error::other(error.to_string()))?;
|
|
||||||
let response = api
|
|
||||||
.worker_session(
|
|
||||||
attach_target.workspace_id.clone(),
|
|
||||||
selected.runtime_id.clone(),
|
|
||||||
selected.worker_id.clone(),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
io::Error::other(format!(
|
io::Error::other(format!(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
isCurrentWorkerSessionRequest,
|
isCurrentWorkerSessionRequest,
|
||||||
workerSessionAction,
|
workerSessionAction,
|
||||||
type WorkerSessionRequestIdentity,
|
type WorkerSessionRequestIdentity,
|
||||||
|
workerSessionRequestInit,
|
||||||
} from "./session-observation";
|
} from "./session-observation";
|
||||||
import type { SessionSnapshot } from "$lib/generated/protocol";
|
import type { SessionSnapshot } from "$lib/generated/protocol";
|
||||||
|
|
||||||
@@ -23,6 +24,19 @@ const snapshot: SessionSnapshot = {
|
|||||||
entries: [],
|
entries: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Deno.test("worker session request includes same-origin credentials", () => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const init = workerSessionRequestInit(controller.signal);
|
||||||
|
assert(
|
||||||
|
init.credentials === "same-origin",
|
||||||
|
"session observation must use the Workspace auth cookie boundary",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
init.signal === controller.signal,
|
||||||
|
"request must retain its abort fence",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
Deno.test("worker session observation selects live, retained, and unavailable behavior", () => {
|
Deno.test("worker session observation selects live, retained, and unavailable behavior", () => {
|
||||||
assert(
|
assert(
|
||||||
workerSessionAction({ availability: "live_protocol" }).kind ===
|
workerSessionAction({ availability: "live_protocol" }).kind ===
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ export interface WorkerSessionRequestIdentity {
|
|||||||
workerId: string;
|
workerId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function workerSessionRequestInit(signal: AbortSignal): RequestInit {
|
||||||
|
return {
|
||||||
|
signal,
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: { accept: "application/json" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function workerSessionAction(
|
export function workerSessionAction(
|
||||||
observation: WorkerSessionObservation,
|
observation: WorkerSessionObservation,
|
||||||
): WorkerSessionAction {
|
): WorkerSessionAction {
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
import {
|
import {
|
||||||
isCurrentWorkerSessionRequest,
|
isCurrentWorkerSessionRequest,
|
||||||
workerSessionAction,
|
workerSessionAction,
|
||||||
|
workerSessionRequestInit,
|
||||||
type WorkerSessionObservation,
|
type WorkerSessionObservation,
|
||||||
type WorkerSessionRequestIdentity,
|
type WorkerSessionRequestIdentity,
|
||||||
} from "$lib/workspace/session-observation";
|
} from "$lib/workspace/session-observation";
|
||||||
@@ -951,7 +952,7 @@
|
|||||||
workerApiPath(
|
workerApiPath(
|
||||||
`/runtimes/${encodeURIComponent(target.runtimeId)}/workers/${encodeURIComponent(target.workerId)}/session`,
|
`/runtimes/${encodeURIComponent(target.runtimeId)}/workers/${encodeURIComponent(target.workerId)}/session`,
|
||||||
),
|
),
|
||||||
{ signal: controller.signal, headers: { accept: "application/json" } },
|
workerSessionRequestInit(controller.signal),
|
||||||
)
|
)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
Reference in New Issue
Block a user