1use crate::{
7 MyError,
8 config::config,
9 db::user::{TUser, find_active_user},
10 lrs::{DB, role::Role},
11};
12use base64::{Engine, prelude::BASE64_STANDARD};
13use chrono::{DateTime, Utc};
14use core::fmt;
15use lru::LruCache;
16use rocket::{
17 Request, State,
18 http::{Status, hyper::header},
19 request::{FromRequest, Outcome},
20};
21use serde::{Deserialize, Serialize};
22use serde_with::{FromInto, serde_as};
23use std::sync::OnceLock;
24use tokio::sync::Mutex;
25use tracing::{debug, error, info};
26use xapi_data::Agent;
27
28#[serde_as]
30#[derive(Debug, Deserialize, Serialize)]
31pub struct User {
32 pub id: i32,
34 pub enabled: bool,
36 pub email: String,
38 #[serde_as(as = "FromInto<u16>")]
40 pub role: Role,
41 pub manager_id: i32,
43 pub created: DateTime<Utc>,
45 pub updated: DateTime<Utc>,
47}
48
49impl Default for User {
50 fn default() -> Self {
53 Self {
54 id: 0,
55 email: config().root_email.clone(),
56 enabled: true,
57 role: Role::Root,
58 manager_id: 0,
59 created: Utc::now(),
60 updated: Utc::now(),
61 }
62 }
63}
64
65impl fmt::Display for User {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 match (&self.role, &self.enabled) {
68 (Role::Guest, _) => write!(f, "guest <{}>", self.email),
69 (Role::User, true) => write!(f, "xapi+ <{}>", self.email),
70 (Role::User, false) => write!(f, "xapi- <{}>", self.email),
71 (Role::AuthUser, true) => write!(f, "auth+ <{}>", self.email),
72 (Role::AuthUser, false) => write!(f, "auth- <{}>", self.email),
73 (Role::Admin, true) => write!(f, "admin+ <{}>", self.email),
74 (Role::Admin, false) => write!(f, "admin- <{}>", self.email),
75 (Role::Root, _) => write!(f, "root"),
76 }
77 }
78}
79
80impl From<TUser> for User {
81 fn from(row: TUser) -> Self {
83 User {
84 id: row.id,
85 email: row.email,
86 enabled: row.enabled,
87 role: Role::from(row.role),
88 manager_id: row.manager_id,
89 created: row.created,
90 updated: row.updated,
91 }
92 }
93}
94
95#[derive(Debug)]
97struct CachedUser {
98 id: i32,
99 email: String,
100 enabled: bool,
101 role: Role,
102 manager_id: i32,
103}
104
105impl From<&CachedUser> for User {
106 fn from(value: &CachedUser) -> Self {
108 User {
109 id: value.id,
110 email: value.email.to_owned(),
111 enabled: value.enabled,
112 role: value.role,
113 manager_id: value.manager_id,
114 ..Default::default()
115 }
116 }
117}
118
119impl From<&User> for CachedUser {
120 fn from(user: &User) -> Self {
122 CachedUser {
123 id: user.id,
124 email: user.email.clone(),
125 enabled: user.enabled,
126 role: user.role,
127 manager_id: user.manager_id,
128 }
129 }
130}
131
132impl User {
133 pub(crate) fn credentials_from(email: &str, password: &str) -> u32 {
135 let basic = format!("{email}:{password}");
136 let encoded = BASE64_STANDARD.encode(basic);
137 fxhash::hash32(&encoded)
138 }
139
140 pub(crate) async fn clear_cache() {
142 let mut cache = cached_users().lock().await;
143 cache.clear();
144 info!("Cache cleared")
145 }
146
147 #[cfg(test)]
149 pub(crate) fn with_email(email: &str) -> Self {
150 Self {
151 email: email.to_owned(),
152 ..Default::default()
153 }
154 }
155
156 pub(crate) fn as_agent(&self) -> Agent {
158 Agent::builder().mbox(&self.email).unwrap().build().unwrap()
159 }
160
161 pub(crate) fn authority(&self) -> Agent {
163 match config().mode {
164 crate::Mode::User => self.as_agent(),
166 _ => Agent::builder()
169 .mbox(&config().root_email)
170 .unwrap()
171 .build()
172 .unwrap(),
173 }
174 }
175
176 fn check_is_enabled(&self) -> Result<(), MyError> {
179 if !self.enabled {
180 Err(MyError::HTTP {
181 status: Status::Forbidden,
182 info: format!("User {self} is NOT active").into(),
183 })
184 } else {
185 Ok(())
186 }
187 }
188
189 pub(crate) fn can_use_xapi(&self) -> Result<(), MyError> {
190 self.check_is_enabled()?;
192 if !matches!(self.role, Role::Root | Role::User | Role::AuthUser) {
193 Err(MyError::HTTP {
194 status: Status::Forbidden,
195 info: format!("User {self} is NOT authorized to use xAPI").into(),
196 })
197 } else {
198 Ok(())
199 }
200 }
201
202 pub(crate) fn can_authorize_statement(&self) -> Result<(), MyError> {
203 self.check_is_enabled()?;
204 if !matches!(self.role, Role::Root | Role::AuthUser) {
205 Err(MyError::HTTP {
206 status: Status::Forbidden,
207 info: format!("User {self} is NOT allowed to authorize Statements").into(),
208 })
209 } else {
210 Ok(())
211 }
212 }
213
214 pub(crate) fn can_use_verbs(&self) -> Result<(), MyError> {
215 self.check_is_enabled()?;
216 if !matches!(self.role, Role::Root | Role::Admin) {
217 Err(MyError::HTTP {
218 status: Status::Forbidden,
219 info: format!("User {self} is NOT authorized to use verbs").into(),
220 })
221 } else {
222 Ok(())
223 }
224 }
225
226 pub(crate) fn can_manage_users(&self) -> Result<(), MyError> {
227 self.check_is_enabled()?;
228 if !matches!(self.role, Role::Root | Role::Admin) {
229 Err(MyError::HTTP {
230 status: Status::Forbidden,
231 info: format!("User {self} is NOT authorized to manage users").into(),
232 })
233 } else {
234 Ok(())
235 }
236 }
237
238 pub(crate) fn is_root(&self) -> bool {
239 matches!(self.role, Role::Root)
240 }
241
242 pub(crate) fn is_admin(&self) -> bool {
243 matches!(self.role, Role::Admin)
244 }
245
246 pub(crate) async fn uncache(&self) {
248 let mut cache = cached_users().lock().await;
249 for (&k, v) in cache.iter() {
250 if v.id == self.id {
251 cache.pop(&k);
252 info!("Evicted user #{}", self.id);
253 break;
254 }
255 }
256 }
257}
258
259static CACHED_USERS: OnceLock<Mutex<LruCache<u32, CachedUser>>> = OnceLock::new();
261fn cached_users() -> &'static Mutex<LruCache<u32, CachedUser>> {
262 CACHED_USERS.get_or_init(|| Mutex::new(LruCache::new(config().user_cache_len)))
263}
264
265async fn find_cached_user(key: &u32) -> Option<User> {
266 let mut cache = cached_users().lock().await;
267 cache.get(key).map(User::from)
268}
269
270async fn cache_user(key: u32, user: &User) {
271 let mut cache = cached_users().lock().await;
272 cache.put(key, CachedUser::from(user));
273}
274
275#[rocket::async_trait]
276impl<'r> FromRequest<'r> for User {
277 type Error = MyError;
278
279 async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
280 match config().mode {
282 crate::Mode::Legacy => Outcome::Success(User::default()),
283 _ => {
284 match req.headers().get_one(header::AUTHORIZATION.as_str()) {
287 Some(basic_auth) => {
288 let trimmed = basic_auth.trim();
289 if trimmed[..6].to_lowercase() != *"basic " {
290 let msg = "Invalid Authorization header";
291 error!("Failed: {}", msg);
292 Outcome::Error((Status::BadRequest, MyError::Runtime(msg.into())))
293 } else {
294 let token = trimmed[6..].trim();
297 let credentials = fxhash::hash32(token);
298 match find_cached_user(&credentials).await {
300 Some(x) => Outcome::Success(x),
301 None => {
302 debug!("Cache miss...");
305 match req.guard::<&State<DB>>().await {
306 Outcome::Success(db) => {
307 let conn = db.pool();
308 match find_active_user(conn, credentials).await {
309 Ok(None) => {
310 error!("Unknown user");
311 Outcome::Forward(Status::Unauthorized)
312 }
313 Ok(Some(x)) => {
314 debug!("User = {}", x);
315 cache_user(credentials, &x).await;
316 Outcome::Success(x)
317 }
318 Err(x) => {
319 error!("Failed: {}", x);
320 Outcome::Forward(Status::Unauthorized)
321 }
322 }
323 }
324 _ => {
325 let msg =
326 "Unable to get DB pool to check user credentials";
327 error!("Failed: {}", msg);
328 return Outcome::Error((
329 Status::BadRequest,
330 MyError::Runtime(msg.into()),
331 ));
332 }
333 }
334 }
335 }
336 }
337 }
338 None => {
339 let msg = "Unauthorized access";
340 error!("Failed: {}", msg);
341 Outcome::Forward(Status::Unauthorized)
342 }
343 }
344 }
345 }
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352 use crate::lrs::TEST_USER_PLAIN_TOKEN;
353 use tracing_test::traced_test;
354
355 #[test]
356 fn test_test_user_credentials() {
357 let plain = BASE64_STANDARD.encode(TEST_USER_PLAIN_TOKEN);
358 assert_eq!(plain, "dGVzdEBteS54YXBpLm5ldDo=");
359 let credentials = fxhash::hash32(plain.as_bytes());
360 assert_eq!(credentials, 3793911390);
361 let credentials = fxhash::hash32(&plain);
362 assert_eq!(credentials, 2175704399);
363 }
364
365 #[test]
366 fn test_class_methods() {
367 let credentials = User::credentials_from("test@my.xapi.net", "");
368 assert_eq!(credentials, 2175704399);
369 }
370
371 #[traced_test]
372 #[tokio::test]
373 async fn test_cache_eviction() {
374 let u1 = User {
375 id: 100,
376 enabled: true,
377 email: "nobody@nowhere".to_owned(),
378 role: Role::User,
379 ..Default::default()
380 };
381 let u2 = User {
382 id: 200,
383 enabled: true,
384 email: "anybody@nowhere".to_owned(),
385 role: Role::User,
386 ..Default::default()
387 };
388
389 cache_user(10, &u1).await;
390 cache_user(20, &u2).await;
391
392 {
394 let c = cached_users().lock().await;
395 assert_eq!(c.len(), 2);
396 }
397
398 u1.uncache().await;
399 {
400 let c = cached_users().lock().await;
401 assert_eq!(c.len(), 1);
402 }
403
404 u2.uncache().await;
405 let c = cached_users().lock().await;
406 assert!(c.is_empty())
407 }
408
409 #[traced_test]
410 #[tokio::test]
411 async fn test_cache_clearing() {
412 let u1 = User {
413 id: 100,
414 enabled: true,
415 email: "nobody@nowhere".to_owned(),
416 role: Role::User,
417 ..Default::default()
418 };
419 let u2 = User {
420 id: 200,
421 enabled: true,
422 email: "anybody@nowhere".to_owned(),
423 role: Role::User,
424 ..Default::default()
425 };
426
427 cache_user(10, &u1).await;
428 cache_user(20, &u2).await;
429
430 User::clear_cache().await;
431
432 let c = cached_users().lock().await;
433 assert!(c.is_empty())
434 }
435}