Skip to main content

mongreldb_core/
auth_state.rs

1//! Shared auth state for per-table enforcement.
2//!
3//! The [`AuthState`] struct bundles the `require_auth` flag (read from the
4//! catalog) and the cached principal into a single `Arc`-clonable handle.
5//! This handle is cloned into every mounted [`crate::engine::Table`] so the
6//! Table layer can enforce `Select`/`Insert`/`Update`/`Delete` permissions
7//! without holding a reference back to `Database` (which would create a
8//! reference cycle, since `Database` owns the `Arc<Mutex<Table>>` handles).
9//!
10//! The design is intentionally extensible: the [`TableAuthChecker`] trait
11//! lets the daemon (or any future multi-tenant layer) provide its own
12//! principal source — e.g. one that reads from per-request state — while the
13//! embedded default checker reads the `Database`'s cached principal. See
14//! `docs/15-credential-enforcement.md` §4.3.
15
16use crate::auth::Principal;
17use crate::error::{MongrelError, Result};
18use parking_lot::RwLock;
19use std::sync::Arc;
20
21/// A cloneable snapshot of the auth state shared between `Database` and every
22/// mounted `Table`. Cloning is cheap (one `Arc` bump).
23///
24/// The `require_auth` flag is read live from the catalog on every check, so
25/// `enable_auth` / offline-disable are reflected immediately. The principal
26/// is the open-time cached value; daemons that need per-request principals
27/// should swap in their own [`TableAuthChecker`].
28#[derive(Clone, Debug)]
29pub struct AuthState {
30    inner: Arc<AuthStateInner>,
31}
32
33#[derive(Debug)]
34struct AuthStateInner {
35    /// The require_auth flag, read live from the catalog. This is a `RwLock<bool>`
36    /// mirror of `Catalog::require_auth` — kept here so the Table layer can
37    /// read it without acquiring the full catalog lock on every operation.
38    /// Updated by `Database` whenever the catalog's flag changes.
39    require_auth: RwLock<bool>,
40    /// The cached principal for this handle. `None` on credentialless
41    /// databases.
42    principal: RwLock<Option<Principal>>,
43}
44
45impl AuthState {
46    /// Create a new auth state. `require_auth` is the initial catalog value;
47    /// `principal` is the cached principal (if any).
48    pub fn new(require_auth: bool, principal: Option<Principal>) -> Self {
49        Self {
50            inner: Arc::new(AuthStateInner {
51                require_auth: RwLock::new(require_auth),
52                principal: RwLock::new(principal),
53            }),
54        }
55    }
56
57    /// Create a credentialless auth state (no enforcement).
58    pub fn disabled() -> Self {
59        Self::new(false, None)
60    }
61
62    /// Whether enforcement is currently active.
63    pub fn require_auth(&self) -> bool {
64        *self.inner.require_auth.read()
65    }
66
67    /// Update the `require_auth` flag. Called by `Database` when the catalog
68    /// flag changes (e.g. `enable_auth`).
69    pub fn set_require_auth(&self, value: bool) {
70        *self.inner.require_auth.write() = value;
71    }
72
73    /// A clone of the cached principal, if any.
74    pub fn principal(&self) -> Option<Principal> {
75        self.inner.principal.read().clone()
76    }
77
78    /// Replace the cached principal. Called by `Database::open_with_credentials`,
79    /// `enable_auth`, and `refresh_principal`.
80    pub fn set_principal(&self, principal: Option<Principal>) {
81        *self.inner.principal.write() = principal;
82    }
83
84    /// Enforcement check: if `require_auth` is true, verify the cached
85    /// principal satisfies `perm` for `table_name`. On credentialless
86    /// databases, this is a no-op (`Ok(())`).
87    ///
88    /// This is the primary entry point called by `Table` and `Transaction`
89    /// enforcement points.
90    pub fn require(&self, table_name: &str, perm: RequiredPermission) -> Result<()> {
91        if !self.require_auth() {
92            return Ok(());
93        }
94        let guard = self.inner.principal.read();
95        let p = guard.as_ref().ok_or(MongrelError::AuthRequired)?;
96        let required = perm.into_permission(table_name);
97        let has = p.has_permission(&required);
98        if has {
99            Ok(())
100        } else {
101            Err(MongrelError::PermissionDenied {
102                required,
103                principal: p.username.clone(),
104            })
105        }
106    }
107}
108
109/// The permission "shape" needed for a table operation. This is separate
110/// from [`crate::auth::Permission`] because at the call site we know the
111/// operation kind (read/insert/update/delete) and the table name, but
112/// constructing the full `Permission` requires allocating a `String` for the
113/// table name. `RequiredPermission` is a lightweight enum that defers the
114/// allocation to `into_permission`, which is only called when enforcement is
115/// actually active (i.e. `require_auth` is true).
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum RequiredPermission {
118    /// `SELECT` on the table.
119    Select,
120    /// `INSERT` on the table.
121    Insert,
122    /// `UPDATE` on the table.
123    Update,
124    /// `DELETE` on the table.
125    Delete,
126}
127
128impl RequiredPermission {
129    /// Construct the full [`crate::auth::Permission`] for this kind, allocating
130    /// the table-name `String`.
131    pub fn into_permission(self, table_name: &str) -> crate::auth::Permission {
132        use crate::auth::Permission;
133        let table = table_name.to_string();
134        match self {
135            RequiredPermission::Select => Permission::Select { table },
136            RequiredPermission::Insert => Permission::Insert { table },
137            RequiredPermission::Update => Permission::Update { table },
138            RequiredPermission::Delete => Permission::Delete { table },
139        }
140    }
141}
142
143/// Extensible auth-checker trait for the Table layer. The default
144/// implementation (used by embedded/CLI) delegates to [`AuthState`]. The
145/// daemon can provide its own implementation that reads the principal from
146/// per-request state, enabling per-user enforcement on a shared `Database`.
147///
148/// This is deliberately a `Fn`-style trait (not `FnMut`) so it can be shared
149/// across threads via `Arc<dyn TableAuthChecker>`.
150pub trait TableAuthChecker: Send + Sync + std::fmt::Debug {
151    /// Check whether the current principal has `perm` on `table_name`.
152    /// Returns `Ok(())` if allowed, `Err(MongrelError::PermissionDenied)`
153    /// (or `AuthRequired`) if not.
154    fn check(&self, table_name: &str, perm: RequiredPermission) -> Result<()>;
155}
156
157/// The default auth checker — delegates to a shared [`AuthState`]. Used by
158/// embedded/CLI/programmatic opens where the principal is the open-time
159/// cached value.
160#[derive(Debug, Clone)]
161pub struct DefaultTableAuthChecker {
162    state: AuthState,
163}
164
165impl DefaultTableAuthChecker {
166    pub fn new(state: AuthState) -> Self {
167        Self { state }
168    }
169}
170
171impl TableAuthChecker for DefaultTableAuthChecker {
172    fn check(&self, table_name: &str, perm: RequiredPermission) -> Result<()> {
173        self.state.require(table_name, perm)
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use crate::auth::Permission;
181
182    #[test]
183    fn disabled_state_is_noop() {
184        let state = AuthState::disabled();
185        assert!(!state.require_auth());
186        state.require("orders", RequiredPermission::Select).unwrap();
187        state.require("orders", RequiredPermission::Insert).unwrap();
188    }
189
190    #[test]
191    fn enabled_state_with_no_principal_is_auth_required() {
192        let state = AuthState::new(true, None);
193        assert!(state.require_auth());
194        let err = state
195            .require("orders", RequiredPermission::Select)
196            .unwrap_err();
197        assert!(matches!(err, MongrelError::AuthRequired));
198    }
199
200    #[test]
201    fn enabled_state_denies_without_permission() {
202        let principal = Principal {
203            username: "alice".into(),
204            is_admin: false,
205            roles: vec![],
206            permissions: vec![Permission::Select {
207                table: "orders".into(),
208            }],
209        };
210        let state = AuthState::new(true, Some(principal));
211        // Select on orders → ok.
212        state.require("orders", RequiredPermission::Select).unwrap();
213        // Insert on orders → denied.
214        let err = state
215            .require("orders", RequiredPermission::Insert)
216            .unwrap_err();
217        assert!(matches!(err, MongrelError::PermissionDenied { .. }));
218    }
219
220    #[test]
221    fn enabled_state_admin_bypasses() {
222        let principal = Principal {
223            username: "admin".into(),
224            is_admin: true,
225            roles: vec![],
226            permissions: vec![],
227        };
228        let state = AuthState::new(true, Some(principal));
229        state.require("orders", RequiredPermission::Select).unwrap();
230        state.require("orders", RequiredPermission::Insert).unwrap();
231        state.require("orders", RequiredPermission::Delete).unwrap();
232    }
233
234    #[test]
235    fn set_require_auth_propagates_to_clones() {
236        let state = AuthState::disabled();
237        let clone = state.clone();
238        assert!(!clone.require_auth());
239        state.set_require_auth(true);
240        assert!(clone.require_auth(), "clone sees the live flag");
241    }
242
243    #[test]
244    fn default_checker_delegates_to_state() {
245        let principal = Principal {
246            username: "alice".into(),
247            is_admin: false,
248            roles: vec![],
249            permissions: vec![Permission::Insert {
250                table: "orders".into(),
251            }],
252        };
253        let checker = DefaultTableAuthChecker::new(AuthState::new(true, Some(principal)));
254        checker.check("orders", RequiredPermission::Insert).unwrap();
255        let err = checker
256            .check("orders", RequiredPermission::Select)
257            .unwrap_err();
258        assert!(matches!(err, MongrelError::PermissionDenied { .. }));
259    }
260}