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