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