xapi_rs/lrs/resources/
users.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3#![doc = include_str!("../../../doc/EXT_USERS.md")]
4
5use crate::{
6    DataError, MyError,
7    db::user::{
8        batch_update_users, find_all_ids, find_group_member_ids, find_group_user, find_user,
9        insert_user, update_user,
10    },
11    emit_response, eval_preconditions,
12    lrs::{
13        DB, Headers, Role, User, etag_from_str, resources::WithResource,
14        server::get_consistent_thru,
15    },
16};
17use chrono::SecondsFormat;
18use rocket::{
19    FromForm, Route, State,
20    form::Form,
21    futures::TryFutureExt,
22    get,
23    http::{Header, Status, hyper::header},
24    post, put, routes,
25    serde::json::Json,
26};
27use tracing::{debug, info};
28
29/// Form to use when creating new users.
30#[derive(Debug, FromForm)]
31struct CreateForm<'a> {
32    email: &'a str,
33    password: &'a str,
34    /// Even root cannot create a user w/ Root (4) role!
35    #[field(validate = range(0..4))]
36    role: u16,
37}
38
39#[derive(Debug, FromForm)]
40#[field(validate = range(0..4))]
41pub(crate) struct RoleUI(pub(crate) u16);
42
43/// Form to use when updating a single User.
44#[derive(Debug, FromForm)]
45pub(crate) struct UpdateForm<'a> {
46    pub(crate) enabled: Option<bool>,
47    pub(crate) email: Option<&'a str>,
48    pub(crate) password: Option<&'a str>,
49    pub(crate) role: Option<RoleUI>,
50    #[field(name = uncased("managerId"))]
51    pub(crate) manager_id: Option<i32>,
52}
53
54/// Form to use when updating multiple Users.
55#[derive(Debug, FromForm)]
56pub(crate) struct BatchUpdateForm {
57    pub(crate) ids: Vec<i32>,
58    pub(crate) enabled: Option<bool>,
59    pub(crate) role: Option<RoleUI>,
60    #[field(name = uncased("managerId"))]
61    pub(crate) manager_id: Option<i32>,
62}
63
64#[doc(hidden)]
65pub fn routes() -> Vec<Route> {
66    routes![post, get_one, get_ids, update_one, update_many]
67}
68
69/// Create a new user w/ given properties. Newly created user is enabled and has
70/// its `manager_id` field assigned the ID of the authenticated user making the
71/// request.
72///
73/// Bare in mind that while _Root_ can assign any _Role, an _Admin_ can only
74/// assign _User_ or _AuthUser_ roles.
75///
76/// When successful the response has a `200` _Status_ and contains _Etag_ and
77/// _Last Modified_ headers.
78#[post("/", data = "<form>")]
79async fn post(
80    form: Form<CreateForm<'_>>,
81    db: &State<DB>,
82    user: User,
83) -> Result<WithResource<User>, MyError> {
84    debug!("----- post ----- {}", user);
85    user.can_manage_users()?;
86
87    // validate new user attributes...
88    // our user's Role can be one of two: 'root' or 'admin'.  new user's Role
89    // depends on which one it is.
90    let z_role = Role::from(form.role);
91    // root can create users w/ any role except Root.  Rocket validation
92    // annotation ensures it's never Root; i.e. the range's upper bound...
93    if !user.is_root() {
94        // it's an admin.  they can create users w/ User | AuthUser roles only
95        if !matches!(z_role, Role::User | Role::AuthUser) {
96            return Err(MyError::HTTP {
97                status: Status::Forbidden,
98                info: format!("Admin ({user}) can only create users w/ [Auth]User roles").into(),
99            });
100        }
101    }
102    let x = insert_user(db.pool(), (form.email, form.password, z_role, user.id)).await?;
103    emit_user_response(x, false).await
104}
105
106/// Fetch the user w/ the designated ID if it exists.
107///
108/// Note though that if the authenticated user making the request is not _Root_,
109/// but only an _Admin_ and the targeted user was found but is managed by a
110/// different _Admin_, then the call will fail w/ a 404 _Status_.
111#[get("/<id>")]
112async fn get_one(id: i32, db: &State<DB>, user: User) -> Result<WithResource<User>, MyError> {
113    debug!("----- get_one ----- {}", user);
114    user.can_manage_users()?;
115
116    if user.is_root() {
117        let x = find_user(db.pool(), id)
118            .map_err(|x| x.with_status(Status::NotFound))
119            .await?;
120        match x {
121            Some(y) => emit_response!(Headers::default(), y => User),
122            None => Err(MyError::HTTP {
123                status: Status::NotFound,
124                info: format!("User #{id} not found").into(),
125            }),
126        }
127    } else if user.is_admin() {
128        let x = find_group_user(db.pool(), id, user.id)
129            .map_err(|x| x.with_status(Status::NotFound))
130            .await?;
131        match x {
132            Some(y) => emit_response!(Headers::default(), y => User),
133            None => Err(MyError::HTTP {
134                status: Status::NotFound,
135                info: format!("User #{id} not found").into(),
136            }),
137        }
138    } else {
139        Err(MyError::HTTP {
140            status: Status::Forbidden,
141            info: "Only Root and Admins can fetch users".into(),
142        })
143    }
144}
145
146/// Fetch all user IDs managed by the requesting authenticated user if they
147/// are an _Admin_ or simply all user IDs if it was _Root_.
148#[get("/")]
149async fn get_ids(db: &State<DB>, user: User) -> Result<Json<Vec<i32>>, MyError> {
150    debug!("----- get_ids ----- {}", user);
151    user.can_manage_users()?;
152
153    if user.is_root() {
154        let x = find_all_ids(db.pool())
155            .map_err(|x| x.with_status(Status::NotFound))
156            .await?;
157        Ok(Json(x))
158    } else if user.is_admin() {
159        let x = find_group_member_ids(db.pool(), user.id)
160            .map_err(|x| x.with_status(Status::NotFound))
161            .await?;
162        Ok(Json(x))
163    } else {
164        Err(MyError::HTTP {
165            status: Status::Forbidden,
166            info: "Only Root and Admins can fetch users IDs".into(),
167        })
168    }
169}
170
171/// Update `enabled` flag, `email`, `password`, `role` or `manager_id` properties
172/// for a single user given their ID.
173///
174/// _Roots_ as usual can modify any property for any user except themselves.
175/// Every other _Role_, incl. _Guests_, can modify their `email` and `password`
176/// properties. _Admins_ can only modify `enabled` and `role` for users they
177/// manage. When changing `role`, _Admins_ can only toggle it between _User_
178/// and _AuthUser_.
179#[put("/<id>", data = "<form>")]
180async fn update_one(
181    c: Headers,
182    id: i32,
183    form: Form<UpdateForm<'_>>,
184    db: &State<DB>,
185    user: User,
186) -> Result<WithResource<User>, MyError> {
187    debug!("----- update_one ----- {user:?}");
188    debug!("form = {form:?}");
189
190    let x = find_user(db.pool(), id)
191        .map_err(|x| x.with_status(Status::NotFound))
192        .await?;
193    let old_user = match x {
194        Some(y) => y,
195        None => {
196            return Err(MyError::HTTP {
197                status: Status::NotFound,
198                info: format!("User #{id} not found").into(),
199            });
200        }
201    };
202    debug!("old_user = {old_user:?}");
203    if old_user.is_root() {
204        return Err(MyError::HTTP {
205            status: Status::BadRequest,
206            info: "Root properties are immutable".into(),
207        });
208    }
209
210    // we do not allow combined updates --except for the special case of the
211    // (email, password) pair b/c we do not store the raw password, only the
212    // credentials which are computed from both...
213    if form.enabled.is_some_and(|x| x != old_user.enabled) {
214        // only root and current admin can alter enabled flag...
215        if !(user.is_root() || user.id == old_user.manager_id) {
216            return Err(MyError::HTTP {
217                status: Status::BadRequest,
218                info: "Only Root and the user's Admin can alter enabled flag".into(),
219            });
220        }
221        debug!("Will update enabled flag...")
222    } else if form
223        .role
224        .as_ref()
225        .is_some_and(|x| Role::from(x.0) != old_user.role)
226    {
227        // Root can always re-assign roles.  Admins however can downgrade
228        // (AuthUser -> User) or upgrade (User -> AuthUser) a role only for
229        // users they manage...
230        if !user.is_root() {
231            if user.id != old_user.manager_id {
232                return Err(MyError::HTTP {
233                    status: Status::Forbidden,
234                    info: "Only Root and the user's Admin can alter roles".into(),
235                });
236            }
237
238            let new_role = Role::from(form.role.as_ref().unwrap().0);
239            if !matches!(new_role, Role::User | Role::AuthUser) {
240                return Err(MyError::HTTP {
241                    status: Status::BadRequest,
242                    info: "Admins can alter roles from User to AuthUser or vice-versa only".into(),
243                });
244            }
245        }
246        debug!("Will update role...")
247    } else if form.manager_id.is_some_and(|x| x != old_user.manager_id) {
248        // only root can re-assign manager_id...
249        if !user.is_root() {
250            return Err(MyError::HTTP {
251                status: Status::BadRequest,
252                info: "Only Root can alter manager_id".into(),
253            });
254        }
255        debug!("Will update manager_id...")
256    } else if form.email.is_some() || form.password.is_some() {
257        // both must be provided...
258        if form.email.is_none() || form.password.is_none() {
259            return Err(MyError::HTTP {
260                status: Status::BadRequest,
261                info: "When updating either 'email' or 'password' both values must be provided"
262                    .into(),
263            });
264        }
265        // only a non-root user can change their email and/or password...
266        if user.is_root() || user.id != id {
267            return Err(MyError::HTTP {
268                status: Status::BadRequest,
269                info: "Only non-Root user can alter their 'email' + 'password' fields".into(),
270            });
271        }
272        debug!("Will update email + credentials...")
273    } else {
274        return Err(MyError::HTTP {
275            status: Status::BadRequest,
276            info: "You're wasting my time :(".into(),
277        });
278    }
279
280    // continue if pre-conditions exist + pass...
281    if c.has_no_conditionals() {
282        Err(MyError::HTTP {
283            status: Status::Conflict,
284            info: "Update User w/ no pre-conditions is NOT allowed".into(),
285        })
286    } else {
287        let x = serde_json::to_string(&old_user).map_err(|x| MyError::Data(DataError::JSON(x)))?;
288        let etag = etag_from_str(&x);
289        debug!("etag (old) = {}", etag);
290        match eval_preconditions!(&etag, c) {
291            s if s != Status::Ok => Err(MyError::HTTP {
292                status: s,
293                info: "Failed pre-condition(s)".into(),
294            }),
295            _ => {
296                let x = update_user(db.pool(), id, form.into_inner()).await?;
297                emit_user_response(x, true).await
298            }
299        }
300    }
301}
302
303/// Batch modification of `enabled` flag, `role` and `manager_id` for a limited
304/// set of users given their IDs.
305///
306/// If the authenticated user making the request is an _Admin_ then the users
307/// targeted must be already managed by them. In addition, _Admins_ can only
308/// batch modify the first two properties; not the `manager_id`. That last
309/// one is only possible with _Root_.
310///
311/// The response will include the IDs of the users that were successfully
312/// modified.
313#[put("/", data = "<form>")]
314async fn update_many(
315    form: Form<BatchUpdateForm>,
316    db: &State<DB>,
317    user: User,
318) -> Result<Status, MyError> {
319    debug!("----- update_many ----- {user:?}");
320    user.can_manage_users()?;
321
322    debug!("form = {form:?}");
323
324    // if IDs array is empty return...
325    let ids = &form.ids;
326    if ids.is_empty() {
327        info!("Empty user IDs array. Do nothing");
328        return Ok(Status::Ok);
329    }
330
331    let conn = db.pool();
332    // if user is Admin, ensures all IDs are for users they manage...
333    if user.is_admin() {
334        let x = find_group_member_ids(conn, user.id).await?;
335        let ok = ids.iter().all(|id| x.contains(id));
336        if !ok {
337            return Err(MyError::HTTP {
338                status: Status::BadRequest,
339                info: "Admins can only do batch updates for users they manage".into(),
340            });
341        }
342
343        // Admins can only change Role to User or AuthUser...
344        if form
345            .role
346            .as_ref()
347            .is_some_and(|x| !matches!(Role::from(x.0), Role::User | Role::AuthUser))
348        {
349            return Err(MyError::HTTP {
350                status: Status::BadRequest,
351                info: "Admins can only toggle role between User and AuthUser".into(),
352            });
353        }
354
355        // only Root can alter manager_id...
356        if form.manager_id.is_some() {
357            return Err(MyError::HTTP {
358                status: Status::Forbidden,
359                info: "Only Root can re-assign manager ID".into(),
360            });
361        }
362    }
363
364    batch_update_users(db.pool(), form.into_inner()).await?;
365    // NOTE (rsn) 20250317 - the safest course of action here is to
366    // clear the LRU cache.
367    User::clear_cache().await;
368
369    Ok(Status::Ok)
370}
371
372/// Construct and return a response based on the User `u`. If in addition,
373/// `uncache` is TRUE then also evict said User from the LRU cache.
374async fn emit_user_response(u: User, uncache: bool) -> Result<WithResource<User>, MyError> {
375    let x = serde_json::to_string(&u).map_err(|x| MyError::Data(DataError::JSON(x)))?;
376    let etag = etag_from_str(&x);
377    debug!("etag (new) = {}", etag);
378    let last_modified = get_consistent_thru()
379        .await
380        .to_rfc3339_opts(SecondsFormat::Millis, true);
381
382    if uncache {
383        u.uncache().await
384    }
385
386    Ok(WithResource {
387        inner: rocket::serde::json::Json(u),
388        etag: Header::new(header::ETAG.as_str(), etag.to_string()),
389        last_modified: Header::new(header::LAST_MODIFIED.as_str(), last_modified),
390    })
391}