Skip to main content

oxidite_auth/
authorization.rs

1use oxidite_core::{OxiditeRequest, Result as OxiditeResult, Error};
2use oxidite_db::Database;
3use oxidite_db::sqlx::Row;
4use std::sync::Arc;
5use crate::rbac::{Role, Permission};
6
7/// Middleware to require a specific role
8pub struct RequireRole {
9    role_name: String,
10    db: Arc<dyn Database>,
11}
12
13impl RequireRole {
14    pub fn new(role_name: impl Into<String>, db: Arc<dyn Database>) -> Self {
15        Self {
16            role_name: role_name.into(),
17            db,
18        }
19    }
20    
21    pub async fn check(&self, req: &OxiditeRequest) -> OxiditeResult<bool> {
22        // Get user_id from request extensions (set by auth middleware)
23        let user_id = req.extensions()
24            .get::<i64>()
25            .ok_or_else(|| Error::Unauthorized("User not authenticated".to_string()))?;
26        
27        // Check if user has the required role
28        let query = oxidite_db::sqlx::query(
29            "SELECT 1 FROM roles r
30             INNER JOIN user_roles ur ON r.id = ur.role_id
31             WHERE ur.user_id = ? AND r.name = ?
32             LIMIT 1"
33        )
34            .bind(*user_id)
35            .bind(&self.role_name);
36
37        let row = self.db.fetch_one(query).await
38            .map_err(|_| Error::InternalServerError("Database error".to_string()))?;
39
40        Ok(row.is_some())
41    }
42}
43
44/// Middleware to require a specific permission
45pub struct RequirePermission {
46    permission_name: String,
47    db: Arc<dyn Database>,
48}
49
50impl RequirePermission {
51    pub fn new(permission_name: impl Into<String>, db: Arc<dyn Database>) -> Self {
52        Self {
53            permission_name: permission_name.into(),
54            db,
55        }
56    }
57    
58    pub async fn check(&self, req: &OxiditeRequest) -> OxiditeResult<bool> {
59        // Get user_id from request extensions
60        let user_id = req.extensions()
61            .get::<i64>()
62            .ok_or_else(|| Error::Unauthorized("User not authenticated".to_string()))?;
63        
64        // Check if user has the required permission through any of their roles
65        let query = oxidite_db::sqlx::query(
66            "SELECT 1 FROM permissions p
67             INNER JOIN role_permissions rp ON p.id = rp.permission_id
68             INNER JOIN user_roles ur ON rp.role_id = ur.role_id
69             WHERE ur.user_id = ? AND p.name = ?
70             LIMIT 1"
71        )
72            .bind(*user_id)
73            .bind(&self.permission_name);
74
75        let row = self.db.fetch_one(query).await
76            .map_err(|_| Error::InternalServerError("Database error".to_string()))?;
77
78        Ok(row.is_some())
79    }
80}
81
82/// Utility functions for authorization checks
83pub struct AuthorizationService {
84    db: Arc<dyn Database>,
85}
86
87impl AuthorizationService {
88    pub fn new(db: Arc<dyn Database>) -> Self {
89        Self { db }
90    }
91    
92    /// Check if user has a specific role
93    pub async fn user_has_role(&self, user_id: i64, role_name: &str) -> oxidite_db::Result<bool> {
94        let query = oxidite_db::sqlx::query(
95            "SELECT COUNT(*) as count FROM user_roles ur
96             INNER JOIN roles r ON ur.role_id = r.id
97             WHERE ur.user_id = ? AND r.name = ?"
98        )
99            .bind(user_id)
100            .bind(role_name);
101
102        let row = self.db.fetch_one(query).await?;
103        Ok(row
104            .and_then(|r| r.try_get::<i64, _>("count").ok())
105            .unwrap_or(0) > 0)
106    }
107    
108    /// Check if user has a specific permission
109    pub async fn user_can(&self, user_id: i64, permission_name: &str) -> oxidite_db::Result<bool> {
110        let query = oxidite_db::sqlx::query(
111            "SELECT COUNT(*) as count FROM permissions p
112             INNER JOIN role_permissions rp ON p.id = rp.permission_id
113             INNER JOIN user_roles ur ON rp.role_id = ur.role_id
114             WHERE ur.user_id = ? AND p.name = ?"
115        )
116            .bind(user_id)
117            .bind(permission_name);
118
119        let row = self.db.fetch_one(query).await?;
120        Ok(row
121            .and_then(|r| r.try_get::<i64, _>("count").ok())
122            .unwrap_or(0) > 0)
123    }
124    
125    /// Get all roles for a user
126    pub async fn user_roles(&self, user_id: i64) -> oxidite_db::Result<Vec<Role>> {
127        use oxidite_db::sqlx::FromRow;
128        
129        let query = oxidite_db::sqlx::query(
130            "SELECT r.* FROM roles r
131             INNER JOIN user_roles ur ON r.id = ur.role_id
132             WHERE ur.user_id = ?"
133        )
134            .bind(user_id);
135
136        let rows = self.db.fetch_all(query).await?;
137        let mut roles = Vec::new();
138        
139        for row in rows {
140            roles.push(Role::from_row(&row)?);
141        }
142        
143        Ok(roles)
144    }
145    
146    /// Get all permissions for a user (through their roles)
147    pub async fn user_permissions(&self, user_id: i64) -> oxidite_db::Result<Vec<Permission>> {
148        use oxidite_db::sqlx::FromRow;
149        
150        let query = oxidite_db::sqlx::query(
151            "SELECT DISTINCT p.* FROM permissions p
152             INNER JOIN role_permissions rp ON p.id = rp.permission_id
153             INNER JOIN user_roles ur ON rp.role_id = ur.role_id
154             WHERE ur.user_id = ?"
155        )
156            .bind(user_id);
157
158        let rows = self.db.fetch_all(query).await?;
159        let mut permissions = Vec::new();
160        
161        for row in rows {
162            permissions.push(Permission::from_row(&row)?);
163        }
164        
165        Ok(permissions)
166    }
167    
168    /// Assign role to user
169    pub async fn assign_role(&self, user_id: i64, role_id: i64) -> oxidite_db::Result<()> {
170        // Use SELECT before INSERT for backend portability.
171        let exists_query = oxidite_db::sqlx::query(
172            "SELECT 1 FROM user_roles WHERE user_id = ? AND role_id = ? LIMIT 1"
173        )
174            .bind(user_id)
175            .bind(role_id);
176
177        if self.db.fetch_one(exists_query).await?.is_none() {
178            let insert_query = oxidite_db::sqlx::query(
179                "INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)"
180            )
181                .bind(user_id)
182                .bind(role_id);
183            self.db.execute_query(insert_query).await?;
184        }
185        Ok(())
186    }
187    
188    /// Remove role from user
189    pub async fn remove_role(&self, user_id: i64, role_id: i64) -> oxidite_db::Result<()> {
190        let query = oxidite_db::sqlx::query(
191            "DELETE FROM user_roles WHERE user_id = ? AND role_id = ?"
192        )
193            .bind(user_id)
194            .bind(role_id);
195        self.db.execute_query(query).await?;
196        Ok(())
197    }
198}