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.
111///
112/// **Upstream's two tests are `!ACL` and then a property assignment onto a JavaScript object**
113/// (`RestWrite.js:1676-1686`, literally `var ACL = this.data.ACL; if (!ACL) { ACL = {}; ... }`
114/// followed by `ACL[objectId] = ...`). Reading either of them as `ParseValue::Object` produced a
115/// **publicly readable `_User`**, and 0.2.0 shipped both mistakes:
116///
117/// - `null`, `false`, `0` and `""` are falsy, so upstream replaces them. Left in place here they
118///   reached `lower_acl`, which drops a falsy ACL without writing `_rperm` or `_wperm`.
119/// - An op envelope, an array and a tagged value are all **objects** in JavaScript, so upstream
120///   assigns the owner into them. Skipped here, `{"__op":"Delete"}` was then removed from the body
121///   by `flatten_for_create`, leaving no permission columns at all.
122///
123/// Both measured against a parse-server at the pin: each of those seven values answered 201 on
124/// both servers. **Five of them produced a public row and two did not.** The four falsy values and
125/// `{"__op":"Delete"}` left no permission columns at all, so an anonymous read answered 200 here
126/// and 404 upstream. The array and the tagged value left two *empty* columns, which is master-only,
127/// so both servers answered 404 and the defect there was the missing owner rather than a
128/// disclosure. A non-`Delete` operation behaved like the array.
129///
130/// The one case left alone is a truthy **scalar**, where upstream throws and answers a bare 500.
131/// See the `Shape` enum below.
132pub(crate) fn ensure_user_identity_and_acl(body: &mut WriteBody) -> Result<String, ParseError> {
133    // **A truthy non-string `objectId` is refused, not replaced.** Upstream's substitution test is
134    // `if (!this.data.objectId)` (`RestWrite.js:429-431`), so it replaces an absent or falsy id and
135    // leaves a truthy one in place, where `enforceFieldExists` then compares it against the
136    // `String` type of the default column and answers `INCORRECT_TYPE`.
137    //
138    // Reading "not a string" as "absent" instead is what generated an id and carried on. Measured
139    // at the pin with `allowCustomObjectId` enabled and a body of `{"objectId": 123, ...}`, through
140    // both `POST /users` and `POST /classes/_User`: upstream answers 400 code 111 and writes no
141    // row, and parse-rust answered 201 with an id the client never asked for, persisted the user
142    // and issued a session for it.
143    //
144    // **That agreement holds for every shape with an inferable type and not for `{"__op":"Delete"}`.**
145    // Upstream infers no type for a `Delete`, skips the field check, and answers 201 having stored
146    // the row under a Mongo-generated `_id` while echoing the operation object back as the
147    // `objectId`. parse-rust refuses it with 107 below. That difference is deliberate, recorded,
148    // and scoped for 0.3.0; it is not the 111 case.
149    //
150    // The pipeline already makes exactly this check for every other class. It could never fire for
151    // `_User`, because this function had rewritten the key before the pipeline saw the body, so
152    // the class with the strongest reason to have it was the one class that did not.
153    if let Some(write) = body.get("objectId") {
154        let truthy_non_string = match write {
155            FieldWrite::Value(ParseValue::String(_)) => false,
156            FieldWrite::Value(v) => parse_rust_core::is_js_truthy(v),
157            // An op envelope is a truthy object, so it is never a substitutable absent id.
158            FieldWrite::Op(_) => true,
159        };
160        if truthy_non_string {
161            let got = match write {
162                FieldWrite::Value(v) => parse_rust_schema::infer_type(v),
163                FieldWrite::Op(op) => parse_rust_schema::infer_op_type(op)?,
164            };
165            return Err(match got {
166                Some(got) => parse_rust_schema::infer::schema_mismatch(
167                    USER_CLASS,
168                    "objectId",
169                    &parse_rust_storage::FieldType::String,
170                    &got,
171                ),
172                None => ParseError::invalid_json("objectId is an invalid field name."),
173            });
174        }
175    }
176
177    // Whatever is left is absent, falsy, or a non-empty string, because the truthy non-string
178    // cases returned above. **The empty string belongs with `null`, not with a usable id**:
179    // `take_string` answered `Some("")` for it, so a body carrying `{"objectId": ""}` created a
180    // `_User` whose objectId was the empty string, and every ACL entry naming it named nothing.
181    // Upstream's `!this.data.objectId` is falsiness, so it generates one, and the pipeline already
182    // agrees for every other class.
183    let existing = match body.get("objectId") {
184        Some(FieldWrite::Value(ParseValue::String(s))) if !s.is_empty() => Some(s.clone()),
185        _ => None,
186    };
187    let object_id = match existing {
188        Some(id) => id,
189        None => {
190            let id = parse_rust_core::new_object_id();
191            body.insert(
192                "objectId".to_string(),
193                FieldWrite::Value(ParseValue::String(id.clone())),
194            );
195            id
196        }
197    };
198
199    let mut permissions = ParseMap::new();
200    permissions.insert("read".to_string(), ParseValue::Bool(true));
201    permissions.insert("write".to_string(), ParseValue::Bool(true));
202
203    // **Which shape the submitted `ACL` is, in JavaScript's terms rather than in ours.** Upstream
204    // does `ACL[objectId] = {read, write}` on whatever `this.data.ACL` holds, so the only question
205    // that matters is what that assignment produces, and the answer is one of three things.
206    //
207    // The trap this encodes: **an op envelope, an array and a tagged value are all objects in
208    // JavaScript and none of them is `ParseValue::Object` here.** Matching only on `Object` looks
209    // like "the client sent an ACL" and is not. Measured at the pin, one of them was a public
210    // `_User`: `{"ACL":{"__op":"Delete"}}` on signup answered 201 on both servers, and an
211    // anonymous read of that user then answered 200 here and 404 upstream.
212    enum Shape {
213        /// Absent, or falsy, which upstream's `if (!ACL)` treats identically. A fresh object
214        /// holding the owner and nothing else.
215        Replace,
216        /// A principal map. The owner is added alongside whatever the caller sent.
217        Merge,
218        /// A truthy scalar. Upstream throws a `TypeError` out of the assignment and answers a bare
219        /// 500 (measured for `"nonsense"`, `123` and `true`). There is no correct answer to
220        /// reproduce, so the value is left for the ACL validator and `lower_acl` writes two empty
221        /// permission columns, which is master-only rather than public.
222        LeaveAlone,
223    }
224    let shape = match body.get("ACL") {
225        None => Shape::Replace,
226        Some(FieldWrite::Value(v)) if !parse_rust_core::is_js_truthy(v) => Shape::Replace,
227        Some(FieldWrite::Value(ParseValue::Object(_))) => Shape::Merge,
228        Some(FieldWrite::Value(
229            ParseValue::Bool(_) | ParseValue::Number(_) | ParseValue::String(_),
230        )) => Shape::LeaveAlone,
231        // **Every remaining case is a JavaScript object**, so upstream assigns the owner onto it
232        // and the owner is normally the only entry that contributes a permission: an op envelope
233        // has `__op`, `objects` and `amount`, a tagged value has `__type` and its payload, and an
234        // array has indices. Measured at the pin for `{"__op":"Delete"}`, `[]`, `[1,2]` and a
235        // tagged Date: all four come back owner-only.
236        //
237        // **"Normally" is doing work, and an array is the exception.** `[{"read":true}]` grants
238        // principal `"0"` upstream, because an index *is* a property name and that element does
239        // carry a `read`. Collapsing to owner-only drops it, which is narrower than upstream and
240        // is a recorded Tier 3 row rather than a claim that it cannot happen. An earlier version of
241        // this comment asserted arrays could never contribute a permission, which is false.
242        Some(FieldWrite::Op(_)) | Some(FieldWrite::Value(_)) => Shape::Replace,
243    };
244    if matches!(shape, Shape::Replace) {
245        let mut acl = ParseMap::new();
246        acl.insert(object_id.clone(), ParseValue::Object(permissions));
247        body.insert(
248            "ACL".to_string(),
249            FieldWrite::Value(ParseValue::Object(acl)),
250        );
251        return Ok(object_id);
252    }
253
254    if let (Shape::Merge, Some(FieldWrite::Value(ParseValue::Object(acl)))) =
255        (shape, body.get_mut("ACL"))
256    {
257        acl.insert(object_id.clone(), ParseValue::Object(permissions));
258    }
259    Ok(object_id)
260}
261
262/// `handleCreate`'s `role:`-prefixed objectId refusal (`ClassesRouter.js:105-112`).
263///
264/// A user whose objectId is `role:Admins` is granted that role by every ACL check, because an ACL
265/// names a role by string and the caller's ACL group carries its own objectId.
266///
267/// **On `ClassesRouter`, not on `UsersRouter`**, which is the detail that matters for where this
268/// is called from. `UsersRouter extends ClassesRouter` and does not override `handleCreate`
269/// (`UsersRouter.js:23`, `:824-826`), so one guard covers `POST /users` and `POST /classes/_User`
270/// alike. parse-rust had it on the signup route only, which left the class route uncovered for
271/// the master key.
272pub(crate) fn reject_role_prefixed_object_id(
273    body: &WriteBody,
274    rc: &RequestContext,
275) -> Result<(), ParseError> {
276    let Some(FieldWrite::Value(ParseValue::String(id))) = body.get("objectId") else {
277        // `typeof req.body?.objectId === 'string'` guards it upstream, so a non-string is not
278        // refused here. It is refused by schema validation instead.
279        return Ok(());
280    };
281    if !id.starts_with("role:") {
282        return Ok(());
283    }
284    // `createSanitizedError` (`ClassesRouter.js:111`). Note this is the sanitized twin of
285    // `Auth.js`'s identically worded refusal, which is a plain `Parse.Error` and stays detailed;
286    // the two are different call sites with the same string.
287    Err(ParseError::permission_denied(
288        ErrorCode::OperationForbidden,
289        "Invalid object ID.",
290        rc.options.error_detail,
291    ))
292}
293
294/// A created `_User` must carry a non-empty username and a non-empty password
295/// (`RestWrite.js:468-473`).
296///
297/// **Not gated on the caller.** Upstream's guard is `!this.query && !hasAuthData`, which asks
298/// whether this is a create and whether an auth adapter is supplying the identity instead. Master
299/// is not exempt, so `POST /classes/_User` with the master key is subject to it too. Without that,
300/// the dashboard's own route admitted a user with no username, or a passwordless row that no
301/// login can ever match and that `verify_dummy` exists to make indistinguishable.
302///
303/// Runs **before** identity validation and hashing, which is upstream's order: `validateAuthData`
304/// is stage 5 of the chain and `transformUser` is stage 12 (`RestWrite.js:122-141`). Checking a
305/// uniqueness query and paying a bcrypt cost for a body that was never well-formed is work done on
306/// behalf of a request that cannot succeed.
307///
308/// The `authData` branch is not modelled: parse-rust refuses `authData` outright, so the only
309/// reachable case is the one that requires both fields. When auth adapters land, this guard grows
310/// the second condition rather than moving.
311pub(crate) fn require_create_credentials(body: &WriteBody) -> Result<(), ParseError> {
312    if take_string(body, "username")
313        .filter(|u| !u.is_empty())
314        .is_none()
315    {
316        return Err(ParseError::new(
317            ErrorCode::UsernameMissing,
318            "bad or missing username",
319        ));
320    }
321    if take_string(body, "password")
322        .filter(|p| !p.is_empty())
323        .is_none()
324    {
325        // No trailing period. Upstream's string is `password is required`, and the message is
326        // contract in the disclosing regime.
327        return Err(ParseError::new(
328            ErrorCode::PasswordMissing,
329            "password is required",
330        ));
331    }
332    Ok(())
333}
334
335/// `POST /users`. Signup.
336pub async fn signup_core(
337    state: &AppState,
338    rc: &RequestContext,
339    authority: &Authority,
340    body: &Json,
341) -> Result<Json, ParseError> {
342    let mut body = parse_rust_rest::decode_write_body(body, parse_rust_core::op::OpPath::Create)?;
343    // Before anything else: a signup body must not carry a `_hashed_password` of the caller's
344    // choosing.
345    parse_rust_rest::reject_reserved_keys_in(body.keys().map(String::as_str))?;
346    // Signup is a create like any other, so the objectId policy applies to it
347    // (`RestWrite.js:50-65`). It has to run here rather than inside the pipeline, because
348    // `ensure_user_identity_and_acl` below puts a server-generated objectId into the body.
349    parse_rust_rest::enforce_object_id_policy(&body, state.config().allow_custom_object_id)?;
350
351    reject_role_prefixed_object_id(&body, rc)?;
352    // Upstream runs this on create as well as update (`RestWrite.js:116`), and running it only on
353    // the update path left signup able to set `emailVerified` and `authData` on its own new row.
354    reject_client_restricted_user_fields(&body, rc, authority)?;
355
356    require_create_credentials(&body)?;
357
358    // **The `create` permission is checked before any identity work** (`RestWrite.js:730-746`,
359    // whose own comment names this exact hazard). `validate_user_identity` queries `_User` by
360    // username and email, and the password is then hashed, so a signup that the CLP will refuse
361    // otherwise answers 202 in milliseconds for a name that exists and 119 after a bcrypt-length
362    // pause for one that does not. Measured on a closed `_User.create`: ~4 ms against ~196 ms.
363    //
364    // That is account enumeration against a class whose whole point is that outsiders may not read
365    // it, and no response body discloses it: the difference is entirely in which check runs first.
366    // Master and maintenance skip the gate, as upstream's `isMaster || isMaintenance` does.
367    if !authority.is_privileged() {
368        parse_rust_rest::validate_permission(
369            rc.snapshot.clp(USER_CLASS),
370            USER_CLASS,
371            &rc.scope.acl_group(),
372            parse_rust_core::Operation::Create,
373            None,
374            rc.options.error_detail,
375        )?;
376    }
377
378    // **Signup runs the same identity validation an update does**, because `transformUser` is one
379    // function and does not branch on create versus update for these two checks
380    // (`RestWrite.js:803-807`). Validating on the update path alone left signup admitting exactly
381    // the identities the update path refuses: a case-only duplicate username, and an email that is
382    // not one.
383    //
384    // The objectId is not known yet, so the `$ne` exclusion is given a value no row can hold. On a
385    // create there is no self to exclude, which is the same thing upstream's `this.objectId()`
386    // returning undefined achieves.
387    validate_user_identity(state, rc, &body, "").await?;
388
389    prepare_user_write(&mut body, true).await?;
390
391    let ctx = rc.ctx(state.storage());
392    let created = parse_rust_rest::create(&ctx, USER_CLASS, body)
393        .await
394        .map_err(map_duplicate)?;
395
396    let session = create_session(
397        state.storage(),
398        &state.config().session,
399        NewSession {
400            user_object_id: &created.object_id,
401            created_with: Some(CreatedWith::signup(None)),
402            installation_id: rc.installation_id.as_deref(),
403        },
404    )
405    .await?;
406
407    Ok(json!({
408        "objectId": created.object_id,
409        "createdAt": created.created_at.to_iso(),
410        "sessionToken": session.session_token,
411    }))
412}
413
414/// Turn a duplicate-key error into the code the SDK expects (`RestWrite.js:1697-1716`).
415///
416/// A `username_1` collision must be 202 `USERNAME_TAKEN`, not a bare 137. Which field collided is
417/// read from the adapter's out-of-band [`ParseError::duplicated_field`], which the adapter fills
418/// in by recognising the auto-generated index name. That is why index names are contractual, and
419/// it is why nothing here reads `message`: the message is the fixed
420/// `A duplicate value for a field with unique values was provided` in every case, and the driver
421/// text it replaced named the database and the colliding value.
422///
423/// **Not modelled: upstream's fallback.** When it cannot recover the field, upstream re-queries
424/// `_User` by username and then by email before settling for 137 (`RestWrite.js:1718-1755`). The
425/// only index Parse creates that reaches that path is a case-insensitive one, which parse-rust
426/// does not create, so a collision it cannot attribute stays 137 here.
427pub(crate) fn map_duplicate(e: ParseError) -> ParseError {
428    if e.code != ErrorCode::DuplicateValue {
429        return e;
430    }
431    match e.duplicated_field() {
432        Some("username") => ParseError::new(
433            ErrorCode::UsernameTaken,
434            "Account already exists for this username.",
435        ),
436        Some("email") => ParseError::new(
437            ErrorCode::EmailTaken,
438            "Account already exists for this email address.",
439        ),
440        _ => e,
441    }
442}
443
444/// `POST /login`.
445pub async fn login_core(
446    state: &AppState,
447    rc: &RequestContext,
448    authority: &Authority,
449    body: &Json,
450) -> Result<Json, ParseError> {
451    let body = parse_rust_rest::decode_write_body(body, parse_rust_core::op::OpPath::Create)?;
452
453    // **Three refusals in upstream's order, each with its own code** (`UsersRouter.js:84-96`).
454    // Collapsing them into one `USERNAME_MISSING`, which is what this did, is wire-visible twice
455    // over: a client with no password got the username error, and a client with a non-string
456    // password got it too where upstream answers `OBJECT_NOT_FOUND`.
457    //
458    // **Each guard tests JavaScript truthiness of the raw value, not "is it a non-empty string".**
459    // The distinction decides which of the three fires: `{"username": 7}` is truthy, so it passes
460    // the first guard and is refused by the third as a type error, where testing for a string here
461    // would report a missing username instead.
462    let has_username = body_has_truthy(&body, "username");
463    let has_email = body_has_truthy(&body, "email");
464    if !has_username && !has_email {
465        return Err(ParseError::new(
466            ErrorCode::UsernameMissing,
467            "username/email is required.",
468        ));
469    }
470    if !body_has_truthy(&body, "password") {
471        return Err(ParseError::new(
472            ErrorCode::PasswordMissing,
473            "password is required.",
474        ));
475    }
476    // A truthy non-string password, username or email is the third refusal, and it deliberately
477    // answers the same thing a wrong password does rather than naming the type: telling a caller
478    // its password was the wrong *type* is one bit more than upstream gives away here.
479    let invalid_credentials =
480        || ParseError::new(ErrorCode::ObjectNotFound, "Invalid username/password.");
481    let username = take_string(&body, "username");
482    let email = take_string(&body, "email");
483    let Some(password) = take_string(&body, "password") else {
484        return Err(invalid_credentials());
485    };
486    if (has_username && username.is_none()) || (has_email && email.is_none()) {
487        return Err(invalid_credentials());
488    }
489
490    // Straight to the adapter. See the module note: the read pipeline strips the hash this has to
491    // check, and the query is built here from the identifier rather than from anything the client
492    // shaped, so nothing client-supplied reaches storage unfiltered.
493    //
494    // **The `$or` is what makes logging in with an email address work** (`UsersRouter.js:99-107`).
495    // Given only an identifier, upstream matches it against `username` *or* `email`, which is what
496    // every SDK's `Parse.User.logIn(emailAddress, password)` relies on. Matching `username` alone,
497    // which is what this did, answered `Invalid username/password.` for a correct email and
498    // password, and an `email` key was not read at all.
499    let schema = rc.snapshot.get_or_default(USER_CLASS);
500    let identifier = username.filter(|_| has_username);
501    // Kept for the multi-row preference below, which compares against the submitted username.
502    let username_for_preference = identifier.clone();
503    let email = email.filter(|_| has_email);
504    let query = match (identifier, email) {
505        // Both given: an AND, so a mismatched pair is not a login.
506        (Some(username), Some(email)) => Query::from_constraints(vec![
507            Constraint::equal("email", ParseValue::String(email)),
508            Constraint::equal("username", ParseValue::String(username)),
509        ]),
510        (None, Some(email)) => {
511            Query::from_constraints(vec![Constraint::equal("email", ParseValue::String(email))])
512        }
513        // The identifier arrived as `username` and may be either.
514        (Some(identifier), None) => Query::any_of(vec![
515            Query::from_constraints(vec![Constraint::equal(
516                "username",
517                ParseValue::String(identifier.clone()),
518            )]),
519            Query::from_constraints(vec![Constraint::equal(
520                "email",
521                ParseValue::String(identifier),
522            )]),
523        ]),
524        // Unreachable: the first guard refused the case where neither is truthy, and the third
525        // refused the case where a truthy one is not a string.
526        (None, None) => return Err(invalid_credentials()),
527    };
528
529    // **No limit.** Upstream passes an empty options object (`UsersRouter.js:108-110`), and the
530    // reason surfaces one line down: an account whose email equals another account's username
531    // matches both rows, and upstream resolves that by preferring the exact username match. Capping
532    // the query at one row makes the winner whichever row MongoDB returns first, which can reject a
533    // valid login or, if the two passwords happen to match, authenticate the wrong account.
534    let rows = state
535        .storage()
536        .find(&schema, &query, &QueryOptions::default())
537        .await?;
538
539    // One error for "no such user" and for "wrong password", so login cannot be used to
540    // enumerate accounts. Upstream does the same.
541    let invalid = || ParseError::new(ErrorCode::ObjectNotFound, "Invalid username/password.");
542
543    // `results.filter(user => user.username === username)[0]` (`UsersRouter.js:121-124`). Upstream
544    // logs a warning here; there is no logger yet, so the preference is applied silently. Falling
545    // back to the first row covers the case upstream would crash on, where more than one row
546    // matched but the identifier arrived as an `email` key and no row's username can equal it.
547    let row = select_login_row(rows, username_for_preference.as_deref());
548
549    // **Both failure paths below pay the bcrypt cost.** Returning early makes a missing account
550    // answer in microseconds where a real one answers in milliseconds, and that difference is
551    // measurable across a network, so the shared error message stops hiding which accounts exist.
552    // Upstream runs the same dummy compare in both branches (`UsersRouter.js:112-118`, `:132-136`).
553    let Some(row) = row else {
554        parse_rust_auth::password::verify_dummy(password).await;
555        return Err(invalid());
556    };
557    let hash = match row.get(HASHED_PASSWORD) {
558        Some(ParseValue::String(hash)) if !hash.is_empty() => hash.clone(),
559        // A passwordless account, which an auth-adapter signup produces upstream. Never a valid
560        // password login, and it must not be a fast one either.
561        _ => {
562            parse_rust_auth::password::verify_dummy(password).await;
563            return Err(invalid());
564        }
565    };
566    if !parse_rust_auth::password::verify(password, hash).await {
567        return Err(invalid());
568    }
569
570    // **An explicitly empty ACL is a disabled account** (`UsersRouter.js:151-153`). A master caller
571    // setting `ACL: {}` is the documented way to lock a user out, and without this the account
572    // still logs in and receives a working session, so the lock does nothing until its existing
573    // sessions are separately destroyed.
574    //
575    // `authority.is_master()` rather than `rc.is_master()`: upstream's guard is `!req.auth.isMaster`
576    // alone, so **maintenance is subject to the check**, and `rc.is_master()` answers for the ACL
577    // scope, which treats master and maintenance alike.
578    if !authority.is_master() && acl_is_explicitly_empty(&row) {
579        return Err(invalid());
580    }
581
582    let Some(ParseValue::String(object_id)) = row.get("objectId") else {
583        // Nothing upstream throws here, because a row without an objectId cannot exist through
584        // any write path. If one does, the shape of the stored row is not the client's business.
585        return Err(ParseError::internal("stored user has no objectId"));
586    };
587
588    let session = create_session(
589        state.storage(),
590        &state.config().session,
591        NewSession {
592            user_object_id: object_id,
593            created_with: Some(CreatedWith::login(None)),
594            installation_id: rc.installation_id.as_deref(),
595        },
596    )
597    .await?;
598
599    // **Re-fetch under the caller's own auth before answering** (`UsersRouter.js:349-387`).
600    //
601    // The row above came from a direct adapter read, deliberately below the pipeline, because the
602    // password check needs the hash that `filterSensitiveData` strips. That read answers to
603    // nothing: not `_User` `get` CLP, not `protectedFields`, not the object's ACL. Returning it is
604    // how a deployment that protects `email` still puts `email` on the wire at every login, and
605    // the response looks completely ordinary while it happens.
606    //
607    // `strip_sensitive` is a denylist over the columns login itself must not echo. It is not an
608    // authorization filter and cannot become one, because the fields at issue are configured per
609    // deployment and are ordinary columns.
610    let object_id = object_id.to_string();
611    let mut out = refetch_for_response(state, rc, &object_id, row).await?;
612    out.insert(
613        "sessionToken".to_string(),
614        ParseValue::String(session.session_token),
615    );
616    Ok(crate::routes::classes::body_of(&out))
617}
618
619/// Choose the account a login refers to when the identifier matched more than one row.
620///
621/// `results.filter(user => user.username === username)[0]` (`UsersRouter.js:121-124`). One user's
622/// email can equal another's username, and the `$or` matches both; upstream prefers the exact
623/// username and logs a warning. There is no logger yet, so the preference is applied silently.
624///
625/// **A pure function so it can be tested against the adverse order.** The integration test cannot
626/// force which row MongoDB returns first from an `$or`, so it passes against a `limit: 1`
627/// implementation whenever the database happens to return the right one, which is most of the
628/// time: a test that reports success for the bug it was written to catch. Deciding here, over a
629/// list the caller supplies, is what makes the failing case reachable on demand.
630///
631/// Falling back to the first row covers what upstream crashes on: more than one match when the
632/// identifier arrived as an `email` key, so no row's username can equal it and
633/// `results.filter(...)[0]` is `undefined`.
634fn select_login_row(rows: Vec<ParseMap>, submitted_username: Option<&str>) -> Option<ParseMap> {
635    if rows.len() <= 1 {
636        return rows.into_iter().next();
637    }
638    let mut rows = rows;
639    let exact = submitted_username.and_then(|name| {
640        rows.iter()
641            .position(|r| matches!(r.get("username"), Some(ParseValue::String(u)) if u == name))
642    });
643    match exact {
644        Some(i) => Some(rows.swap_remove(i)),
645        None => rows.into_iter().next(),
646    }
647}
648
649/// The authenticated user's own view of their row, for a login response.
650///
651/// Master and maintenance keep the raw row: they bypass CLP and `protectedFields` everywhere else,
652/// so re-reading would only narrow a view they are entitled to, and an empty result for them is a
653/// genuine not-found rather than a denial (`UsersRouter.js:378-387`).
654///
655/// **A denied or empty re-fetch falls back to the identity alone, never to the raw row.** That is
656/// upstream's explicit choice at `:376` and it is the whole point: the fallback is reached exactly
657/// when access control refused the record, which is the case where returning the raw row would
658/// disclose the most. Login still succeeds, because authentication and authorization are separate
659/// questions and passing the first does not entitle the caller to read the row.
660async fn refetch_for_response(
661    state: &AppState,
662    rc: &RequestContext,
663    object_id: &str,
664    row: ParseMap,
665) -> Result<ParseMap, ParseError> {
666    if rc.is_master() {
667        return Ok(parse_rust_rest::acl::raise_acl(strip_sensitive(row)));
668    }
669
670    let identity_only = || {
671        let mut map = ParseMap::new();
672        map.insert(
673            "objectId".to_string(),
674            ParseValue::String(object_id.to_string()),
675        );
676        map
677    };
678
679    // The caller is whoever just authenticated, not whoever the request arrived as. A login
680    // carrying somebody else's token still answers about the account whose password was verified.
681    let roles = parse_rust_auth::expand_roles(
682        state.storage(),
683        parse_rust_auth::RolePrincipal::User(object_id),
684    )
685    .await?;
686    let scope = parse_rust_rest::AclScope::user(
687        object_id.to_string(),
688        roles.iter().map(|r| r.as_str().to_string()).collect(),
689    )?;
690
691    let ctx = parse_rust_rest::Ctx::new(state.storage(), &rc.snapshot, &scope, &rc.options);
692    match parse_rust_rest::get(&ctx, USER_CLASS, object_id, FindOptions::default()).await {
693        Ok(row) => Ok(row),
694        // Any refusal, not just `ObjectNotFound`: a CLP of `get: {}` answers
695        // `OPERATION_FORBIDDEN`, and both mean access control withheld the row.
696        Err(_) => Ok(identity_only()),
697    }
698}
699
700/// Is the row's ACL present and empty, which upstream treats as a disabled account?
701///
702/// Upstream tests `user.ACL && Object.keys(user.ACL).length == 0` on the rehydrated object
703/// (`UsersRouter.js:151`). The stored form is `_rperm`/`_wperm`, so this raises them the same way a
704/// read would and asks whether the result is an ACL with no entries. A row with neither column has
705/// no ACL at all and is not disabled, which is the `user.ACL &&` half of upstream's test.
706fn acl_is_explicitly_empty(row: &ParseMap) -> bool {
707    matches!(
708        parse_rust_rest::acl::raise_acl(row.clone()).get("ACL"),
709        Some(ParseValue::Object(acl)) if acl.is_empty()
710    )
711}
712
713/// `GET /users/me`.
714///
715/// The token is validated first, then the user is re-fetched **with the caller's own auth**
716/// (`UsersRouter.js:214-223`) so protected fields and CLP apply. Both failures answer
717/// `Invalid session token`, which is a different string from `/sessions/me`'s.
718pub async fn me_core(state: &AppState, rc: &RequestContext) -> Result<Json, ParseError> {
719    // `createSanitizedError` at all three of `handleMe`'s refusals (`UsersRouter.js:193`, `:211`,
720    // `:225`). `GET /sessions/me` is a different router with different strings and is not
721    // sanitized upstream, so it stays detailed.
722    let invalid = || {
723        ParseError::permission_denied(
724            ErrorCode::InvalidSessionToken,
725            "Invalid session token",
726            rc.options.error_detail,
727        )
728    };
729    let (Some(token), Some(user_id)) = (rc.session_token.as_deref(), rc.user_id.as_deref()) else {
730        return Err(invalid());
731    };
732
733    let ctx = rc.ctx(state.storage());
734    let row = parse_rust_rest::get(&ctx, USER_CLASS, user_id, FindOptions::default())
735        .await
736        .map_err(|e| {
737            if e.code == ErrorCode::ObjectNotFound {
738                invalid()
739            } else {
740                e
741            }
742        })?;
743
744    let mut out = row;
745    // Send the token back on the response, because SDKs expect that (`UsersRouter.js:228-229`).
746    out.insert(
747        "sessionToken".to_string(),
748        ParseValue::String(token.to_string()),
749    );
750    Ok(crate::routes::classes::body_of(&out))
751}
752
753/// `POST /logout`.
754///
755/// Deletes the `_Session` row (`UsersRouter.js:509-538`). A request with no token, or with one
756/// that no longer resolves, still answers `{}`.
757pub async fn logout_core(state: &AppState, rc: &RequestContext) -> Result<Json, ParseError> {
758    if let Some(token) = rc.session_token.as_deref() {
759        parse_rust_auth::revoke(state.storage(), token).await?;
760    }
761    Ok(json!({}))
762}
763
764// ---------------------------------------------------------------------------------------------
765// `_User` update policy
766//
767// `PUT /classes/_User/:objectId` is the route `user.save()` compiles to, and opening it to
768// non-master callers means running the stages `RestWrite.transformUser` runs. Reserving the
769// password and the ACL, which is all this module did before, is not enough: it leaves the row's
770// server-controlled columns writable and its identity columns unvalidated.
771// ---------------------------------------------------------------------------------------------
772
773/// `_User` columns a client may never write, whatever the ACL says.
774///
775/// `emailVerified` is the one that matters: it is the output of a verification flow, so a client
776/// that can set it has verified its own email. Upstream refuses it with `OPERATION_FORBIDDEN`
777/// (`RestWrite.js:1543-1556`).
778///
779/// `authData` is refused rather than validated, which is a deliberate fail-closed gap: upstream
780/// hands it to an auth adapter that decides whether the credential is real, and parse-rust has no
781/// adapter host. Accepting it unvalidated would let a client write a third-party identity that a
782/// later login could match on.
783const CLIENT_FORBIDDEN_USER_FIELDS: [&str; 2] = ["emailVerified", "authData"];
784
785/// The noun upstream uses in the refusal, which is not the column name.
786///
787/// `emailVerified` is reported as `email verification` (`RestWrite.js:724`). The message is
788/// contract in the disclosing regime, and a client matching on it would see the difference.
789fn forbidden_label(field: &str) -> &str {
790    match field {
791        "emailVerified" => "email verification",
792        other => other,
793    }
794}
795
796/// Refuse the `_User` columns a client may not set on itself.
797///
798/// **Create and update alike, which is the half signup was missing.** Upstream's
799/// `checkRestrictedFields` sits in the chain `RestWrite.execute` runs for both
800/// (`RestWrite.js:116`, defined at `:716-728`), so `POST /users` is covered there. parse-rust
801/// applied it only on the update path, which left signup able to set both fields on the row it was
802/// creating.
803///
804/// The two fields are here for different reasons and only the first is upstream's:
805///
806/// - `emailVerified` is upstream's own restriction, with upstream's message. A client that can set
807///   it at signup marks its own address verified without ever receiving mail.
808/// - `authData` is **not** in upstream's list, because upstream validates it instead: every
809///   provider block goes to the configured auth adapter, which decides whether the credential is
810///   real (`RestWrite.js:409-460`). parse-rust has no adapter host, so there is nothing to validate
811///   against, and storing the block unvalidated would let a client write a third-party identity
812///   that a later login could match on. Refusing is the fail-closed stand-in until adapters exist,
813///   and it is recorded as a deliberate difference rather than left implicit.
814fn reject_client_restricted_user_fields(
815    body: &WriteBody,
816    rc: &RequestContext,
817    authority: &Authority,
818) -> Result<(), ParseError> {
819    if authority.is_privileged() {
820        return Ok(());
821    }
822    for field in CLIENT_FORBIDDEN_USER_FIELDS {
823        if body.get(field).is_some() {
824            return Err(ParseError::permission_denied(
825                ErrorCode::OperationForbidden,
826                format!(
827                    "Clients aren't allowed to manually update {}.",
828                    forbidden_label(field)
829                ),
830                rc.options.error_detail,
831            ));
832        }
833    }
834    Ok(())
835}
836
837/// The checks that need no database read, run before the write.
838///
839/// **The first is the one that was missing and it is the serious one.** `enforce_class_security`
840/// asks whether a *class* is writable, not whether the caller is anybody. A `_User` row whose ACL
841/// grants public write was therefore updatable by an anonymous request, and because a password
842/// change mints a replacement session, that is account takeover against any user with a permissive
843/// ACL. Upstream refuses an unauthenticated `_User` update outright, before the ACL is consulted
844/// (`RestWrite.js:1572-1576`), and so does this.
845pub(crate) fn enforce_user_update_policy(
846    body: &WriteBody,
847    rc: &RequestContext,
848    authority: &Authority,
849    object_id: &str,
850) -> Result<(), ParseError> {
851    if !authority.is_privileged() && rc.user_id.is_none() {
852        return Err(ParseError::permission_denied(
853            ErrorCode::SessionMissing,
854            format!("Cannot modify user {object_id}."),
855            rc.options.error_detail,
856        ));
857    }
858
859    reject_client_restricted_user_fields(body, rc, authority)?;
860
861    // A non-string password reaches bcrypt upstream and throws out of the hashing library, which
862    // answers a bare 500 (`RestWrite.js:636`, `password.js:17`). Measured: `{"password": null}`
863    // answers `{"code":1,"message":"Internal server error."}`.
864    //
865    // Refused cleanly here instead. Reproducing a 500 has no client value, and the specific shape
866    // matters: `password: null` previously read as "no password" to the hasher and as "a password
867    // change" to the followup, so it revoked every session and issued a replacement while leaving
868    // the old password working. A false report of a security-relevant change is worse than either
869    // behaviour.
870    match body.get("password") {
871        None | Some(FieldWrite::Value(ParseValue::String(_))) => {}
872        Some(_) => {
873            return Err(ParseError::incorrect_type(
874                "password must be a string".to_string(),
875            ))
876        }
877    }
878    Ok(())
879}
880
881/// Force the row's own principal back into a submitted ACL.
882///
883/// Upstream re-adds it after the client's ACL is applied, so a `_User` cannot be made unreadable
884/// or unwritable by its owner. Measured against parse-server 9.10.1-alpha.6: saving
885/// `{"ACL": {"*": {"read": true, "write": true}}}` reads back with the owner entry still present.
886/// Without this a client can lock itself out of its own row, and can do it to another user
887/// wherever an ACL permits the write.
888pub(crate) fn force_owner_into_acl(body: &mut WriteBody, object_id: &str, privileged: bool) {
889    // **Master and maintenance are exempt.** Upstream applies the owner entry only for a
890    // non-privileged caller, so an administrator replacing a user's ACL with one that excludes
891    // them gets exactly that. Forcing it back in unconditionally means an operator cannot revoke a
892    // user's access to their own row, which is a legitimate administrative action and one a
893    // dashboard offers.
894    if privileged {
895        return;
896    }
897    let mut permissions = ParseMap::new();
898    permissions.insert("read".to_string(), ParseValue::Bool(true));
899    permissions.insert("write".to_string(), ParseValue::Bool(true));
900    let owner_entry = ParseValue::Object(permissions);
901
902    // **Upstream's test is `this.data.ACL &&` followed by a property assignment**
903    // (`RestWrite.js:1590-1599`), which is the same pair the create path applies and has the same
904    // trap: an op envelope, an array and a tagged value are all truthy objects in JavaScript and
905    // none of them is `ParseValue::Object` here.
906    //
907    // **Getting this wrong disables accounts.** Measured at the pin with
908    // `{"ACL":{"__op":"Increment","amount":1}}` on `PUT /classes/_User/:id`: both servers answer
909    // 200, upstream keeps the owner entry and the caller's session still resolves, and parse-rust
910    // stored `{}` and the same session then answered 209 `INVALID_SESSION_TOKEN`. Any principal
911    // permitted to write a `_User` could therefore lock that user out, which is the exact failure
912    // this function exists to prevent and it covered only two of the shapes that reach it.
913    //
914    // A **falsy** ACL is the one case that must not be touched, and it is upstream's `&&` doing
915    // the work: the assignment is skipped, and the falsy value is then dropped by the lowering,
916    // which leaves the stored columns exactly as they were. Forcing an owner in here would replace
917    // a deliberate ACL with an owner-only one on any request that happened to send `ACL: null`.
918    let shape = match body.get("ACL") {
919        None => None,
920        Some(FieldWrite::Value(v)) if !parse_rust_core::is_js_truthy(v) => None,
921        Some(FieldWrite::Value(ParseValue::Object(_))) => Some(OwnerInto::ExistingMap),
922        // Every other truthy shape, including a truthy **scalar**. Upstream throws a `TypeError`
923        // out of the assignment there and writes nothing at all, so there is no answer to
924        // reproduce; what matters is that the alternative here is worse than either. Left alone,
925        // the scalar reaches the lowering, which writes two empty permission columns, and on
926        // `_User` that is an account its owner can no longer read, write or log in to. An
927        // owner-only ACL is narrower than upstream's "nothing changed" and it keeps the invariant
928        // this function is named for.
929        Some(_) => Some(OwnerInto::FreshMap),
930    };
931
932    match shape {
933        None => {}
934        Some(OwnerInto::ExistingMap) => {
935            if let Some(FieldWrite::Value(ParseValue::Object(acl))) = body.get_mut("ACL") {
936                acl.insert(object_id.to_string(), owner_entry);
937            }
938        }
939        // Rewritten rather than dropped, so a `{"__op":"Delete"}` still deletes: every other
940        // principal goes and the owner stays, which is what upstream's assignment onto the op
941        // object produces once the lowering walks it. `user.unset("ACL").save()` sends exactly
942        // that op.
943        Some(OwnerInto::FreshMap) => {
944            let mut acl = ParseMap::new();
945            acl.insert(object_id.to_string(), owner_entry);
946            body.insert(
947                "ACL".to_string(),
948                FieldWrite::Value(ParseValue::Object(acl)),
949            );
950        }
951    }
952}
953
954/// Where the owner entry goes when a `_User` update carries an `ACL`.
955enum OwnerInto {
956    /// The client sent a principal map; the owner joins it.
957    ExistingMap,
958    /// The client sent something that is an object to JavaScript but not a principal map, so the
959    /// only entry that survives upstream's lowering is the owner. Replaced wholesale.
960    FreshMap,
961}
962
963/// `_validateUserName` and `_validateEmail` (`RestWrite.js:811-884`), for the update path.
964///
965/// **Case-insensitive, and that is the whole point of doing it here rather than leaving it to the
966/// unique indexes.** The indexes parse-rust creates are the plain `username_1` and `email_1`, which
967/// compare case-sensitively, so `CaseOnly` and `caseonly` are two different keys to MongoDB and
968/// both are stored. Upstream refuses the second with 202. The changelog claimed the indexes held
969/// uniqueness; they hold only exact-match uniqueness, and this is the half that was missing.
970///
971/// The comparison runs under upstream's collation, through `QueryOptions::case_insensitive`, which
972/// is upstream's own `{caseInsensitive: true}` find rather than an approximation of it.
973///
974/// The `objectId: {$ne: <self>}` term is load-bearing: without it, saving a row with its own
975/// username unchanged collides with itself.
976pub(crate) async fn validate_user_identity(
977    state: &AppState,
978    rc: &RequestContext,
979    body: &WriteBody,
980    object_id: &str,
981) -> Result<(), ParseError> {
982    // **Username first, then email**, which is `transformUser`'s chain order
983    // (`RestWrite.js:803-807`). A body carrying both a colliding username and a malformed email
984    // reports the username, and checking email first reported the email instead.
985    // **A `Delete` on `username` is refused, and on `email` it is allowed.** The asymmetry is
986    // upstream's and is visible on the wire. `_validateEmail` opens with
987    // `if (!this.data.email || this.data.email.__op === 'Delete') return` (`RestWrite.js:886`);
988    // `_validateUserName` has no such branch, so the op object is truthy, reaches the uniqueness
989    // query as a value, and answers `107 You cannot use [object Object] as a query parameter.`
990    //
991    // Matching only the string form let `user.unset("username").save()` through, removing the
992    // username with no validation at all. The message is upstream's rendering of a query built
993    // from an op object, which is an accident of how it fails rather than a designed error, but it
994    // is the string a client sees.
995    if matches!(body.get("username"), Some(FieldWrite::Op(_))) {
996        return Err(ParseError::invalid_json(
997            "You cannot use [object Object] as a query parameter.",
998        ));
999    }
1000    if let Some(FieldWrite::Value(ParseValue::String(username))) = body.get("username") {
1001        if taken(state, rc, "username", username, object_id).await? {
1002            return Err(ParseError::new(
1003                ErrorCode::UsernameTaken,
1004                "Account already exists for this username.",
1005            ));
1006        }
1007    }
1008
1009    let Some(FieldWrite::Value(ParseValue::String(email))) = body.get("email") else {
1010        return Ok(());
1011    };
1012    // `if (!this.data.email ...) return` (`RestWrite.js:886`). An empty string is falsy, so it is
1013    // skipped rather than rejected, and upstream answers 200 for `{"email": ""}`.
1014    if email.is_empty() {
1015        return Ok(());
1016    }
1017    if !is_valid_email(email) {
1018        return Err(ParseError::new(
1019            ErrorCode::InvalidEmailAddress,
1020            "Email address format is invalid.",
1021        ));
1022    }
1023    if taken(state, rc, "email", email, object_id).await? {
1024        return Err(ParseError::new(
1025            ErrorCode::EmailTaken,
1026            "Account already exists for this email address.",
1027        ));
1028    }
1029    Ok(())
1030}
1031
1032/// `/^.+@.+$/` as JavaScript evaluates it (`RestWrite.js:890`).
1033///
1034/// Deliberately not an address grammar. Matching upstream's laxness matters more than being right
1035/// about RFC 5322, and `a b@c d` is a valid address to this check on both servers.
1036///
1037/// **Two things about that regex are easy to get wrong, and a naive `find('@')` gets both wrong.**
1038///
1039/// `.` does not match a line terminator, and JavaScript counts four: `\n`, `\r`, U+2028 and
1040/// U+2029. Since `^` and `$` with no `m` flag anchor to the whole string, and `.+@.+` has to span
1041/// it, a line terminator *anywhere* means no match. The two Unicode ones are the trap: they are
1042/// invisible in most editors and are not what `char::is_whitespace` or a `\n` check would catch.
1043/// Ordinary spaces and tabs are not line terminators and are accepted.
1044///
1045/// And the `@` may be neither the first nor the last character, but need not be the only one:
1046/// `.` matches `@` too, so `@a@b` and `a@b@` both match through an interior `@`. Looking at only
1047/// the first or only the last `@` therefore answers wrongly for one of those two.
1048///
1049/// Verified against Node itself for each case in the test below.
1050fn is_valid_email(email: &str) -> bool {
1051    const LINE_TERMINATORS: [char; 4] = ['\n', '\r', '\u{2028}', '\u{2029}'];
1052    if email.chars().any(|c| LINE_TERMINATORS.contains(&c)) {
1053        return false;
1054    }
1055    // At least one `@` strictly inside, which is what `.+@.+` requires.
1056    let chars: Vec<char> = email.chars().collect();
1057    chars.len() >= 3 && chars[1..chars.len() - 1].contains(&'@')
1058}
1059
1060/// Does another `_User` already hold this value, compared case-insensitively?
1061///
1062/// An exact-equality constraint run under upstream's collation, which is upstream's own mechanism
1063/// (`RestWrite.js:818-826` passing `{caseInsensitive: true}`). An anchored `/i` regex was the
1064/// first shape of this and it was wrong in a way worth recording: a collation at strength 2
1065/// normalizes, so a precomposed `Café` and a decomposed `Cafe` plus a combining acute are one key
1066/// to it and two distinct byte strings to any regex. The regex therefore admitted identities
1067/// upstream treats as duplicates, which is the direction that matters for a uniqueness check.
1068///
1069/// Note what strength 2 does *not* do: it is case-insensitive but diacritic-**sensitive**, so
1070/// `Café` and `Cafe` remain different identities. Secondary strength ignores case and normalizes
1071/// equivalent Unicode forms; it does not fold accents away.
1072///
1073/// The `objectId: {$ne: <self>}` term is load-bearing: without it, saving a row with its own
1074/// username unchanged collides with itself.
1075async fn taken(
1076    state: &AppState,
1077    rc: &RequestContext,
1078    field: &str,
1079    value: &str,
1080    object_id: &str,
1081) -> Result<bool, ParseError> {
1082    let query = Query::from_constraints(vec![
1083        Constraint::equal(field, ParseValue::String(value.to_string())),
1084        Constraint {
1085            field: "objectId".to_string(),
1086            comparison: parse_rust_storage::Comparison::NotEqual(ParseValue::String(
1087                object_id.to_string(),
1088            )),
1089        },
1090    ]);
1091    // The request's own snapshot, not a second read. One request evaluates one schema.
1092    let schema = rc.snapshot.get_or_default(USER_CLASS);
1093    let rows = state
1094        .storage()
1095        .find(
1096            &schema,
1097            &query,
1098            &QueryOptions {
1099                limit: Some(1),
1100                case_insensitive: true,
1101                ..QueryOptions::default()
1102            },
1103        )
1104        .await?;
1105    Ok(!rows.is_empty())
1106}
1107
1108#[cfg(test)]
1109mod tests {
1110
1111    fn row(username: &str, id: &str) -> ParseMap {
1112        let mut m = ParseMap::new();
1113        m.insert("objectId".into(), ParseValue::String(id.into()));
1114        m.insert("username".into(), ParseValue::String(username.into()));
1115        m
1116    }
1117
1118    /// The exact username wins **regardless of the order the database returned the rows in**.
1119    ///
1120    /// This is the assertion the integration test cannot make. An `$or` over two indexes has no
1121    /// defined result order, so end to end the naive `limit: 1` implementation passes whenever
1122    /// MongoDB happens to hand back the right row, which it usually does. Here the adverse order is
1123    /// simply the input.
1124    #[test]
1125    fn the_exact_username_wins_over_a_matching_email_in_either_order() {
1126        let target = row("collide@example.com", "TARGET");
1127        let other = row("other_user", "OTHER");
1128
1129        for rows in [
1130            vec![other.clone(), target.clone()],
1131            vec![target.clone(), other.clone()],
1132        ] {
1133            let picked = select_login_row(rows, Some("collide@example.com")).expect("a row");
1134            assert!(
1135                matches!(picked.get("objectId"), Some(ParseValue::String(id)) if id == "TARGET"),
1136                "the username owner is chosen whichever row came first"
1137            );
1138        }
1139    }
1140
1141    /// More than one match with no submitted username, which is what upstream crashes on.
1142    #[test]
1143    fn a_multi_match_without_a_username_falls_back_to_the_first_row() {
1144        let rows = vec![row("a", "FIRST"), row("b", "SECOND")];
1145        let picked = select_login_row(rows, None).expect("a row");
1146        assert!(matches!(picked.get("objectId"), Some(ParseValue::String(id)) if id == "FIRST"));
1147    }
1148
1149    /// Every case measured against Node's own evaluation of `/^.+@.+$/`, which is the only
1150    /// authority for what that regex accepts.
1151    #[test]
1152    fn email_validity_matches_javascripts_regex() {
1153        for (email, expected) in [
1154            ("a@b", true),
1155            ("ab@cd", true),
1156            // `.` matches `@`, so an interior one is enough and there may be more than one.
1157            ("@a@b", true),
1158            ("a@b@", true),
1159            // Spaces and tabs are ordinary characters to `.`.
1160            ("a b@c d", true),
1161            ("a\t@b", true),
1162            ("not-an-email", false),
1163            ("a@", false),
1164            ("@a", false),
1165            ("@", false),
1166            ("", false),
1167            // The four JavaScript line terminators, which `.` never matches. The last two are
1168            // invisible in most editors and are the reason this is not a `\n` check.
1169            ("a\n@b", false),
1170            ("a@\nb", false),
1171            ("a\r@b", false),
1172            ("a@\rb", false),
1173            ("a\u{2028}@b", false),
1174            ("a@\u{2029}b", false),
1175        ] {
1176            assert_eq!(
1177                is_valid_email(email),
1178                expected,
1179                "{email:?} ({:?})",
1180                email.chars().map(|c| c as u32).collect::<Vec<_>>()
1181            );
1182        }
1183    }
1184    use super::*;
1185
1186    fn body(json: &str) -> WriteBody {
1187        parse_rust_rest::decode_write_body(
1188            &serde_json::from_str(json).expect("test literal"),
1189            parse_rust_core::op::OpPath::Create,
1190        )
1191        .expect("decode")
1192    }
1193
1194    #[tokio::test]
1195    async fn shared_user_transform_removes_plaintext_and_writes_a_bcrypt_hash() {
1196        let mut b = body(r#"{"password":"hunter2"}"#);
1197        prepare_user_write(&mut b, false)
1198            .await
1199            .expect("password hashes");
1200
1201        assert!(!b.contains_key("password"));
1202        let Some(FieldWrite::Value(ParseValue::String(hash))) = b.get(HASHED_PASSWORD) else {
1203            panic!("hash missing");
1204        };
1205        assert!(parse_rust_auth::password::verify("hunter2".into(), hash.clone()).await);
1206    }
1207
1208    #[test]
1209    fn user_owner_acl_is_added_without_discarding_a_master_supplied_acl() {
1210        let mut b = body(r#"{"objectId":"user123456","ACL":{"*":{"read":true}}}"#);
1211        let object_id = ensure_user_identity_and_acl(&mut b).expect("string id");
1212
1213        assert_eq!(object_id, "user123456");
1214        let Some(FieldWrite::Value(ParseValue::Object(acl))) = b.get("ACL") else {
1215            panic!("ACL missing");
1216        };
1217        assert!(acl.contains_key("*"));
1218        let Some(ParseValue::Object(owner)) = acl.get("user123456") else {
1219            panic!("owner ACL missing");
1220        };
1221        assert!(matches!(owner.get("read"), Some(ParseValue::Bool(true))));
1222        assert!(matches!(owner.get("write"), Some(ParseValue::Bool(true))));
1223    }
1224
1225    /// **A falsy `ACL` is not "leave it alone", it is "there is no ACL"**, which is upstream's
1226    /// `if (!ACL)`. 0.2.0 left all four in place, they were dropped by `lower_acl` without writing
1227    /// permission columns, and an absent `_rperm` is public: signing up with `{"ACL": null}`
1228    /// produced a `_User` row any anonymous caller could read. Measured against a parse-server at
1229    /// the pin, which answers 404 to that read.
1230    ///
1231    /// The values are looped rather than sampled, because checking one is exactly how the other
1232    /// three survived the last review of this function.
1233    #[test]
1234    fn a_falsy_acl_on_signup_becomes_the_owner_acl_rather_than_a_public_row() {
1235        for literal in [
1236            r#"{"objectId":"user123456","ACL":null}"#,
1237            r#"{"objectId":"user123456","ACL":false}"#,
1238            r#"{"objectId":"user123456","ACL":0}"#,
1239            r#"{"objectId":"user123456","ACL":""}"#,
1240        ] {
1241            let mut b = body(literal);
1242            ensure_user_identity_and_acl(&mut b).expect("string id");
1243            let Some(FieldWrite::Value(ParseValue::Object(acl))) = b.get("ACL") else {
1244                panic!("ACL missing or not an object for {literal}");
1245            };
1246            assert_eq!(acl.len(), 1, "{literal} produced {acl:?}");
1247            let Some(ParseValue::Object(owner)) = acl.get("user123456") else {
1248                panic!("owner entry missing for {literal}");
1249            };
1250            assert!(matches!(owner.get("read"), Some(ParseValue::Bool(true))));
1251            assert!(matches!(owner.get("write"), Some(ParseValue::Bool(true))));
1252        }
1253    }
1254
1255    /// **An op envelope, an array and a tagged value are all objects in JavaScript**, and none of
1256    /// them is `ParseValue::Object` here. Matching only on `Object` looks like "the client sent an
1257    /// ACL" and is not: `{"ACL":{"__op":"Delete"}}` produced a **publicly readable `_User`**,
1258    /// because `flatten_for_create` then removed the key and no permission columns were written.
1259    ///
1260    /// Every case below is measured against a parse-server at the pin, where all four answer with
1261    /// the owner-only ACL. They do so because an op's keys are `__op`, `objects` and `amount`, an
1262    /// array's are indices and a tagged value's are `__type` and its payload, so none of them
1263    /// contributes a `read` or a `write` and the owner is the only entry left.
1264    #[test]
1265    fn a_js_object_acl_that_is_not_a_principal_map_still_gets_the_owner() {
1266        for literal in [
1267            r#"{"objectId":"user123456","ACL":{"__op":"Delete"}}"#,
1268            r#"{"objectId":"user123456","ACL":{"__op":"Increment","amount":1}}"#,
1269            r#"{"objectId":"user123456","ACL":[]}"#,
1270            r#"{"objectId":"user123456","ACL":[1,2]}"#,
1271            r#"{"objectId":"user123456","ACL":{"__type":"Date","iso":"2020-01-01T00:00:00.000Z"}}"#,
1272        ] {
1273            let mut b = body(literal);
1274            ensure_user_identity_and_acl(&mut b).expect("string id");
1275            let Some(FieldWrite::Value(ParseValue::Object(acl))) = b.get("ACL") else {
1276                panic!("ACL missing or not an object for {literal}");
1277            };
1278            assert_eq!(acl.len(), 1, "{literal} produced {acl:?}");
1279            assert!(acl.contains_key("user123456"), "{literal}");
1280        }
1281    }
1282
1283    /// The one shape that is left as it arrived. Upstream throws a `TypeError` out of
1284    /// `ACL[objectId] = ...` and answers a bare 500, measured for `"nonsense"`, `123` and `true`,
1285    /// so there is no upstream answer to reproduce. `lower_acl` writes two empty columns for it,
1286    /// which is master-only rather than public, so the failure direction is closed.
1287    #[test]
1288    fn a_truthy_scalar_acl_on_signup_is_left_for_the_validator() {
1289        for literal in [
1290            r#"{"objectId":"user123456","ACL":"nonsense"}"#,
1291            r#"{"objectId":"user123456","ACL":123}"#,
1292            r#"{"objectId":"user123456","ACL":true}"#,
1293        ] {
1294            let mut b = body(literal);
1295            ensure_user_identity_and_acl(&mut b).expect("string id");
1296            assert!(
1297                !matches!(b.get("ACL"), Some(FieldWrite::Value(ParseValue::Object(_)))),
1298                "{literal} must be left alone"
1299            );
1300        }
1301    }
1302
1303    /// **The update path has the same JavaScript-object trap as the create path, and getting it
1304    /// wrong disables accounts rather than exposing them.** `force_owner_into_acl` handled a
1305    /// principal map and `{"__op":"Delete"}` and nothing else, so every other truthy shape reached
1306    /// the lowering and cleared both permission columns. On `_User` that is a row its owner can no
1307    /// longer read, write or log in with.
1308    ///
1309    /// Measured at the pin with `{"__op":"Increment","amount":1}` on `PUT /classes/_User/:id`:
1310    /// both servers answer 200, upstream keeps the owner entry and the caller's session still
1311    /// resolves, and parse-rust stored `{}` and the same session then answered 209.
1312    #[test]
1313    fn every_truthy_acl_shape_on_an_update_keeps_the_owner() {
1314        for literal in [
1315            r#"{"ACL":{"__op":"Increment","amount":1}}"#,
1316            r#"{"ACL":{"__op":"Delete"}}"#,
1317            r#"{"ACL":{"__op":"Add","objects":[1]}}"#,
1318            r#"{"ACL":[]}"#,
1319            r#"{"ACL":[1,2]}"#,
1320            r#"{"ACL":{"__type":"Date","iso":"2020-01-01T00:00:00.000Z"}}"#,
1321            // A truthy scalar, where upstream throws and writes nothing. There is no answer to
1322            // reproduce, and the alternative here is an account nobody can reach.
1323            r#"{"ACL":"nonsense"}"#,
1324            r#"{"ACL":123}"#,
1325            r#"{"ACL":true}"#,
1326        ] {
1327            let mut b = body(literal);
1328            force_owner_into_acl(&mut b, "user123456", false);
1329            let Some(FieldWrite::Value(ParseValue::Object(acl))) = b.get("ACL") else {
1330                panic!("ACL missing or not an object for {literal}");
1331            };
1332            let Some(ParseValue::Object(owner)) = acl.get("user123456") else {
1333                panic!("owner entry missing for {literal}");
1334            };
1335            assert!(matches!(owner.get("read"), Some(ParseValue::Bool(true))));
1336            assert!(matches!(owner.get("write"), Some(ParseValue::Bool(true))));
1337        }
1338    }
1339
1340    /// The half that must **not** change, and the reason the rule is not "always force an owner
1341    /// in". Upstream's test is `this.data.ACL &&`, so a falsy value skips the assignment and is
1342    /// then dropped by the lowering, which leaves the stored columns exactly as they were. Forcing
1343    /// an owner here would replace a deliberate ACL on any request that sent `ACL: null`.
1344    #[test]
1345    fn a_falsy_acl_on_an_update_is_left_for_the_lowering_to_drop() {
1346        for literal in [
1347            r#"{"ACL":null}"#,
1348            r#"{"ACL":false}"#,
1349            r#"{"ACL":0}"#,
1350            r#"{"ACL":""}"#,
1351        ] {
1352            let mut b = body(literal);
1353            force_owner_into_acl(&mut b, "user123456", false);
1354            assert!(
1355                !matches!(b.get("ACL"), Some(FieldWrite::Value(ParseValue::Object(_)))),
1356                "{literal} must be left alone"
1357            );
1358        }
1359    }
1360
1361    /// Master and maintenance are exempt, which is upstream's `isMaster !== true` guard. An
1362    /// operator revoking a user's access to their own row is a legitimate administrative action.
1363    #[test]
1364    fn a_privileged_caller_can_still_remove_the_owner() {
1365        let mut b = body(r#"{"ACL":{"__op":"Delete"}}"#);
1366        force_owner_into_acl(&mut b, "user123456", true);
1367        assert!(matches!(b.get("ACL"), Some(FieldWrite::Op(_))));
1368    }
1369
1370    /// **A truthy non-string `objectId` is refused rather than replaced.** Upstream's substitution
1371    /// test is `if (!this.data.objectId)`, so a truthy one survives to the type check and answers
1372    /// `INCORRECT_TYPE`. Reading "not a string" as "absent" generated an id instead: measured at
1373    /// the pin with `allowCustomObjectId` on, upstream answered 400 code 111 and wrote no row
1374    /// through either `POST /users` or `POST /classes/_User`, and parse-rust answered 201 with an
1375    /// id the client never asked for.
1376    #[test]
1377    fn a_truthy_non_string_object_id_is_refused_rather_than_replaced() {
1378        for literal in [
1379            r#"{"objectId":123,"username":"u"}"#,
1380            r#"{"objectId":true,"username":"u"}"#,
1381            r#"{"objectId":["a"],"username":"u"}"#,
1382            r#"{"objectId":{"a":1},"username":"u"}"#,
1383        ] {
1384            let mut b = body(literal);
1385            let e = ensure_user_identity_and_acl(&mut b).expect_err(literal);
1386            assert_eq!(e.code, ErrorCode::IncorrectType, "{literal}");
1387        }
1388    }
1389
1390    /// **The one shape that is refused with a different code, and a deliberate difference from
1391    /// upstream rather than a match.** A `Delete` operation is truthy, so it is not a substitutable
1392    /// absent id, and upstream infers no type for it, skips the field check and answers 201 having
1393    /// stored the row under a Mongo-generated `_id` while echoing the operation back as the
1394    /// `objectId`. parse-rust refuses with `INVALID_JSON`.
1395    ///
1396    /// Pinned here rather than in Gate E, which may hold only assertions that pass against both
1397    /// servers, and this one does not.
1398    #[test]
1399    fn a_delete_operation_as_an_object_id_is_refused_with_invalid_json() {
1400        let mut b = body(r#"{"objectId":{"__op":"Delete"},"username":"u"}"#);
1401        let e = ensure_user_identity_and_acl(&mut b).expect_err("Delete op");
1402        assert_eq!(e.code, ErrorCode::InvalidJson);
1403        assert_eq!(e.message, "objectId is an invalid field name.");
1404    }
1405
1406    /// The other side of that test, and the reason it is truthiness rather than presence: an empty
1407    /// string and a `null` are falsy, so upstream replaces them with a generated id exactly as it
1408    /// replaces an absent one.
1409    #[test]
1410    fn a_falsy_object_id_is_still_replaced_with_a_generated_one() {
1411        for literal in [
1412            r#"{"objectId":"","username":"u"}"#,
1413            r#"{"objectId":null,"username":"u"}"#,
1414            r#"{"username":"u"}"#,
1415        ] {
1416            let mut b = body(literal);
1417            let id = ensure_user_identity_and_acl(&mut b).expect(literal);
1418            assert_eq!(id.len(), 10, "{literal} produced {id}");
1419        }
1420    }
1421
1422    /// An update must not acquire an ACL or an objectId it did not ask for.
1423    #[tokio::test]
1424    async fn an_update_is_only_hashed() {
1425        let mut b = body(r#"{"password":"x","nickname":"n"}"#);
1426        prepare_user_write(&mut b, false).await.expect("hash");
1427        assert!(!b.contains_key("ACL"));
1428        assert!(!b.contains_key("objectId"));
1429    }
1430}