1mod 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#[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 pub fn new(user_id: UserId, permissions: PermissionSet) -> Self {
31 Self {
32 user_id,
33 permissions,
34 namespace: None,
35 }
36 }
37
38 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 pub fn has_permission(&self, permission: Permission, resource: ResourceType) -> bool {
49 self.permissions.has_permission(permission, resource)
50 }
51
52 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 pub fn system() -> Self {
66 Self {
67 user_id: UserId::system(),
68 permissions: PermissionSet::admin(),
69 namespace: None,
70 }
71 }
72
73 pub fn anonymous() -> Self {
75 Self {
76 user_id: UserId::anonymous(),
77 permissions: PermissionSet::read_only(),
78 namespace: None,
79 }
80 }
81}
82
83pub 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 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}