role_system/
database.rs

1//! Database storage backend for the role system.
2
3#[cfg(feature = "database")]
4use crate::{
5    error::{Error, Result},
6    role::Role,
7    storage::Storage,
8};
9
10#[cfg(not(feature = "database"))]
11use crate::error::Error;
12
13#[cfg(feature = "database")]
14use sqlx::{PgPool, Row};
15
16#[cfg(feature = "database")]
17/// PostgreSQL storage backend for the role system.
18pub struct DatabaseStorage {
19    pool: PgPool,
20    table_prefix: String,
21}
22
23#[cfg(feature = "database")]
24impl DatabaseStorage {
25    /// Create a new database storage backend.
26    pub async fn new(database_url: &str) -> Result<Self> {
27        let pool = PgPool::connect(database_url)
28            .await
29            .map_err(|e| Error::Storage(format!("Database connection failed: {}", e)))?;
30
31        let storage = Self {
32            pool,
33            table_prefix: "rbac_".to_string(),
34        };
35
36        // Initialize database schema
37        storage.initialize_schema().await?;
38
39        Ok(storage)
40    }
41
42    /// Create a new database storage backend with custom table prefix.
43    pub async fn new_with_prefix(database_url: &str, table_prefix: String) -> Result<Self> {
44        let pool = PgPool::connect(database_url)
45            .await
46            .map_err(|e| Error::Storage(format!("Database connection failed: {}", e)))?;
47
48        let storage = Self { pool, table_prefix };
49
50        // Initialize database schema
51        storage.initialize_schema().await?;
52
53        Ok(storage)
54    }
55
56    /// Initialize the database schema.
57    async fn initialize_schema(&self) -> Result<()> {
58        let roles_table = format!("{}roles", self.table_prefix);
59        let role_permissions_table = format!("{}role_permissions", self.table_prefix);
60        let subjects_table = format!("{}subjects", self.table_prefix);
61        let subject_roles_table = format!("{}subject_roles", self.table_prefix);
62
63        // Create roles table
64        let create_roles = format!(
65            r#"
66            CREATE TABLE IF NOT EXISTS {} (
67                name VARCHAR(255) PRIMARY KEY,
68                description TEXT,
69                active BOOLEAN NOT NULL DEFAULT true,
70                created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
71                updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
72            )
73            "#,
74            roles_table
75        );
76
77        // Create role permissions table
78        let create_role_permissions = format!(
79            r#"
80            CREATE TABLE IF NOT EXISTS {} (
81                id SERIAL PRIMARY KEY,
82                role_name VARCHAR(255) NOT NULL REFERENCES {}(name) ON DELETE CASCADE,
83                action VARCHAR(255) NOT NULL,
84                resource_type VARCHAR(255) NOT NULL,
85                instance_id VARCHAR(255),
86                condition_json JSONB,
87                created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
88                UNIQUE(role_name, action, resource_type, instance_id)
89            )
90            "#,
91            role_permissions_table, roles_table
92        );
93
94        // Create subjects table
95        let create_subjects = format!(
96            r#"
97            CREATE TABLE IF NOT EXISTS {} (
98                id VARCHAR(255) PRIMARY KEY,
99                subject_type VARCHAR(50) NOT NULL,
100                display_name VARCHAR(255),
101                metadata JSONB,
102                created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
103                updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
104            )
105            "#,
106            subjects_table
107        );
108
109        // Create subject roles table
110        let create_subject_roles = format!(
111            r#"
112            CREATE TABLE IF NOT EXISTS {} (
113                id SERIAL PRIMARY KEY,
114                subject_id VARCHAR(255) NOT NULL REFERENCES {}(id) ON DELETE CASCADE,
115                role_name VARCHAR(255) NOT NULL REFERENCES {}(name) ON DELETE CASCADE,
116                assigned_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
117                assigned_by VARCHAR(255),
118                UNIQUE(subject_id, role_name)
119            )
120            "#,
121            subject_roles_table, subjects_table, roles_table
122        );
123
124        // Execute schema creation
125        sqlx::query(&create_roles)
126            .execute(&self.pool)
127            .await
128            .map_err(|e| Error::Storage(format!("Failed to create roles table: {}", e)))?;
129
130        sqlx::query(&create_role_permissions)
131            .execute(&self.pool)
132            .await
133            .map_err(|e| {
134                Error::Storage(format!("Failed to create role_permissions table: {}", e))
135            })?;
136
137        sqlx::query(&create_subjects)
138            .execute(&self.pool)
139            .await
140            .map_err(|e| Error::Storage(format!("Failed to create subjects table: {}", e)))?;
141
142        sqlx::query(&create_subject_roles)
143            .execute(&self.pool)
144            .await
145            .map_err(|e| Error::Storage(format!("Failed to create subject_roles table: {}", e)))?;
146
147        Ok(())
148    }
149
150    /// Get the roles table name.
151    fn roles_table(&self) -> String {
152        format!("{}roles", self.table_prefix)
153    }
154
155    /// Get the role permissions table name.
156    fn role_permissions_table(&self) -> String {
157        format!("{}role_permissions", self.table_prefix)
158    }
159
160    /// Get the subjects table name.
161    #[allow(dead_code)]
162    fn subjects_table(&self) -> String {
163        format!("{}subjects", self.table_prefix)
164    }
165
166    /// Get the subject roles table name.
167    #[allow(dead_code)]
168    fn subject_roles_table(&self) -> String {
169        format!("{}subject_roles", self.table_prefix)
170    }
171
172    /// Store role permissions in the database.
173    async fn store_role_permissions(&self, role_name: &str, role: &Role) -> Result<()> {
174        let table = self.role_permissions_table();
175
176        // Delete existing permissions for this role
177        let delete_query = format!("DELETE FROM {} WHERE role_name = $1", table);
178        sqlx::query(&delete_query)
179            .bind(role_name)
180            .execute(&self.pool)
181            .await
182            .map_err(|e| Error::Storage(format!("Failed to delete old permissions: {}", e)))?;
183
184        // Insert new permissions
185        for permission in role.permissions().permissions() {
186            let insert_query = format!(
187                "INSERT INTO {} (role_name, action, resource_type, instance_id) VALUES ($1, $2, $3, $4)",
188                table
189            );
190
191            sqlx::query(&insert_query)
192                .bind(role_name)
193                .bind(permission.action())
194                .bind(permission.resource_type())
195                .bind(permission.instance())
196                .execute(&self.pool)
197                .await
198                .map_err(|e| Error::Storage(format!("Failed to insert permission: {}", e)))?;
199        }
200
201        Ok(())
202    }
203
204    /// Load role permissions from the database.
205    async fn load_role_permissions(
206        &self,
207        role_name: &str,
208    ) -> Result<Vec<crate::permission::Permission>> {
209        let table = self.role_permissions_table();
210        let query = format!(
211            "SELECT action, resource_type, instance_id FROM {} WHERE role_name = $1",
212            table
213        );
214
215        let rows = sqlx::query(&query)
216            .bind(role_name)
217            .fetch_all(&self.pool)
218            .await
219            .map_err(|e| Error::Storage(format!("Failed to load permissions: {}", e)))?;
220
221        let mut permissions = Vec::new();
222        for row in rows {
223            let action: String = row.get("action");
224            let resource_type: String = row.get("resource_type");
225            let instance_id: Option<String> = row.get("instance_id");
226
227            let permission = if let Some(instance) = instance_id {
228                crate::permission::Permission::with_instance(action, resource_type, instance)
229            } else {
230                crate::permission::Permission::new(action, resource_type)
231            };
232
233            permissions.push(permission);
234        }
235
236        Ok(permissions)
237    }
238
239    /// Store a role in the database (async implementation).
240    pub async fn store_role_async(&mut self, role: Role) -> Result<()> {
241        let roles_table = self.roles_table();
242
243        // Start a transaction
244        let mut tx = self
245            .pool
246            .begin()
247            .await
248            .map_err(|e| Error::Storage(format!("Failed to start transaction: {}", e)))?;
249
250        // Insert or update role
251        let upsert_query = format!(
252            r#"
253            INSERT INTO {} (name, description, active, updated_at)
254            VALUES ($1, $2, $3, NOW())
255            ON CONFLICT (name) DO UPDATE SET
256                description = EXCLUDED.description,
257                active = EXCLUDED.active,
258                updated_at = NOW()
259            "#,
260            roles_table
261        );
262
263        sqlx::query(&upsert_query)
264            .bind(role.name())
265            .bind(role.description())
266            .bind(role.is_active())
267            .execute(&mut *tx)
268            .await
269            .map_err(|e| Error::Storage(format!("Failed to store role: {}", e)))?;
270
271        // Store permissions
272        self.store_role_permissions(role.name(), &role).await?;
273
274        // Commit transaction
275        tx.commit()
276            .await
277            .map_err(|e| Error::Storage(format!("Failed to commit transaction: {}", e)))?;
278
279        Ok(())
280    }
281
282    /// Get a role from the database (async implementation).
283    pub async fn get_role_async(&self, name: &str) -> Result<Option<Role>> {
284        let roles_table = self.roles_table();
285        let query = format!(
286            "SELECT name, description, active FROM {} WHERE name = $1 AND active = true",
287            roles_table
288        );
289
290        let row = sqlx::query(&query)
291            .bind(name)
292            .fetch_optional(&self.pool)
293            .await
294            .map_err(|e| Error::Storage(format!("Failed to get role: {}", e)))?;
295
296        if let Some(row) = row {
297            let role_name: String = row.get("name");
298            let description: Option<String> = row.get("description");
299            let is_active: bool = row.get("active");
300
301            let mut role = Role::new(role_name);
302            if let Some(desc) = description {
303                role = role.with_description(desc);
304            }
305            if !is_active {
306                role = role.deactivate();
307            }
308
309            // Load permissions
310            let permissions = self.load_role_permissions(name).await?;
311            for permission in permissions {
312                role = role.add_permission(permission);
313            }
314
315            Ok(Some(role))
316        } else {
317            Ok(None)
318        }
319    }
320
321    /// Delete a role from the database (async implementation).
322    pub async fn delete_role_async(&mut self, name: &str) -> Result<bool> {
323        let roles_table = self.roles_table();
324
325        // Soft delete by setting active = false
326        let update_query = format!(
327            "UPDATE {} SET active = false, updated_at = NOW() WHERE name = $1 AND active = true",
328            roles_table
329        );
330
331        let result = sqlx::query(&update_query)
332            .bind(name)
333            .execute(&self.pool)
334            .await
335            .map_err(|e| Error::Storage(format!("Failed to delete role: {}", e)))?;
336
337        Ok(result.rows_affected() > 0)
338    }
339
340    /// Check if a role exists in the database (async implementation).
341    pub async fn role_exists_async(&self, name: &str) -> Result<bool> {
342        let roles_table = self.roles_table();
343        let query = format!(
344            "SELECT 1 FROM {} WHERE name = $1 AND active = true LIMIT 1",
345            roles_table
346        );
347
348        let row = sqlx::query(&query)
349            .bind(name)
350            .fetch_optional(&self.pool)
351            .await
352            .map_err(|e| Error::Storage(format!("Failed to check role existence: {}", e)))?;
353
354        Ok(row.is_some())
355    }
356
357    /// List all roles in the database (async implementation).
358    pub async fn list_roles_async(&self) -> Result<Vec<String>> {
359        let roles_table = self.roles_table();
360        let query = format!(
361            "SELECT name FROM {} WHERE active = true ORDER BY name",
362            roles_table
363        );
364
365        let rows = sqlx::query(&query)
366            .fetch_all(&self.pool)
367            .await
368            .map_err(|e| Error::Storage(format!("Failed to list roles: {}", e)))?;
369
370        let roles: Vec<String> = rows.into_iter().map(|row| row.get("name")).collect();
371        Ok(roles)
372    }
373
374    /// Get the number of roles in the database (async implementation).
375    pub async fn role_count_async(&self) -> Result<usize> {
376        let roles_table = self.roles_table();
377        let query = format!(
378            "SELECT COUNT(*) as count FROM {} WHERE active = true",
379            roles_table
380        );
381
382        let row = sqlx::query(&query)
383            .fetch_one(&self.pool)
384            .await
385            .map_err(|e| Error::Storage(format!("Failed to count roles: {}", e)))?;
386
387        let count: i64 = row.get("count");
388        Ok(count as usize)
389    }
390}
391
392#[cfg(feature = "database")]
393impl Storage for DatabaseStorage {
394    fn store_role(&mut self, _role: Role) -> Result<()> {
395        Err(Error::Storage(
396            "Use store_role_async for database storage".to_string(),
397        ))
398    }
399
400    fn get_role(&self, _name: &str) -> Result<Option<Role>> {
401        Err(Error::Storage(
402            "Use get_role_async for database storage".to_string(),
403        ))
404    }
405
406    fn role_exists(&self, _name: &str) -> Result<bool> {
407        Err(Error::Storage(
408            "Use role_exists_async for database storage".to_string(),
409        ))
410    }
411
412    fn delete_role(&mut self, _name: &str) -> Result<bool> {
413        Err(Error::Storage(
414            "Use delete_role_async for database storage".to_string(),
415        ))
416    }
417
418    fn list_roles(&self) -> Result<Vec<String>> {
419        Err(Error::Storage(
420            "Use list_roles_async for database storage".to_string(),
421        ))
422    }
423
424    fn update_role(&mut self, role: Role) -> Result<()> {
425        // For database storage, update is the same as store
426        self.store_role(role)
427    }
428
429    fn role_count(&self) -> usize {
430        // For database storage, this should use the async method
431        // Return 0 as a placeholder since we can't use async here
432        0
433    }
434}
435
436#[cfg(feature = "database")]
437impl DatabaseStorage {
438    /// Perform a health check on the database connection.
439    pub async fn health_check(&self) -> Result<bool> {
440        let result = sqlx::query("SELECT 1").fetch_optional(&self.pool).await;
441
442        match result {
443            Ok(Some(_)) => Ok(true),
444            Ok(None) => Ok(false),
445            Err(_) => Ok(false),
446        }
447    }
448
449    /// Get database connection statistics.
450    pub fn connection_stats(&self) -> DatabaseStats {
451        DatabaseStats {
452            active_connections: self.pool.size() as usize,
453            idle_connections: self.pool.num_idle(),
454            max_connections: self.pool.options().get_max_connections() as usize,
455        }
456    }
457
458    /// Close all database connections.
459    pub async fn close(&self) {
460        self.pool.close().await;
461    }
462}
463
464#[cfg(feature = "database")]
465/// Database connection statistics.
466#[derive(Debug, Clone)]
467pub struct DatabaseStats {
468    pub active_connections: usize,
469    pub idle_connections: usize,
470    pub max_connections: usize,
471}
472
473#[cfg(not(feature = "database"))]
474/// Placeholder when database feature is not enabled.
475pub struct DatabaseStorage;
476
477#[cfg(not(feature = "database"))]
478impl DatabaseStorage {
479    /// Create a new database storage (disabled).
480    pub async fn new(_database_url: &str) -> std::result::Result<Self, Error> {
481        Err(Error::Storage(
482            "Database storage not available. Enable 'database' feature.".to_string(),
483        ))
484    }
485}
486
487#[cfg(all(test, feature = "database"))]
488mod tests {
489    use super::*;
490    use crate::{permission::Permission, role::Role};
491
492    // Note: These tests require a running PostgreSQL database
493    // and the DATABASE_URL environment variable to be set
494
495    async fn setup_test_db() -> DatabaseStorage {
496        let database_url = std::env::var("DATABASE_URL")
497            .unwrap_or_else(|_| "postgresql://postgres:password@localhost/rbac_test".to_string());
498
499        DatabaseStorage::new(&database_url)
500            .await
501            .expect("Failed to setup test database")
502    }
503
504    #[tokio::test]
505    #[ignore] // Ignore by default since it requires a database
506    async fn test_database_storage_role_operations() {
507        let mut storage = setup_test_db().await;
508
509        let role = Role::new("test_role")
510            .with_description("Test role for database storage")
511            .add_permission(Permission::new("read", "documents"))
512            .add_permission(Permission::new("write", "documents"));
513
514        // Store role
515        storage.store_role_async(role.clone()).await.unwrap();
516
517        // Check existence
518        assert!(storage.role_exists_async("test_role").await.unwrap());
519
520        // Get role
521        let retrieved = storage.get_role_async("test_role").await.unwrap().unwrap();
522        assert_eq!(retrieved.name(), "test_role");
523        assert_eq!(
524            retrieved.description(),
525            Some("Test role for database storage")
526        );
527        assert_eq!(retrieved.permissions().permissions().len(), 2);
528
529        // List roles
530        let roles = storage.list_roles_async().await.unwrap();
531        assert!(roles.contains(&"test_role".to_string()));
532
533        // Delete role
534        assert!(storage.delete_role_async("test_role").await.unwrap());
535        assert!(!storage.role_exists_async("test_role").await.unwrap());
536    }
537
538    #[tokio::test]
539    #[ignore] // Ignore by default since it requires a database
540    async fn test_database_health_check() {
541        let storage = setup_test_db().await;
542        assert!(storage.health_check().await.unwrap());
543    }
544}