qrush 2.0.0

Lightweight Job Queue and Task Scheduler for Rust (Actix + Redis + Cron)
Documentation
// qrush/src/services/basic_auth_service.rs
//
// Basic-auth for the dashboard. The credential check itself
// (`check_basic_auth_header`) is framework-neutral; each web framework gets a
// thin middleware wrapper gated behind its own feature.

use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use subtle::ConstantTimeEq;
use crate::config::{get_basic_auth, QrushBasicAuthConfig};

/// Validate a raw `Authorization` header value against the configured
/// credentials. Returns `true` when auth passes (or when no credentials are
/// configured, in which case access is open by default).
pub fn check_basic_auth_header(auth_header: Option<&str>) -> bool {
    check_credentials(auth_header, get_basic_auth())
}

/// Pure credential check, independent of global config — the `config` is passed
/// in so it can be unit-tested. `None` config means "no auth configured", which
/// allows the request. Kept `pub(crate)` for tests and the public wrapper above.
pub(crate) fn check_credentials(
    auth_header: Option<&str>,
    config: Option<&QrushBasicAuthConfig>,
) -> bool {
    let Some(config) = config else {
        // If config is not set, allow by default.
        return true;
    };

    if let Some(auth_str) = auth_header {
        if let Some(encoded) = auth_str.strip_prefix("Basic ") {
            if let Ok(decoded) = STANDARD.decode(encoded) {
                if let Ok(credentials) = std::str::from_utf8(&decoded) {
                    let mut parts = credentials.splitn(2, ':');
                    let user = parts.next().unwrap_or_default();
                    let pass = parts.next().unwrap_or_default();
                    // Constant-time comparison to avoid leaking credentials via
                    // response timing. `&` (not `&&`) checks both without
                    // short-circuiting between the username and password.
                    let user_ok = user.as_bytes().ct_eq(config.username.as_bytes());
                    let pass_ok = pass.as_bytes().ct_eq(config.password.as_bytes());
                    if (user_ok & pass_ok).into() {
                        return true;
                    }
                }
            }
        }
    }
    false
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::QrushBasicAuthConfig;

    fn cfg() -> QrushBasicAuthConfig {
        QrushBasicAuthConfig {
            username: "user".into(),
            password: "pass".into(),
        }
    }

    fn header(user_pass: &str) -> String {
        format!("Basic {}", STANDARD.encode(user_pass))
    }

    #[test]
    fn allows_when_unconfigured() {
        assert!(check_credentials(None, None));
        assert!(check_credentials(Some(&header("anyone:anything")), None));
    }

    #[test]
    fn accepts_correct_credentials() {
        assert!(check_credentials(Some(&header("user:pass")), Some(&cfg())));
    }

    #[test]
    fn rejects_wrong_credentials() {
        assert!(!check_credentials(Some(&header("user:wrong")), Some(&cfg())));
        assert!(!check_credentials(Some(&header("admin:pass")), Some(&cfg())));
    }

    #[test]
    fn rejects_missing_or_malformed_header() {
        assert!(!check_credentials(None, Some(&cfg())));
        assert!(!check_credentials(Some("Bearer abc"), Some(&cfg())));
        assert!(!check_credentials(Some("Basic %%%not-base64"), Some(&cfg())));
    }
}

// ---------------------------------------------------------------------------
// Actix middleware
// ---------------------------------------------------------------------------
#[cfg(feature = "dashboard-actix")]
mod actix_impl {
    use super::check_basic_auth_header;
    use actix_web::{
        dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
        Error, HttpRequest, HttpResponse,
    };
    use futures_util::future::{ready, Either, Ready};
    use std::future::Ready as StdReady;

    pub struct BasicAuthMiddleware;

    impl<S> Transform<S, ServiceRequest> for BasicAuthMiddleware
    where
        S: Service<ServiceRequest, Response = ServiceResponse, Error = Error> + 'static,
        S::Future: 'static,
    {
        type Response = ServiceResponse;
        type Error = Error;
        type InitError = ();
        type Transform = BasicAuthMiddlewareService<S>;
        type Future = StdReady<Result<Self::Transform, Self::InitError>>;

        fn new_transform(&self, service: S) -> Self::Future {
            std::future::ready(Ok(BasicAuthMiddlewareService { service }))
        }
    }

    pub struct BasicAuthMiddlewareService<S> {
        service: S,
    }

    impl<S> Service<ServiceRequest> for BasicAuthMiddlewareService<S>
    where
        S: Service<ServiceRequest, Response = ServiceResponse, Error = Error> + 'static,
        S::Future: 'static,
    {
        type Response = ServiceResponse;
        type Error = Error;

        // Correct Future type: Either<Ready<Result<...>>, S::Future>
        type Future = Either<Ready<Result<Self::Response, Self::Error>>, S::Future>;

        forward_ready!(service);

        fn call(&self, req: ServiceRequest) -> Self::Future {
            if check_basic_auth(req.request()) {
                Either::Right(self.service.call(req))
            } else {
                let response = req.into_response(unauthorized_response());
                Either::Left(ready(Ok(response)))
            }
        }
    }

    pub fn check_basic_auth(req: &HttpRequest) -> bool {
        let header = req
            .headers()
            .get("Authorization")
            .and_then(|v| v.to_str().ok());
        check_basic_auth_header(header)
    }

    pub fn unauthorized_response() -> HttpResponse {
        HttpResponse::Unauthorized()
            .append_header(("WWW-Authenticate", r#"Basic realm="QRush""#))
            .finish()
    }
}

#[cfg(feature = "dashboard-actix")]
pub use actix_impl::{
    check_basic_auth, unauthorized_response, BasicAuthMiddleware, BasicAuthMiddlewareService,
};

// ---------------------------------------------------------------------------
// Axum middleware
// ---------------------------------------------------------------------------
#[cfg(feature = "dashboard-axum")]
pub async fn axum_basic_auth(
    req: axum::extract::Request,
    next: axum::middleware::Next,
) -> axum::response::Response {
    use axum::http::{header, StatusCode};
    use axum::response::IntoResponse;

    let auth = req
        .headers()
        .get(header::AUTHORIZATION)
        .and_then(|v| v.to_str().ok());

    if check_basic_auth_header(auth) {
        next.run(req).await
    } else {
        (
            StatusCode::UNAUTHORIZED,
            [(header::WWW_AUTHENTICATE, r#"Basic realm="QRush""#)],
            "Unauthorized",
        )
            .into_response()
    }
}