Skip to main content

mongreldb_core/
handle.rs

1//! `DatabaseHandle`: a lightweight caller-specific object referencing one
2//! `DatabaseCore` (spec §10.1, S1A-001).
3//!
4//! Handles are issued by [`crate::manager::DatabaseManager::open_shared`].
5//! Every handle carries its own [`HandleIdentity`] and [`HandleAccess`], while
6//! recovery, WAL opening, open-generation advancement, and table mounting all
7//! happened exactly once on the shared core. Dropping one handle never closes
8//! storage; the core closes when the last reference drops or when
9//! [`DatabaseHandle::shutdown`] drains it (S1A-004).
10//!
11//! Handles expose only principal-aware operations. They never dereference to
12//! the raw [`Database`] facade or return mutable table handles.
13
14use std::sync::Arc;
15use std::time::Duration;
16
17use crate::core::{LifecycleState, OperationGuard};
18use crate::database::Database;
19use crate::error::{MongrelError, Result};
20
21/// A password that zeroizes its allocation and never reveals itself through
22/// `Debug` output.
23pub struct SecretString(zeroize::Zeroizing<String>);
24
25impl SecretString {
26    pub fn new(secret: impl Into<String>) -> Self {
27        Self(zeroize::Zeroizing::new(secret.into()))
28    }
29
30    pub(crate) fn expose(&self) -> &str {
31        self.0.as_str()
32    }
33}
34
35impl std::fmt::Debug for SecretString {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        f.write_str("SecretString([REDACTED])")
38    }
39}
40
41/// The per-caller identity bound to one handle (spec §10.1, S1A-001).
42///
43/// Catalog identities pin one user generation and are re-resolved against the
44/// live catalog on each authorized operation. Service identities carry only
45/// their explicitly granted scopes.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum HandleIdentity {
48    /// No credentials were supplied for this handle.
49    Credentialless,
50    /// A user resolved from the database catalog. `user_id` +
51    /// `created_version` pin the exact catalog generation so username reuse
52    /// cannot revive a stale identity.
53    CatalogUser {
54        username: String,
55        user_id: u64,
56        created_version: u64,
57    },
58    /// A non-catalog principal (service account, daemon worker, test actor).
59    ServicePrincipal { principal_id: [u8; 16] },
60}
61
62/// The identity requested when attaching a shared handle (spec §2.2).
63///
64/// Catalog credentials are verified against the live shared catalog before a
65/// handle is issued. The password allocation is zeroized when attach returns.
66#[derive(Debug)]
67pub enum OpenIdentity {
68    /// Attach without credentials. Rejected (fail closed) when the database
69    /// catalog has `require_auth` enabled.
70    Credentialless,
71    /// Attach as a non-catalog service principal.
72    ServicePrincipal { principal_id: [u8; 16] },
73    /// Attach as a service principal restricted to these exact permissions.
74    ScopedServicePrincipal {
75        principal_id: [u8; 16],
76        permissions: Vec<crate::auth::Permission>,
77    },
78    /// Authenticate one catalog user on the existing shared core.
79    CatalogCredentials {
80        username: String,
81        password: SecretString,
82    },
83}
84
85/// Per-handle access restriction (spec §2.2: "each handle may have its own
86/// principal and read-only restriction").
87///
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
89pub struct HandleAccess {
90    read_only: bool,
91}
92
93impl HandleAccess {
94    /// A read-write handle (the Stage 1A default).
95    pub fn read_write() -> Self {
96        Self { read_only: false }
97    }
98
99    /// A read-only handle.
100    pub fn read_only() -> Self {
101        Self { read_only: true }
102    }
103
104    /// Whether this handle is restricted to reads.
105    pub fn is_read_only(&self) -> bool {
106        self.read_only
107    }
108}
109
110/// A lightweight caller-specific handle onto one shared [`DatabaseCore`]
111/// (spec §10.1, S1A-001).
112///
113/// Obtained from [`crate::manager::DatabaseManager::open_shared`]. Cloning or
114/// dropping a handle has no storage side effects; storage closes when the
115/// last core reference drops or [`Self::shutdown`] runs.
116pub struct DatabaseHandle {
117    /// The facade carrying this handle's live authorization context.
118    database: Database,
119    identity: HandleIdentity,
120    access: HandleAccess,
121    service_permissions: Option<Vec<crate::auth::Permission>>,
122}
123
124impl DatabaseHandle {
125    pub(crate) fn new(
126        database: Database,
127        identity: HandleIdentity,
128        access: HandleAccess,
129        service_permissions: Option<Vec<crate::auth::Permission>>,
130    ) -> Self {
131        Self {
132            database,
133            identity,
134            access,
135            service_permissions,
136        }
137    }
138
139    /// The identity bound to this handle.
140    pub fn identity(&self) -> &HandleIdentity {
141        &self.identity
142    }
143
144    /// The access restriction bound to this handle.
145    pub fn access(&self) -> HandleAccess {
146        self.access
147    }
148
149    /// Whether two handles reference the exact same process-local core.
150    pub fn shares_core_with(&self, other: &Self) -> bool {
151        Arc::ptr_eq(&self.database.core(), &other.database.core())
152    }
153
154    /// The current lifecycle state of the shared core (S1A-004).
155    pub fn lifecycle_state(&self) -> LifecycleState {
156        self.database.lifecycle_state()
157    }
158
159    /// Admit one operation against the shared core (S1A-004). The RAII guard
160    /// releases the operation slot on drop; new operations are rejected once
161    /// the core leaves [`LifecycleState::Open`].
162    pub fn operation_guard(&self) -> Result<OperationGuard> {
163        self.database.operation_guard()
164    }
165
166    fn authorize_write(&self, operation: &'static str) -> Result<()> {
167        if self.access.is_read_only() {
168            Err(MongrelError::ReadOnlyHandle { operation })
169        } else {
170            Ok(())
171        }
172    }
173
174    fn authorize_permission(&self, permission: &crate::auth::Permission) -> Result<()> {
175        if let Some(permissions) = &self.service_permissions {
176            if !permissions
177                .iter()
178                .any(|granted| granted.satisfies(permission))
179            {
180                return Err(MongrelError::PermissionDenied {
181                    required: permission.clone(),
182                    principal: match &self.identity {
183                        HandleIdentity::ServicePrincipal { principal_id } => {
184                            format!("service:{principal_id:02x?}")
185                        }
186                        _ => "service".into(),
187                    },
188                });
189            }
190        }
191        self.database.require(permission)
192    }
193
194    /// Execute an authorized native query with live roles, RLS, masks, and
195    /// column grants.
196    pub fn query(
197        &self,
198        table: &str,
199        query: &crate::query::Query,
200        projection: Option<&[u16]>,
201    ) -> Result<Vec<crate::memtable::Row>> {
202        self.database
203            .query_for_current_principal(table, query, projection)
204    }
205
206    /// Count rows visible to this handle after live authorization and RLS.
207    pub fn count(&self, table: &str) -> Result<u64> {
208        self.database
209            .count_for(table, self.database.principal().as_ref())
210    }
211
212    /// Return all rows visible to this handle after RLS and masks.
213    pub fn rows(&self, table: &str) -> Result<Vec<crate::memtable::Row>> {
214        self.database
215            .rows_for(table, self.database.principal().as_ref())
216    }
217
218    /// Create a table through this handle's live principal.
219    pub fn create_table(&self, name: &str, schema: crate::schema::Schema) -> Result<u64> {
220        self.authorize_write("create table")?;
221        self.authorize_permission(&crate::auth::Permission::Ddl)?;
222        self.database.create_table(name, schema)
223    }
224
225    /// Atomically insert one row through this handle's live principal.
226    pub fn put(
227        &self,
228        table: &str,
229        cells: Vec<(u16, crate::memtable::Value)>,
230    ) -> Result<Option<i64>> {
231        self.authorize_write("put")?;
232        self.database
233            .transaction_for_current_principal(|transaction| transaction.put(table, cells))
234    }
235
236    /// Atomically delete one row through this handle's live principal.
237    pub fn delete(&self, table: &str, row_id: crate::RowId) -> Result<()> {
238        self.authorize_write("delete")?;
239        self.database
240            .transaction_for_current_principal(|transaction| transaction.delete(table, row_id))
241    }
242
243    /// Create a catalog user. Admin only.
244    pub fn create_user(&self, username: &str, password: &str) -> Result<crate::auth::UserEntry> {
245        self.authorize_write("create user")?;
246        self.authorize_permission(&crate::auth::Permission::Admin)?;
247        self.database.create_user(username, password)
248    }
249
250    /// Drop a catalog user. Admin only.
251    pub fn drop_user(&self, username: &str) -> Result<()> {
252        self.authorize_write("drop user")?;
253        self.authorize_permission(&crate::auth::Permission::Admin)?;
254        self.database.drop_user(username)
255    }
256
257    /// Create a role. Admin only.
258    pub fn create_role(&self, role: &str) -> Result<crate::auth::RoleEntry> {
259        self.authorize_write("create role")?;
260        self.authorize_permission(&crate::auth::Permission::Admin)?;
261        self.database.create_role(role)
262    }
263
264    /// Grant a role to a catalog user. Admin only.
265    pub fn grant_role(&self, username: &str, role: &str) -> Result<()> {
266        self.authorize_write("grant role")?;
267        self.authorize_permission(&crate::auth::Permission::Admin)?;
268        self.database.grant_role(username, role)
269    }
270
271    /// Revoke a role from a catalog user. Admin only.
272    pub fn revoke_role(&self, username: &str, role: &str) -> Result<()> {
273        self.authorize_write("revoke role")?;
274        self.authorize_permission(&crate::auth::Permission::Admin)?;
275        self.database.revoke_role(username, role)
276    }
277
278    /// Grant one permission to a role. Admin only.
279    pub fn grant_permission(&self, role: &str, permission: crate::auth::Permission) -> Result<()> {
280        self.authorize_write("grant permission")?;
281        self.authorize_permission(&crate::auth::Permission::Admin)?;
282        self.database.grant_permission(role, permission)
283    }
284
285    /// Revoke one permission from a role. Admin only.
286    pub fn revoke_permission(&self, role: &str, permission: crate::auth::Permission) -> Result<()> {
287        self.authorize_write("revoke permission")?;
288        self.authorize_permission(&crate::auth::Permission::Admin)?;
289        self.database.revoke_permission(role, permission)
290    }
291
292    /// Shut the shared core down (spec §10.1, S1A-004): drain in-flight
293    /// operations within `drain_deadline`, sync durable state, release the
294    /// file lock, and mark the core `Closed`. Every handle — including this
295    /// one — then rejects further operations. Dropping a handle never closes
296    /// storage; only this method or the last core drop does.
297    pub fn shutdown(&self, drain_deadline: Duration) -> Result<()> {
298        self.authorize_write("shutdown")?;
299        self.authorize_permission(&crate::auth::Permission::Admin)?;
300        self.database.core().shutdown(drain_deadline)
301    }
302}
303
304impl std::fmt::Debug for DatabaseHandle {
305    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306        f.debug_struct("DatabaseHandle")
307            .field("identity", &self.identity)
308            .field("access", &self.access)
309            .field("database", &self.database)
310            .finish()
311    }
312}