Skip to main content

allowthem_core/
permissions.rs

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