1use crate::db::Db;
2use crate::error::AuthError;
3use crate::types::{Role, RoleId, RoleName, UserId};
4
5fn 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 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 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 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 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 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 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 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 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 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 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 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 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(); db.assign_role(&user.id, &roles[2].id).await.unwrap(); 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}