Skip to main content

stormchaser_api/auth/
opa.rs

1use crate::AppState;
2use anyhow::Result;
3use axum::{
4    extract::{Request, State},
5    http::{header, StatusCode},
6    middleware::Next,
7    response::Response,
8};
9use stormchaser_model::auth::ApiOpaContext;
10use tracing::{debug, error, info};
11
12/// Middleware to evaluate Open Policy Agent (OPA) rules for incoming requests
13pub async fn opa_middleware(
14    State(state): State<AppState>,
15    request: Request,
16    next: Next,
17) -> Result<Response, StatusCode> {
18    let authorizer = &*state.opa;
19    if !authorizer.is_configured() {
20        return Ok(next.run(request).await);
21    }
22
23    let path = request.uri().path().to_string();
24    let method = request.method().to_string();
25
26    // Extract token manually from headers if present
27    let token = request
28        .headers()
29        .get(header::AUTHORIZATION)
30        .and_then(|h| h.to_str().ok())
31        .and_then(|s| s.strip_prefix("Bearer "));
32
33    let context = ApiOpaContext {
34        path: &path,
35        method: &method,
36        token,
37    };
38
39    match authorizer.check(context).await {
40        Ok(true) => {
41            debug!("OPA allowed access to {} {}", method, path);
42            Ok(next.run(request).await)
43        }
44        Ok(false) => {
45            info!("OPA denied access to {} {}", method, path);
46            Err(StatusCode::FORBIDDEN)
47        }
48        Err(e) => {
49            error!("OPA check failed for {} {}: {:?}", method, path, e);
50            // Hard failure if OPA is unreachable or errors
51            Err(StatusCode::INTERNAL_SERVER_ERROR)
52        }
53    }
54}