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