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(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}
136
137// ---------------------------------------------------------------------------
138// UserId
139// ---------------------------------------------------------------------------
140
141/// Composite identity key: `(tenant_id, username)`.
142///
143/// `tenant_id == None` means the platform/system tenant (the bootstrap
144/// admin lives there). `tenant_id == Some("acme")` scopes the user to
145/// the `acme` tenant: `alice@acme` and `alice@globex` are two distinct
146/// identities with their own credentials and roles.
147///
148/// Display format is `tenant/username` for scoped users and just
149/// `username` for platform users so audit logs can be parsed back into
150/// the same shape.
151#[derive(Debug, Clone, PartialEq, Eq, Hash)]
152pub struct UserId {
153    pub tenant: Option<String>,
154    pub username: String,
155}
156
157impl UserId {
158    /// Platform / system-tenant user (no tenant scoping).
159    pub fn platform(name: impl Into<String>) -> Self {
160        Self {
161            tenant: None,
162            username: name.into(),
163        }
164    }
165
166    /// Tenant-scoped user.
167    pub fn scoped(tenant: impl Into<String>, name: impl Into<String>) -> Self {
168        Self {
169            tenant: Some(tenant.into()),
170            username: name.into(),
171        }
172    }
173
174    /// Build a `UserId` from an optional tenant + username pair (the
175    /// shape most call-sites already have).
176    pub fn from_parts(tenant: Option<&str>, username: &str) -> Self {
177        Self {
178            tenant: tenant.map(|t| t.to_string()),
179            username: username.to_string(),
180        }
181    }
182}
183
184impl fmt::Display for UserId {
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        match &self.tenant {
187            Some(t) => write!(f, "{}/{}", t, self.username),
188            None => f.write_str(&self.username),
189        }
190    }
191}
192
193// ---------------------------------------------------------------------------
194// ApiKey
195// ---------------------------------------------------------------------------
196
197/// A persistent API key bound to a user.
198#[derive(Debug, Clone)]
199pub struct ApiKey {
200    /// Credential id: `"rkh_<sha256 hex of the secret>"`.
201    ///
202    /// The secret itself (`"rk_<hex64>"`) exists only in the value that
203    /// `create_api_key*` returns — the store, its index and the vault hold
204    /// the id, so a dumped vault or a heap snapshot yields nothing a client
205    /// can present.
206    pub key: String,
207    /// Human-readable label.
208    pub name: String,
209    /// Role granted by this key (cannot exceed user's role).
210    pub role: Role,
211    pub created_at: u128,
212    /// Absolute expiry (ms since epoch); `None` never expires.
213    pub expires_at: Option<u128>,
214}
215
216// ---------------------------------------------------------------------------
217// Session
218// ---------------------------------------------------------------------------
219
220/// An ephemeral session created by login.
221#[derive(Clone)]
222pub struct Session {
223    /// Token value: `"rs_<hex32>"`
224    pub token: String,
225    pub username: String,
226    /// Tenant the session is scoped to. Mirrors the `User.tenant_id`
227    /// at login time and is what `CURRENT_TENANT()` should default to.
228    pub tenant_id: Option<String>,
229    pub role: Role,
230    pub created_at: u128,
231    /// Absolute expiry (ms since epoch).
232    pub expires_at: u128,
233}
234
235// ---------------------------------------------------------------------------
236// AuthConfig
237// ---------------------------------------------------------------------------
238
239/// Configuration knobs for the auth subsystem.
240#[derive(Debug, Clone)]
241pub struct AuthConfig {
242    /// Master switch -- when `false` auth is completely bypassed.
243    pub enabled: bool,
244    /// Session time-to-live in seconds (default 3600 = 1 h).
245    pub session_ttl_secs: u64,
246    /// When `true`, unauthenticated requests are rejected even for reads.
247    pub require_auth: bool,
248    /// When `true`, storage files are encrypted when auth is active.
249    pub auto_encrypt_storage: bool,
250    /// When `true`, auth state (users, api keys, bootstrap flag) is persisted
251    /// to reserved vault pages inside the main `.rdb` database file using
252    /// AES-256-GCM encryption.  The encryption key is derived from the
253    /// certificate supplied by `REDDB_CERTIFICATE` / `REDDB_CERTIFICATE_FILE`.
254    pub vault_enabled: bool,
255    /// Optional mTLS client-certificate auth policy (Phase 3.4 PG parity).
256    /// Disabled by default; TLS listeners opt-in per config.
257    pub cert: CertAuthConfig,
258    /// Optional OAuth/OIDC Bearer-token validator (Phase 3.4 PG parity).
259    /// Disabled by default.
260    pub oauth: OAuthConfig,
261}
262
263impl Default for AuthConfig {
264    fn default() -> Self {
265        Self {
266            enabled: false,
267            session_ttl_secs: 3600,
268            require_auth: false,
269            auto_encrypt_storage: false,
270            vault_enabled: false,
271            cert: CertAuthConfig::default(),
272            oauth: OAuthConfig::default(),
273        }
274    }
275}
276
277// ---------------------------------------------------------------------------
278// AuthError
279// ---------------------------------------------------------------------------
280
281/// Errors produced by auth operations.
282#[derive(Debug, Clone)]
283pub enum AuthError {
284    UserExists(String),
285    UserNotFound(String),
286    InvalidCredentials,
287    KeyNotFound(String),
288    RoleExceeded { requested: Role, ceiling: Role },
289    Disabled,
290    Forbidden(String),
291    Internal(String),
292}
293
294impl fmt::Display for AuthError {
295    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296        match self {
297            Self::UserExists(u) => write!(f, "user already exists: {u}"),
298            Self::UserNotFound(u) => write!(f, "user not found: {u}"),
299            Self::InvalidCredentials => write!(f, "invalid credentials"),
300            Self::KeyNotFound(k) => write!(f, "api key not found: {k}"),
301            Self::RoleExceeded { requested, ceiling } => {
302                write!(
303                    f,
304                    "requested role '{requested}' exceeds ceiling '{ceiling}'"
305                )
306            }
307            Self::Disabled => write!(f, "authentication is disabled"),
308            Self::Forbidden(msg) => write!(f, "forbidden: {msg}"),
309            Self::Internal(msg) => write!(f, "internal auth error: {msg}"),
310        }
311    }
312}
313
314impl std::error::Error for AuthError {}
315
316// ---------------------------------------------------------------------------
317// Helpers -- timestamp
318// ---------------------------------------------------------------------------
319
320/// Current time in milliseconds since the UNIX epoch.
321pub(crate) fn now_ms() -> u128 {
322    std::time::SystemTime::now()
323        .duration_since(std::time::UNIX_EPOCH)
324        .unwrap_or_default()
325        .as_millis()
326}
327
328// ---------------------------------------------------------------------------
329// Tests
330// ---------------------------------------------------------------------------
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335
336    #[test]
337    fn test_role_ordering() {
338        assert!(Role::Read < Role::Write);
339        assert!(Role::Write < Role::Admin);
340    }
341
342    #[test]
343    fn test_role_roundtrip() {
344        for role in [Role::Read, Role::Write, Role::Admin] {
345            assert_eq!(Role::from_str(role.as_str()), Some(role));
346        }
347        assert_eq!(Role::from_str("unknown"), None);
348    }
349
350    #[test]
351    fn test_role_permissions() {
352        assert!(Role::Read.can_read());
353        assert!(!Role::Read.can_write());
354        assert!(!Role::Read.can_admin());
355
356        assert!(Role::Write.can_read());
357        assert!(Role::Write.can_write());
358        assert!(!Role::Write.can_admin());
359
360        assert!(Role::Admin.can_read());
361        assert!(Role::Admin.can_write());
362        assert!(Role::Admin.can_admin());
363    }
364
365    #[test]
366    fn test_auth_config_default() {
367        let cfg = AuthConfig::default();
368        assert!(!cfg.enabled);
369        assert_eq!(cfg.session_ttl_secs, 3600);
370        assert!(!cfg.require_auth);
371        assert!(!cfg.auto_encrypt_storage);
372    }
373
374    #[test]
375    fn test_auth_error_display() {
376        let err = AuthError::UserExists("alice".into());
377        assert!(err.to_string().contains("alice"));
378
379        let err = AuthError::InvalidCredentials;
380        assert!(err.to_string().contains("invalid"));
381    }
382}
383
384// Credential-bearing structs print their secrets redacted. A derived
385// `Debug` would put the password hash, the SCRAM verifier and live
386// session tokens into any `{:?}` log line or panic message.
387impl std::fmt::Debug for User {
388    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
389        f.debug_struct("User")
390            .field("username", &self.username)
391            .field("tenant_id", &self.tenant_id)
392            .field("password_hash", &"<redacted>")
393            .field(
394                "scram_verifier",
395                &self.scram_verifier.as_ref().map(|_| "<redacted>"),
396            )
397            .field("role", &self.role)
398            .field("api_keys", &self.api_keys.len())
399            .field("created_at", &self.created_at)
400            .field("updated_at", &self.updated_at)
401            .field("enabled", &self.enabled)
402            .finish()
403    }
404}
405
406impl std::fmt::Debug for Session {
407    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
408        f.debug_struct("Session")
409            .field("token", &"<redacted>")
410            .field("username", &self.username)
411            .field("tenant_id", &self.tenant_id)
412            .field("role", &self.role)
413            .field("created_at", &self.created_at)
414            .field("expires_at", &self.expires_at)
415            .finish()
416    }
417}