Skip to main content

parse_rust_server/routes/
users.rs

1//! Signup, login, `/users/me`, logout.
2//!
3//! Upstream: `src/Routers/UsersRouter.js`. Three facts shape this module.
4//!
5//! - **`POST /users` is not `POST /classes/_User`.** Only the signup route returns a session
6//!   token. A non-master **create or delete** through the class route is refused; an **update** is
7//!   allowed, because that is what `user.save()` sends. See `classes::enforce_class_security`.
8//! - **The password hash must never reach a response.** Upstream reattaches the hash onto the
9//!   object as `password` and strips it in exactly one place, so every response path depends on
10//!   that one step running. Here the hash is never placed on a response object at all, so there
11//!   is nothing to strip and no path that can forget to.
12//! - **Login reads below the pipeline.** `filterSensitiveData` removes every `_`-prefixed key,
13//!   including the hash the password check needs, so a login that went through the read pipeline
14//!   could never verify anything. Upstream has the same problem and solves it by reading under
15//!   `Auth.maintenance` (`UsersRouter.js:108-110`), whose bypass parse-rust does not model; the
16//!   equivalent here is one direct adapter read, built from a username and nothing client-shaped.
17
18use parse_rust_auth::{create_session, CreatedWith, NewSession};
19use parse_rust_core::{ErrorCode, FieldWrite, ParseError, ParseMap, ParseValue};
20use parse_rust_rest::{FindOptions, WriteBody};
21use parse_rust_storage::{Constraint, Query, QueryOptions, StorageAdapter};
22use serde_json::{json, Value as Json};
23
24use crate::auth::Authority;
25use crate::request::RequestContext;
26use crate::state::AppState;
27
28pub const USER_CLASS: &str = "_User";
29/// The column upstream stores the bcrypt hash in. Never a response key.
30const HASHED_PASSWORD: &str = "_hashed_password";
31
32/// Remove everything a client must never see from a raw `_User` row.
33///
34/// Only the direct-adapter login path needs this, because every other read goes through
35/// `filterSensitiveData`. A denylist is the wrong shape in general; here the set is closed and the
36/// stronger property is that `to_response_body` strips every `_`-prefixed key afterwards anyway.
37fn strip_sensitive(mut row: ParseMap) -> ParseMap {
38    for key in [
39        HASHED_PASSWORD,
40        "password",
41        "_perishable_token",
42        "_email_verify_token",
43    ] {
44        row.shift_remove(key);
45    }
46    row
47}
48
49/// Whether a key is present with a JS-truthy value, whatever its type.
50///
51/// `!password` is upstream's test, and it fires on an absent key, `null`, `false`, `0` and `""`
52/// alike, but **not** on a non-string that happens to be truthy. That one falls to the type check
53/// below it, which is a different code.
54fn body_has_truthy(body: &WriteBody, key: &str) -> bool {
55    match body.get(key) {
56        Some(FieldWrite::Value(v)) => parse_rust_core::is_js_truthy(v),
57        // An op envelope is an object on the JS side, so it is truthy.
58        Some(FieldWrite::Op(_)) => true,
59        None => false,
60    }
61}
62
63fn take_string(body: &WriteBody, key: &str) -> Option<String> {
64    match body.get(key) {
65        Some(FieldWrite::Value(ParseValue::String(s))) => Some(s.clone()),
66        _ => None,
67    }
68}
69
70/// Replace a user-facing password with the bcrypt column that is safe to persist, and, on a
71/// create, give the row an id and an owner ACL.
72///
73/// Shared by signup and master-key writes through `/classes/_User`. Authorization decides whether
74/// the class route is allowed; it must not decide whether a password is hashed.
75pub(crate) async fn prepare_user_write(
76    body: &mut WriteBody,
77    is_create: bool,
78) -> Result<(), ParseError> {
79    hash_user_password(body).await?;
80    if is_create {
81        ensure_user_identity_and_acl(body);
82    }
83    Ok(())
84}
85
86async fn hash_user_password(body: &mut WriteBody) -> Result<(), ParseError> {
87    let Some(password) = take_string(body, "password") else {
88        // Schema validation reports the wire-compatible error for a non-string value. An absent
89        // password is also valid for updates that change another field.
90        return Ok(());
91    };
92    let hash = parse_rust_auth::password::hash(password).await?;
93    body.shift_remove("password");
94    body.insert(
95        HASHED_PASSWORD.to_string(),
96        FieldWrite::Value(ParseValue::String(hash)),
97    );
98    Ok(())
99}
100
101/// Give a newly created user an id and ensure its ACL always contains its own principal.
102///
103/// Upstream preserves any ACL supplied by a master caller, then adds the owner entry. Leaving an
104/// invalid ACL untouched lets the schema/ACL validator return the correct Parse error.
105///
106/// The result is a user readable and writable by itself and nobody else, which is what
107/// `enforcePrivateUsers` produces at its default of **true** (`Options/Definitions.js:263-268`).
108/// With that option set to `false` upstream additionally grants `{"*": {"read": true}}`. The
109/// option is not modeled, so the private form is the only one produced here: the safe direction,
110/// and the one matching the default.
111pub(crate) fn ensure_user_identity_and_acl(body: &mut WriteBody) -> String {
112    let object_id = match take_string(body, "objectId") {
113        Some(id) => id,
114        None => {
115            let id = parse_rust_core::new_object_id();
116            body.insert(
117                "objectId".to_string(),
118                FieldWrite::Value(ParseValue::String(id.clone())),
119            );
120            id
121        }
122    };
123
124    let mut permissions = ParseMap::new();
125    permissions.insert("read".to_string(), ParseValue::Bool(true));
126    permissions.insert("write".to_string(), ParseValue::Bool(true));
127
128    match body.get_mut("ACL") {
129        Some(FieldWrite::Value(ParseValue::Object(acl))) => {
130            acl.insert(object_id.clone(), ParseValue::Object(permissions));
131        }
132        // A non-object ACL is left alone so the ACL validator reports it.
133        Some(_) => {}
134        None => {
135            let mut acl = ParseMap::new();
136            acl.insert(object_id.clone(), ParseValue::Object(permissions));
137            body.insert(
138                "ACL".to_string(),
139                FieldWrite::Value(ParseValue::Object(acl)),
140            );
141        }
142    }
143    object_id
144}
145
146/// `handleCreate`'s `role:`-prefixed objectId refusal (`ClassesRouter.js:105-112`).
147///
148/// A user whose objectId is `role:Admins` is granted that role by every ACL check, because an ACL
149/// names a role by string and the caller's ACL group carries its own objectId.
150///
151/// **On `ClassesRouter`, not on `UsersRouter`**, which is the detail that matters for where this
152/// is called from. `UsersRouter extends ClassesRouter` and does not override `handleCreate`
153/// (`UsersRouter.js:23`, `:824-826`), so one guard covers `POST /users` and `POST /classes/_User`
154/// alike. parse-rust had it on the signup route only, which left the class route uncovered for
155/// the master key.
156pub(crate) fn reject_role_prefixed_object_id(
157    body: &WriteBody,
158    rc: &RequestContext,
159) -> Result<(), ParseError> {
160    let Some(FieldWrite::Value(ParseValue::String(id))) = body.get("objectId") else {
161        // `typeof req.body?.objectId === 'string'` guards it upstream, so a non-string is not
162        // refused here. It is refused by schema validation instead.
163        return Ok(());
164    };
165    if !id.starts_with("role:") {
166        return Ok(());
167    }
168    // `createSanitizedError` (`ClassesRouter.js:111`). Note this is the sanitized twin of
169    // `Auth.js`'s identically worded refusal, which is a plain `Parse.Error` and stays detailed;
170    // the two are different call sites with the same string.
171    Err(ParseError::permission_denied(
172        ErrorCode::OperationForbidden,
173        "Invalid object ID.",
174        rc.options.error_detail,
175    ))
176}
177
178/// A created `_User` must carry a non-empty username and a non-empty password
179/// (`RestWrite.js:468-473`).
180///
181/// **Not gated on the caller.** Upstream's guard is `!this.query && !hasAuthData`, which asks
182/// whether this is a create and whether an auth adapter is supplying the identity instead. Master
183/// is not exempt, so `POST /classes/_User` with the master key is subject to it too. Without that,
184/// the dashboard's own route admitted a user with no username, or a passwordless row that no
185/// login can ever match and that `verify_dummy` exists to make indistinguishable.
186///
187/// Runs **before** identity validation and hashing, which is upstream's order: `validateAuthData`
188/// is stage 5 of the chain and `transformUser` is stage 12 (`RestWrite.js:122-141`). Checking a
189/// uniqueness query and paying a bcrypt cost for a body that was never well-formed is work done on
190/// behalf of a request that cannot succeed.
191///
192/// The `authData` branch is not modelled: parse-rust refuses `authData` outright, so the only
193/// reachable case is the one that requires both fields. When auth adapters land, this guard grows
194/// the second condition rather than moving.
195pub(crate) fn require_create_credentials(body: &WriteBody) -> Result<(), ParseError> {
196    if take_string(body, "username")
197        .filter(|u| !u.is_empty())
198        .is_none()
199    {
200        return Err(ParseError::new(
201            ErrorCode::UsernameMissing,
202            "bad or missing username",
203        ));
204    }
205    if take_string(body, "password")
206        .filter(|p| !p.is_empty())
207        .is_none()
208    {
209        // No trailing period. Upstream's string is `password is required`, and the message is
210        // contract in the disclosing regime.
211        return Err(ParseError::new(
212            ErrorCode::PasswordMissing,
213            "password is required",
214        ));
215    }
216    Ok(())
217}
218
219/// `POST /users`. Signup.
220pub async fn signup_core(
221    state: &AppState,
222    rc: &RequestContext,
223    authority: &Authority,
224    body: &Json,
225) -> Result<Json, ParseError> {
226    let mut body = parse_rust_rest::decode_write_body(body, parse_rust_core::op::OpPath::Create)?;
227    // Before anything else: a signup body must not carry a `_hashed_password` of the caller's
228    // choosing.
229    parse_rust_rest::reject_reserved_keys_in(body.keys().map(String::as_str))?;
230    // Signup is a create like any other, so the objectId policy applies to it
231    // (`RestWrite.js:50-65`). It has to run here rather than inside the pipeline, because
232    // `ensure_user_identity_and_acl` below puts a server-generated objectId into the body.
233    parse_rust_rest::enforce_object_id_policy(&body, state.config().allow_custom_object_id)?;
234
235    reject_role_prefixed_object_id(&body, rc)?;
236    // Upstream runs this on create as well as update (`RestWrite.js:116`), and running it only on
237    // the update path left signup able to set `emailVerified` and `authData` on its own new row.
238    reject_client_restricted_user_fields(&body, rc, authority)?;
239
240    require_create_credentials(&body)?;
241
242    // **The `create` permission is checked before any identity work** (`RestWrite.js:730-746`,
243    // whose own comment names this exact hazard). `validate_user_identity` queries `_User` by
244    // username and email, and the password is then hashed, so a signup that the CLP will refuse
245    // otherwise answers 202 in milliseconds for a name that exists and 119 after a bcrypt-length
246    // pause for one that does not. Measured on a closed `_User.create`: ~4 ms against ~196 ms.
247    //
248    // That is account enumeration against a class whose whole point is that outsiders may not read
249    // it, and no response body discloses it: the difference is entirely in which check runs first.
250    // Master and maintenance skip the gate, as upstream's `isMaster || isMaintenance` does.
251    if !authority.is_privileged() {
252        parse_rust_rest::validate_permission(
253            rc.snapshot.clp(USER_CLASS),
254            USER_CLASS,
255            &rc.scope.acl_group(),
256            parse_rust_core::Operation::Create,
257            None,
258            rc.options.error_detail,
259        )?;
260    }
261
262    // **Signup runs the same identity validation an update does**, because `transformUser` is one
263    // function and does not branch on create versus update for these two checks
264    // (`RestWrite.js:803-807`). Validating on the update path alone left signup admitting exactly
265    // the identities the update path refuses: a case-only duplicate username, and an email that is
266    // not one.
267    //
268    // The objectId is not known yet, so the `$ne` exclusion is given a value no row can hold. On a
269    // create there is no self to exclude, which is the same thing upstream's `this.objectId()`
270    // returning undefined achieves.
271    validate_user_identity(state, rc, &body, "").await?;
272
273    prepare_user_write(&mut body, true).await?;
274
275    let ctx = rc.ctx(state.storage());
276    let created = parse_rust_rest::create(&ctx, USER_CLASS, body)
277        .await
278        .map_err(map_duplicate)?;
279
280    let session = create_session(
281        state.storage(),
282        &state.config().session,
283        NewSession {
284            user_object_id: &created.object_id,
285            created_with: Some(CreatedWith::signup(None)),
286            installation_id: rc.installation_id.as_deref(),
287        },
288    )
289    .await?;
290
291    Ok(json!({
292        "objectId": created.object_id,
293        "createdAt": created.created_at.to_iso(),
294        "sessionToken": session.session_token,
295    }))
296}
297
298/// Turn a duplicate-key error into the code the SDK expects (`RestWrite.js:1697-1716`).
299///
300/// A `username_1` collision must be 202 `USERNAME_TAKEN`, not a bare 137. Which field collided is
301/// read from the adapter's out-of-band [`ParseError::duplicated_field`], which the adapter fills
302/// in by recognising the auto-generated index name. That is why index names are contractual, and
303/// it is why nothing here reads `message`: the message is the fixed
304/// `A duplicate value for a field with unique values was provided` in every case, and the driver
305/// text it replaced named the database and the colliding value.
306///
307/// **Not modelled: upstream's fallback.** When it cannot recover the field, upstream re-queries
308/// `_User` by username and then by email before settling for 137 (`RestWrite.js:1718-1755`). The
309/// only index Parse creates that reaches that path is a case-insensitive one, which parse-rust
310/// does not create, so a collision it cannot attribute stays 137 here.
311pub(crate) fn map_duplicate(e: ParseError) -> ParseError {
312    if e.code != ErrorCode::DuplicateValue {
313        return e;
314    }
315    match e.duplicated_field() {
316        Some("username") => ParseError::new(
317            ErrorCode::UsernameTaken,
318            "Account already exists for this username.",
319        ),
320        Some("email") => ParseError::new(
321            ErrorCode::EmailTaken,
322            "Account already exists for this email address.",
323        ),
324        _ => e,
325    }
326}
327
328/// `POST /login`.
329pub async fn login_core(
330    state: &AppState,
331    rc: &RequestContext,
332    authority: &Authority,
333    body: &Json,
334) -> Result<Json, ParseError> {
335    let body = parse_rust_rest::decode_write_body(body, parse_rust_core::op::OpPath::Create)?;
336
337    // **Three refusals in upstream's order, each with its own code** (`UsersRouter.js:84-96`).
338    // Collapsing them into one `USERNAME_MISSING`, which is what this did, is wire-visible twice
339    // over: a client with no password got the username error, and a client with a non-string
340    // password got it too where upstream answers `OBJECT_NOT_FOUND`.
341    //
342    // **Each guard tests JavaScript truthiness of the raw value, not "is it a non-empty string".**
343    // The distinction decides which of the three fires: `{"username": 7}` is truthy, so it passes
344    // the first guard and is refused by the third as a type error, where testing for a string here
345    // would report a missing username instead.
346    let has_username = body_has_truthy(&body, "username");
347    let has_email = body_has_truthy(&body, "email");
348    if !has_username && !has_email {
349        return Err(ParseError::new(
350            ErrorCode::UsernameMissing,
351            "username/email is required.",
352        ));
353    }
354    if !body_has_truthy(&body, "password") {
355        return Err(ParseError::new(
356            ErrorCode::PasswordMissing,
357            "password is required.",
358        ));
359    }
360    // A truthy non-string password, username or email is the third refusal, and it deliberately
361    // answers the same thing a wrong password does rather than naming the type: telling a caller
362    // its password was the wrong *type* is one bit more than upstream gives away here.
363    let invalid_credentials =
364        || ParseError::new(ErrorCode::ObjectNotFound, "Invalid username/password.");
365    let username = take_string(&body, "username");
366    let email = take_string(&body, "email");
367    let Some(password) = take_string(&body, "password") else {
368        return Err(invalid_credentials());
369    };
370    if (has_username && username.is_none()) || (has_email && email.is_none()) {
371        return Err(invalid_credentials());
372    }
373
374    // Straight to the adapter. See the module note: the read pipeline strips the hash this has to
375    // check, and the query is built here from the identifier rather than from anything the client
376    // shaped, so nothing client-supplied reaches storage unfiltered.
377    //
378    // **The `$or` is what makes logging in with an email address work** (`UsersRouter.js:99-107`).
379    // Given only an identifier, upstream matches it against `username` *or* `email`, which is what
380    // every SDK's `Parse.User.logIn(emailAddress, password)` relies on. Matching `username` alone,
381    // which is what this did, answered `Invalid username/password.` for a correct email and
382    // password, and an `email` key was not read at all.
383    let schema = rc.snapshot.get_or_default(USER_CLASS);
384    let identifier = username.filter(|_| has_username);
385    // Kept for the multi-row preference below, which compares against the submitted username.
386    let username_for_preference = identifier.clone();
387    let email = email.filter(|_| has_email);
388    let query = match (identifier, email) {
389        // Both given: an AND, so a mismatched pair is not a login.
390        (Some(username), Some(email)) => Query::from_constraints(vec![
391            Constraint::equal("email", ParseValue::String(email)),
392            Constraint::equal("username", ParseValue::String(username)),
393        ]),
394        (None, Some(email)) => {
395            Query::from_constraints(vec![Constraint::equal("email", ParseValue::String(email))])
396        }
397        // The identifier arrived as `username` and may be either.
398        (Some(identifier), None) => Query::any_of(vec![
399            Query::from_constraints(vec![Constraint::equal(
400                "username",
401                ParseValue::String(identifier.clone()),
402            )]),
403            Query::from_constraints(vec![Constraint::equal(
404                "email",
405                ParseValue::String(identifier),
406            )]),
407        ]),
408        // Unreachable: the first guard refused the case where neither is truthy, and the third
409        // refused the case where a truthy one is not a string.
410        (None, None) => return Err(invalid_credentials()),
411    };
412
413    // **No limit.** Upstream passes an empty options object (`UsersRouter.js:108-110`), and the
414    // reason surfaces one line down: an account whose email equals another account's username
415    // matches both rows, and upstream resolves that by preferring the exact username match. Capping
416    // the query at one row makes the winner whichever row MongoDB returns first, which can reject a
417    // valid login or, if the two passwords happen to match, authenticate the wrong account.
418    let rows = state
419        .storage()
420        .find(&schema, &query, &QueryOptions::default())
421        .await?;
422
423    // One error for "no such user" and for "wrong password", so login cannot be used to
424    // enumerate accounts. Upstream does the same.
425    let invalid = || ParseError::new(ErrorCode::ObjectNotFound, "Invalid username/password.");
426
427    // `results.filter(user => user.username === username)[0]` (`UsersRouter.js:121-124`). Upstream
428    // logs a warning here; there is no logger yet, so the preference is applied silently. Falling
429    // back to the first row covers the case upstream would crash on, where more than one row
430    // matched but the identifier arrived as an `email` key and no row's username can equal it.
431    let row = select_login_row(rows, username_for_preference.as_deref());
432
433    // **Both failure paths below pay the bcrypt cost.** Returning early makes a missing account
434    // answer in microseconds where a real one answers in milliseconds, and that difference is
435    // measurable across a network, so the shared error message stops hiding which accounts exist.
436    // Upstream runs the same dummy compare in both branches (`UsersRouter.js:112-118`, `:132-136`).
437    let Some(row) = row else {
438        parse_rust_auth::password::verify_dummy(password).await;
439        return Err(invalid());
440    };
441    let hash = match row.get(HASHED_PASSWORD) {
442        Some(ParseValue::String(hash)) if !hash.is_empty() => hash.clone(),
443        // A passwordless account, which an auth-adapter signup produces upstream. Never a valid
444        // password login, and it must not be a fast one either.
445        _ => {
446            parse_rust_auth::password::verify_dummy(password).await;
447            return Err(invalid());
448        }
449    };
450    if !parse_rust_auth::password::verify(password, hash).await {
451        return Err(invalid());
452    }
453
454    // **An explicitly empty ACL is a disabled account** (`UsersRouter.js:151-153`). A master caller
455    // setting `ACL: {}` is the documented way to lock a user out, and without this the account
456    // still logs in and receives a working session, so the lock does nothing until its existing
457    // sessions are separately destroyed.
458    //
459    // `authority.is_master()` rather than `rc.is_master()`: upstream's guard is `!req.auth.isMaster`
460    // alone, so **maintenance is subject to the check**, and `rc.is_master()` answers for the ACL
461    // scope, which treats master and maintenance alike.
462    if !authority.is_master() && acl_is_explicitly_empty(&row) {
463        return Err(invalid());
464    }
465
466    let Some(ParseValue::String(object_id)) = row.get("objectId") else {
467        // Nothing upstream throws here, because a row without an objectId cannot exist through
468        // any write path. If one does, the shape of the stored row is not the client's business.
469        return Err(ParseError::internal("stored user has no objectId"));
470    };
471
472    let session = create_session(
473        state.storage(),
474        &state.config().session,
475        NewSession {
476            user_object_id: object_id,
477            created_with: Some(CreatedWith::login(None)),
478            installation_id: rc.installation_id.as_deref(),
479        },
480    )
481    .await?;
482
483    // **Re-fetch under the caller's own auth before answering** (`UsersRouter.js:349-387`).
484    //
485    // The row above came from a direct adapter read, deliberately below the pipeline, because the
486    // password check needs the hash that `filterSensitiveData` strips. That read answers to
487    // nothing: not `_User` `get` CLP, not `protectedFields`, not the object's ACL. Returning it is
488    // how a deployment that protects `email` still puts `email` on the wire at every login, and
489    // the response looks completely ordinary while it happens.
490    //
491    // `strip_sensitive` is a denylist over the columns login itself must not echo. It is not an
492    // authorization filter and cannot become one, because the fields at issue are configured per
493    // deployment and are ordinary columns.
494    let object_id = object_id.to_string();
495    let mut out = refetch_for_response(state, rc, &object_id, row).await?;
496    out.insert(
497        "sessionToken".to_string(),
498        ParseValue::String(session.session_token),
499    );
500    Ok(crate::routes::classes::body_of(&out))
501}
502
503/// Choose the account a login refers to when the identifier matched more than one row.
504///
505/// `results.filter(user => user.username === username)[0]` (`UsersRouter.js:121-124`). One user's
506/// email can equal another's username, and the `$or` matches both; upstream prefers the exact
507/// username and logs a warning. There is no logger yet, so the preference is applied silently.
508///
509/// **A pure function so it can be tested against the adverse order.** The integration test cannot
510/// force which row MongoDB returns first from an `$or`, so it passes against a `limit: 1`
511/// implementation whenever the database happens to return the right one, which is most of the
512/// time: a test that reports success for the bug it was written to catch. Deciding here, over a
513/// list the caller supplies, is what makes the failing case reachable on demand.
514///
515/// Falling back to the first row covers what upstream crashes on: more than one match when the
516/// identifier arrived as an `email` key, so no row's username can equal it and
517/// `results.filter(...)[0]` is `undefined`.
518fn select_login_row(rows: Vec<ParseMap>, submitted_username: Option<&str>) -> Option<ParseMap> {
519    if rows.len() <= 1 {
520        return rows.into_iter().next();
521    }
522    let mut rows = rows;
523    let exact = submitted_username.and_then(|name| {
524        rows.iter()
525            .position(|r| matches!(r.get("username"), Some(ParseValue::String(u)) if u == name))
526    });
527    match exact {
528        Some(i) => Some(rows.swap_remove(i)),
529        None => rows.into_iter().next(),
530    }
531}
532
533/// The authenticated user's own view of their row, for a login response.
534///
535/// Master and maintenance keep the raw row: they bypass CLP and `protectedFields` everywhere else,
536/// so re-reading would only narrow a view they are entitled to, and an empty result for them is a
537/// genuine not-found rather than a denial (`UsersRouter.js:378-387`).
538///
539/// **A denied or empty re-fetch falls back to the identity alone, never to the raw row.** That is
540/// upstream's explicit choice at `:376` and it is the whole point: the fallback is reached exactly
541/// when access control refused the record, which is the case where returning the raw row would
542/// disclose the most. Login still succeeds, because authentication and authorization are separate
543/// questions and passing the first does not entitle the caller to read the row.
544async fn refetch_for_response(
545    state: &AppState,
546    rc: &RequestContext,
547    object_id: &str,
548    row: ParseMap,
549) -> Result<ParseMap, ParseError> {
550    if rc.is_master() {
551        return Ok(parse_rust_rest::acl::raise_acl(strip_sensitive(row)));
552    }
553
554    let identity_only = || {
555        let mut map = ParseMap::new();
556        map.insert(
557            "objectId".to_string(),
558            ParseValue::String(object_id.to_string()),
559        );
560        map
561    };
562
563    // The caller is whoever just authenticated, not whoever the request arrived as. A login
564    // carrying somebody else's token still answers about the account whose password was verified.
565    let roles = parse_rust_auth::expand_roles(
566        state.storage(),
567        parse_rust_auth::RolePrincipal::User(object_id),
568    )
569    .await?;
570    let scope = parse_rust_rest::AclScope::user(
571        object_id.to_string(),
572        roles.iter().map(|r| r.as_str().to_string()).collect(),
573    )?;
574
575    let ctx = parse_rust_rest::Ctx::new(state.storage(), &rc.snapshot, &scope, &rc.options);
576    match parse_rust_rest::get(&ctx, USER_CLASS, object_id, FindOptions::default()).await {
577        Ok(row) => Ok(row),
578        // Any refusal, not just `ObjectNotFound`: a CLP of `get: {}` answers
579        // `OPERATION_FORBIDDEN`, and both mean access control withheld the row.
580        Err(_) => Ok(identity_only()),
581    }
582}
583
584/// Is the row's ACL present and empty, which upstream treats as a disabled account?
585///
586/// Upstream tests `user.ACL && Object.keys(user.ACL).length == 0` on the rehydrated object
587/// (`UsersRouter.js:151`). The stored form is `_rperm`/`_wperm`, so this raises them the same way a
588/// read would and asks whether the result is an ACL with no entries. A row with neither column has
589/// no ACL at all and is not disabled, which is the `user.ACL &&` half of upstream's test.
590fn acl_is_explicitly_empty(row: &ParseMap) -> bool {
591    matches!(
592        parse_rust_rest::acl::raise_acl(row.clone()).get("ACL"),
593        Some(ParseValue::Object(acl)) if acl.is_empty()
594    )
595}
596
597/// `GET /users/me`.
598///
599/// The token is validated first, then the user is re-fetched **with the caller's own auth**
600/// (`UsersRouter.js:214-223`) so protected fields and CLP apply. Both failures answer
601/// `Invalid session token`, which is a different string from `/sessions/me`'s.
602pub async fn me_core(state: &AppState, rc: &RequestContext) -> Result<Json, ParseError> {
603    // `createSanitizedError` at all three of `handleMe`'s refusals (`UsersRouter.js:193`, `:211`,
604    // `:225`). `GET /sessions/me` is a different router with different strings and is not
605    // sanitized upstream, so it stays detailed.
606    let invalid = || {
607        ParseError::permission_denied(
608            ErrorCode::InvalidSessionToken,
609            "Invalid session token",
610            rc.options.error_detail,
611        )
612    };
613    let (Some(token), Some(user_id)) = (rc.session_token.as_deref(), rc.user_id.as_deref()) else {
614        return Err(invalid());
615    };
616
617    let ctx = rc.ctx(state.storage());
618    let row = parse_rust_rest::get(&ctx, USER_CLASS, user_id, FindOptions::default())
619        .await
620        .map_err(|e| {
621            if e.code == ErrorCode::ObjectNotFound {
622                invalid()
623            } else {
624                e
625            }
626        })?;
627
628    let mut out = row;
629    // Send the token back on the response, because SDKs expect that (`UsersRouter.js:228-229`).
630    out.insert(
631        "sessionToken".to_string(),
632        ParseValue::String(token.to_string()),
633    );
634    Ok(crate::routes::classes::body_of(&out))
635}
636
637/// `POST /logout`.
638///
639/// Deletes the `_Session` row (`UsersRouter.js:509-538`). A request with no token, or with one
640/// that no longer resolves, still answers `{}`.
641pub async fn logout_core(state: &AppState, rc: &RequestContext) -> Result<Json, ParseError> {
642    if let Some(token) = rc.session_token.as_deref() {
643        parse_rust_auth::revoke(state.storage(), token).await?;
644    }
645    Ok(json!({}))
646}
647
648// ---------------------------------------------------------------------------------------------
649// `_User` update policy
650//
651// `PUT /classes/_User/:objectId` is the route `user.save()` compiles to, and opening it to
652// non-master callers means running the stages `RestWrite.transformUser` runs. Reserving the
653// password and the ACL, which is all this module did before, is not enough: it leaves the row's
654// server-controlled columns writable and its identity columns unvalidated.
655// ---------------------------------------------------------------------------------------------
656
657/// `_User` columns a client may never write, whatever the ACL says.
658///
659/// `emailVerified` is the one that matters: it is the output of a verification flow, so a client
660/// that can set it has verified its own email. Upstream refuses it with `OPERATION_FORBIDDEN`
661/// (`RestWrite.js:1543-1556`).
662///
663/// `authData` is refused rather than validated, which is a deliberate fail-closed gap: upstream
664/// hands it to an auth adapter that decides whether the credential is real, and parse-rust has no
665/// adapter host. Accepting it unvalidated would let a client write a third-party identity that a
666/// later login could match on.
667const CLIENT_FORBIDDEN_USER_FIELDS: [&str; 2] = ["emailVerified", "authData"];
668
669/// The noun upstream uses in the refusal, which is not the column name.
670///
671/// `emailVerified` is reported as `email verification` (`RestWrite.js:724`). The message is
672/// contract in the disclosing regime, and a client matching on it would see the difference.
673fn forbidden_label(field: &str) -> &str {
674    match field {
675        "emailVerified" => "email verification",
676        other => other,
677    }
678}
679
680/// Refuse the `_User` columns a client may not set on itself.
681///
682/// **Create and update alike, which is the half signup was missing.** Upstream's
683/// `checkRestrictedFields` sits in the chain `RestWrite.execute` runs for both
684/// (`RestWrite.js:116`, defined at `:716-728`), so `POST /users` is covered there. parse-rust
685/// applied it only on the update path, which left signup able to set both fields on the row it was
686/// creating.
687///
688/// The two fields are here for different reasons and only the first is upstream's:
689///
690/// - `emailVerified` is upstream's own restriction, with upstream's message. A client that can set
691///   it at signup marks its own address verified without ever receiving mail.
692/// - `authData` is **not** in upstream's list, because upstream validates it instead: every
693///   provider block goes to the configured auth adapter, which decides whether the credential is
694///   real (`RestWrite.js:409-460`). parse-rust has no adapter host, so there is nothing to validate
695///   against, and storing the block unvalidated would let a client write a third-party identity
696///   that a later login could match on. Refusing is the fail-closed stand-in until adapters exist,
697///   and it is recorded as a deliberate difference rather than left implicit.
698fn reject_client_restricted_user_fields(
699    body: &WriteBody,
700    rc: &RequestContext,
701    authority: &Authority,
702) -> Result<(), ParseError> {
703    if authority.is_privileged() {
704        return Ok(());
705    }
706    for field in CLIENT_FORBIDDEN_USER_FIELDS {
707        if body.get(field).is_some() {
708            return Err(ParseError::permission_denied(
709                ErrorCode::OperationForbidden,
710                format!(
711                    "Clients aren't allowed to manually update {}.",
712                    forbidden_label(field)
713                ),
714                rc.options.error_detail,
715            ));
716        }
717    }
718    Ok(())
719}
720
721/// The checks that need no database read, run before the write.
722///
723/// **The first is the one that was missing and it is the serious one.** `enforce_class_security`
724/// asks whether a *class* is writable, not whether the caller is anybody. A `_User` row whose ACL
725/// grants public write was therefore updatable by an anonymous request, and because a password
726/// change mints a replacement session, that is account takeover against any user with a permissive
727/// ACL. Upstream refuses an unauthenticated `_User` update outright, before the ACL is consulted
728/// (`RestWrite.js:1572-1576`), and so does this.
729pub(crate) fn enforce_user_update_policy(
730    body: &WriteBody,
731    rc: &RequestContext,
732    authority: &Authority,
733    object_id: &str,
734) -> Result<(), ParseError> {
735    if !authority.is_privileged() && rc.user_id.is_none() {
736        return Err(ParseError::permission_denied(
737            ErrorCode::SessionMissing,
738            format!("Cannot modify user {object_id}."),
739            rc.options.error_detail,
740        ));
741    }
742
743    reject_client_restricted_user_fields(body, rc, authority)?;
744
745    // A non-string password reaches bcrypt upstream and throws out of the hashing library, which
746    // answers a bare 500 (`RestWrite.js:636`, `password.js:17`). Measured: `{"password": null}`
747    // answers `{"code":1,"message":"Internal server error."}`.
748    //
749    // Refused cleanly here instead. Reproducing a 500 has no client value, and the specific shape
750    // matters: `password: null` previously read as "no password" to the hasher and as "a password
751    // change" to the followup, so it revoked every session and issued a replacement while leaving
752    // the old password working. A false report of a security-relevant change is worse than either
753    // behaviour.
754    match body.get("password") {
755        None | Some(FieldWrite::Value(ParseValue::String(_))) => {}
756        Some(_) => {
757            return Err(ParseError::incorrect_type(
758                "password must be a string".to_string(),
759            ))
760        }
761    }
762    Ok(())
763}
764
765/// Force the row's own principal back into a submitted ACL.
766///
767/// Upstream re-adds it after the client's ACL is applied, so a `_User` cannot be made unreadable
768/// or unwritable by its owner. Measured against parse-server 9.10.1-alpha.6: saving
769/// `{"ACL": {"*": {"read": true, "write": true}}}` reads back with the owner entry still present.
770/// Without this a client can lock itself out of its own row, and can do it to another user
771/// wherever an ACL permits the write.
772pub(crate) fn force_owner_into_acl(body: &mut WriteBody, object_id: &str, privileged: bool) {
773    // **Master and maintenance are exempt.** Upstream applies the owner entry only for a
774    // non-privileged caller, so an administrator replacing a user's ACL with one that excludes
775    // them gets exactly that. Forcing it back in unconditionally means an operator cannot revoke a
776    // user's access to their own row, which is a legitimate administrative action and one a
777    // dashboard offers.
778    if privileged {
779        return;
780    }
781    let mut permissions = ParseMap::new();
782    permissions.insert("read".to_string(), ParseValue::Bool(true));
783    permissions.insert("write".to_string(), ParseValue::Bool(true));
784    let owner_entry = ParseValue::Object(permissions);
785
786    match body.get_mut("ACL") {
787        Some(FieldWrite::Value(ParseValue::Object(acl))) => {
788            acl.insert(object_id.to_string(), owner_entry);
789        }
790        // **`{"ACL": {"__op": "Delete"}}` is the other way to replace an ACL, and it was not
791        // covered.** `user.unset("ACL").save()` sends exactly this. Matching only the object form
792        // let it through to the lowering, which cleared both permission columns, and an empty ACL
793        // on `_User` is a row its owner can no longer read or write: their next save answered 101
794        // and the account read as disabled at login. Upstream answers 200 and the row comes back
795        // holding the owner entry alone.
796        //
797        // Rewritten to that value rather than dropped, so the delete still happens: every other
798        // principal goes, the owner stays.
799        Some(entry @ FieldWrite::Op(parse_rust_core::Op::Delete)) => {
800            let mut acl = ParseMap::new();
801            acl.insert(object_id.to_string(), owner_entry);
802            *entry = FieldWrite::Value(ParseValue::Object(acl));
803        }
804        _ => {}
805    }
806}
807
808/// `_validateUserName` and `_validateEmail` (`RestWrite.js:811-884`), for the update path.
809///
810/// **Case-insensitive, and that is the whole point of doing it here rather than leaving it to the
811/// unique indexes.** The indexes parse-rust creates are the plain `username_1` and `email_1`, which
812/// compare case-sensitively, so `CaseOnly` and `caseonly` are two different keys to MongoDB and
813/// both are stored. Upstream refuses the second with 202. The changelog claimed the indexes held
814/// uniqueness; they hold only exact-match uniqueness, and this is the half that was missing.
815///
816/// The comparison runs under upstream's collation, through `QueryOptions::case_insensitive`, which
817/// is upstream's own `{caseInsensitive: true}` find rather than an approximation of it.
818///
819/// The `objectId: {$ne: <self>}` term is load-bearing: without it, saving a row with its own
820/// username unchanged collides with itself.
821pub(crate) async fn validate_user_identity(
822    state: &AppState,
823    rc: &RequestContext,
824    body: &WriteBody,
825    object_id: &str,
826) -> Result<(), ParseError> {
827    // **Username first, then email**, which is `transformUser`'s chain order
828    // (`RestWrite.js:803-807`). A body carrying both a colliding username and a malformed email
829    // reports the username, and checking email first reported the email instead.
830    // **A `Delete` on `username` is refused, and on `email` it is allowed.** The asymmetry is
831    // upstream's and is visible on the wire. `_validateEmail` opens with
832    // `if (!this.data.email || this.data.email.__op === 'Delete') return` (`RestWrite.js:886`);
833    // `_validateUserName` has no such branch, so the op object is truthy, reaches the uniqueness
834    // query as a value, and answers `107 You cannot use [object Object] as a query parameter.`
835    //
836    // Matching only the string form let `user.unset("username").save()` through, removing the
837    // username with no validation at all. The message is upstream's rendering of a query built
838    // from an op object, which is an accident of how it fails rather than a designed error, but it
839    // is the string a client sees.
840    if matches!(body.get("username"), Some(FieldWrite::Op(_))) {
841        return Err(ParseError::invalid_json(
842            "You cannot use [object Object] as a query parameter.",
843        ));
844    }
845    if let Some(FieldWrite::Value(ParseValue::String(username))) = body.get("username") {
846        if taken(state, rc, "username", username, object_id).await? {
847            return Err(ParseError::new(
848                ErrorCode::UsernameTaken,
849                "Account already exists for this username.",
850            ));
851        }
852    }
853
854    let Some(FieldWrite::Value(ParseValue::String(email))) = body.get("email") else {
855        return Ok(());
856    };
857    // `if (!this.data.email ...) return` (`RestWrite.js:886`). An empty string is falsy, so it is
858    // skipped rather than rejected, and upstream answers 200 for `{"email": ""}`.
859    if email.is_empty() {
860        return Ok(());
861    }
862    if !is_valid_email(email) {
863        return Err(ParseError::new(
864            ErrorCode::InvalidEmailAddress,
865            "Email address format is invalid.",
866        ));
867    }
868    if taken(state, rc, "email", email, object_id).await? {
869        return Err(ParseError::new(
870            ErrorCode::EmailTaken,
871            "Account already exists for this email address.",
872        ));
873    }
874    Ok(())
875}
876
877/// `/^.+@.+$/` as JavaScript evaluates it (`RestWrite.js:890`).
878///
879/// Deliberately not an address grammar. Matching upstream's laxness matters more than being right
880/// about RFC 5322, and `a b@c d` is a valid address to this check on both servers.
881///
882/// **Two things about that regex are easy to get wrong, and a naive `find('@')` gets both wrong.**
883///
884/// `.` does not match a line terminator, and JavaScript counts four: `\n`, `\r`, U+2028 and
885/// U+2029. Since `^` and `$` with no `m` flag anchor to the whole string, and `.+@.+` has to span
886/// it, a line terminator *anywhere* means no match. The two Unicode ones are the trap: they are
887/// invisible in most editors and are not what `char::is_whitespace` or a `\n` check would catch.
888/// Ordinary spaces and tabs are not line terminators and are accepted.
889///
890/// And the `@` may be neither the first nor the last character, but need not be the only one:
891/// `.` matches `@` too, so `@a@b` and `a@b@` both match through an interior `@`. Looking at only
892/// the first or only the last `@` therefore answers wrongly for one of those two.
893///
894/// Verified against Node itself for each case in the test below.
895fn is_valid_email(email: &str) -> bool {
896    const LINE_TERMINATORS: [char; 4] = ['\n', '\r', '\u{2028}', '\u{2029}'];
897    if email.chars().any(|c| LINE_TERMINATORS.contains(&c)) {
898        return false;
899    }
900    // At least one `@` strictly inside, which is what `.+@.+` requires.
901    let chars: Vec<char> = email.chars().collect();
902    chars.len() >= 3 && chars[1..chars.len() - 1].contains(&'@')
903}
904
905/// Does another `_User` already hold this value, compared case-insensitively?
906///
907/// An exact-equality constraint run under upstream's collation, which is upstream's own mechanism
908/// (`RestWrite.js:818-826` passing `{caseInsensitive: true}`). An anchored `/i` regex was the
909/// first shape of this and it was wrong in a way worth recording: a collation at strength 2
910/// normalizes, so a precomposed `Café` and a decomposed `Cafe` plus a combining acute are one key
911/// to it and two distinct byte strings to any regex. The regex therefore admitted identities
912/// upstream treats as duplicates, which is the direction that matters for a uniqueness check.
913///
914/// Note what strength 2 does *not* do: it is case-insensitive but diacritic-**sensitive**, so
915/// `Café` and `Cafe` remain different identities. Secondary strength ignores case and normalizes
916/// equivalent Unicode forms; it does not fold accents away.
917///
918/// The `objectId: {$ne: <self>}` term is load-bearing: without it, saving a row with its own
919/// username unchanged collides with itself.
920async fn taken(
921    state: &AppState,
922    rc: &RequestContext,
923    field: &str,
924    value: &str,
925    object_id: &str,
926) -> Result<bool, ParseError> {
927    let query = Query::from_constraints(vec![
928        Constraint::equal(field, ParseValue::String(value.to_string())),
929        Constraint {
930            field: "objectId".to_string(),
931            comparison: parse_rust_storage::Comparison::NotEqual(ParseValue::String(
932                object_id.to_string(),
933            )),
934        },
935    ]);
936    // The request's own snapshot, not a second read. One request evaluates one schema.
937    let schema = rc.snapshot.get_or_default(USER_CLASS);
938    let rows = state
939        .storage()
940        .find(
941            &schema,
942            &query,
943            &QueryOptions {
944                limit: Some(1),
945                case_insensitive: true,
946                ..QueryOptions::default()
947            },
948        )
949        .await?;
950    Ok(!rows.is_empty())
951}
952
953#[cfg(test)]
954mod tests {
955
956    fn row(username: &str, id: &str) -> ParseMap {
957        let mut m = ParseMap::new();
958        m.insert("objectId".into(), ParseValue::String(id.into()));
959        m.insert("username".into(), ParseValue::String(username.into()));
960        m
961    }
962
963    /// The exact username wins **regardless of the order the database returned the rows in**.
964    ///
965    /// This is the assertion the integration test cannot make. An `$or` over two indexes has no
966    /// defined result order, so end to end the naive `limit: 1` implementation passes whenever
967    /// MongoDB happens to hand back the right row, which it usually does. Here the adverse order is
968    /// simply the input.
969    #[test]
970    fn the_exact_username_wins_over_a_matching_email_in_either_order() {
971        let target = row("collide@example.com", "TARGET");
972        let other = row("other_user", "OTHER");
973
974        for rows in [
975            vec![other.clone(), target.clone()],
976            vec![target.clone(), other.clone()],
977        ] {
978            let picked = select_login_row(rows, Some("collide@example.com")).expect("a row");
979            assert!(
980                matches!(picked.get("objectId"), Some(ParseValue::String(id)) if id == "TARGET"),
981                "the username owner is chosen whichever row came first"
982            );
983        }
984    }
985
986    /// More than one match with no submitted username, which is what upstream crashes on.
987    #[test]
988    fn a_multi_match_without_a_username_falls_back_to_the_first_row() {
989        let rows = vec![row("a", "FIRST"), row("b", "SECOND")];
990        let picked = select_login_row(rows, None).expect("a row");
991        assert!(matches!(picked.get("objectId"), Some(ParseValue::String(id)) if id == "FIRST"));
992    }
993
994    /// Every case measured against Node's own evaluation of `/^.+@.+$/`, which is the only
995    /// authority for what that regex accepts.
996    #[test]
997    fn email_validity_matches_javascripts_regex() {
998        for (email, expected) in [
999            ("a@b", true),
1000            ("ab@cd", true),
1001            // `.` matches `@`, so an interior one is enough and there may be more than one.
1002            ("@a@b", true),
1003            ("a@b@", true),
1004            // Spaces and tabs are ordinary characters to `.`.
1005            ("a b@c d", true),
1006            ("a\t@b", true),
1007            ("not-an-email", false),
1008            ("a@", false),
1009            ("@a", false),
1010            ("@", false),
1011            ("", false),
1012            // The four JavaScript line terminators, which `.` never matches. The last two are
1013            // invisible in most editors and are the reason this is not a `\n` check.
1014            ("a\n@b", false),
1015            ("a@\nb", false),
1016            ("a\r@b", false),
1017            ("a@\rb", false),
1018            ("a\u{2028}@b", false),
1019            ("a@\u{2029}b", false),
1020        ] {
1021            assert_eq!(
1022                is_valid_email(email),
1023                expected,
1024                "{email:?} ({:?})",
1025                email.chars().map(|c| c as u32).collect::<Vec<_>>()
1026            );
1027        }
1028    }
1029    use super::*;
1030
1031    fn body(json: &str) -> WriteBody {
1032        parse_rust_rest::decode_write_body(
1033            &serde_json::from_str(json).expect("test literal"),
1034            parse_rust_core::op::OpPath::Create,
1035        )
1036        .expect("decode")
1037    }
1038
1039    #[tokio::test]
1040    async fn shared_user_transform_removes_plaintext_and_writes_a_bcrypt_hash() {
1041        let mut b = body(r#"{"password":"hunter2"}"#);
1042        prepare_user_write(&mut b, false)
1043            .await
1044            .expect("password hashes");
1045
1046        assert!(!b.contains_key("password"));
1047        let Some(FieldWrite::Value(ParseValue::String(hash))) = b.get(HASHED_PASSWORD) else {
1048            panic!("hash missing");
1049        };
1050        assert!(parse_rust_auth::password::verify("hunter2".into(), hash.clone()).await);
1051    }
1052
1053    #[test]
1054    fn user_owner_acl_is_added_without_discarding_a_master_supplied_acl() {
1055        let mut b = body(r#"{"objectId":"user123456","ACL":{"*":{"read":true}}}"#);
1056        let object_id = ensure_user_identity_and_acl(&mut b);
1057
1058        assert_eq!(object_id, "user123456");
1059        let Some(FieldWrite::Value(ParseValue::Object(acl))) = b.get("ACL") else {
1060            panic!("ACL missing");
1061        };
1062        assert!(acl.contains_key("*"));
1063        let Some(ParseValue::Object(owner)) = acl.get("user123456") else {
1064            panic!("owner ACL missing");
1065        };
1066        assert!(matches!(owner.get("read"), Some(ParseValue::Bool(true))));
1067        assert!(matches!(owner.get("write"), Some(ParseValue::Bool(true))));
1068    }
1069
1070    /// An update must not acquire an ACL or an objectId it did not ask for.
1071    #[tokio::test]
1072    async fn an_update_is_only_hashed() {
1073        let mut b = body(r#"{"password":"x","nickname":"n"}"#);
1074        prepare_user_write(&mut b, false).await.expect("hash");
1075        assert!(!b.contains_key("ACL"));
1076        assert!(!b.contains_key("objectId"));
1077    }
1078}