Skip to main content

reddb_server/auth/
mod.rs

1//! Authentication & Authorization
2//!
3//! Provides user management, RBAC, and token-based auth for RedDB.
4//!
5//! # Roles
6//! - `admin`: Full access (user management, index ops, read, write)
7//! - `write`: Read + write data
8//! - `read`: Read-only access
9//!
10//! # Auth Methods
11//! - User/Password login -> session token
12//! - API key -> direct auth with assigned role
13
14pub mod action_catalog;
15pub mod cert;
16pub mod column_policy_gate;
17pub mod enforcement_mode;
18pub mod locks;
19pub mod managed_config;
20pub mod managed_policy;
21pub mod middleware;
22pub mod oauth;
23pub mod policies;
24pub mod privileges;
25pub mod registry;
26pub mod scope_cache;
27pub mod scram;
28pub mod self_lock_guard;
29pub mod store;
30pub mod vault;
31
32pub use scope_cache::{AuthCache, AuthCacheStats, ScopeKey, DEFAULT_TTL as DEFAULT_SCOPE_TTL};
33
34pub use cert::{
35    CertAuthConfig, CertAuthError, CertAuthenticator, CertIdentity, CertIdentityMode,
36    ParsedClientCert,
37};
38pub use column_policy_gate::{
39    ColumnAccessRequest, ColumnDecision, ColumnDecisionEffect, ColumnPolicyGate,
40    ColumnPolicyOutcome, ColumnRef,
41};
42pub use oauth::{
43    DecodedJwt, Jwk, JwtClaims, JwtHeader, OAuthConfig, OAuthError, OAuthIdentity,
44    OAuthIdentityMode, OAuthValidator,
45};
46pub use privileges::{
47    check_grant, Action, AuthzContext, AuthzError, Grant, GrantPrincipal, GrantsView,
48    PermissionCache, Resource, UserAttributes,
49};
50pub use store::AuthStore;
51
52use std::fmt;
53
54// ---------------------------------------------------------------------------
55// Role
56// ---------------------------------------------------------------------------
57
58/// Access role within the RedDB authorization model.
59///
60/// Roles form an ordered hierarchy: `Read < Write < Admin`.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
62pub enum Role {
63    Read,
64    Write,
65    Admin,
66}
67
68impl Role {
69    pub fn as_str(&self) -> &'static str {
70        match self {
71            Self::Read => "read",
72            Self::Write => "write",
73            Self::Admin => "admin",
74        }
75    }
76
77    pub fn from_str(s: &str) -> Option<Self> {
78        match s {
79            "read" => Some(Self::Read),
80            "write" => Some(Self::Write),
81            "admin" => Some(Self::Admin),
82            _ => None,
83        }
84    }
85
86    pub fn can_read(&self) -> bool {
87        true
88    }
89
90    pub fn can_write(&self) -> bool {
91        matches!(self, Self::Write | Self::Admin)
92    }
93
94    pub fn can_admin(&self) -> bool {
95        matches!(self, Self::Admin)
96    }
97}
98
99impl fmt::Display for Role {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        f.write_str(self.as_str())
102    }
103}
104
105// ---------------------------------------------------------------------------
106// User
107// ---------------------------------------------------------------------------
108
109/// A registered user in the RedDB auth system.
110///
111/// Stores both the legacy bcrypt-style `password_hash` (used by
112/// HTTP `/auth/login` for token minting) and the SCRAM-SHA-256
113/// verifier (used by the v2 wire handshake). Both derive from the
114/// same plaintext at user creation; the SCRAM path never sees
115/// plaintext or the salted password again.
116#[derive(Debug, Clone)]
117pub struct User {
118    pub username: String,
119    /// Tenant scope. `None` = platform-wide (the bootstrap admin and any
120    /// platform-level operators); `Some("acme")` = scoped to a SaaS
121    /// tenant. `(tenant_id, username)` is the unique identity key.
122    pub tenant_id: Option<String>,
123    pub password_hash: String,
124    /// SCRAM-SHA-256 verifier — `{ salt, iter, stored_key, server_key }`.
125    /// Populated alongside `password_hash` on user creation.
126    pub scram_verifier: Option<scram::ScramVerifier>,
127    pub role: Role,
128    pub api_keys: Vec<ApiKey>,
129    pub created_at: u128,
130    pub updated_at: u128,
131    pub enabled: bool,
132    pub system_owned: bool,
133}
134
135// ---------------------------------------------------------------------------
136// UserId
137// ---------------------------------------------------------------------------
138
139/// Composite identity key: `(tenant_id, username)`.
140///
141/// `tenant_id == None` means the platform/system tenant (the bootstrap
142/// admin lives there). `tenant_id == Some("acme")` scopes the user to
143/// the `acme` tenant: `alice@acme` and `alice@globex` are two distinct
144/// identities with their own credentials and roles.
145///
146/// Display format is `tenant/username` for scoped users and just
147/// `username` for platform users so audit logs can be parsed back into
148/// the same shape.
149#[derive(Debug, Clone, PartialEq, Eq, Hash)]
150pub struct UserId {
151    pub tenant: Option<String>,
152    pub username: String,
153}
154
155impl UserId {
156    /// Platform / system-tenant user (no tenant scoping).
157    pub fn platform(name: impl Into<String>) -> Self {
158        Self {
159            tenant: None,
160            username: name.into(),
161        }
162    }
163
164    /// Tenant-scoped user.
165    pub fn scoped(tenant: impl Into<String>, name: impl Into<String>) -> Self {
166        Self {
167            tenant: Some(tenant.into()),
168            username: name.into(),
169        }
170    }
171
172    /// Build a `UserId` from an optional tenant + username pair (the
173    /// shape most call-sites already have).
174    pub fn from_parts(tenant: Option<&str>, username: &str) -> Self {
175        Self {
176            tenant: tenant.map(|t| t.to_string()),
177            username: username.to_string(),
178        }
179    }
180}
181
182impl fmt::Display for UserId {
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        match &self.tenant {
185            Some(t) => write!(f, "{}/{}", t, self.username),
186            None => f.write_str(&self.username),
187        }
188    }
189}
190
191// ---------------------------------------------------------------------------
192// ApiKey
193// ---------------------------------------------------------------------------
194
195/// A persistent API key bound to a user.
196#[derive(Debug, Clone)]
197pub struct ApiKey {
198    /// Token value: `"rk_<hex32>"`
199    pub key: String,
200    /// Human-readable label.
201    pub name: String,
202    /// Role granted by this key (cannot exceed user's role).
203    pub role: Role,
204    pub created_at: u128,
205}
206
207// ---------------------------------------------------------------------------
208// Session
209// ---------------------------------------------------------------------------
210
211/// An ephemeral session created by login.
212#[derive(Debug, Clone)]
213pub struct Session {
214    /// Token value: `"rs_<hex32>"`
215    pub token: String,
216    pub username: String,
217    /// Tenant the session is scoped to. Mirrors the `User.tenant_id`
218    /// at login time and is what `CURRENT_TENANT()` should default to.
219    pub tenant_id: Option<String>,
220    pub role: Role,
221    pub created_at: u128,
222    /// Absolute expiry (ms since epoch).
223    pub expires_at: u128,
224}
225
226// ---------------------------------------------------------------------------
227// AuthConfig
228// ---------------------------------------------------------------------------
229
230/// Configuration knobs for the auth subsystem.
231#[derive(Debug, Clone)]
232pub struct AuthConfig {
233    /// Master switch -- when `false` auth is completely bypassed.
234    pub enabled: bool,
235    /// Session time-to-live in seconds (default 3600 = 1 h).
236    pub session_ttl_secs: u64,
237    /// When `true`, unauthenticated requests are rejected even for reads.
238    pub require_auth: bool,
239    /// When `true`, storage files are encrypted when auth is active.
240    pub auto_encrypt_storage: bool,
241    /// When `true`, auth state (users, api keys, bootstrap flag) is persisted
242    /// to reserved vault pages inside the main `.rdb` database file using
243    /// AES-256-GCM encryption.  The encryption key is read from
244    /// `REDDB_VAULT_KEY` env var or a passphrase.
245    pub vault_enabled: bool,
246    /// Optional mTLS client-certificate auth policy (Phase 3.4 PG parity).
247    /// Disabled by default; TLS listeners opt-in per config.
248    pub cert: CertAuthConfig,
249    /// Optional OAuth/OIDC Bearer-token validator (Phase 3.4 PG parity).
250    /// Disabled by default.
251    pub oauth: OAuthConfig,
252}
253
254impl Default for AuthConfig {
255    fn default() -> Self {
256        Self {
257            enabled: false,
258            session_ttl_secs: 3600,
259            require_auth: false,
260            auto_encrypt_storage: false,
261            vault_enabled: false,
262            cert: CertAuthConfig::default(),
263            oauth: OAuthConfig::default(),
264        }
265    }
266}
267
268// ---------------------------------------------------------------------------
269// AuthError
270// ---------------------------------------------------------------------------
271
272/// Errors produced by auth operations.
273#[derive(Debug, Clone)]
274pub enum AuthError {
275    UserExists(String),
276    UserNotFound(String),
277    InvalidCredentials,
278    KeyNotFound(String),
279    RoleExceeded { requested: Role, ceiling: Role },
280    SystemUserImmutable { username: String },
281    Disabled,
282    Forbidden(String),
283    Internal(String),
284}
285
286impl fmt::Display for AuthError {
287    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288        match self {
289            Self::UserExists(u) => write!(f, "user already exists: {u}"),
290            Self::UserNotFound(u) => write!(f, "user not found: {u}"),
291            Self::InvalidCredentials => write!(f, "invalid credentials"),
292            Self::KeyNotFound(k) => write!(f, "api key not found: {k}"),
293            Self::RoleExceeded { requested, ceiling } => {
294                write!(
295                    f,
296                    "requested role '{requested}' exceeds ceiling '{ceiling}'"
297                )
298            }
299            Self::SystemUserImmutable { username } => {
300                write!(f, "system-owned user is immutable: {username}")
301            }
302            Self::Disabled => write!(f, "authentication is disabled"),
303            Self::Forbidden(msg) => write!(f, "forbidden: {msg}"),
304            Self::Internal(msg) => write!(f, "internal auth error: {msg}"),
305        }
306    }
307}
308
309impl std::error::Error for AuthError {}
310
311// ---------------------------------------------------------------------------
312// Helpers -- timestamp
313// ---------------------------------------------------------------------------
314
315/// Current time in milliseconds since the UNIX epoch.
316pub(crate) fn now_ms() -> u128 {
317    std::time::SystemTime::now()
318        .duration_since(std::time::UNIX_EPOCH)
319        .unwrap_or_default()
320        .as_millis()
321}
322
323// ---------------------------------------------------------------------------
324// Tests
325// ---------------------------------------------------------------------------
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330
331    #[test]
332    fn test_role_ordering() {
333        assert!(Role::Read < Role::Write);
334        assert!(Role::Write < Role::Admin);
335    }
336
337    #[test]
338    fn test_role_roundtrip() {
339        for role in [Role::Read, Role::Write, Role::Admin] {
340            assert_eq!(Role::from_str(role.as_str()), Some(role));
341        }
342        assert_eq!(Role::from_str("unknown"), None);
343    }
344
345    #[test]
346    fn test_role_permissions() {
347        assert!(Role::Read.can_read());
348        assert!(!Role::Read.can_write());
349        assert!(!Role::Read.can_admin());
350
351        assert!(Role::Write.can_read());
352        assert!(Role::Write.can_write());
353        assert!(!Role::Write.can_admin());
354
355        assert!(Role::Admin.can_read());
356        assert!(Role::Admin.can_write());
357        assert!(Role::Admin.can_admin());
358    }
359
360    #[test]
361    fn test_auth_config_default() {
362        let cfg = AuthConfig::default();
363        assert!(!cfg.enabled);
364        assert_eq!(cfg.session_ttl_secs, 3600);
365        assert!(!cfg.require_auth);
366        assert!(!cfg.auto_encrypt_storage);
367    }
368
369    #[test]
370    fn test_auth_error_display() {
371        let err = AuthError::UserExists("alice".into());
372        assert!(err.to_string().contains("alice"));
373
374        let err = AuthError::InvalidCredentials;
375        assert!(err.to_string().contains("invalid"));
376    }
377}