graphile_worker_admin_ui 0.2.1

Embedded Leptos admin UI for graphile_worker
Documentation
use axum::http::{HeaderMap, HeaderName};
use serde::Serialize;

use super::secret::generate_secret;
use super::verify::{authorize_basic, authorize_bearer, constant_time_eq};
use crate::native::error::AdminUiError;

#[derive(Clone, Debug, Serialize)]
pub enum PublicAuthMode {
    Basic,
    Bearer,
    Header,
    None,
}

impl PublicAuthMode {
    pub(crate) fn as_str(&self) -> &'static str {
        match self {
            Self::Basic => "basic",
            Self::Bearer => "bearer",
            Self::Header => "header",
            Self::None => "none",
        }
    }
}

#[derive(Clone, Debug)]
pub enum AdminAuthConfig {
    Basic {
        username: String,
        password: String,
        generated_password: bool,
    },
    Bearer {
        token: String,
        generated_token: bool,
    },
    Header {
        header_name: HeaderName,
        token: String,
        generated_token: bool,
    },
    None,
}

#[derive(Clone, Debug, Serialize)]
pub struct AdminAuthSummary {
    pub mode: PublicAuthMode,
    pub username: Option<String>,
    pub header_name: Option<String>,
    pub generated_secret: bool,
}

impl AdminAuthConfig {
    pub fn basic(username: impl Into<String>, password: impl Into<String>) -> Self {
        Self::Basic {
            username: username.into(),
            password: password.into(),
            generated_password: false,
        }
    }

    pub fn basic_with_random_password(username: impl Into<String>) -> Self {
        Self::Basic {
            username: username.into(),
            password: generate_secret(),
            generated_password: true,
        }
    }

    pub fn bearer(token: impl Into<String>, generated_token: bool) -> Self {
        Self::Bearer {
            token: token.into(),
            generated_token,
        }
    }

    pub fn header(
        header_name: impl AsRef<str>,
        token: impl Into<String>,
        generated_token: bool,
    ) -> Result<Self, AdminUiError> {
        let header_name = HeaderName::from_bytes(header_name.as_ref().as_bytes())
            .map_err(|_| AdminUiError::InvalidHeaderName(header_name.as_ref().to_string()))?;
        Ok(Self::Header {
            header_name,
            token: token.into(),
            generated_token,
        })
    }

    pub fn summary(&self) -> AdminAuthSummary {
        match self {
            Self::Basic {
                username,
                generated_password,
                ..
            } => AdminAuthSummary {
                mode: PublicAuthMode::Basic,
                username: Some(username.clone()),
                header_name: None,
                generated_secret: *generated_password,
            },
            Self::Bearer {
                generated_token, ..
            } => AdminAuthSummary {
                mode: PublicAuthMode::Bearer,
                username: None,
                header_name: None,
                generated_secret: *generated_token,
            },
            Self::Header {
                header_name,
                generated_token,
                ..
            } => AdminAuthSummary {
                mode: PublicAuthMode::Header,
                username: None,
                header_name: Some(header_name.as_str().to_string()),
                generated_secret: *generated_token,
            },
            Self::None => AdminAuthSummary {
                mode: PublicAuthMode::None,
                username: None,
                header_name: None,
                generated_secret: false,
            },
        }
    }

    pub fn secret_for_display(&self) -> Option<&str> {
        match self {
            Self::Basic { password, .. } => Some(password),
            Self::Bearer { token, .. } | Self::Header { token, .. } => Some(token),
            Self::None => None,
        }
    }

    pub(crate) fn is_authorized(&self, headers: &HeaderMap) -> bool {
        match self {
            Self::None => true,
            Self::Basic {
                username, password, ..
            } => authorize_basic(headers, username, password),
            Self::Bearer { token, .. } => authorize_bearer(headers, token),
            Self::Header {
                header_name, token, ..
            } => headers
                .get(header_name)
                .and_then(|value| value.to_str().ok())
                .is_some_and(|value| constant_time_eq(value.as_bytes(), token.as_bytes())),
        }
    }
}