Unify manual route inventory and retention semantics
This commit is contained in:
@@ -633,107 +633,132 @@ pub struct RemainingRuntimeRoute {
|
|||||||
pub reason: &'static str,
|
pub reason: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const RUNTIME_ROUTE_VERIFICATION_CHALLENGE: &str =
|
||||||
|
"/v1/workspace-runtime-verification/challenge";
|
||||||
|
pub const RUNTIME_ROUTE_VERIFICATION_ACK: &str =
|
||||||
|
"/v1/workspace-runtime-verification/acknowledgement";
|
||||||
|
pub const RUNTIME_ROUTE_CONFIG_BUNDLES: &str = "/v1/config-bundles";
|
||||||
|
pub const RUNTIME_ROUTE_CONFIG_BUNDLE_AVAILABILITY: &str =
|
||||||
|
"/v1/config-bundles/{bundle_id}/availability";
|
||||||
|
pub const RUNTIME_ROUTE_WORKSPACE_PROMPT_PROJECTIONS: &str = "/v1/workspace-prompt-projections";
|
||||||
|
pub const RUNTIME_ROUTE_WORKING_DIRECTORIES: &str = "/v1/working-directories";
|
||||||
|
pub const RUNTIME_ROUTE_WORKING_DIRECTORY_REPOSITORY_ACCESS: &str =
|
||||||
|
"/v1/working-directories/repository-access";
|
||||||
|
pub const RUNTIME_ROUTE_SSH_PROBE: &str = "/v1/repositories/ssh/probe";
|
||||||
|
pub const RUNTIME_ROUTE_REPOSITORY_REFS_OBSERVE: &str = "/v1/repository-refs/observe";
|
||||||
|
pub const RUNTIME_ROUTE_WORKDIR_SESSIONS: &str =
|
||||||
|
"/v1/working-directories/{working_directory_id}/sessions";
|
||||||
|
pub const RUNTIME_ROUTE_WORKDIR_SESSION_OPERATIONS: &str =
|
||||||
|
"/v1/workdir-sessions/{session_id}/operations";
|
||||||
|
pub const RUNTIME_ROUTE_WORKDIR_SESSION: &str = "/v1/workdir-sessions/{session_id}";
|
||||||
|
pub const RUNTIME_ROUTE_WORKING_DIRECTORY: &str = "/v1/working-directories/{working_directory_id}";
|
||||||
|
pub const RUNTIME_ROUTE_PROTOCOL_WS: &str = "/v1/protocol/ws";
|
||||||
|
pub const RUNTIME_ROUTE_WORKER_PROTOCOL_WS: &str = "/v1/workers/{worker_id}/protocol/ws";
|
||||||
|
pub const RUNTIME_ROUTE_WORKER_ATTACHMENTS: &str = "/v1/workers/{worker_id}/attachments";
|
||||||
|
pub const RUNTIME_ROUTE_WORKER_ATTACHMENT: &str =
|
||||||
|
"/v1/workers/{worker_id}/attachments/{artifact_id}";
|
||||||
|
|
||||||
/// Intentionally out-of-contract Runtime routes. This inventory keeps manual paths visible rather
|
/// Intentionally out-of-contract Runtime routes. This inventory keeps manual paths visible rather
|
||||||
/// than letting them be mistaken for management-contract omissions.
|
/// than letting them be mistaken for management-contract omissions.
|
||||||
pub const REMAINING_RUNTIME_ROUTES: &[RemainingRuntimeRoute] = &[
|
pub const REMAINING_RUNTIME_ROUTES: &[RemainingRuntimeRoute] = &[
|
||||||
RemainingRuntimeRoute {
|
RemainingRuntimeRoute {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: "/v1/workspace-runtime-verification/challenge",
|
path: RUNTIME_ROUTE_VERIFICATION_CHALLENGE,
|
||||||
reason: "Workspace verification handshake",
|
reason: "Workspace verification handshake",
|
||||||
},
|
},
|
||||||
RemainingRuntimeRoute {
|
RemainingRuntimeRoute {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: "/v1/workspace-runtime-verification/acknowledgement",
|
path: RUNTIME_ROUTE_VERIFICATION_ACK,
|
||||||
reason: "Workspace verification handshake",
|
reason: "Workspace verification handshake",
|
||||||
},
|
},
|
||||||
RemainingRuntimeRoute {
|
RemainingRuntimeRoute {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: "/v1/config-bundles",
|
path: RUNTIME_ROUTE_CONFIG_BUNDLES,
|
||||||
reason: "Workspace Config transport",
|
reason: "Workspace Config transport",
|
||||||
},
|
},
|
||||||
RemainingRuntimeRoute {
|
RemainingRuntimeRoute {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: "/v1/config-bundles",
|
path: RUNTIME_ROUTE_CONFIG_BUNDLES,
|
||||||
reason: "Workspace Config transport",
|
reason: "Workspace Config transport",
|
||||||
},
|
},
|
||||||
RemainingRuntimeRoute {
|
RemainingRuntimeRoute {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: "/v1/config-bundles/{bundle_id}/availability",
|
path: RUNTIME_ROUTE_CONFIG_BUNDLE_AVAILABILITY,
|
||||||
reason: "Workspace Config transport",
|
reason: "Workspace Config transport",
|
||||||
},
|
},
|
||||||
RemainingRuntimeRoute {
|
RemainingRuntimeRoute {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: "/v1/workspace-prompt-projections",
|
path: RUNTIME_ROUTE_WORKSPACE_PROMPT_PROJECTIONS,
|
||||||
reason: "Workspace Config transport",
|
reason: "Workspace Config transport",
|
||||||
},
|
},
|
||||||
RemainingRuntimeRoute {
|
RemainingRuntimeRoute {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: "/v1/working-directories",
|
path: RUNTIME_ROUTE_WORKING_DIRECTORIES,
|
||||||
reason: "Workdir transport",
|
reason: "Workdir transport",
|
||||||
},
|
},
|
||||||
RemainingRuntimeRoute {
|
RemainingRuntimeRoute {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: "/v1/working-directories",
|
path: RUNTIME_ROUTE_WORKING_DIRECTORIES,
|
||||||
reason: "Workdir transport",
|
reason: "Workdir transport",
|
||||||
},
|
},
|
||||||
RemainingRuntimeRoute {
|
RemainingRuntimeRoute {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: "/v1/working-directories/repository-access",
|
path: RUNTIME_ROUTE_WORKING_DIRECTORY_REPOSITORY_ACCESS,
|
||||||
reason: "Workdir transport",
|
reason: "Workdir transport",
|
||||||
},
|
},
|
||||||
RemainingRuntimeRoute {
|
RemainingRuntimeRoute {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: "/v1/repositories/ssh/probe",
|
path: RUNTIME_ROUTE_SSH_PROBE,
|
||||||
reason: "Repository SSH transport",
|
reason: "Repository SSH transport",
|
||||||
},
|
},
|
||||||
RemainingRuntimeRoute {
|
RemainingRuntimeRoute {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: "/v1/repository-refs/observe",
|
path: RUNTIME_ROUTE_REPOSITORY_REFS_OBSERVE,
|
||||||
reason: "Repository observation transport",
|
reason: "Repository observation transport",
|
||||||
},
|
},
|
||||||
RemainingRuntimeRoute {
|
RemainingRuntimeRoute {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: "/v1/working-directories/{working_directory_id}/sessions",
|
path: RUNTIME_ROUTE_WORKDIR_SESSIONS,
|
||||||
reason: "Workdir session transport",
|
reason: "Workdir session transport",
|
||||||
},
|
},
|
||||||
RemainingRuntimeRoute {
|
RemainingRuntimeRoute {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: "/v1/workdir-sessions/{session_id}/operations",
|
path: RUNTIME_ROUTE_WORKDIR_SESSION_OPERATIONS,
|
||||||
reason: "Workdir session transport",
|
reason: "Workdir session transport",
|
||||||
},
|
},
|
||||||
RemainingRuntimeRoute {
|
RemainingRuntimeRoute {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
path: "/v1/workdir-sessions/{session_id}",
|
path: RUNTIME_ROUTE_WORKDIR_SESSION,
|
||||||
reason: "Workdir session transport",
|
reason: "Workdir session transport",
|
||||||
},
|
},
|
||||||
RemainingRuntimeRoute {
|
RemainingRuntimeRoute {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: "/v1/working-directories/{working_directory_id}",
|
path: RUNTIME_ROUTE_WORKING_DIRECTORY,
|
||||||
reason: "Workdir transport",
|
reason: "Workdir transport",
|
||||||
},
|
},
|
||||||
RemainingRuntimeRoute {
|
RemainingRuntimeRoute {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
path: "/v1/working-directories/{working_directory_id}",
|
path: RUNTIME_ROUTE_WORKING_DIRECTORY,
|
||||||
reason: "Workdir transport",
|
reason: "Workdir transport",
|
||||||
},
|
},
|
||||||
RemainingRuntimeRoute {
|
RemainingRuntimeRoute {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: "/v1/protocol/ws",
|
path: RUNTIME_ROUTE_PROTOCOL_WS,
|
||||||
reason: "WebSocket protocol transport (ws-server feature)",
|
reason: "WebSocket protocol transport (ws-server feature)",
|
||||||
},
|
},
|
||||||
RemainingRuntimeRoute {
|
RemainingRuntimeRoute {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: "/v1/workers/{worker_id}/protocol/ws",
|
path: RUNTIME_ROUTE_WORKER_PROTOCOL_WS,
|
||||||
reason: "WebSocket protocol transport (ws-server feature)",
|
reason: "WebSocket protocol transport (ws-server feature)",
|
||||||
},
|
},
|
||||||
RemainingRuntimeRoute {
|
RemainingRuntimeRoute {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: "/v1/workers/{worker_id}/attachments",
|
path: RUNTIME_ROUTE_WORKER_ATTACHMENTS,
|
||||||
reason: "Binary attachment transport",
|
reason: "Binary attachment transport",
|
||||||
},
|
},
|
||||||
RemainingRuntimeRoute {
|
RemainingRuntimeRoute {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
path: "/v1/workers/{worker_id}/attachments/{artifact_id}",
|
path: RUNTIME_ROUTE_WORKER_ATTACHMENT,
|
||||||
reason: "Binary attachment transport",
|
reason: "Binary attachment transport",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -27,13 +27,11 @@ use crate::retention::{
|
|||||||
#[cfg(feature = "ws-server")]
|
#[cfg(feature = "ws-server")]
|
||||||
use crate::runtime::RuntimeSubscriptionRecvError;
|
use crate::runtime::RuntimeSubscriptionRecvError;
|
||||||
use crate::ssh_host_key_probe::{
|
use crate::ssh_host_key_probe::{
|
||||||
SSH_HOST_KEY_PROBE_OPERATION, SSH_HOST_KEY_PROBE_PATH, SSH_KEYSCAN_TIMEOUT,
|
SSH_HOST_KEY_PROBE_OPERATION, SSH_KEYSCAN_TIMEOUT, SshHostKeyProbeError,
|
||||||
SshHostKeyProbeError, SshHostKeyProbeRequest, SshHostKeyProbeResponse,
|
SshHostKeyProbeRequest, SshHostKeyProbeResponse, probe_ssh_host_keys_with_program,
|
||||||
probe_ssh_host_keys_with_program,
|
|
||||||
};
|
};
|
||||||
use crate::workspace_issuer::{
|
use crate::workspace_issuer::{
|
||||||
RuntimeVerificationSigner, VerifiedWorkspaceCapability, WORKSPACE_VERIFICATION_ACK_PATH,
|
RuntimeVerificationSigner, VerifiedWorkspaceCapability, WORKSPACE_VERIFICATION_OPERATION,
|
||||||
WORKSPACE_VERIFICATION_CHALLENGE_PATH, WORKSPACE_VERIFICATION_OPERATION,
|
|
||||||
WorkspaceCapabilityExpectation, WorkspaceCapabilityVerifier,
|
WorkspaceCapabilityExpectation, WorkspaceCapabilityVerifier,
|
||||||
WorkspaceRuntimeVerificationAcknowledgement, WorkspaceRuntimeVerificationAuthority,
|
WorkspaceRuntimeVerificationAcknowledgement, WorkspaceRuntimeVerificationAuthority,
|
||||||
WorkspaceRuntimeVerificationChallenge, WorkspaceRuntimeVerificationReceipt,
|
WorkspaceRuntimeVerificationChallenge, WorkspaceRuntimeVerificationReceipt,
|
||||||
@@ -166,6 +164,18 @@ pub async fn serve_runtime_http_with_workspace_auth(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn remaining_route(path: &'static str, methods: &[&str]) -> &'static str {
|
||||||
|
for method in methods {
|
||||||
|
assert!(
|
||||||
|
runtime_api::REMAINING_RUNTIME_ROUTES
|
||||||
|
.iter()
|
||||||
|
.any(|route| route.method == *method && route.path == path),
|
||||||
|
"handwritten Runtime route {method} {path} is missing from runtime-api inventory",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
/// Build the REST router for an existing Runtime.
|
/// Build the REST router for an existing Runtime.
|
||||||
///
|
///
|
||||||
/// Handlers delegate to [`Runtime`] methods and keep Worker authority Runtime-local.
|
/// Handlers delegate to [`Runtime`] methods and keep Worker authority Runtime-local.
|
||||||
@@ -212,68 +222,95 @@ fn runtime_http_router_with_auth_and_ssh_keyscan_program(
|
|||||||
|
|
||||||
let router = Router::new()
|
let router = Router::new()
|
||||||
.route(
|
.route(
|
||||||
WORKSPACE_VERIFICATION_CHALLENGE_PATH,
|
remaining_route(runtime_api::RUNTIME_ROUTE_VERIFICATION_CHALLENGE, &["POST"]),
|
||||||
post(post_workspace_verification_challenge),
|
post(post_workspace_verification_challenge),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
WORKSPACE_VERIFICATION_ACK_PATH,
|
remaining_route(runtime_api::RUNTIME_ROUTE_VERIFICATION_ACK, &["POST"]),
|
||||||
post(post_workspace_verification_acknowledgement),
|
post(post_workspace_verification_acknowledgement),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/v1/config-bundles",
|
remaining_route(runtime_api::RUNTIME_ROUTE_CONFIG_BUNDLES, &["GET", "POST"]),
|
||||||
get(list_config_bundles).post(store_config_bundle),
|
get(list_config_bundles).post(store_config_bundle),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/v1/config-bundles/{bundle_id}/availability",
|
remaining_route(
|
||||||
|
runtime_api::RUNTIME_ROUTE_CONFIG_BUNDLE_AVAILABILITY,
|
||||||
|
&["GET"],
|
||||||
|
),
|
||||||
get(check_config_bundle),
|
get(check_config_bundle),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/v1/workspace-prompt-projections",
|
remaining_route(
|
||||||
|
runtime_api::RUNTIME_ROUTE_WORKSPACE_PROMPT_PROJECTIONS,
|
||||||
|
&["POST"],
|
||||||
|
),
|
||||||
post(observe_workspace_prompt_projection),
|
post(observe_workspace_prompt_projection),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/v1/working-directories",
|
remaining_route(
|
||||||
|
runtime_api::RUNTIME_ROUTE_WORKING_DIRECTORIES,
|
||||||
|
&["GET", "POST"],
|
||||||
|
),
|
||||||
get(list_working_directories).post(create_working_directory),
|
get(list_working_directories).post(create_working_directory),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/v1/working-directories/repository-access",
|
remaining_route(
|
||||||
|
runtime_api::RUNTIME_ROUTE_WORKING_DIRECTORY_REPOSITORY_ACCESS,
|
||||||
|
&["POST"],
|
||||||
|
),
|
||||||
post(authorize_working_directory_repository_access),
|
post(authorize_working_directory_repository_access),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
SSH_HOST_KEY_PROBE_PATH,
|
remaining_route(runtime_api::RUNTIME_ROUTE_SSH_PROBE, &["POST"]),
|
||||||
post(probe_repository_ssh_host_keys),
|
post(probe_repository_ssh_host_keys),
|
||||||
)
|
)
|
||||||
.route("/v1/repository-refs/observe", post(observe_repository_ref))
|
|
||||||
.route(
|
.route(
|
||||||
"/v1/working-directories/{working_directory_id}/sessions",
|
remaining_route(
|
||||||
|
runtime_api::RUNTIME_ROUTE_REPOSITORY_REFS_OBSERVE,
|
||||||
|
&["POST"],
|
||||||
|
),
|
||||||
|
post(observe_repository_ref),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
remaining_route(runtime_api::RUNTIME_ROUTE_WORKDIR_SESSIONS, &["POST"]),
|
||||||
post(open_workdir_session),
|
post(open_workdir_session),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/v1/workdir-sessions/{session_id}/operations",
|
remaining_route(
|
||||||
|
runtime_api::RUNTIME_ROUTE_WORKDIR_SESSION_OPERATIONS,
|
||||||
|
&["POST"],
|
||||||
|
),
|
||||||
post(run_workdir_session_operation),
|
post(run_workdir_session_operation),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/v1/workdir-sessions/{session_id}",
|
remaining_route(runtime_api::RUNTIME_ROUTE_WORKDIR_SESSION, &["DELETE"]),
|
||||||
delete(close_workdir_session),
|
delete(close_workdir_session),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/v1/working-directories/{working_directory_id}",
|
remaining_route(
|
||||||
|
runtime_api::RUNTIME_ROUTE_WORKING_DIRECTORY,
|
||||||
|
&["GET", "DELETE"],
|
||||||
|
),
|
||||||
get(get_working_directory).delete(cleanup_working_directory),
|
get(get_working_directory).delete(cleanup_working_directory),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/v1/workers/{worker_id}/attachments",
|
remaining_route(runtime_api::RUNTIME_ROUTE_WORKER_ATTACHMENTS, &["POST"]),
|
||||||
post(upload_worker_file).layer(DefaultBodyLimit::max(MAX_WORKER_FILE_UPLOAD_BYTES)),
|
post(upload_worker_file).layer(DefaultBodyLimit::max(MAX_WORKER_FILE_UPLOAD_BYTES)),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/v1/workers/{worker_id}/attachments/{artifact_id}",
|
remaining_route(runtime_api::RUNTIME_ROUTE_WORKER_ATTACHMENT, &["DELETE"]),
|
||||||
delete(delete_worker_uploaded_file),
|
delete(delete_worker_uploaded_file),
|
||||||
);
|
);
|
||||||
|
|
||||||
#[cfg(feature = "ws-server")]
|
#[cfg(feature = "ws-server")]
|
||||||
let router = router
|
let router = router
|
||||||
.route("/v1/protocol/ws", get(runtime_protocol_ws))
|
|
||||||
.route(
|
.route(
|
||||||
"/v1/workers/{worker_id}/protocol/ws",
|
remaining_route(runtime_api::RUNTIME_ROUTE_PROTOCOL_WS, &["GET"]),
|
||||||
|
get(runtime_protocol_ws),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
remaining_route(runtime_api::RUNTIME_ROUTE_WORKER_PROTOCOL_WS, &["GET"]),
|
||||||
get(worker_protocol_ws),
|
get(worker_protocol_ws),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2072,8 +2109,9 @@ async fn require_runtime_auth(
|
|||||||
};
|
};
|
||||||
match workspace_auth.verifier.verify(token, &expected) {
|
match workspace_auth.verifier.verify(token, &expected) {
|
||||||
Ok(verified) => {
|
Ok(verified) => {
|
||||||
let is_verification = path_and_query == WORKSPACE_VERIFICATION_CHALLENGE_PATH
|
let is_verification = path_and_query
|
||||||
|| path_and_query == WORKSPACE_VERIFICATION_ACK_PATH;
|
== runtime_api::RUNTIME_ROUTE_VERIFICATION_CHALLENGE
|
||||||
|
|| path_and_query == runtime_api::RUNTIME_ROUTE_VERIFICATION_ACK;
|
||||||
if !is_verification {
|
if !is_verification {
|
||||||
let record = match workspace_auth
|
let record = match workspace_auth
|
||||||
.verifications
|
.verifications
|
||||||
@@ -2139,7 +2177,8 @@ async fn require_runtime_auth(
|
|||||||
let workspace_bootstrap_request = request.method() == Method::POST
|
let workspace_bootstrap_request = request.method() == Method::POST
|
||||||
&& matches!(
|
&& matches!(
|
||||||
request.uri().path(),
|
request.uri().path(),
|
||||||
WORKSPACE_VERIFICATION_CHALLENGE_PATH | WORKSPACE_VERIFICATION_ACK_PATH
|
runtime_api::RUNTIME_ROUTE_VERIFICATION_CHALLENGE
|
||||||
|
| runtime_api::RUNTIME_ROUTE_VERIFICATION_ACK
|
||||||
);
|
);
|
||||||
if state.workspace_auth.is_some() && !workspace_bootstrap_request {
|
if state.workspace_auth.is_some() && !workspace_bootstrap_request {
|
||||||
let local_token_matches = state
|
let local_token_matches = state
|
||||||
@@ -2211,7 +2250,8 @@ fn auth_workspace_scope(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn workspace_runtime_operation(method: &Method, path: &str) -> &'static str {
|
fn workspace_runtime_operation(method: &Method, path: &str) -> &'static str {
|
||||||
if (path == WORKSPACE_VERIFICATION_CHALLENGE_PATH || path == WORKSPACE_VERIFICATION_ACK_PATH)
|
if (path == runtime_api::RUNTIME_ROUTE_VERIFICATION_CHALLENGE
|
||||||
|
|| path == runtime_api::RUNTIME_ROUTE_VERIFICATION_ACK)
|
||||||
&& *method == Method::POST
|
&& *method == Method::POST
|
||||||
{
|
{
|
||||||
return WORKSPACE_VERIFICATION_OPERATION;
|
return WORKSPACE_VERIFICATION_OPERATION;
|
||||||
@@ -2238,9 +2278,9 @@ fn required_runtime_permission(method: &Method, path: &str) -> Option<&'static s
|
|||||||
if path == "/v1/workers" && *method == Method::POST {
|
if path == "/v1/workers" && *method == Method::POST {
|
||||||
return Some("workers:create");
|
return Some("workers:create");
|
||||||
}
|
}
|
||||||
if (path == "/v1/working-directories/repository-access"
|
if (path == runtime_api::RUNTIME_ROUTE_WORKING_DIRECTORY_REPOSITORY_ACCESS
|
||||||
|| path == "/v1/repository-refs/observe"
|
|| path == runtime_api::RUNTIME_ROUTE_REPOSITORY_REFS_OBSERVE
|
||||||
|| path == SSH_HOST_KEY_PROBE_PATH)
|
|| path == runtime_api::RUNTIME_ROUTE_SSH_PROBE)
|
||||||
&& *method == Method::POST
|
&& *method == Method::POST
|
||||||
{
|
{
|
||||||
return Some(SSH_HOST_KEY_PROBE_OPERATION);
|
return Some(SSH_HOST_KEY_PROBE_OPERATION);
|
||||||
@@ -2250,9 +2290,9 @@ fn required_runtime_permission(method: &Method, path: &str) -> Option<&'static s
|
|||||||
{
|
{
|
||||||
return Some("workdirs:operate");
|
return Some("workdirs:operate");
|
||||||
}
|
}
|
||||||
if path.starts_with("/v1/config-bundles")
|
if path.starts_with(runtime_api::RUNTIME_ROUTE_CONFIG_BUNDLES)
|
||||||
|| path.starts_with("/v1/workspace-prompt-projections")
|
|| path.starts_with(runtime_api::RUNTIME_ROUTE_WORKSPACE_PROMPT_PROJECTIONS)
|
||||||
|| path.starts_with("/v1/working-directories")
|
|| path.starts_with(runtime_api::RUNTIME_ROUTE_WORKING_DIRECTORIES)
|
||||||
{
|
{
|
||||||
return Some("workers:create");
|
return Some("workers:create");
|
||||||
}
|
}
|
||||||
@@ -2265,7 +2305,7 @@ fn required_runtime_permission(method: &Method, path: &str) -> Option<&'static s
|
|||||||
if path.ends_with("/stop") || path.ends_with("/cancel") {
|
if path.ends_with("/stop") || path.ends_with("/cancel") {
|
||||||
return Some("workers:stop");
|
return Some("workers:stop");
|
||||||
}
|
}
|
||||||
if path == "/v1/protocol/ws" {
|
if path == runtime_api::RUNTIME_ROUTE_PROTOCOL_WS {
|
||||||
return Some("workers:list");
|
return Some("workers:list");
|
||||||
}
|
}
|
||||||
if path.ends_with("/protocol") || path.ends_with("/protocol/ws") {
|
if path.ends_with("/protocol") || path.ends_with("/protocol/ws") {
|
||||||
@@ -2551,6 +2591,7 @@ mod tests {
|
|||||||
WorkerExecutionSpawnResult,
|
WorkerExecutionSpawnResult,
|
||||||
};
|
};
|
||||||
use crate::management::RuntimeOptions;
|
use crate::management::RuntimeOptions;
|
||||||
|
use crate::retention::{DiagnosticsDisposition, SessionDisposition};
|
||||||
use crate::workspace_issuer::{
|
use crate::workspace_issuer::{
|
||||||
InMemoryWorkspaceClaimReplayProtection, InMemoryWorkspaceRuntimeVerificationAuthority,
|
InMemoryWorkspaceClaimReplayProtection, InMemoryWorkspaceRuntimeVerificationAuthority,
|
||||||
WorkspaceCapabilityClaims, WorkspaceCapabilityVerifier, WorkspaceIssuerTrustRecord,
|
WorkspaceCapabilityClaims, WorkspaceCapabilityVerifier, WorkspaceIssuerTrustRecord,
|
||||||
@@ -2629,23 +2670,23 @@ mod tests {
|
|||||||
.map(|route| route.path)
|
.map(|route| route.path)
|
||||||
.collect::<std::collections::BTreeSet<_>>();
|
.collect::<std::collections::BTreeSet<_>>();
|
||||||
for path in [
|
for path in [
|
||||||
WORKSPACE_VERIFICATION_CHALLENGE_PATH,
|
runtime_api::RUNTIME_ROUTE_VERIFICATION_CHALLENGE,
|
||||||
WORKSPACE_VERIFICATION_ACK_PATH,
|
runtime_api::RUNTIME_ROUTE_VERIFICATION_ACK,
|
||||||
"/v1/config-bundles",
|
runtime_api::RUNTIME_ROUTE_CONFIG_BUNDLES,
|
||||||
"/v1/config-bundles/{bundle_id}/availability",
|
runtime_api::RUNTIME_ROUTE_CONFIG_BUNDLE_AVAILABILITY,
|
||||||
"/v1/workspace-prompt-projections",
|
runtime_api::RUNTIME_ROUTE_WORKSPACE_PROMPT_PROJECTIONS,
|
||||||
"/v1/working-directories",
|
runtime_api::RUNTIME_ROUTE_WORKING_DIRECTORIES,
|
||||||
"/v1/working-directories/repository-access",
|
runtime_api::RUNTIME_ROUTE_WORKING_DIRECTORY_REPOSITORY_ACCESS,
|
||||||
SSH_HOST_KEY_PROBE_PATH,
|
runtime_api::RUNTIME_ROUTE_SSH_PROBE,
|
||||||
"/v1/repository-refs/observe",
|
runtime_api::RUNTIME_ROUTE_REPOSITORY_REFS_OBSERVE,
|
||||||
"/v1/working-directories/{working_directory_id}/sessions",
|
runtime_api::RUNTIME_ROUTE_WORKDIR_SESSIONS,
|
||||||
"/v1/workdir-sessions/{session_id}/operations",
|
runtime_api::RUNTIME_ROUTE_WORKDIR_SESSION_OPERATIONS,
|
||||||
"/v1/workdir-sessions/{session_id}",
|
runtime_api::RUNTIME_ROUTE_WORKDIR_SESSION,
|
||||||
"/v1/working-directories/{working_directory_id}",
|
runtime_api::RUNTIME_ROUTE_WORKING_DIRECTORY,
|
||||||
"/v1/workers/{worker_id}/attachments",
|
runtime_api::RUNTIME_ROUTE_WORKER_ATTACHMENTS,
|
||||||
"/v1/workers/{worker_id}/attachments/{artifact_id}",
|
runtime_api::RUNTIME_ROUTE_WORKER_ATTACHMENT,
|
||||||
"/v1/protocol/ws",
|
runtime_api::RUNTIME_ROUTE_PROTOCOL_WS,
|
||||||
"/v1/workers/{worker_id}/protocol/ws",
|
runtime_api::RUNTIME_ROUTE_WORKER_PROTOCOL_WS,
|
||||||
] {
|
] {
|
||||||
assert!(
|
assert!(
|
||||||
inventoried.contains(path),
|
inventoried.contains(path),
|
||||||
@@ -2722,7 +2763,7 @@ mod tests {
|
|||||||
worker_id: None,
|
worker_id: None,
|
||||||
operation: WORKSPACE_VERIFICATION_OPERATION.to_string(),
|
operation: WORKSPACE_VERIFICATION_OPERATION.to_string(),
|
||||||
method: "POST".to_string(),
|
method: "POST".to_string(),
|
||||||
path_and_query: WORKSPACE_VERIFICATION_CHALLENGE_PATH.to_string(),
|
path_and_query: runtime_api::RUNTIME_ROUTE_VERIFICATION_CHALLENGE.to_string(),
|
||||||
body_digest: workspace_request_body_digest(&body),
|
body_digest: workspace_request_body_digest(&body),
|
||||||
iat: unix_now_i64(),
|
iat: unix_now_i64(),
|
||||||
exp: challenge.expires_at,
|
exp: challenge.expires_at,
|
||||||
@@ -2736,7 +2777,7 @@ mod tests {
|
|||||||
.oneshot(
|
.oneshot(
|
||||||
Request::builder()
|
Request::builder()
|
||||||
.method(Method::POST)
|
.method(Method::POST)
|
||||||
.uri(WORKSPACE_VERIFICATION_CHALLENGE_PATH)
|
.uri(runtime_api::RUNTIME_ROUTE_VERIFICATION_CHALLENGE)
|
||||||
.header(header::AUTHORIZATION, format!("Bearer {token}"))
|
.header(header::AUTHORIZATION, format!("Bearer {token}"))
|
||||||
.header(header::CONTENT_TYPE, "application/json")
|
.header(header::CONTENT_TYPE, "application/json")
|
||||||
.body(Body::from(body))
|
.body(Body::from(body))
|
||||||
@@ -2782,7 +2823,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let acknowledgement_body = serde_json::to_vec(&acknowledgement).unwrap();
|
let acknowledgement_body = serde_json::to_vec(&acknowledgement).unwrap();
|
||||||
let acknowledgement_claims = WorkspaceCapabilityClaims {
|
let acknowledgement_claims = WorkspaceCapabilityClaims {
|
||||||
path_and_query: WORKSPACE_VERIFICATION_ACK_PATH.to_string(),
|
path_and_query: runtime_api::RUNTIME_ROUTE_VERIFICATION_ACK.to_string(),
|
||||||
body_digest: workspace_request_body_digest(&acknowledgement_body),
|
body_digest: workspace_request_body_digest(&acknowledgement_body),
|
||||||
jti: "ack-token".to_string(),
|
jti: "ack-token".to_string(),
|
||||||
..claims
|
..claims
|
||||||
@@ -2797,7 +2838,7 @@ mod tests {
|
|||||||
.oneshot(
|
.oneshot(
|
||||||
Request::builder()
|
Request::builder()
|
||||||
.method(Method::POST)
|
.method(Method::POST)
|
||||||
.uri(WORKSPACE_VERIFICATION_ACK_PATH)
|
.uri(runtime_api::RUNTIME_ROUTE_VERIFICATION_ACK)
|
||||||
.header(
|
.header(
|
||||||
header::AUTHORIZATION,
|
header::AUTHORIZATION,
|
||||||
format!("Bearer {acknowledgement_token}"),
|
format!("Bearer {acknowledgement_token}"),
|
||||||
@@ -2815,7 +2856,7 @@ mod tests {
|
|||||||
for authorization in [None, Some("Bearer malformed")] {
|
for authorization in [None, Some("Bearer malformed")] {
|
||||||
let mut request = Request::builder()
|
let mut request = Request::builder()
|
||||||
.method(Method::POST)
|
.method(Method::POST)
|
||||||
.uri("/v1/config-bundles");
|
.uri(runtime_api::RUNTIME_ROUTE_CONFIG_BUNDLES);
|
||||||
if let Some(authorization) = authorization {
|
if let Some(authorization) = authorization {
|
||||||
request = request.header(header::AUTHORIZATION, authorization);
|
request = request.header(header::AUTHORIZATION, authorization);
|
||||||
}
|
}
|
||||||
@@ -2841,6 +2882,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let response = app
|
let response = app
|
||||||
|
.clone()
|
||||||
.oneshot(
|
.oneshot(
|
||||||
Request::builder()
|
Request::builder()
|
||||||
.method(Method::GET)
|
.method(Method::GET)
|
||||||
@@ -2853,6 +2895,58 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
let retention_worker_id = task_request("retention-scope").worker_id;
|
||||||
|
let retention_path = format!("/v1/workers/{retention_worker_id}/retention/execute");
|
||||||
|
let retention_request = WorkerRetentionExecutionRequest {
|
||||||
|
operation_id: "retention-op".to_string(),
|
||||||
|
input_fingerprint: "fingerprint".to_string(),
|
||||||
|
archive_id: None,
|
||||||
|
workspace_id: "workspace-b".to_string(),
|
||||||
|
source_runtime_id: "runtime-a".to_string(),
|
||||||
|
worker_id: retention_worker_id,
|
||||||
|
expected_worker_revision: "revision".to_string(),
|
||||||
|
source_created_at: "2025-01-01T00:00:00Z".to_string(),
|
||||||
|
removed_at: "2025-01-02T00:00:00Z".to_string(),
|
||||||
|
effective_profile: None,
|
||||||
|
retention_class: None,
|
||||||
|
policy_id: "policy".to_string(),
|
||||||
|
policy_revision: 1,
|
||||||
|
session_disposition: SessionDisposition::Purge,
|
||||||
|
diagnostics_disposition: DiagnosticsDisposition::Retain,
|
||||||
|
};
|
||||||
|
let retention_body = serde_json::to_vec(&retention_request).unwrap();
|
||||||
|
let retention_claims = WorkspaceCapabilityClaims {
|
||||||
|
operation: required_runtime_permission(&Method::POST, &retention_path)
|
||||||
|
.unwrap()
|
||||||
|
.to_string(),
|
||||||
|
method: "POST".to_string(),
|
||||||
|
path_and_query: retention_path.clone(),
|
||||||
|
worker_id: Some(retention_worker_id.to_string()),
|
||||||
|
body_digest: workspace_request_body_digest(&retention_body),
|
||||||
|
jti: "retention-scope-token".to_string(),
|
||||||
|
..ping_claims
|
||||||
|
};
|
||||||
|
let retention_token = issue_workspace_capability_token(
|
||||||
|
&workspace_identity.signing_key().unwrap(),
|
||||||
|
&retention_claims,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let response = app
|
||||||
|
.oneshot(
|
||||||
|
Request::builder()
|
||||||
|
.method(Method::POST)
|
||||||
|
.uri(retention_path)
|
||||||
|
.header(header::AUTHORIZATION, format!("Bearer {retention_token}"))
|
||||||
|
.header(header::CONTENT_TYPE, "application/json")
|
||||||
|
.body(Body::from(retention_body))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||||
|
let error: RuntimeHttpErrorResponse = read_json(response).await;
|
||||||
|
assert_eq!(error.error.code, "worker_not_found");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3010,15 +3104,21 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn workdir_routes_require_dedicated_operation_permission() {
|
fn workdir_routes_require_dedicated_operation_permission() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
required_runtime_permission(&Method::POST, "/v1/working-directories/repository-access",),
|
required_runtime_permission(
|
||||||
|
&Method::POST,
|
||||||
|
runtime_api::RUNTIME_ROUTE_WORKING_DIRECTORY_REPOSITORY_ACCESS,
|
||||||
|
),
|
||||||
Some("workdirs:operate")
|
Some("workdirs:operate")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
required_runtime_permission(&Method::POST, "/v1/repository-refs/observe"),
|
required_runtime_permission(
|
||||||
|
&Method::POST,
|
||||||
|
runtime_api::RUNTIME_ROUTE_REPOSITORY_REFS_OBSERVE
|
||||||
|
),
|
||||||
Some("workdirs:operate")
|
Some("workdirs:operate")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
required_runtime_permission(&Method::POST, SSH_HOST_KEY_PROBE_PATH),
|
required_runtime_permission(&Method::POST, runtime_api::RUNTIME_ROUTE_SSH_PROBE),
|
||||||
Some(SSH_HOST_KEY_PROBE_OPERATION)
|
Some(SSH_HOST_KEY_PROBE_OPERATION)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -3600,11 +3700,11 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn ssh_probe_uses_workdir_operation_capability() {
|
fn ssh_probe_uses_workdir_operation_capability() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
required_runtime_permission(&Method::POST, SSH_HOST_KEY_PROBE_PATH),
|
required_runtime_permission(&Method::POST, runtime_api::RUNTIME_ROUTE_SSH_PROBE),
|
||||||
Some(SSH_HOST_KEY_PROBE_OPERATION)
|
Some(SSH_HOST_KEY_PROBE_OPERATION)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
workspace_runtime_operation(&Method::POST, SSH_HOST_KEY_PROBE_PATH),
|
workspace_runtime_operation(&Method::POST, runtime_api::RUNTIME_ROUTE_SSH_PROBE),
|
||||||
SSH_HOST_KEY_PROBE_OPERATION
|
SSH_HOST_KEY_PROBE_OPERATION
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -3621,7 +3721,7 @@ mod tests {
|
|||||||
let response = authed_json_request(
|
let response = authed_json_request(
|
||||||
app,
|
app,
|
||||||
Method::POST,
|
Method::POST,
|
||||||
SSH_HOST_KEY_PROBE_PATH,
|
runtime_api::RUNTIME_ROUTE_SSH_PROBE,
|
||||||
token,
|
token,
|
||||||
&SshHostKeyProbeRequest {
|
&SshHostKeyProbeRequest {
|
||||||
hostname: "-oProxyCommand=malicious".to_string(),
|
hostname: "-oProxyCommand=malicious".to_string(),
|
||||||
@@ -3686,8 +3786,14 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(unauthenticated.status(), StatusCode::UNAUTHORIZED);
|
assert_eq!(unauthenticated.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
|
||||||
let response =
|
let response = authed_json_request(
|
||||||
authed_json_request(app, Method::POST, SSH_HOST_KEY_PROBE_PATH, token, &body).await;
|
app,
|
||||||
|
Method::POST,
|
||||||
|
runtime_api::RUNTIME_ROUTE_SSH_PROBE,
|
||||||
|
token,
|
||||||
|
&body,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
std::fs::read_to_string(recorded_arguments).unwrap(),
|
std::fs::read_to_string(recorded_arguments).unwrap(),
|
||||||
|
|||||||
Reference in New Issue
Block a user