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