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}