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
7fn 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 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 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 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 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 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 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 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 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 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 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 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 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(); db.assign_role(&user.id, &roles[2].id).await.unwrap(); 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}