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