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