feat(enabled): runtime honoring for disabled scripts and routes
Make the `enabled` flag bite at the data plane (§4.3).
- Routes: `compile_routes` drops disabled rows from the match table, so a
request to a disabled route 404s indistinguishably from an absent one
(no info leak). Rebuilt on every route write + at startup, as today.
- Scripts: the orchestrator's two execute paths — `/execute/{id}` bypass
and the user-route handler — 404 when the resolved script is disabled.
The route-handler check also covers an enabled-route-bound-to-disabled-
script (§4.7): the binding is unreachable rather than 500/executing.
Still pending: the dispatcher fire-time re-check for already-enqueued
outbox rows (next commit).
Tested: route_admin disabled-route-dropped unit test + orchestrator suite
(75) green; clippy -D warnings clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -396,6 +396,9 @@ async fn refresh_table<RR: RouteRepository, SR: ScriptRepository>(
|
|||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn compile_routes(rows: &[Route]) -> Vec<CompiledRoute> {
|
pub fn compile_routes(rows: &[Route]) -> Vec<CompiledRoute> {
|
||||||
rows.iter()
|
rows.iter()
|
||||||
|
// A disabled route (§4.3) is dropped from the match table entirely, so
|
||||||
|
// a request to it 404s indistinguishably from an absent route.
|
||||||
|
.filter(|r| r.enabled)
|
||||||
.filter_map(|r| match compile_route(r) {
|
.filter_map(|r| match compile_route(r) {
|
||||||
Ok(compiled) => Some(compiled),
|
Ok(compiled) => Some(compiled),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -654,4 +657,16 @@ mod tests {
|
|||||||
"a reserved-path route must be skipped, never abort the compile"
|
"a reserved-path route must be skipped, never abort the compile"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disabled_route_is_dropped_from_compiled_table() {
|
||||||
|
// §4.3: a disabled route is excluded from the match table, so a
|
||||||
|
// request to it 404s indistinguishably from an absent route.
|
||||||
|
let active = route_with_path("/on");
|
||||||
|
let mut disabled = route_with_path("/off");
|
||||||
|
disabled.enabled = false;
|
||||||
|
let compiled = compile_routes(&[active.clone(), disabled.clone()]);
|
||||||
|
let ids: Vec<Uuid> = compiled.iter().map(|c| c.route_id).collect();
|
||||||
|
assert_eq!(ids, vec![active.id], "only the enabled route compiles");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,6 +121,11 @@ where
|
|||||||
.resolve(id)
|
.resolve(id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(ApiError::NotFound(id))?;
|
.ok_or(ApiError::NotFound(id))?;
|
||||||
|
// A disabled script (§4.3) is not invocable — 404, indistinguishable
|
||||||
|
// from an absent one (no info leak that the id exists but is off).
|
||||||
|
if !script.enabled {
|
||||||
|
return Err(ApiError::NotFound(id));
|
||||||
|
}
|
||||||
|
|
||||||
let mut req = build_exec_request(id, &script.name, &headers, &body, script.app_id, principal)?;
|
let mut req = build_exec_request(id, &script.name, &headers, &body, script.app_id, principal)?;
|
||||||
req.sandbox_overrides = script.sandbox;
|
req.sandbox_overrides = script.sandbox;
|
||||||
@@ -164,6 +169,7 @@ where
|
|||||||
Ok(exec_response_to_http(outcome?))
|
Ok(exec_response_to_http(outcome?))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
async fn user_route_handler<E, R>(
|
async fn user_route_handler<E, R>(
|
||||||
State(state): State<DataPlaneState<E, R>>,
|
State(state): State<DataPlaneState<E, R>>,
|
||||||
Extension(principal): Extension<Option<Principal>>,
|
Extension(principal): Extension<Option<Principal>>,
|
||||||
@@ -221,6 +227,12 @@ where
|
|||||||
.resolve(matched.matched.script_id)
|
.resolve(matched.matched.script_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(ApiError::NotFound(matched.matched.script_id))?;
|
.ok_or(ApiError::NotFound(matched.matched.script_id))?;
|
||||||
|
// An enabled route bound to a disabled script (§4.3, §4.7) is
|
||||||
|
// unreachable: 404 as if no route matched (the disabled route is already
|
||||||
|
// dropped from the match table; this covers the route-on/script-off case).
|
||||||
|
if !script.enabled {
|
||||||
|
return Err(ApiError::NotFound(matched.matched.script_id));
|
||||||
|
}
|
||||||
|
|
||||||
// Drain the body now that we know we'll execute. 10 MiB cap matches
|
// Drain the body now that we know we'll execute. 10 MiB cap matches
|
||||||
// the conservative default response/request size in the blueprint.
|
// the conservative default response/request size in the blueprint.
|
||||||
|
|||||||
Reference in New Issue
Block a user