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}
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    Disabled,
283    Forbidden(String),
284    Internal(String),
285}
286
287impl fmt::Display for AuthError {
288    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289        match self {
290            Self::UserExists(u) => write!(f, "user already exists: {u}"),
291            Self::UserNotFound(u) => write!(f, "user not found: {u}"),
292            Self::InvalidCredentials => write!(f, "invalid credentials"),
293            Self::KeyNotFound(k) => write!(f, "api key not found: {k}"),
294            Self::RoleExceeded { requested, ceiling } => {
295                write!(
296                    f,
297                    "requested role '{requested}' exceeds ceiling '{ceiling}'"
298                )
299            }
300            Self::Disabled => write!(f, "authentication is disabled"),
301            Self::Forbidden(msg) => write!(f, "forbidden: {msg}"),
302            Self::Internal(msg) => write!(f, "internal auth error: {msg}"),
303        }
304    }
305}
306
307impl std::error::Error for AuthError {}
308
309// ---------------------------------------------------------------------------
310// Helpers -- timestamp
311// ---------------------------------------------------------------------------
312
313/// Current time in milliseconds since the UNIX epoch.
314pub(crate) fn now_ms() -> u128 {
315    std::time::SystemTime::now()
316        .duration_since(std::time::UNIX_EPOCH)
317        .unwrap_or_default()
318        .as_millis()
319}
320
321// ---------------------------------------------------------------------------
322// Tests
323// ---------------------------------------------------------------------------
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328
329    #[test]
330    fn test_role_ordering() {
331        assert!(Role::Read < Role::Write);
332        assert!(Role::Write < Role::Admin);
333    }
334
335    #[test]
336    fn test_role_roundtrip() {
337        for role in [Role::Read, Role::Write, Role::Admin] {
338            assert_eq!(Role::from_str(role.as_str()), Some(role));
339        }
340        assert_eq!(Role::from_str("unknown"), None);
341    }
342
343    #[test]
344    fn test_role_permissions() {
345        assert!(Role::Read.can_read());
346        assert!(!Role::Read.can_write());
347        assert!(!Role::Read.can_admin());
348
349        assert!(Role::Write.can_read());
350        assert!(Role::Write.can_write());
351        assert!(!Role::Write.can_admin());
352
353        assert!(Role::Admin.can_read());
354        assert!(Role::Admin.can_write());
355        assert!(Role::Admin.can_admin());
356    }
357
358    #[test]
359    fn test_auth_config_default() {
360        let cfg = AuthConfig::default();
361        assert!(!cfg.enabled);
362        assert_eq!(cfg.session_ttl_secs, 3600);
363        assert!(!cfg.require_auth);
364        assert!(!cfg.auto_encrypt_storage);
365    }
366
367    #[test]
368    fn test_auth_error_display() {
369        let err = AuthError::UserExists("alice".into());
370        assert!(err.to_string().contains("alice"));
371
372        let err = AuthError::InvalidCredentials;
373        assert!(err.to_string().contains("invalid"));
374    }
375}