1use std::collections::HashMap;
4use std::sync::Arc;
5
6use rainier_database::{Model, Repository};
7use rainier_orm::sea_query::Value;
8use rainier_support::Result;
9
10use crate::abilities::Abilities;
11use crate::hashing::Hasher;
12
13pub trait Authenticatable: Send + Sync + 'static {
19 fn auth_identifier(&self) -> String;
21
22 fn auth_password_hash(&self) -> Option<&str>;
25
26 fn auth_username_column() -> &'static str
28 where
29 Self: Sized,
30 {
31 "email"
32 }
33
34 fn auth_token_column() -> &'static str
36 where
37 Self: Sized,
38 {
39 "api_token"
40 }
41}
42
43#[derive(Debug, Clone, Default, PartialEq, Eq)]
49pub struct Credentials {
50 values: HashMap<String, String>,
51}
52
53impl Credentials {
54 pub fn new() -> Self {
56 Self::default()
57 }
58
59 pub fn password(username: impl Into<String>, password: impl Into<String>) -> Self {
61 Self::new().with("email", username).with("password", password)
62 }
63
64 pub fn with(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
66 self.values.insert(key.into(), value.into());
67 self
68 }
69
70 pub fn get(&self, key: &str) -> Option<&str> {
72 self.values.get(key).map(String::as_str)
73 }
74
75 pub fn password_value(&self) -> Option<&str> {
77 self.get("password")
78 }
79
80 pub fn lookup_fields(&self) -> Vec<(&str, &str)> {
86 self.values
87 .iter()
88 .filter(|(key, _)| key.as_str() != "password")
89 .map(|(key, value)| (key.as_str(), value.as_str()))
90 .collect()
91 }
92
93 pub fn is_empty(&self) -> bool {
95 self.lookup_fields().is_empty()
96 }
97}
98
99#[async_trait::async_trait]
105pub trait UserProvider<U: Authenticatable>: Send + Sync + 'static {
106 async fn retrieve_by_id(&self, id: &str) -> Result<Option<U>>;
108
109 async fn retrieve_by_credentials(&self, credentials: &Credentials) -> Result<Option<U>>;
111
112 async fn validate_credentials(&self, user: &U, credentials: &Credentials) -> Result<bool>;
114
115 async fn retrieve_by_token(&self, token: &str) -> Result<Option<U>> {
117 let _ = token;
118 Ok(None)
119 }
120
121 async fn retrieve_abilities_by_token(&self, token: &str) -> Result<Abilities> {
132 let _ = token;
133 Ok(Abilities::everything())
134 }
135}
136
137pub struct RepositoryUserProvider<U: Model + Authenticatable> {
139 users: Arc<dyn Repository<U>>,
140 hasher: Arc<dyn Hasher>,
141}
142
143impl<U: Model + Authenticatable> RepositoryUserProvider<U> {
144 pub fn new(users: Arc<dyn Repository<U>>, hasher: Arc<dyn Hasher>) -> Self {
146 Self { users, hasher }
147 }
148
149 pub fn repository(&self) -> &Arc<dyn Repository<U>> {
151 &self.users
152 }
153}
154
155#[async_trait::async_trait]
156impl<U> UserProvider<U> for RepositoryUserProvider<U>
157where
158 U: Model + Authenticatable,
159{
160 async fn retrieve_by_id(&self, id: &str) -> Result<Option<U>> {
161 let key: Value = match id.parse::<i64>() {
166 Ok(numeric) => numeric.into(),
167 Err(_) => id.into(),
168 };
169 self.users.find(key).await
170 }
171
172 async fn retrieve_by_credentials(&self, credentials: &Credentials) -> Result<Option<U>> {
173 let fields = credentials.lookup_fields();
174 if fields.is_empty() {
175 return Ok(None);
178 }
179
180 let mut criteria = rainier_database::Criteria::new();
181 for (column, value) in fields {
182 criteria = criteria.where_eq(column, value);
183 }
184 self.users.first_matching(criteria).await
185 }
186
187 async fn validate_credentials(&self, user: &U, credentials: &Credentials) -> Result<bool> {
188 let (Some(submitted), Some(stored)) =
189 (credentials.password_value(), user.auth_password_hash())
190 else {
191 return Ok(false);
194 };
195 Ok(self.hasher.verify(submitted, stored))
196 }
197
198 async fn retrieve_by_token(&self, token: &str) -> Result<Option<U>> {
199 if token.is_empty() {
200 return Ok(None);
201 }
202 self.users.first_by(U::auth_token_column(), token.into()).await
203 }
204}
205
206impl<U: Model + Authenticatable> std::fmt::Debug for RepositoryUserProvider<U> {
207 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208 f.debug_struct("RepositoryUserProvider").field("model", &U::model_name()).finish()
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use crate::hashing::Argon2Hasher;
216 use rainier_database::testing::{fake_database, MemoryConnection};
217 use rainier_database::{EntityRepository, OwnedRow};
218 use rainier_orm::Dialect;
219
220 #[derive(rainier_orm::Entity, Clone, Debug, PartialEq)]
221 #[orm(table = "users")]
222 struct User {
223 #[orm(pk, auto_increment)]
224 id: u64,
225 #[orm(unique)]
226 email: String,
227 password: String,
228 api_token: Option<String>,
229 }
230
231 impl Model for User {}
232
233 impl Authenticatable for User {
234 fn auth_identifier(&self) -> String {
235 self.id.to_string()
236 }
237 fn auth_password_hash(&self) -> Option<&str> {
238 Some(&self.password)
239 }
240 }
241
242 fn hasher() -> Arc<dyn Hasher> {
243 Arc::new(Argon2Hasher::insecure_for_tests())
244 }
245
246 fn user_row(hash: &str) -> OwnedRow {
247 OwnedRow::new()
248 .with("id", 1_u64)
249 .with("email", "ada@example.com")
250 .with("password", hash)
251 .with("api_token", "tok_123")
252 }
253
254 fn provider(
255 connection: MemoryConnection,
256 ) -> (RepositoryUserProvider<User>, Arc<MemoryConnection>) {
257 let (db, handle) = fake_database(connection);
258 let users: Arc<dyn Repository<User>> = Arc::new(EntityRepository::<User>::new(db));
259 (RepositoryUserProvider::new(users, hasher()), handle)
260 }
261
262 #[test]
263 fn credentials_exclude_the_password_from_lookups() {
264 let credentials = Credentials::password("ada@example.com", "secret");
265
266 assert_eq!(credentials.password_value(), Some("secret"));
267 assert_eq!(credentials.lookup_fields(), vec![("email", "ada@example.com")]);
268 assert!(!credentials.is_empty());
269 }
270
271 #[test]
272 fn credentials_can_carry_extra_lookup_fields() {
273 let credentials = Credentials::password("ada@example.com", "secret").with("tenant_id", "7");
274
275 let mut fields = credentials.lookup_fields();
276 fields.sort();
277 assert_eq!(fields, vec![("email", "ada@example.com"), ("tenant_id", "7")]);
278 }
279
280 #[test]
281 fn credentials_with_only_a_password_have_nothing_to_look_up_by() {
282 let credentials = Credentials::new().with("password", "secret");
283 assert!(credentials.is_empty());
284 }
285
286 #[tokio::test]
287 async fn retrieves_a_user_by_credentials() {
288 let (provider, connection) =
289 provider(MemoryConnection::new(Dialect::Sqlite).returning([user_row("$argon2id$x")]));
290
291 let found = provider
292 .retrieve_by_credentials(&Credentials::password("ada@example.com", "secret"))
293 .await
294 .unwrap();
295
296 assert_eq!(found.unwrap().email, "ada@example.com");
297
298 let sql = connection.last_statement().unwrap();
302 let (_, filters) = sql.split_once("WHERE").expect("the lookup should be filtered");
303 assert!(filters.contains("email"), "{sql}");
304 assert!(!filters.contains("password"), "the password must never be queried: {sql}");
305 assert_eq!(connection.bindings()[0][0], "ada@example.com".into());
306 }
307
308 #[tokio::test]
309 async fn empty_credentials_never_match_a_user() {
310 let (provider, connection) =
313 provider(MemoryConnection::new(Dialect::Sqlite).returning([user_row("$argon2id$x")]));
314
315 assert!(provider.retrieve_by_credentials(&Credentials::new()).await.unwrap().is_none());
316 assert_eq!(connection.statement_count(), 0, "and it should not even query");
317 }
318
319 #[tokio::test]
320 async fn validates_a_password_against_the_stored_hash() {
321 let hasher = Argon2Hasher::insecure_for_tests();
322 let stored = hasher.hash("secret").unwrap();
323 let (provider, _) = provider(MemoryConnection::new(Dialect::Sqlite));
324
325 let user =
326 User { id: 1, email: "ada@example.com".into(), password: stored, api_token: None };
327
328 assert!(provider
329 .validate_credentials(&user, &Credentials::password("ada@example.com", "secret"))
330 .await
331 .unwrap());
332
333 assert!(!provider
334 .validate_credentials(&user, &Credentials::password("ada@example.com", "wrong"))
335 .await
336 .unwrap());
337 }
338
339 #[tokio::test]
340 async fn a_missing_password_never_validates() {
341 let (provider, _) = provider(MemoryConnection::new(Dialect::Sqlite));
342 let user = User {
343 id: 1,
344 email: "ada@example.com".into(),
345 password: String::new(),
346 api_token: None,
347 };
348
349 let no_password = Credentials::new().with("email", "ada@example.com");
351 assert!(!provider.validate_credentials(&user, &no_password).await.unwrap());
352 }
353
354 #[tokio::test]
355 async fn retrieves_by_id_binding_a_numeric_key_as_a_number() {
356 let (provider, connection) =
357 provider(MemoryConnection::new(Dialect::Sqlite).returning([user_row("h")]));
358
359 assert!(provider.retrieve_by_id("1").await.unwrap().is_some());
360 assert_eq!(connection.bindings()[0][0], 1_i64.into(), "not the string \"1\"");
361 }
362
363 #[tokio::test]
364 async fn retrieves_by_token() {
365 let (provider, connection) =
366 provider(MemoryConnection::new(Dialect::Sqlite).returning([user_row("h")]));
367
368 assert!(provider.retrieve_by_token("tok_123").await.unwrap().is_some());
369 assert!(connection.last_statement().unwrap().contains("api_token"));
370 }
371
372 #[tokio::test]
373 async fn an_empty_token_never_matches() {
374 let (provider, connection) =
375 provider(MemoryConnection::new(Dialect::Sqlite).returning([user_row("h")]));
376
377 assert!(provider.retrieve_by_token("").await.unwrap().is_none());
378 assert_eq!(connection.statement_count(), 0);
379 }
380}