Make inherited request resolution unforgeable via ResolvedHttpRequest

This commit is contained in:
Gregory Schier
2026-08-15 07:08:18 -07:00
parent 85ed7ba170
commit 5fc719491b
+66 -24
View File
@@ -236,14 +236,46 @@ pub struct SendHttpRequestByIdParams<'a, T: TemplateCallback> {
pub executor: &'a dyn SendRequestExecutor, pub executor: &'a dyn SendRequestExecutor,
} }
/// An [`HttpRequest`] carrying the authentication and headers it inherits from its folder or
/// workspace, alongside the id of the model that authentication came from.
///
/// Sending requires inheritance to be applied first, and this type is the only way to say it has
/// been. There is no public constructor beyond [`resolve_inherited_request`], which resolves it
/// against a database, and [`ResolvedHttpRequest::assume_resolved`], which a caller without a
/// database must name explicitly — so skipping inheritance is a deliberate, greppable act rather
/// than something a new call site forgets.
#[derive(Clone)]
pub struct ResolvedHttpRequest {
request: HttpRequest,
auth_context_id: String,
}
impl ResolvedHttpRequest {
/// Declare a request already resolved, for callers with no database to resolve against. The
/// caller owns the promise that inherited authentication and headers are applied.
pub fn assume_resolved(request: HttpRequest, auth_context_id: String) -> Self {
Self { request, auth_context_id }
}
pub fn request(&self) -> &HttpRequest {
&self.request
}
pub fn auth_context_id(&self) -> &str {
&self.auth_context_id
}
fn into_parts(self) -> (HttpRequest, String) {
(self.request, self.auth_context_id)
}
}
/// Everything a send needs that would otherwise be read from the database. /// Everything a send needs that would otherwise be read from the database.
/// ///
/// Callers backed by a database build this with [`resolve_send_inputs`]. A stateless caller /// Callers backed by a database build this with [`resolve_send_inputs`]. A stateless caller
/// constructs it directly, which is what lets [`send_http_request`] run with no database at all. /// constructs it directly, which is what lets [`send_http_request`] run with no database at all.
pub struct HttpSendInputs { pub struct HttpSendInputs {
/// The request with inherited authentication and headers already applied. pub request: ResolvedHttpRequest,
pub request: HttpRequest,
pub auth_context_id: String,
pub environment_chain: Vec<Environment>, pub environment_chain: Vec<Environment>,
pub runtime_config: HttpSendRuntimeConfig, pub runtime_config: HttpSendRuntimeConfig,
/// Cookies the send starts with. The store is shared, so reading it back after the send /// Cookies the send starts with. The store is shared, so reading it back after the send
@@ -351,12 +383,7 @@ pub fn resolve_send_inputs(
.resolve_environments(&request.workspace_id, request.folder_id.as_deref(), environment_id) .resolve_environments(&request.workspace_id, request.folder_id.as_deref(), environment_id)
.map_err(SendHttpRequestError::ResolveEnvironments)?; .map_err(SendHttpRequestError::ResolveEnvironments)?;
let (authentication_type, authentication, auth_context_id) = db let resolved_request = resolve_inherited_request(query_manager, request)?;
.resolve_auth_for_http_request(request)
.map_err(SendHttpRequestError::ResolveRequestInheritance)?;
let headers = db
.resolve_headers_for_http_request(request)
.map_err(SendHttpRequestError::ResolveRequestInheritance)?;
let workspace = let workspace =
db.get_workspace(&request.workspace_id).map_err(SendHttpRequestError::LoadWorkspace)?; db.get_workspace(&request.workspace_id).map_err(SendHttpRequestError::LoadWorkspace)?;
@@ -366,8 +393,7 @@ pub fn resolve_send_inputs(
.map_err(SendHttpRequestError::ResolveRequestInheritance)?; .map_err(SendHttpRequestError::ResolveRequestInheritance)?;
Ok(HttpSendInputs { Ok(HttpSendInputs {
request: HttpRequest { authentication_type, authentication, headers, ..request.clone() }, request: resolved_request,
auth_context_id,
environment_chain, environment_chain,
runtime_config: HttpSendRuntimeConfig { runtime_config: HttpSendRuntimeConfig {
settings: resolved_settings, settings: resolved_settings,
@@ -379,6 +405,25 @@ pub fn resolve_send_inputs(
}) })
} }
/// Apply the authentication and headers a request inherits from its folder or workspace.
pub fn resolve_inherited_request(
query_manager: &QueryManager,
request: &HttpRequest,
) -> Result<ResolvedHttpRequest> {
let db = query_manager.connect();
let (authentication_type, authentication, auth_context_id) = db
.resolve_auth_for_http_request(request)
.map_err(SendHttpRequestError::ResolveRequestInheritance)?;
let headers = db
.resolve_headers_for_http_request(request)
.map_err(SendHttpRequestError::ResolveRequestInheritance)?;
Ok(ResolvedHttpRequest {
request: HttpRequest { authentication_type, authentication, headers, ..request.clone() },
auth_context_id,
})
}
pub async fn send_http_request_by_id_with_plugins( pub async fn send_http_request_by_id_with_plugins(
params: SendHttpRequestByIdWithPluginsParams<'_>, params: SendHttpRequestByIdWithPluginsParams<'_>,
) -> Result<SendHttpRequestResult> { ) -> Result<SendHttpRequestResult> {
@@ -504,13 +549,8 @@ pub async fn send_http_request_by_id<T: TemplateCallback>(
pub async fn send_http_request<T: TemplateCallback>( pub async fn send_http_request<T: TemplateCallback>(
params: SendHttpRequestParams<'_, T>, params: SendHttpRequestParams<'_, T>,
) -> Result<SendHttpRequestResult> { ) -> Result<SendHttpRequestResult> {
let HttpSendInputs { let HttpSendInputs { request, environment_chain, runtime_config, cookie_store } = params.inputs;
request, let (request, auth_context_id) = request.into_parts();
auth_context_id,
environment_chain,
runtime_config,
cookie_store,
} = params.inputs;
let storage = params.storage; let storage = params.storage;
let send_options = runtime_config.send_options(); let send_options = runtime_config.send_options();
let resolved_settings = &runtime_config.settings; let resolved_settings = &runtime_config.settings;
@@ -1284,12 +1324,14 @@ mod tests {
let result = send_http_request(SendHttpRequestParams { let result = send_http_request(SendHttpRequestParams {
inputs: HttpSendInputs { inputs: HttpSendInputs {
request: HttpRequest { request: ResolvedHttpRequest::assume_resolved(
workspace_id: "wk_test".to_string(), HttpRequest {
url: "http://localhost/test".to_string(), workspace_id: "wk_test".to_string(),
..Default::default() url: "http://localhost/test".to_string(),
}, ..Default::default()
auth_context_id: String::new(), },
String::new(),
),
environment_chain: Vec::new(), environment_chain: Vec::new(),
runtime_config: HttpSendRuntimeConfig { runtime_config: HttpSendRuntimeConfig {
settings: ResolvedHttpRequestSettings::default(), settings: ResolvedHttpRequestSettings::default(),