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