fix: verify signed Workspace query targets
This commit is contained in:
@@ -639,8 +639,8 @@ fn unix_now_seconds() -> u64 {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::auth::{
|
use crate::auth::{
|
||||||
WorkerMutationSourceExpectation, decode_worker_mutation_source_claims,
|
WorkerMutationSourceExpectation, decode_runtime_request_source_claims,
|
||||||
verify_worker_mutation_source_proof,
|
decode_worker_mutation_source_claims, verify_worker_mutation_source_proof,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -846,7 +846,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ordinary_workspace_forwarding_stamps_runtime_identity_and_signed_source_proof() {
|
fn ordinary_workspace_forwarding_stamps_runtime_identity_and_signs_path_and_query() {
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::net::TcpListener;
|
use std::net::TcpListener;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
@@ -875,15 +875,32 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.with_runtime_request_source(&identity, "server-a");
|
.with_runtime_request_source(&identity, "server-a");
|
||||||
let response = client
|
let response = client
|
||||||
.execute(WorkspaceRequest::get("/api/w/workspace-a/tickets/search"))
|
.execute(WorkspaceRequest::get(
|
||||||
|
"/api/w/workspace-a/tickets/search?state=planning&limit=20",
|
||||||
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(response.status, 200);
|
assert_eq!(response.status, 200);
|
||||||
server.join().unwrap();
|
server.join().unwrap();
|
||||||
let request = received.lock().unwrap().to_ascii_lowercase();
|
let request = received.lock().unwrap().clone();
|
||||||
assert!(request.contains("x-yoi-runtime-id: runtime-a"));
|
let lowercase_request = request.to_ascii_lowercase();
|
||||||
assert!(request.contains("x-yoi-worker-id: worker-a"));
|
assert!(lowercase_request.contains("x-yoi-runtime-id: runtime-a"));
|
||||||
assert!(request.contains("x-yoi-runtime-request-proof: yoi-runtime-request-v1."));
|
assert!(lowercase_request.contains("x-yoi-worker-id: worker-a"));
|
||||||
assert!(!request.contains("authorization:"));
|
assert!(lowercase_request.contains("x-yoi-runtime-request-proof: yoi-runtime-request-v1."));
|
||||||
|
assert!(!lowercase_request.contains("authorization:"));
|
||||||
|
let token = request
|
||||||
|
.lines()
|
||||||
|
.find_map(|line| {
|
||||||
|
line.split_once(':').and_then(|(name, value)| {
|
||||||
|
name.eq_ignore_ascii_case(RUNTIME_REQUEST_SOURCE_PROOF_HEADER)
|
||||||
|
.then(|| value.trim())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.expect("runtime proof header");
|
||||||
|
let claims = decode_runtime_request_source_claims(token).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
claims.path,
|
||||||
|
"/api/w/workspace-a/tickets/search?state=planning&limit=20"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -903,6 +903,12 @@ async fn resolve_server_actor(
|
|||||||
resolve_request_actor(api.store.as_ref(), headers, &cookie_name).await
|
resolve_request_actor(api.store.as_ref(), headers, &cookie_name).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn signed_request_target(uri: &Uri) -> &str {
|
||||||
|
uri.path_and_query()
|
||||||
|
.map(|path_and_query| path_and_query.as_str())
|
||||||
|
.unwrap_or_else(|| uri.path())
|
||||||
|
}
|
||||||
|
|
||||||
async fn authorize_scoped_workspace_request(
|
async fn authorize_scoped_workspace_request(
|
||||||
api: &WorkspaceServerApi,
|
api: &WorkspaceServerApi,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
@@ -915,7 +921,7 @@ async fn authorize_scoped_workspace_request(
|
|||||||
.map(str::to_owned);
|
.map(str::to_owned);
|
||||||
if let Some(proof) = proof {
|
if let Some(proof) = proof {
|
||||||
let method = request.method().as_str().to_owned();
|
let method = request.method().as_str().to_owned();
|
||||||
let path = request.uri().path().to_owned();
|
let path = signed_request_target(request.uri()).to_owned();
|
||||||
let body = std::mem::take(request.body_mut());
|
let body = std::mem::take(request.body_mut());
|
||||||
let body = axum::body::to_bytes(body, 16 * 1024 * 1024)
|
let body = axum::body::to_bytes(body, 16 * 1024 * 1024)
|
||||||
.await
|
.await
|
||||||
@@ -987,7 +993,7 @@ async fn authorize_workspace_api_request(
|
|||||||
.map(str::to_owned);
|
.map(str::to_owned);
|
||||||
if let Some(proof) = proof {
|
if let Some(proof) = proof {
|
||||||
let method = request.method().as_str().to_owned();
|
let method = request.method().as_str().to_owned();
|
||||||
let path = request.uri().path().to_owned();
|
let path = signed_request_target(request.uri()).to_owned();
|
||||||
let body = std::mem::take(request.body_mut());
|
let body = std::mem::take(request.body_mut());
|
||||||
let Ok(body) = axum::body::to_bytes(body, 16 * 1024 * 1024).await else {
|
let Ok(body) = axum::body::to_bytes(body, 16 * 1024 * 1024).await else {
|
||||||
return StatusCode::BAD_REQUEST.into_response();
|
return StatusCode::BAD_REQUEST.into_response();
|
||||||
@@ -10851,7 +10857,7 @@ async fn scoped_post_internal_runtime_resource_fetch(
|
|||||||
.get::<crate::worker_source::VerifiedRuntimeRequestSource>()
|
.get::<crate::worker_source::VerifiedRuntimeRequestSource>()
|
||||||
.cloned();
|
.cloned();
|
||||||
let method = request.method().as_str().to_owned();
|
let method = request.method().as_str().to_owned();
|
||||||
let path = request.uri().path().to_owned();
|
let path = signed_request_target(request.uri()).to_owned();
|
||||||
let body = axum::body::to_bytes(request.into_body(), 16 * 1024 * 1024)
|
let body = axum::body::to_bytes(request.into_body(), 16 * 1024 * 1024)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
@@ -19859,6 +19865,68 @@ mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn runtime_request_proof_verifies_path_and_query_for_ticket_search() {
|
||||||
|
let workspace = tempfile::tempdir().unwrap();
|
||||||
|
let mut api = test_api(workspace.path()).await;
|
||||||
|
let identity =
|
||||||
|
worker_runtime::auth::RuntimeIdentityMaterial::generate("runtime-test").unwrap();
|
||||||
|
configure_runtime_request_auth(&mut api, &identity, "runtime-test");
|
||||||
|
seed_worker_source_member(&api, "runtime-test", "worker-test");
|
||||||
|
let signer = worker_runtime::auth::RuntimeRequestSourceSigner::from_identity(&identity);
|
||||||
|
let signed_target =
|
||||||
|
format!("/api/w/{TEST_WORKSPACE_ID}/tickets/search?state=active&limit=20");
|
||||||
|
let tampered_target =
|
||||||
|
format!("/api/w/{TEST_WORKSPACE_ID}/tickets/search?state=all&limit=20");
|
||||||
|
let issue = |target: &str| {
|
||||||
|
signer
|
||||||
|
.issue(
|
||||||
|
"server-test",
|
||||||
|
TEST_WORKSPACE_ID,
|
||||||
|
Some("worker-test"),
|
||||||
|
worker_runtime::auth::WORKSPACE_REQUEST_PERMISSION,
|
||||||
|
"GET",
|
||||||
|
target,
|
||||||
|
b"",
|
||||||
|
i64::try_from(worker_runtime::auth::unix_now_seconds()).unwrap_or(i64::MAX),
|
||||||
|
30,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
let app = build_router(api);
|
||||||
|
|
||||||
|
let tampered = app
|
||||||
|
.clone()
|
||||||
|
.oneshot(
|
||||||
|
Request::builder()
|
||||||
|
.uri(&tampered_target)
|
||||||
|
.header(
|
||||||
|
worker_runtime::auth::RUNTIME_REQUEST_SOURCE_PROOF_HEADER,
|
||||||
|
issue(&signed_target),
|
||||||
|
)
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(tampered.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
|
||||||
|
let valid = app
|
||||||
|
.oneshot(
|
||||||
|
Request::builder()
|
||||||
|
.uri(&signed_target)
|
||||||
|
.header(
|
||||||
|
worker_runtime::auth::RUNTIME_REQUEST_SOURCE_PROOF_HEADER,
|
||||||
|
issue(&signed_target),
|
||||||
|
)
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(valid.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn internal_resource_fetch_rest_returns_typed_missing_resource() {
|
async fn internal_resource_fetch_rest_returns_typed_missing_resource() {
|
||||||
let workspace = tempfile::tempdir().unwrap();
|
let workspace = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user