Skip to main content

allowthem_core/
roles.rs

1use crate::db::Db;
2use crate::error::AuthError;
3use crate::event_sink::AuthEvent;
4use crate::handle::AllowThem;
5use crate::types::{Role, RoleId, RoleName, UserId};
6
7/// Map a SQLite UNIQUE constraint violation on `allowthem_roles.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("role name already exists".into());
14        }
15    }
16    AuthError::Database(err)
17}
18
19impl Db {
20    /// Create a role with a unique name and optional description.
21    pub async fn create_role(
22        &self,
23        name: &RoleName,
24        description: Option<&str>,
25    ) -> Result<Role, AuthError> {
26        let id = RoleId::new();
27        sqlx::query_as::<_, Role>(
28            "INSERT INTO allowthem_roles (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 role by ID. Returns `None` if not found.
41    pub async fn get_role(&self, id: &RoleId) -> Result<Option<Role>, AuthError> {
42        sqlx::query_as::<_, Role>(
43            "SELECT id, name, description, created_at FROM allowthem_roles WHERE id = ?",
44        )
45        .bind(*id)
46        .fetch_optional(self.pool())
47        .await
48        .map_err(AuthError::Database)
49    }
50
51    /// Get a role by name. Returns `None` if not found.
52    pub async fn get_role_by_name(&self, name: &RoleName) -> Result<Option<Role>, AuthError> {
53        sqlx::query_as::<_, Role>(
54            "SELECT id, name, description, created_at FROM allowthem_roles WHERE name = ?",
55        )
56        .bind(name)
57        .fetch_optional(self.pool())
58        .await
59        .map_err(AuthError::Database)
60    }
61
62    /// List all roles, ordered by creation time.
63    pub async fn list_roles(&self) -> Result<Vec<Role>, AuthError> {
64        sqlx::query_as::<_, Role>(
65            "SELECT id, name, description, created_at FROM allowthem_roles ORDER BY created_at",
66        )
67        .fetch_all(self.pool())
68        .await
69        .map_err(AuthError::Database)
70    }
71
72    /// Update a role's name and description. Returns the updated role.
73    ///
74    /// Returns `AuthError::NotFound` if no role with `id` exists.
75    /// Returns `AuthError::Conflict` if `name` is already taken by another role.
76    pub async fn update_role(
77        &self,
78        id: &RoleId,
79        name: &RoleName,
80        description: Option<&str>,
81    ) -> Result<Role, AuthError> {
82        sqlx::query_as::<_, Role>(
83            "UPDATE allowthem_roles SET name = ?1, description = ?2 WHERE id = ?3 \
84             RETURNING id, name, description, created_at",
85        )
86        .bind(name)
87        .bind(description)
88        .bind(*id)
89        .fetch_optional(self.pool())
90        .await
91        .map_err(map_unique_violation)?
92        .ok_or(AuthError::NotFound)
93    }
94
95    /// Delete a role by ID. Returns `true` if a row was deleted, `false` if not found.
96    ///
97    /// Cascades to `allowthem_user_roles` and `allowthem_role_permissions`.
98    pub async fn delete_role(&self, id: &RoleId) -> Result<bool, AuthError> {
99        let result = sqlx::query("DELETE FROM allowthem_roles WHERE id = ?")
100            .bind(*id)
101            .execute(self.pool())
102            .await
103            .map_err(AuthError::Database)?;
104        Ok(result.rows_affected() > 0)
105    }
106
107    /// Assign a role to a user. Silently succeeds if already assigned (idempotent).
108    pub async fn assign_role(&self, user_id: &UserId, role_id: &RoleId) -> Result<(), AuthError> {
109        sqlx::query("INSERT OR IGNORE INTO allowthem_user_roles (user_id, role_id) VALUES (?, ?)")
110            .bind(*user_id)
111            .bind(*role_id)
112            .execute(self.pool())
113            .await
114            .map_err(AuthError::Database)?;
115        Ok(())
116    }
117
118    /// Unassign a role from a user. Returns `true` if removed, `false` if the assignment
119    /// did not exist.
120    pub async fn unassign_role(
121        &self,
122        user_id: &UserId,
123        role_id: &RoleId,
124    ) -> Result<bool, AuthError> {
125        let result =
126            sqlx::query("DELETE FROM allowthem_user_roles WHERE user_id = ? AND role_id = ?")
127                .bind(*user_id)
128                .bind(*role_id)
129                .execute(self.pool())
130                .await
131                .map_err(AuthError::Database)?;
132        Ok(result.rows_affected() > 0)
133    }
134
135    /// Check whether a user has a specific role by name.
136    pub async fn has_role(
137        &self,
138        user_id: &UserId,
139        role_name: &RoleName,
140    ) -> Result<bool, AuthError> {
141        let count: i64 = sqlx::query_scalar(
142            "SELECT COUNT(*) \
143             FROM allowthem_user_roles ur \
144             JOIN allowthem_roles r ON r.id = ur.role_id \
145             WHERE ur.user_id = ? AND r.name = ?",
146        )
147        .bind(*user_id)
148        .bind(role_name)
149        .fetch_one(self.pool())
150        .await
151        .map_err(AuthError::Database)?;
152        Ok(count > 0)
153    }
154
155    /// Return all roles assigned to a user, ordered by creation time.
156    pub async fn get_user_roles(&self, user_id: &UserId) -> Result<Vec<Role>, AuthError> {
157        sqlx::query_as::<_, Role>(
158            "SELECT r.id, r.name, r.description, r.created_at \
159             FROM allowthem_roles r \
160             JOIN allowthem_user_roles ur ON ur.role_id = r.id \
161             WHERE ur.user_id = ? \
162             ORDER BY r.created_at",
163        )
164        .bind(*user_id)
165        .fetch_all(self.pool())
166        .await
167        .map_err(AuthError::Database)
168    }
169
170    /// Create each named role if it does not already exist.
171    ///
172    /// Returns roles in the same order as `names`. Idempotent: existing roles
173    /// are fetched, not re-created. Duplicates within `names` are allowed; each
174    /// name is resolved independently.
175    pub async fn bootstrap_roles(&self, names: &[&str]) -> Result<Vec<Role>, AuthError> {
176        let mut roles = Vec::with_capacity(names.len());
177        for &name in names {
178            let rn = RoleName::new(name);
179            let role = match self.get_role_by_name(&rn).await? {
180                Some(r) => r,
181                None => self.create_role(&rn, None).await?,
182            };
183            roles.push(role);
184        }
185        Ok(roles)
186    }
187
188    /// Return the name of the first role in `hierarchy` that the user holds.
189    ///
190    /// `hierarchy[0]` is treated as the highest role. Returns `None` if the user
191    /// holds none of the listed roles. An empty `hierarchy` always returns `None`.
192    pub async fn resolve_highest_role(
193        &self,
194        user_id: &UserId,
195        hierarchy: &[&str],
196    ) -> Result<Option<String>, AuthError> {
197        for &name in hierarchy {
198            let rn = RoleName::new(name);
199            if self.has_role(user_id, &rn).await? {
200                return Ok(Some(name.to_owned()));
201            }
202        }
203        Ok(None)
204    }
205}
206
207impl AllowThem {
208    pub async fn assign_role(&self, user_id: &UserId, role_id: &RoleId) -> Result<(), AuthError> {
209        self.db().assign_role(user_id, role_id).await?;
210        self.emit_event(AuthEvent::new(
211            "role.assigned",
212            Some(*user_id),
213            serde_json::json!({ "user_id": user_id, "role_id": role_id }),
214        ))
215        .await;
216        Ok(())
217    }
218
219    pub async fn unassign_role(&self, user_id: &UserId, role_id: &RoleId) -> Result<(), AuthError> {
220        self.db().unassign_role(user_id, role_id).await?;
221        self.emit_event(AuthEvent::new(
222            "role.revoked",
223            Some(*user_id),
224            serde_json::json!({ "user_id": user_id, "role_id": role_id }),
225        ))
226        .await;
227        Ok(())
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234    use crate::handle::{AllowThem, AllowThemBuilder};
235    use crate::types::Email;
236
237    async fn setup() -> AllowThem {
238        AllowThemBuilder::new("sqlite::memory:")
239            .cookie_secure(false)
240            .build()
241            .await
242            .unwrap()
243    }
244
245    #[tokio::test]
246    async fn update_role_changes_name_and_description() {
247        let ath = setup().await;
248        let db = ath.db();
249        let rn = RoleName::new("old-name");
250        let role = db.create_role(&rn, Some("old desc")).await.unwrap();
251        let new_name = RoleName::new("new-name");
252        let updated = db
253            .update_role(&role.id, &new_name, Some("new desc"))
254            .await
255            .unwrap();
256        assert_eq!(updated.name.as_str(), "new-name");
257        assert_eq!(updated.description.as_deref(), Some("new desc"));
258        assert_eq!(updated.id, role.id);
259    }
260
261    #[tokio::test]
262    async fn update_role_not_found_returns_error() {
263        let ath = setup().await;
264        let db = ath.db();
265        let missing = RoleId::new();
266        let name = RoleName::new("x");
267        let err = db.update_role(&missing, &name, None).await.unwrap_err();
268        assert!(matches!(err, AuthError::NotFound));
269    }
270
271    #[tokio::test]
272    async fn bootstrap_roles_creates_missing_roles() {
273        let ath = setup().await;
274        let db = ath.db();
275        let roles = db.bootstrap_roles(&["admin", "editor"]).await.unwrap();
276        assert_eq!(roles.len(), 2);
277        assert_eq!(roles[0].name.as_str(), "admin");
278        assert_eq!(roles[1].name.as_str(), "editor");
279    }
280
281    #[tokio::test]
282    async fn bootstrap_roles_idempotent() {
283        let ath = setup().await;
284        let db = ath.db();
285        let first = db.bootstrap_roles(&["admin", "editor"]).await.unwrap();
286        let second = db.bootstrap_roles(&["admin", "editor"]).await.unwrap();
287        assert_eq!(first[0].id, second[0].id);
288        assert_eq!(first[1].id, second[1].id);
289    }
290
291    #[tokio::test]
292    async fn bootstrap_roles_returns_in_input_order() {
293        let ath = setup().await;
294        let db = ath.db();
295        let roles = db
296            .bootstrap_roles(&["viewer", "admin", "editor"])
297            .await
298            .unwrap();
299        assert_eq!(roles[0].name.as_str(), "viewer");
300        assert_eq!(roles[1].name.as_str(), "admin");
301        assert_eq!(roles[2].name.as_str(), "editor");
302    }
303
304    #[tokio::test]
305    async fn bootstrap_roles_mixed_existing_and_new() {
306        let ath = setup().await;
307        let db = ath.db();
308        let rn = RoleName::new("admin");
309        db.create_role(&rn, None).await.unwrap();
310        let roles = db.bootstrap_roles(&["admin", "viewer"]).await.unwrap();
311        assert_eq!(roles.len(), 2);
312        assert_eq!(roles[0].name.as_str(), "admin");
313        assert_eq!(roles[1].name.as_str(), "viewer");
314    }
315
316    #[tokio::test]
317    async fn bootstrap_roles_empty_slice_returns_empty_vec() {
318        let ath = setup().await;
319        let db = ath.db();
320        let roles = db.bootstrap_roles(&[]).await.unwrap();
321        assert!(roles.is_empty());
322    }
323
324    #[tokio::test]
325    async fn resolve_highest_role_returns_first_match() {
326        let ath = setup().await;
327        let db = ath.db();
328        let email = Email::new("user@example.com".into()).unwrap();
329        let user = db
330            .create_user(email, "password123", None, None)
331            .await
332            .unwrap();
333        let roles = db
334            .bootstrap_roles(&["admin", "editor", "viewer"])
335            .await
336            .unwrap();
337        db.assign_role(&user.id, &roles[1].id).await.unwrap(); // editor
338        db.assign_role(&user.id, &roles[2].id).await.unwrap(); // viewer
339        let result = db
340            .resolve_highest_role(&user.id, &["admin", "editor", "viewer"])
341            .await
342            .unwrap();
343        assert_eq!(result, Some("editor".to_owned()));
344    }
345
346    #[tokio::test]
347    async fn resolve_highest_role_returns_none_when_no_roles() {
348        let ath = setup().await;
349        let db = ath.db();
350        let email = Email::new("noroles@example.com".into()).unwrap();
351        let user = db
352            .create_user(email, "password123", None, None)
353            .await
354            .unwrap();
355        let result = db
356            .resolve_highest_role(&user.id, &["admin", "editor"])
357            .await
358            .unwrap();
359        assert!(result.is_none());
360    }
361
362    #[tokio::test]
363    async fn resolve_highest_role_returns_none_for_empty_hierarchy() {
364        let ath = setup().await;
365        let db = ath.db();
366        let email = Email::new("emptyhier@example.com".into()).unwrap();
367        let user = db
368            .create_user(email, "password123", None, None)
369            .await
370            .unwrap();
371        let result = db.resolve_highest_role(&user.id, &[]).await.unwrap();
372        assert!(result.is_none());
373    }
374
375    #[tokio::test]
376    async fn resolve_highest_role_returns_highest_when_user_has_all() {
377        let ath = setup().await;
378        let db = ath.db();
379        let email = Email::new("allroles@example.com".into()).unwrap();
380        let user = db
381            .create_user(email, "password123", None, None)
382            .await
383            .unwrap();
384        let roles = db
385            .bootstrap_roles(&["admin", "editor", "viewer"])
386            .await
387            .unwrap();
388        for role in &roles {
389            db.assign_role(&user.id, &role.id).await.unwrap();
390        }
391        let result = db
392            .resolve_highest_role(&user.id, &["admin", "editor", "viewer"])
393            .await
394            .unwrap();
395        assert_eq!(result, Some("admin".to_owned()));
396    }
397
398    #[tokio::test]
399    async fn resolve_highest_role_only_considers_listed_roles() {
400        let ath = setup().await;
401        let db = ath.db();
402        let email = Email::new("unlisted@example.com".into()).unwrap();
403        let user = db
404            .create_user(email, "password123", None, None)
405            .await
406            .unwrap();
407        let rn = RoleName::new("superuser");
408        let role = db.create_role(&rn, None).await.unwrap();
409        db.assign_role(&user.id, &role.id).await.unwrap();
410        let result = db
411            .resolve_highest_role(&user.id, &["admin", "editor"])
412            .await
413            .unwrap();
414        assert!(result.is_none());
415    }
416}