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