Skip to main content

engram/auth/
mod.rs

1//! Multi-User Authentication & Authorization (RML-886)
2//!
3//! Provides:
4//! - User management with API keys
5//! - Permission-based access control
6//! - Memory ownership and sharing
7//! - Namespace isolation
8
9mod permissions;
10mod tokens;
11mod users;
12
13pub use permissions::{Permission, PermissionSet, ResourceType};
14pub use tokens::{ApiKey, ApiKeyManager, TokenClaims};
15pub use users::{User, UserId, UserManager};
16
17use crate::error::{EngramError, Result};
18use rusqlite::Connection;
19
20/// Authentication context for a request
21#[derive(Debug, Clone)]
22pub struct AuthContext {
23    pub user_id: UserId,
24    pub permissions: PermissionSet,
25    pub namespace: Option<String>,
26}
27
28impl AuthContext {
29    /// Create a new auth context
30    pub fn new(user_id: UserId, permissions: PermissionSet) -> Self {
31        Self {
32            user_id,
33            permissions,
34            namespace: None,
35        }
36    }
37
38    /// Create auth context with namespace
39    pub fn with_namespace(user_id: UserId, permissions: PermissionSet, namespace: String) -> Self {
40        Self {
41            user_id,
42            permissions,
43            namespace: Some(namespace),
44        }
45    }
46
47    /// Check if user has permission
48    pub fn has_permission(&self, permission: Permission, resource: ResourceType) -> bool {
49        self.permissions.has_permission(permission, resource)
50    }
51
52    /// Require permission or return error
53    pub fn require_permission(&self, permission: Permission, resource: ResourceType) -> Result<()> {
54        if self.has_permission(permission, resource) {
55            Ok(())
56        } else {
57            Err(EngramError::Unauthorized(format!(
58                "Missing permission {:?} for {:?}",
59                permission, resource
60            )))
61        }
62    }
63
64    /// Create a system-level context with full permissions
65    pub fn system() -> Self {
66        Self {
67            user_id: UserId::system(),
68            permissions: PermissionSet::admin(),
69            namespace: None,
70        }
71    }
72
73    /// Create an anonymous context with read-only public access
74    pub fn anonymous() -> Self {
75        Self {
76            user_id: UserId::anonymous(),
77            permissions: PermissionSet::read_only(),
78            namespace: None,
79        }
80    }
81}
82
83/// Initialize auth tables in database
84pub fn init_auth_tables(conn: &Connection) -> Result<()> {
85    conn.execute_batch(
86        r#"
87        -- Users table
88        CREATE TABLE IF NOT EXISTS users (
89            id TEXT PRIMARY KEY,
90            username TEXT UNIQUE NOT NULL,
91            display_name TEXT,
92            email TEXT,
93            password_hash TEXT,
94            is_active INTEGER NOT NULL DEFAULT 1,
95            is_admin INTEGER NOT NULL DEFAULT 0,
96            created_at TEXT NOT NULL DEFAULT (datetime('now')),
97            updated_at TEXT NOT NULL DEFAULT (datetime('now'))
98        );
99
100        -- API keys table
101        CREATE TABLE IF NOT EXISTS api_keys (
102            id TEXT PRIMARY KEY,
103            user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
104            key_hash TEXT UNIQUE NOT NULL,
105            key_prefix TEXT NOT NULL,
106            name TEXT NOT NULL,
107            permissions TEXT NOT NULL DEFAULT '[]',
108            namespace TEXT,
109            expires_at TEXT,
110            last_used_at TEXT,
111            is_active INTEGER NOT NULL DEFAULT 1,
112            created_at TEXT NOT NULL DEFAULT (datetime('now'))
113        );
114
115        -- User namespaces (for multi-tenant isolation)
116        CREATE TABLE IF NOT EXISTS namespaces (
117            id TEXT PRIMARY KEY,
118            name TEXT UNIQUE NOT NULL,
119            owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
120            is_public INTEGER NOT NULL DEFAULT 0,
121            created_at TEXT NOT NULL DEFAULT (datetime('now'))
122        );
123
124        -- Namespace memberships (shared access)
125        CREATE TABLE IF NOT EXISTS namespace_members (
126            namespace_id TEXT NOT NULL REFERENCES namespaces(id) ON DELETE CASCADE,
127            user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
128            role TEXT NOT NULL DEFAULT 'reader',
129            created_at TEXT NOT NULL DEFAULT (datetime('now')),
130            PRIMARY KEY (namespace_id, user_id)
131        );
132
133        -- Memory ownership (links memories to users/namespaces)
134        CREATE TABLE IF NOT EXISTS memory_ownership (
135            memory_id TEXT NOT NULL,
136            user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
137            namespace_id TEXT REFERENCES namespaces(id) ON DELETE CASCADE,
138            is_public INTEGER NOT NULL DEFAULT 0,
139            created_at TEXT NOT NULL DEFAULT (datetime('now')),
140            PRIMARY KEY (memory_id)
141        );
142
143        -- Indexes
144        CREATE INDEX IF NOT EXISTS idx_api_keys_user ON api_keys(user_id);
145        CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(key_prefix);
146        CREATE INDEX IF NOT EXISTS idx_namespace_members_user ON namespace_members(user_id);
147        CREATE INDEX IF NOT EXISTS idx_memory_ownership_user ON memory_ownership(user_id);
148        CREATE INDEX IF NOT EXISTS idx_memory_ownership_namespace ON memory_ownership(namespace_id);
149        "#,
150    )?;
151    Ok(())
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use rusqlite::Connection;
158
159    fn setup_db() -> Connection {
160        let conn = Connection::open_in_memory().unwrap();
161        init_auth_tables(&conn).unwrap();
162        conn
163    }
164
165    #[test]
166    fn test_auth_context_permissions() {
167        let ctx = AuthContext::new(
168            UserId::new(),
169            PermissionSet::from_permissions(vec![
170                (Permission::Read, ResourceType::Memory),
171                (Permission::Write, ResourceType::Memory),
172            ]),
173        );
174
175        assert!(ctx.has_permission(Permission::Read, ResourceType::Memory));
176        assert!(ctx.has_permission(Permission::Write, ResourceType::Memory));
177        assert!(!ctx.has_permission(Permission::Delete, ResourceType::Memory));
178        assert!(!ctx.has_permission(Permission::Read, ResourceType::User));
179    }
180
181    #[test]
182    fn test_system_context() {
183        let ctx = AuthContext::system();
184        assert!(ctx.has_permission(Permission::Admin, ResourceType::System));
185        assert!(ctx.has_permission(Permission::Delete, ResourceType::Memory));
186    }
187
188    #[test]
189    fn test_anonymous_context() {
190        let ctx = AuthContext::anonymous();
191        assert!(ctx.has_permission(Permission::Read, ResourceType::Memory));
192        assert!(!ctx.has_permission(Permission::Write, ResourceType::Memory));
193    }
194
195    #[test]
196    fn test_init_auth_tables() {
197        let conn = setup_db();
198
199        // Verify tables exist
200        let tables: Vec<String> = conn
201            .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%user%' OR name LIKE '%api%' OR name LIKE '%namespace%' OR name LIKE '%ownership%'")
202            .unwrap()
203            .query_map([], |row| row.get(0))
204            .unwrap()
205            .filter_map(|r| r.ok())
206            .collect();
207
208        assert!(tables.contains(&"users".to_string()));
209        assert!(tables.contains(&"api_keys".to_string()));
210        assert!(tables.contains(&"namespaces".to_string()));
211    }
212}