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    /// Delete a role by ID. Returns `true` if a row was deleted, `false` if not found.
71    ///
72    /// Cascades to `allowthem_user_roles` and `allowthem_role_permissions`.
73    pub async fn delete_role(&self, id: &RoleId) -> Result<bool, AuthError> {
74        let result = sqlx::query("DELETE FROM allowthem_roles WHERE id = ?")
75            .bind(*id)
76            .execute(self.pool())
77            .await
78            .map_err(AuthError::Database)?;
79        Ok(result.rows_affected() > 0)
80    }
81
82    /// Assign a role to a user. Silently succeeds if already assigned (idempotent).
83    pub async fn assign_role(&self, user_id: &UserId, role_id: &RoleId) -> Result<(), AuthError> {
84        sqlx::query("INSERT OR IGNORE INTO allowthem_user_roles (user_id, role_id) VALUES (?, ?)")
85            .bind(*user_id)
86            .bind(*role_id)
87            .execute(self.pool())
88            .await
89            .map_err(AuthError::Database)?;
90        Ok(())
91    }
92
93    /// Unassign a role from a user. Returns `true` if removed, `false` if the assignment
94    /// did not exist.
95    pub async fn unassign_role(
96        &self,
97        user_id: &UserId,
98        role_id: &RoleId,
99    ) -> Result<bool, AuthError> {
100        let result =
101            sqlx::query("DELETE FROM allowthem_user_roles WHERE user_id = ? AND role_id = ?")
102                .bind(*user_id)
103                .bind(*role_id)
104                .execute(self.pool())
105                .await
106                .map_err(AuthError::Database)?;
107        Ok(result.rows_affected() > 0)
108    }
109
110    /// Check whether a user has a specific role by name.
111    pub async fn has_role(
112        &self,
113        user_id: &UserId,
114        role_name: &RoleName,
115    ) -> Result<bool, AuthError> {
116        let count: i64 = sqlx::query_scalar(
117            "SELECT COUNT(*) \
118             FROM allowthem_user_roles ur \
119             JOIN allowthem_roles r ON r.id = ur.role_id \
120             WHERE ur.user_id = ? AND r.name = ?",
121        )
122        .bind(*user_id)
123        .bind(role_name)
124        .fetch_one(self.pool())
125        .await
126        .map_err(AuthError::Database)?;
127        Ok(count > 0)
128    }
129
130    /// Return all roles assigned to a user, ordered by creation time.
131    pub async fn get_user_roles(&self, user_id: &UserId) -> Result<Vec<Role>, AuthError> {
132        sqlx::query_as::<_, Role>(
133            "SELECT r.id, r.name, r.description, r.created_at \
134             FROM allowthem_roles r \
135             JOIN allowthem_user_roles ur ON ur.role_id = r.id \
136             WHERE ur.user_id = ? \
137             ORDER BY r.created_at",
138        )
139        .bind(*user_id)
140        .fetch_all(self.pool())
141        .await
142        .map_err(AuthError::Database)
143    }
144
145    /// Create each named role if it does not already exist.
146    ///
147    /// Returns roles in the same order as `names`. Idempotent: existing roles
148    /// are fetched, not re-created. Duplicates within `names` are allowed; each
149    /// name is resolved independently.
150    pub async fn bootstrap_roles(&self, names: &[&str]) -> Result<Vec<Role>, AuthError> {
151        let mut roles = Vec::with_capacity(names.len());
152        for &name in names {
153            let rn = RoleName::new(name);
154            let role = match self.get_role_by_name(&rn).await? {
155                Some(r) => r,
156                None => self.create_role(&rn, None).await?,
157            };
158            roles.push(role);
159        }
160        Ok(roles)
161    }
162
163    /// Return the name of the first role in `hierarchy` that the user holds.
164    ///
165    /// `hierarchy[0]` is treated as the highest role. Returns `None` if the user
166    /// holds none of the listed roles. An empty `hierarchy` always returns `None`.
167    pub async fn resolve_highest_role(
168        &self,
169        user_id: &UserId,
170        hierarchy: &[&str],
171    ) -> Result<Option<String>, AuthError> {
172        for &name in hierarchy {
173            let rn = RoleName::new(name);
174            if self.has_role(user_id, &rn).await? {
175                return Ok(Some(name.to_owned()));
176            }
177        }
178        Ok(None)
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use crate::handle::{AllowThem, AllowThemBuilder};
186    use crate::types::Email;
187
188    async fn setup() -> AllowThem {
189        AllowThemBuilder::new("sqlite::memory:")
190            .cookie_secure(false)
191            .build()
192            .await
193            .unwrap()
194    }
195
196    #[tokio::test]
197    async fn bootstrap_roles_creates_missing_roles() {
198        let ath = setup().await;
199        let db = ath.db();
200        let roles = db.bootstrap_roles(&["admin", "editor"]).await.unwrap();
201        assert_eq!(roles.len(), 2);
202        assert_eq!(roles[0].name.as_str(), "admin");
203        assert_eq!(roles[1].name.as_str(), "editor");
204    }
205
206    #[tokio::test]
207    async fn bootstrap_roles_idempotent() {
208        let ath = setup().await;
209        let db = ath.db();
210        let first = db.bootstrap_roles(&["admin", "editor"]).await.unwrap();
211        let second = db.bootstrap_roles(&["admin", "editor"]).await.unwrap();
212        assert_eq!(first[0].id, second[0].id);
213        assert_eq!(first[1].id, second[1].id);
214    }
215
216    #[tokio::test]
217    async fn bootstrap_roles_returns_in_input_order() {
218        let ath = setup().await;
219        let db = ath.db();
220        let roles = db
221            .bootstrap_roles(&["viewer", "admin", "editor"])
222            .await
223            .unwrap();
224        assert_eq!(roles[0].name.as_str(), "viewer");
225        assert_eq!(roles[1].name.as_str(), "admin");
226        assert_eq!(roles[2].name.as_str(), "editor");
227    }
228
229    #[tokio::test]
230    async fn bootstrap_roles_mixed_existing_and_new() {
231        let ath = setup().await;
232        let db = ath.db();
233        let rn = RoleName::new("admin");
234        db.create_role(&rn, None).await.unwrap();
235        let roles = db.bootstrap_roles(&["admin", "viewer"]).await.unwrap();
236        assert_eq!(roles.len(), 2);
237        assert_eq!(roles[0].name.as_str(), "admin");
238        assert_eq!(roles[1].name.as_str(), "viewer");
239    }
240
241    #[tokio::test]
242    async fn bootstrap_roles_empty_slice_returns_empty_vec() {
243        let ath = setup().await;
244        let db = ath.db();
245        let roles = db.bootstrap_roles(&[]).await.unwrap();
246        assert!(roles.is_empty());
247    }
248
249    #[tokio::test]
250    async fn resolve_highest_role_returns_first_match() {
251        let ath = setup().await;
252        let db = ath.db();
253        let email = Email::new("user@example.com".into()).unwrap();
254        let user = db
255            .create_user(email, "password123", None, None)
256            .await
257            .unwrap();
258        let roles = db
259            .bootstrap_roles(&["admin", "editor", "viewer"])
260            .await
261            .unwrap();
262        db.assign_role(&user.id, &roles[1].id).await.unwrap(); // editor
263        db.assign_role(&user.id, &roles[2].id).await.unwrap(); // viewer
264        let result = db
265            .resolve_highest_role(&user.id, &["admin", "editor", "viewer"])
266            .await
267            .unwrap();
268        assert_eq!(result, Some("editor".to_owned()));
269    }
270
271    #[tokio::test]
272    async fn resolve_highest_role_returns_none_when_no_roles() {
273        let ath = setup().await;
274        let db = ath.db();
275        let email = Email::new("noroles@example.com".into()).unwrap();
276        let user = db
277            .create_user(email, "password123", None, None)
278            .await
279            .unwrap();
280        let result = db
281            .resolve_highest_role(&user.id, &["admin", "editor"])
282            .await
283            .unwrap();
284        assert!(result.is_none());
285    }
286
287    #[tokio::test]
288    async fn resolve_highest_role_returns_none_for_empty_hierarchy() {
289        let ath = setup().await;
290        let db = ath.db();
291        let email = Email::new("emptyhier@example.com".into()).unwrap();
292        let user = db
293            .create_user(email, "password123", None, None)
294            .await
295            .unwrap();
296        let result = db.resolve_highest_role(&user.id, &[]).await.unwrap();
297        assert!(result.is_none());
298    }
299
300    #[tokio::test]
301    async fn resolve_highest_role_returns_highest_when_user_has_all() {
302        let ath = setup().await;
303        let db = ath.db();
304        let email = Email::new("allroles@example.com".into()).unwrap();
305        let user = db
306            .create_user(email, "password123", None, None)
307            .await
308            .unwrap();
309        let roles = db
310            .bootstrap_roles(&["admin", "editor", "viewer"])
311            .await
312            .unwrap();
313        for role in &roles {
314            db.assign_role(&user.id, &role.id).await.unwrap();
315        }
316        let result = db
317            .resolve_highest_role(&user.id, &["admin", "editor", "viewer"])
318            .await
319            .unwrap();
320        assert_eq!(result, Some("admin".to_owned()));
321    }
322
323    #[tokio::test]
324    async fn resolve_highest_role_only_considers_listed_roles() {
325        let ath = setup().await;
326        let db = ath.db();
327        let email = Email::new("unlisted@example.com".into()).unwrap();
328        let user = db
329            .create_user(email, "password123", None, None)
330            .await
331            .unwrap();
332        let rn = RoleName::new("superuser");
333        let role = db.create_role(&rn, None).await.unwrap();
334        db.assign_role(&user.id, &role.id).await.unwrap();
335        let result = db
336            .resolve_highest_role(&user.id, &["admin", "editor"])
337            .await
338            .unwrap();
339        assert!(result.is_none());
340    }
341}