use crate::auth::Principal;
use crate::error::{MongrelError, Result};
use parking_lot::RwLock;
use std::sync::Arc;
#[derive(Clone, Debug)]
pub struct AuthState {
inner: Arc<AuthStateInner>,
}
#[derive(Debug)]
struct AuthStateInner {
require_auth: RwLock<bool>,
principal: RwLock<Option<Principal>>,
}
impl AuthState {
pub fn new(require_auth: bool, principal: Option<Principal>) -> Self {
Self {
inner: Arc::new(AuthStateInner {
require_auth: RwLock::new(require_auth),
principal: RwLock::new(principal),
}),
}
}
pub fn disabled() -> Self {
Self::new(false, None)
}
pub fn require_auth(&self) -> bool {
*self.inner.require_auth.read()
}
pub fn set_require_auth(&self, value: bool) {
*self.inner.require_auth.write() = value;
}
pub fn principal(&self) -> Option<Principal> {
self.inner.principal.read().clone()
}
pub fn set_principal(&self, principal: Option<Principal>) {
*self.inner.principal.write() = principal;
}
pub fn require(&self, table_name: &str, perm: RequiredPermission) -> Result<()> {
if !self.require_auth() {
return Ok(());
}
let guard = self.inner.principal.read();
let p = guard.as_ref().ok_or(MongrelError::AuthRequired)?;
let required = perm.into_permission(table_name);
if p.has_permission(&required) {
Ok(())
} else {
Err(MongrelError::PermissionDenied {
required,
principal: p.username.clone(),
})
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RequiredPermission {
Select,
Insert,
Update,
Delete,
}
impl RequiredPermission {
pub fn into_permission(self, table_name: &str) -> crate::auth::Permission {
use crate::auth::Permission;
let table = table_name.to_string();
match self {
RequiredPermission::Select => Permission::Select { table },
RequiredPermission::Insert => Permission::Insert { table },
RequiredPermission::Update => Permission::Update { table },
RequiredPermission::Delete => Permission::Delete { table },
}
}
}
pub trait TableAuthChecker: Send + Sync + std::fmt::Debug {
fn check(&self, table_name: &str, perm: RequiredPermission) -> Result<()>;
}
#[derive(Debug, Clone)]
pub struct DefaultTableAuthChecker {
state: AuthState,
}
impl DefaultTableAuthChecker {
pub fn new(state: AuthState) -> Self {
Self { state }
}
}
impl TableAuthChecker for DefaultTableAuthChecker {
fn check(&self, table_name: &str, perm: RequiredPermission) -> Result<()> {
self.state.require(table_name, perm)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::Permission;
#[test]
fn disabled_state_is_noop() {
let state = AuthState::disabled();
assert!(!state.require_auth());
state.require("orders", RequiredPermission::Select).unwrap();
state.require("orders", RequiredPermission::Insert).unwrap();
}
#[test]
fn enabled_state_with_no_principal_is_auth_required() {
let state = AuthState::new(true, None);
assert!(state.require_auth());
let err = state
.require("orders", RequiredPermission::Select)
.unwrap_err();
assert!(matches!(err, MongrelError::AuthRequired));
}
#[test]
fn enabled_state_denies_without_permission() {
let principal = Principal {
username: "alice".into(),
is_admin: false,
roles: vec![],
permissions: vec![Permission::Select {
table: "orders".into(),
}],
};
let state = AuthState::new(true, Some(principal));
state.require("orders", RequiredPermission::Select).unwrap();
let err = state
.require("orders", RequiredPermission::Insert)
.unwrap_err();
assert!(matches!(err, MongrelError::PermissionDenied { .. }));
}
#[test]
fn enabled_state_admin_bypasses() {
let principal = Principal {
username: "admin".into(),
is_admin: true,
roles: vec![],
permissions: vec![],
};
let state = AuthState::new(true, Some(principal));
state.require("orders", RequiredPermission::Select).unwrap();
state.require("orders", RequiredPermission::Insert).unwrap();
state.require("orders", RequiredPermission::Delete).unwrap();
}
#[test]
fn set_require_auth_propagates_to_clones() {
let state = AuthState::disabled();
let clone = state.clone();
assert!(!clone.require_auth());
state.set_require_auth(true);
assert!(clone.require_auth(), "clone sees the live flag");
}
#[test]
fn default_checker_delegates_to_state() {
let principal = Principal {
username: "alice".into(),
is_admin: false,
roles: vec![],
permissions: vec![Permission::Insert {
table: "orders".into(),
}],
};
let checker = DefaultTableAuthChecker::new(AuthState::new(true, Some(principal)));
checker.check("orders", RequiredPermission::Insert).unwrap();
let err = checker
.check("orders", RequiredPermission::Select)
.unwrap_err();
assert!(matches!(err, MongrelError::PermissionDenied { .. }));
}
}