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