Skip to main content

allowthem_core/
roles.rs

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