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/auth-enforcement-spec.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 if p.has_permission(&required) {
98 Ok(())
99 } else {
100 Err(MongrelError::PermissionDenied {
101 required,
102 principal: p.username.clone(),
103 })
104 }
105 }
106}
107
108/// The permission "shape" needed for a table operation. This is separate
109/// from [`crate::auth::Permission`] because at the call site we know the
110/// operation kind (read/insert/update/delete) and the table name, but
111/// constructing the full `Permission` requires allocating a `String` for the
112/// table name. `RequiredPermission` is a lightweight enum that defers the
113/// allocation to `into_permission`, which is only called when enforcement is
114/// actually active (i.e. `require_auth` is true).
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum RequiredPermission {
117 /// `SELECT` on the table.
118 Select,
119 /// `INSERT` on the table.
120 Insert,
121 /// `UPDATE` on the table.
122 Update,
123 /// `DELETE` on the table.
124 Delete,
125}
126
127impl RequiredPermission {
128 /// Construct the full [`crate::auth::Permission`] for this kind, allocating
129 /// the table-name `String`.
130 pub fn into_permission(self, table_name: &str) -> crate::auth::Permission {
131 use crate::auth::Permission;
132 let table = table_name.to_string();
133 match self {
134 RequiredPermission::Select => Permission::Select { table },
135 RequiredPermission::Insert => Permission::Insert { table },
136 RequiredPermission::Update => Permission::Update { table },
137 RequiredPermission::Delete => Permission::Delete { table },
138 }
139 }
140}
141
142/// Extensible auth-checker trait for the Table layer. The default
143/// implementation (used by embedded/CLI) delegates to [`AuthState`]. The
144/// daemon can provide its own implementation that reads the principal from
145/// per-request state, enabling per-user enforcement on a shared `Database`.
146///
147/// This is deliberately a `Fn`-style trait (not `FnMut`) so it can be shared
148/// across threads via `Arc<dyn TableAuthChecker>`.
149pub trait TableAuthChecker: Send + Sync + std::fmt::Debug {
150 /// Check whether the current principal has `perm` on `table_name`.
151 /// Returns `Ok(())` if allowed, `Err(MongrelError::PermissionDenied)`
152 /// (or `AuthRequired`) if not.
153 fn check(&self, table_name: &str, perm: RequiredPermission) -> Result<()>;
154}
155
156/// The default auth checker — delegates to a shared [`AuthState`]. Used by
157/// embedded/CLI/programmatic opens where the principal is the open-time
158/// cached value.
159#[derive(Debug, Clone)]
160pub struct DefaultTableAuthChecker {
161 state: AuthState,
162}
163
164impl DefaultTableAuthChecker {
165 pub fn new(state: AuthState) -> Self {
166 Self { state }
167 }
168}
169
170impl TableAuthChecker for DefaultTableAuthChecker {
171 fn check(&self, table_name: &str, perm: RequiredPermission) -> Result<()> {
172 self.state.require(table_name, perm)
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use crate::auth::Permission;
180
181 #[test]
182 fn disabled_state_is_noop() {
183 let state = AuthState::disabled();
184 assert!(!state.require_auth());
185 state.require("orders", RequiredPermission::Select).unwrap();
186 state.require("orders", RequiredPermission::Insert).unwrap();
187 }
188
189 #[test]
190 fn enabled_state_with_no_principal_is_auth_required() {
191 let state = AuthState::new(true, None);
192 assert!(state.require_auth());
193 let err = state
194 .require("orders", RequiredPermission::Select)
195 .unwrap_err();
196 assert!(matches!(err, MongrelError::AuthRequired));
197 }
198
199 #[test]
200 fn enabled_state_denies_without_permission() {
201 let principal = Principal {
202 username: "alice".into(),
203 is_admin: false,
204 roles: vec![],
205 permissions: vec![Permission::Select {
206 table: "orders".into(),
207 }],
208 };
209 let state = AuthState::new(true, Some(principal));
210 // Select on orders → ok.
211 state.require("orders", RequiredPermission::Select).unwrap();
212 // Insert on orders → denied.
213 let err = state
214 .require("orders", RequiredPermission::Insert)
215 .unwrap_err();
216 assert!(matches!(err, MongrelError::PermissionDenied { .. }));
217 }
218
219 #[test]
220 fn enabled_state_admin_bypasses() {
221 let principal = Principal {
222 username: "admin".into(),
223 is_admin: true,
224 roles: vec![],
225 permissions: vec![],
226 };
227 let state = AuthState::new(true, Some(principal));
228 state.require("orders", RequiredPermission::Select).unwrap();
229 state.require("orders", RequiredPermission::Insert).unwrap();
230 state.require("orders", RequiredPermission::Delete).unwrap();
231 }
232
233 #[test]
234 fn set_require_auth_propagates_to_clones() {
235 let state = AuthState::disabled();
236 let clone = state.clone();
237 assert!(!clone.require_auth());
238 state.set_require_auth(true);
239 assert!(clone.require_auth(), "clone sees the live flag");
240 }
241
242 #[test]
243 fn default_checker_delegates_to_state() {
244 let principal = Principal {
245 username: "alice".into(),
246 is_admin: false,
247 roles: vec![],
248 permissions: vec![Permission::Insert {
249 table: "orders".into(),
250 }],
251 };
252 let checker = DefaultTableAuthChecker::new(AuthState::new(true, Some(principal)));
253 checker.check("orders", RequiredPermission::Insert).unwrap();
254 let err = checker
255 .check("orders", RequiredPermission::Select)
256 .unwrap_err();
257 assert!(matches!(err, MongrelError::PermissionDenied { .. }));
258 }
259}