Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 56 additions & 5 deletions rust-core/verisim-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -850,16 +850,17 @@ pub fn build_router(state: AppState) -> Router {
"/mv_prover_success_by_class",
get(proof_attempts::success_by_class_handler),
)
// Authentication middleware layer
.layer(axum_middleware::from_fn_with_state(
auth_state,
auth::auth_middleware,
))
.with_state(state.clone())
// GraphQL endpoint
.merge(graphql::graphql_router(state))
// Federation endpoints (separate state)
.merge(federation_routes)
// Apply authorization to the complete router, including merged
// GraphQL and federation routes (not only the initial REST routes).
.layer(axum_middleware::from_fn_with_state(
auth_state,
auth::auth_middleware,
))
Comment on lines +860 to +863

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve PSK-only access for federation peers

When AuthConfig.enabled is true, this outer layer rejects legitimate federation protocol requests before their handlers can validate federation credentials: heartbeat accepts the documented X-Federation-PSK header, and registration accepts its PSK in the request body, whereas auth_middleware recognizes only X-API-Key or Bearer credentials. Consequently, peers following the existing federation contract now receive 401 responses unless they are also provisioned with an unrelated general API credential; exempt the PSK-authenticated peer routes or integrate federation identities into this authentication boundary.

Useful? React with 👍 / 👎.

Comment on lines +860 to +863

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Classify federated queries as execute operations

When authentication is enabled, a Reader credential can no longer issue the read-only POST /federation/query operation: the newly applied middleware sends this path through required_permission, but is_execute_path does not include /federation/query, so the POST fallback requires Write and returns 403 even though the built-in reader role explicitly has Execute. Add this endpoint to the execute-path classification so read-only clients retain query access.

Useful? React with 👍 / 👎.

Comment on lines +860 to +863

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enforce admin permission inside GraphQL mutations

When authentication is enabled, any Writer can submit the GraphQL updatePlannerConfig mutation because this middleware authorizes only the envelope POST /graphql, which maps to Write. The equivalent REST operation, PUT /planner/config, is explicitly classified as Admin, so routing GraphQL solely through method-based RBAC creates a role bypass for the same configuration change. Inspect the GraphQL operation or enforce permissions in the resolver before allowing this admin mutation.

Useful? React with 👍 / 👎.

Comment on lines +860 to +863

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Require admin permission for peer deregistration

When authentication is enabled, any Writer credential can now POST /federation/deregister/{store_id} because the outer middleware classifies this unlisted POST route as Write; the handler then removes the peer without requiring its federation PSK. This lets an ordinary data writer disrupt federation membership, so classify peer deregistration as Admin or enforce an equivalent permission in the handler.

Useful? React with 👍 / 👎.

}

/// Mount the API router at both the root and under `version_prefix`.
Expand Down Expand Up @@ -2536,6 +2537,56 @@ mod tests {
AppState::new_async(config).await.unwrap()
}

#[tokio::test]
async fn test_auth_covers_merged_graphql_and_federation_routes() {
let mut state = create_test_state().await;
state.auth = auth::AuthState::new(auth::AuthConfig {
enabled: true,
allow_public_health: true,
..Default::default()
});
state.auth.key_registry.register("writer-key", "test writer", auth::ClientRole::Writer);
let app = build_router(state);

// This GraphQL mutation must never reach its handler without auth.
let mutation = serde_json::json!({
"query": "mutation { deleteOctad(id: \"missing\") }"
}).to_string();
let response = app.clone().oneshot(
Request::builder()
.method("POST")
.uri("/graphql")
.header("content-type", "application/json")
.body(Body::from(mutation.clone()))
.unwrap(),
).await.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);

// The federation router was previously merged after auth middleware.
let response = app.clone().oneshot(
Request::builder().uri("/federation/peers").body(Body::empty()).unwrap(),
).await.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);

// Valid writer credentials pass the middleware and reach GraphQL.
let response = app.clone().oneshot(
Request::builder()
.method("POST")
.uri("/graphql")
.header("content-type", "application/json")
.header("x-api-key", "writer-key")
.body(Body::from(mutation))
.unwrap(),
).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);

// The public health exception remains narrowly available.
let response = app.oneshot(
Request::builder().uri("/health").body(Body::empty()).unwrap(),
).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}

#[tokio::test]
async fn test_health_endpoint() {
let state = create_test_state().await;
Expand Down
Loading