Skip to main content

allowthem_core/
permissions.rs

1use crate::db::Db;
2use crate::error::AuthError;
3use crate::event_sink::AuthEvent;
4use crate::handle::AllowThem;
5use crate::types::{Permission, PermissionId, PermissionName, RoleId, UserId};
6
7/// Map a SQLite UNIQUE constraint violation on `allowthem_permissions.name` to
8/// `AuthError::Conflict`. Other errors pass through as `AuthError::Database`.
9fn map_unique_violation(err: sqlx::Error) -> AuthError {
10    if let sqlx::Error::Database(ref db_err) = err {
11        let msg = db_err.message();
12        if msg.contains("UNIQUE constraint failed") && msg.contains("name") {
13            return AuthError::Conflict("permission name already exists".into());
14        }
15    }
16    AuthError::Database(err)
17}
18
19impl Db {
20    /// Create a permission with a unique name and optional description.
21    pub async fn create_permission(
22        &self,
23        name: &PermissionName,
24        description: Option<&str>,
25    ) -> Result<Permission, AuthError> {
26        let id = PermissionId::new();
27        sqlx::query_as::<_, Permission>(
28            "INSERT INTO allowthem_permissions (id, name, description) \
29             VALUES (?, ?, ?) \
30             RETURNING id, name, description, created_at",
31        )
32        .bind(id)
33        .bind(name)
34        .bind(description)
35        .fetch_one(self.pool())
36        .await
37        .map_err(map_unique_violation)
38    }
39
40    /// Get a permission by ID. Returns `None` if not found.
41    pub async fn get_permission(&self, id: &PermissionId) -> Result<Option<Permission>, AuthError> {
42        sqlx::query_as::<_, Permission>(
43            "SELECT id, name, description, created_at FROM allowthem_permissions WHERE id = ?",
44        )
45        .bind(*id)
46        .fetch_optional(self.pool())
47        .await
48        .map_err(AuthError::Database)
49    }
50
51    /// Get a permission by name. Returns `None` if not found.
52    pub async fn get_permission_by_name(
53        &self,
54        name: &PermissionName,
55    ) -> Result<Option<Permission>, AuthError> {
56        sqlx::query_as::<_, Permission>(
57            "SELECT id, name, description, created_at FROM allowthem_permissions WHERE name = ?",
58        )
59        .bind(name)
60        .fetch_optional(self.pool())
61        .await
62        .map_err(AuthError::Database)
63    }
64
65    /// List all permissions, ordered by creation time.
66    pub async fn list_permissions(&self) -> Result<Vec<Permission>, AuthError> {
67        sqlx::query_as::<_, Permission>(
68            "SELECT id, name, description, created_at \
69             FROM allowthem_permissions \
70             ORDER BY created_at",
71        )
72        .fetch_all(self.pool())
73        .await
74        .map_err(AuthError::Database)
75    }
76
77    /// Delete a permission by ID. Returns `true` if a row was deleted, `false` if not found.
78    ///
79    /// Cascades to `allowthem_role_permissions` and `allowthem_user_permissions`.
80    pub async fn delete_permission(&self, id: &PermissionId) -> Result<bool, AuthError> {
81        let result = sqlx::query("DELETE FROM allowthem_permissions WHERE id = ?")
82            .bind(*id)
83            .execute(self.pool())
84            .await
85            .map_err(AuthError::Database)?;
86        Ok(result.rows_affected() > 0)
87    }
88
89    /// Assign a permission to a role. Idempotent — silently succeeds if already assigned.
90    pub async fn assign_permission_to_role(
91        &self,
92        role_id: &RoleId,
93        permission_id: &PermissionId,
94    ) -> Result<(), AuthError> {
95        sqlx::query(
96            "INSERT OR IGNORE INTO allowthem_role_permissions (role_id, permission_id) \
97             VALUES (?, ?)",
98        )
99        .bind(*role_id)
100        .bind(*permission_id)
101        .execute(self.pool())
102        .await
103        .map_err(AuthError::Database)?;
104        Ok(())
105    }
106
107    /// Assign a permission directly to a user. Idempotent — silently succeeds if already assigned.
108    pub async fn assign_permission_to_user(
109        &self,
110        user_id: &UserId,
111        permission_id: &PermissionId,
112    ) -> Result<(), AuthError> {
113        sqlx::query(
114            "INSERT OR IGNORE INTO allowthem_user_permissions (user_id, permission_id) \
115             VALUES (?, ?)",
116        )
117        .bind(*user_id)
118        .bind(*permission_id)
119        .execute(self.pool())
120        .await
121        .map_err(AuthError::Database)?;
122        Ok(())
123    }
124
125    /// Unassign a permission from a role. Returns `true` if removed, `false` if not found.
126    pub async fn unassign_permission_from_role(
127        &self,
128        role_id: &RoleId,
129        permission_id: &PermissionId,
130    ) -> Result<bool, AuthError> {
131        let result = sqlx::query(
132            "DELETE FROM allowthem_role_permissions WHERE role_id = ? AND permission_id = ?",
133        )
134        .bind(*role_id)
135        .bind(*permission_id)
136        .execute(self.pool())
137        .await
138        .map_err(AuthError::Database)?;
139        Ok(result.rows_affected() > 0)
140    }
141
142    /// Unassign a permission from a user. Returns `true` if removed, `false` if not found.
143    pub async fn unassign_permission_from_user(
144        &self,
145        user_id: &UserId,
146        permission_id: &PermissionId,
147    ) -> Result<bool, AuthError> {
148        let result = sqlx::query(
149            "DELETE FROM allowthem_user_permissions WHERE user_id = ? AND permission_id = ?",
150        )
151        .bind(*user_id)
152        .bind(*permission_id)
153        .execute(self.pool())
154        .await
155        .map_err(AuthError::Database)?;
156        Ok(result.rows_affected() > 0)
157    }
158
159    /// Check whether a user has a permission by name via either path:
160    /// direct user assignment or any of the user's roles.
161    pub async fn has_permission(
162        &self,
163        user_id: &UserId,
164        perm_name: &PermissionName,
165    ) -> Result<bool, AuthError> {
166        let exists: bool = sqlx::query_scalar(
167            "SELECT EXISTS(
168               SELECT 1
169               FROM allowthem_user_permissions up
170               JOIN allowthem_permissions p ON p.id = up.permission_id
171               WHERE up.user_id = ? AND p.name = ?
172               UNION ALL
173               SELECT 1
174               FROM allowthem_role_permissions rp
175               JOIN allowthem_user_roles ur ON ur.role_id = rp.role_id
176               JOIN allowthem_permissions p ON p.id = rp.permission_id
177               WHERE ur.user_id = ? AND p.name = ?
178             )",
179        )
180        .bind(*user_id)
181        .bind(perm_name)
182        .bind(*user_id)
183        .bind(perm_name)
184        .fetch_one(self.pool())
185        .await
186        .map_err(AuthError::Database)?;
187        Ok(exists)
188    }
189
190    /// Return all permissions for a user — both directly assigned and via roles —
191    /// deduplicated and ordered by name.
192    pub async fn get_user_permissions(
193        &self,
194        user_id: &UserId,
195    ) -> Result<Vec<Permission>, AuthError> {
196        sqlx::query_as::<_, Permission>(
197            "SELECT DISTINCT p.id, p.name, p.description, p.created_at
198             FROM allowthem_permissions p
199             WHERE p.id IN (
200               SELECT permission_id FROM allowthem_user_permissions WHERE user_id = ?
201               UNION
202               SELECT rp.permission_id
203               FROM allowthem_role_permissions rp
204               JOIN allowthem_user_roles ur ON ur.role_id = rp.role_id
205               WHERE ur.user_id = ?
206             )
207             ORDER BY p.name",
208        )
209        .bind(*user_id)
210        .bind(*user_id)
211        .fetch_all(self.pool())
212        .await
213        .map_err(AuthError::Database)
214    }
215
216    /// List all permissions assigned to a role, ordered by name.
217    pub async fn list_role_permissions(
218        &self,
219        role_id: &RoleId,
220    ) -> Result<Vec<Permission>, AuthError> {
221        sqlx::query_as::<_, Permission>(
222            "SELECT p.id, p.name, p.description, p.created_at \
223             FROM allowthem_permissions p \
224             JOIN allowthem_role_permissions rp ON rp.permission_id = p.id \
225             WHERE rp.role_id = ? \
226             ORDER BY p.name",
227        )
228        .bind(*role_id)
229        .fetch_all(self.pool())
230        .await
231        .map_err(AuthError::Database)
232    }
233}
234
235impl AllowThem {
236    pub async fn assign_permission_to_user(
237        &self,
238        user_id: &UserId,
239        permission_id: &PermissionId,
240    ) -> Result<(), AuthError> {
241        self.db()
242            .assign_permission_to_user(user_id, permission_id)
243            .await?;
244        self.emit_event(AuthEvent::new(
245            "permission.assigned",
246            Some(*user_id),
247            serde_json::json!({ "user_id": user_id, "permission_id": permission_id }),
248        ))
249        .await;
250        Ok(())
251    }
252
253    pub async fn unassign_permission_from_user(
254        &self,
255        user_id: &UserId,
256        permission_id: &PermissionId,
257    ) -> Result<(), AuthError> {
258        self.db()
259            .unassign_permission_from_user(user_id, permission_id)
260            .await?;
261        self.emit_event(AuthEvent::new(
262            "permission.revoked",
263            Some(*user_id),
264            serde_json::json!({ "user_id": user_id, "permission_id": permission_id }),
265        ))
266        .await;
267        Ok(())
268    }
269}