Skip to main content

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