Skip to main content

parse_rust_server/routes/
users.rs

1//! Signup, login, `/users/me`, logout.
2//!
3//! Upstream: `src/Routers/UsersRouter.js`. Two facts shape this module and both are easy to lose:
4//!
5//! - **`POST /users` is not `POST /classes/_User`.** Only the signup route returns a session
6//!   token. Non-master class writes are refused, while an allowed master write still has to pass
7//!   through the same password and ACL preparation as signup.
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 is
11//!   nothing to strip and no path that can forget to.
12
13use axum::extract::State;
14use axum::response::{IntoResponse, Response};
15use axum::Json;
16use parse_rust_core::{ErrorCode, ParseError, ParseMap, ParseValue};
17use parse_rust_rest::AclScope;
18use serde_json::{json, Value as Json_};
19
20use crate::auth::Authority;
21use crate::response::ParseErrorResponse;
22use crate::state::AppState;
23
24const USER_CLASS: &str = "_User";
25/// The column upstream stores the bcrypt hash in. Never a response key.
26const HASHED_PASSWORD: &str = "_hashed_password";
27
28fn err(e: ParseError) -> Response {
29    ParseErrorResponse(e).into_response()
30}
31
32/// Remove everything a client must never see from a `_User` row.
33///
34/// A denylist is the wrong shape in general, but here the set is closed and short, and the
35/// stronger property is upstream: the hash is stored under an `_`-prefixed key, and the Mongo
36/// untransform already refuses to raise unknown `_` keys. This is the second line.
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/// As in the classes router: strip every internal key before serializing.
50fn body_of(row: &ParseMap) -> Json_ {
51    let row = parse_rust_rest::to_response_body(row);
52    serde_json::from_str(&ParseValue::Object(row).to_json()).unwrap_or(Json_::Null)
53}
54
55fn take_string(body: &ParseMap, key: &str) -> Option<String> {
56    match body.get(key) {
57        Some(ParseValue::String(s)) => Some(s.clone()),
58        _ => None,
59    }
60}
61
62/// Replace a user-facing password with the bcrypt column that is safe to persist.
63///
64/// This is shared by signup and master-key writes through `/classes/_User`. Authorization decides
65/// whether the class route is allowed; it must not decide whether a password is hashed.
66pub(crate) fn hash_user_password(body: &mut ParseMap) -> Result<(), ParseError> {
67    let password = match body.get("password") {
68        Some(ParseValue::String(password)) => password.clone(),
69        // Schema validation reports the wire-compatible error for a non-string value. An absent
70        // password is also valid for updates that change another field.
71        _ => return Ok(()),
72    };
73    let hash = parse_rust_auth::password::hash(&password)?;
74    body.shift_remove("password");
75    body.insert(HASHED_PASSWORD.to_string(), ParseValue::String(hash));
76    Ok(())
77}
78
79/// Give a newly created user an id and ensure its ACL always contains its own principal.
80///
81/// Upstream preserves any ACL supplied by a master caller, then adds the owner entry. Leaving an
82/// invalid ACL untouched lets the schema/ACL validator return the correct Parse error.
83///
84/// The result is a user readable and writable by itself and nobody else, which is what
85/// `enforcePrivateUsers` produces at its default of **true** (`Options/Definitions.js:263-268`).
86/// With that option set to `false` upstream additionally grants `{"*": {"read": true}}`. The
87/// option is not modeled, so the private form is the only one produced here: the safe direction,
88/// and the one matching the default.
89pub(crate) fn ensure_user_identity_and_acl(body: &mut ParseMap) -> String {
90    let object_id = match body.get("objectId") {
91        Some(ParseValue::String(id)) => id.clone(),
92        _ => {
93            let id = parse_rust_core::new_object_id();
94            body.insert("objectId".to_string(), ParseValue::String(id.clone()));
95            id
96        }
97    };
98
99    let mut permissions = ParseMap::new();
100    permissions.insert("read".to_string(), ParseValue::Bool(true));
101    permissions.insert("write".to_string(), ParseValue::Bool(true));
102
103    match body.get_mut("ACL") {
104        Some(ParseValue::Object(acl)) => {
105            acl.insert(object_id.clone(), ParseValue::Object(permissions));
106        }
107        Some(_) => {}
108        None => {
109            let mut acl = ParseMap::new();
110            acl.insert(object_id.clone(), ParseValue::Object(permissions));
111            body.insert("ACL".to_string(), ParseValue::Object(acl));
112        }
113    }
114    object_id
115}
116
117/// `POST /users`. Signup.
118pub async fn signup(
119    State(state): State<AppState>,
120    _authority: Authority,
121    Json(body): Json<Json_>,
122) -> Response {
123    let mut body = match parse_rust_core::classify(body) {
124        Ok(ParseValue::Object(m)) => m,
125        Ok(_) => return err(ParseError::invalid_json("body must be an object")),
126        Err(e) => return err(e),
127    };
128    // Before anything else: a signup body must not carry `_hashed_password` of the caller's
129    // choosing.
130    if let Err(e) = parse_rust_rest::reject_reserved_keys(&body) {
131        return err(e);
132    }
133
134    let Some(username) = take_string(&body, "username") else {
135        return err(ParseError::new(
136            ErrorCode::UsernameMissing,
137            "bad or missing username",
138        ));
139    };
140    let Some(password) = take_string(&body, "password") else {
141        return err(ParseError::new(
142            ErrorCode::PasswordMissing,
143            "password is required.",
144        ));
145    };
146    if username.is_empty() {
147        return err(ParseError::new(
148            ErrorCode::UsernameMissing,
149            "bad or missing username",
150        ));
151    }
152    if password.is_empty() {
153        return err(ParseError::new(
154            ErrorCode::PasswordMissing,
155            "password is required.",
156        ));
157    }
158
159    if let Err(e) = hash_user_password(&mut body) {
160        return err(e);
161    }
162
163    // `enforcePrivateUsers` defaults to **true** at the pin (`Options/Definitions.js:263-268`),
164    // so a new user is readable and writable only by itself.
165    //
166    // This used to be a comment with no code behind it, which meant every user was world
167    // readable. The ACL cannot be built until the objectId exists, and the objectId is generated
168    // inside `create`, so signup generates it here and passes it in. That is also what keeps the
169    // row from existing unprotected for even one write.
170    ensure_user_identity_and_acl(&mut body);
171
172    let created = match parse_rust_rest::create(
173        state.storage(),
174        USER_CLASS,
175        body.clone(),
176        &AclScope::Unrestricted,
177    )
178    .await
179    {
180        Ok(c) => c,
181        Err(e) => return err(map_duplicate(e)),
182    };
183
184    let token = state.sessions().create(&created.object_id);
185    (
186        axum::http::StatusCode::CREATED,
187        Json(json!({
188            "objectId": created.object_id,
189            "createdAt": created.created_at.to_iso(),
190            "sessionToken": token,
191        })),
192    )
193        .into_response()
194}
195
196/// Turn a duplicate-key error into the code the SDK expects.
197///
198/// A `username_1` collision must be 202 `USERNAME_TAKEN`, not a bare 137. Upstream recovers which
199/// field collided by regex over the index name, which is why the index names are contractual.
200fn map_duplicate(e: ParseError) -> ParseError {
201    if e.code != ErrorCode::DuplicateValue {
202        return e;
203    }
204    if e.message.contains("username_1") {
205        return ParseError::new(
206            ErrorCode::UsernameTaken,
207            "Account already exists for this username.",
208        );
209    }
210    if e.message.contains("email_1") {
211        return ParseError::new(
212            ErrorCode::EmailTaken,
213            "Account already exists for this email address.",
214        );
215    }
216    e
217}
218
219/// `POST /login`.
220pub async fn login(
221    State(state): State<AppState>,
222    _authority: Authority,
223    Json(body): Json<Json_>,
224) -> Response {
225    let body = match parse_rust_core::classify(body) {
226        Ok(ParseValue::Object(m)) => m,
227        Ok(_) => return err(ParseError::invalid_json("body must be an object")),
228        Err(e) => return err(e),
229    };
230
231    let (Some(username), Some(password)) = (
232        take_string(&body, "username"),
233        take_string(&body, "password"),
234    ) else {
235        return err(ParseError::new(
236            ErrorCode::UsernameMissing,
237            "username/email is required.",
238        ));
239    };
240
241    let rows = match parse_rust_rest::find(
242        state.storage(),
243        USER_CLASS,
244        vec![parse_rust_storage::Constraint::equal(
245            "username",
246            ParseValue::String(username),
247        )],
248        parse_rust_storage::QueryOptions {
249            limit: Some(1),
250            ..Default::default()
251        },
252        &AclScope::Unrestricted,
253    )
254    .await
255    {
256        Ok(r) => r,
257        Err(e) => return err(e),
258    };
259
260    // One error for "no such user" and for "wrong password", so login cannot be used to
261    // enumerate accounts. Upstream does the same.
262    let invalid = || ParseError::new(ErrorCode::ObjectNotFound, "Invalid username/password.");
263
264    let Some(row) = rows.into_iter().next() else {
265        return err(invalid());
266    };
267    let Some(ParseValue::String(hash)) = row.get(HASHED_PASSWORD) else {
268        return err(invalid());
269    };
270    if !parse_rust_auth::password::verify(&password, hash) {
271        return err(invalid());
272    }
273
274    let Some(ParseValue::String(object_id)) = row.get("objectId") else {
275        return err(ParseError::new(
276            ErrorCode::InternalServerError,
277            "stored user has no objectId",
278        ));
279    };
280
281    let token = state.sessions().create(object_id);
282    let mut out = strip_sensitive(row.clone());
283    out.insert("sessionToken".to_string(), ParseValue::String(token));
284    Json(body_of(&out)).into_response()
285}
286
287/// `GET /users/me`.
288pub async fn me(State(state): State<AppState>, authority: Authority) -> Response {
289    let Authority::Client {
290        session_token: Some(token),
291    } = &authority
292    else {
293        return err(ParseError::new(
294            ErrorCode::InvalidSessionToken,
295            "Invalid session token",
296        ));
297    };
298    let Some(object_id) = state.sessions().user_for(token) else {
299        return err(ParseError::new(
300            ErrorCode::InvalidSessionToken,
301            "Invalid session token",
302        ));
303    };
304
305    match parse_rust_rest::get(
306        state.storage(),
307        USER_CLASS,
308        &object_id,
309        &AclScope::Unrestricted,
310    )
311    .await
312    {
313        Ok(row) => {
314            let mut out = strip_sensitive(row);
315            out.insert(
316                "sessionToken".to_string(),
317                ParseValue::String(token.clone()),
318            );
319            Json(body_of(&out)).into_response()
320        }
321        Err(e) => err(e),
322    }
323}
324
325/// `POST /logout`.
326pub async fn logout(State(state): State<AppState>, authority: Authority) -> Response {
327    if let Authority::Client {
328        session_token: Some(token),
329    } = &authority
330    {
331        state.sessions().revoke(token);
332    }
333    Json(json!({})).into_response()
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    #[test]
341    fn shared_user_transform_removes_plaintext_and_writes_a_bcrypt_hash() {
342        let mut body = ParseMap::new();
343        body.insert(
344            "password".to_string(),
345            ParseValue::String("hunter2".to_string()),
346        );
347
348        hash_user_password(&mut body).expect("password hashes");
349
350        assert!(!body.contains_key("password"));
351        let Some(ParseValue::String(hash)) = body.get(HASHED_PASSWORD) else {
352            panic!("hash missing");
353        };
354        assert!(parse_rust_auth::password::verify("hunter2", hash));
355    }
356
357    #[test]
358    fn user_owner_acl_is_added_without_discarding_a_master_supplied_acl() {
359        let mut public = ParseMap::new();
360        public.insert("read".to_string(), ParseValue::Bool(true));
361        let mut acl = ParseMap::new();
362        acl.insert("*".to_string(), ParseValue::Object(public));
363
364        let mut body = ParseMap::new();
365        body.insert(
366            "objectId".to_string(),
367            ParseValue::String("user123456".to_string()),
368        );
369        body.insert("ACL".to_string(), ParseValue::Object(acl));
370
371        let object_id = ensure_user_identity_and_acl(&mut body);
372
373        assert_eq!(object_id, "user123456");
374        let Some(ParseValue::Object(acl)) = body.get("ACL") else {
375            panic!("ACL missing");
376        };
377        assert!(acl.contains_key("*"));
378        let Some(ParseValue::Object(owner)) = acl.get("user123456") else {
379            panic!("owner ACL missing");
380        };
381        assert!(matches!(owner.get("read"), Some(ParseValue::Bool(true))));
382        assert!(matches!(owner.get("write"), Some(ParseValue::Bool(true))));
383    }
384}