Skip to main content

parse_rust_server/routes/
classes.rs

1//! The five `/classes` verbs, and the cores every other class-shaped route reuses.
2//!
3//! Upstream: `src/Routers/ClassesRouter.js`. Response shapes are wire contract and narrower than
4//! they look: a create returns `{objectId, createdAt}`, an update returns `{updatedAt}`, and a
5//! delete returns `{}`. Returning the whole object would be more helpful and would not match.
6//!
7//! `RolesRouter` and `SessionsRouter` are `ClassesRouter` with `className()` pinned
8//! (`RolesRouter.js:4-6`, `SessionsRouter.js:8-10`), so they call the cores below with a fixed
9//! class name rather than reimplementing them. `/batch` dispatches into the same cores, which is
10//! what makes a sub-request and a top-level request the same code path rather than two that drift.
11
12use parse_rust_core::{ErrorCode, ParseError, ParseMap, ParseValue};
13use parse_rust_rest::{FindOptions, ParsedClause, ParsedWhere};
14use parse_rust_storage::{Constraint, StorageAdapter};
15use serde_json::{json, Value as Json};
16
17use crate::auth::Authority;
18use crate::params::Params;
19use crate::request::RequestContext;
20use crate::state::AppState;
21
22/// The class sessions live in.
23///
24/// A non-master read of it is narrowed to the caller's own sessions, but that happens in
25/// `parse_rust_rest`'s read pipeline rather than here. See `pipeline::narrow_sessions` for why the
26/// layer matters.
27pub const SESSION_CLASS: &str = "_Session";
28
29/// Convert a Parse-format map into a JSON response body.
30///
31/// Strips every `_`-prefixed key and flattens the top-level timestamps. This is the single audit
32/// point for "nothing internal reaches a client", and it runs on every response body this module
33/// produces.
34pub fn body_of(row: &ParseMap) -> Json {
35    let row = parse_rust_rest::to_response_body(row);
36    serde_json::from_str(&ParseValue::Object(row).to_json()).unwrap_or(Json::Null)
37}
38
39/// Decode a JSON request body into a write body.
40fn decode_body(
41    value: &Json,
42    path: parse_rust_core::op::OpPath,
43) -> Result<parse_rust_rest::WriteBody, ParseError> {
44    let body = parse_rust_rest::decode_write_body(value, path)?;
45    // A client must not supply a server-internal column. Without this, a caller could write its
46    // own `_rperm` and grant itself read access to a row.
47    parse_rust_rest::reject_reserved_keys_in(body.keys().map(String::as_str))?;
48    Ok(body)
49}
50
51// -------------------------------------------------------------------------------------------
52// Cores
53// -------------------------------------------------------------------------------------------
54
55pub async fn find_core(
56    state: &AppState,
57    rc: &RequestContext,
58    authority: &Authority,
59    class_name: &str,
60    params: &Params,
61) -> Result<Json, ParseError> {
62    parse_rust_rest::enforce_class_security(
63        class_name,
64        authority.is_privileged(),
65        "find",
66        rc.options.error_detail,
67    )?;
68    params.reject_unknown_find_keys()?;
69
70    let where_ = params.parse_where()?;
71    let options = params.find_options()?;
72    let wants_count = params.wants_count();
73
74    let ctx = rc.ctx(state.storage());
75    let results = parse_rust_rest::find(&ctx, class_name, where_.clone(), options).await?;
76
77    let mut body = json!({ "results": results.iter().map(body_of).collect::<Vec<_>>() });
78    if wants_count {
79        let n = parse_rust_rest::count(&ctx, class_name, where_).await?;
80        body["count"] = json!(n);
81    }
82    Ok(body)
83}
84
85pub async fn get_core(
86    state: &AppState,
87    rc: &RequestContext,
88    authority: &Authority,
89    class_name: &str,
90    object_id: &str,
91    params: &Params,
92) -> Result<Json, ParseError> {
93    parse_rust_rest::enforce_class_security(
94        class_name,
95        authority.is_privileged(),
96        "get",
97        rc.options.error_detail,
98    )?;
99    params.reject_unknown_get_keys()?;
100
101    // `handleGet` is `rest.get`, which pins the query to an objectId **and carries the `get`
102    // method** (`rest.js:150`, `:183`). Routing it through `find` instead would re-derive the
103    // method as `find` inside the pipeline, and `enforceRoleSecurity` distinguishes the two: a
104    // client may `get` an installation and may not `find` one.
105    //
106    // The `_Session` narrowing this needs is applied by the pipeline, because upstream applies it
107    // in the query constructor (`RestQuery.js:116-134`) rather than at a route handler.
108    let options = FindOptions {
109        limit: Some(1),
110        ..params.get_options()?
111    };
112    let ctx = rc.ctx(state.storage());
113    let row = parse_rust_rest::get(&ctx, class_name, object_id, options).await?;
114    Ok(body_of(&row))
115}
116
117pub async fn create_core(
118    state: &AppState,
119    rc: &RequestContext,
120    authority: &Authority,
121    class_name: &str,
122    body: &Json,
123) -> Result<Json, ParseError> {
124    parse_rust_rest::enforce_class_security(
125        class_name,
126        authority.is_privileged(),
127        "create",
128        rc.options.error_detail,
129    )?;
130    let mut body = decode_body(body, parse_rust_core::op::OpPath::Create)?;
131    // The `RestWrite` constructor's first check, and it runs on the client's body before any
132    // server-side identity is folded in (`RestWrite.js:50-65`).
133    parse_rust_rest::enforce_object_id_policy(&body, state.config().allow_custom_object_id)?;
134    if class_name == crate::routes::users::USER_CLASS {
135        // `handleCreate`'s guard, which lives on `ClassesRouter` and therefore covers this route
136        // as well as signup (`ClassesRouter.js:105-112`).
137        crate::routes::users::reject_role_prefixed_object_id(&body, rc)?;
138        // Upstream's `!this.query && !hasAuthData` guard is not gated on the caller
139        // (`RestWrite.js:468-473`), so the master key does not buy an exemption from it. Before
140        // the uniqueness query and the hash, as upstream orders those stages.
141        crate::routes::users::require_create_credentials(&body)?;
142        // **`transformUser` is not gated on the caller**, so a master create through this route
143        // gets the same username and email validation a signup does (`RestWrite.js:803-807`).
144        // Without it, `POST /classes/_User` with the master key admitted case-only duplicate
145        // usernames and malformed email addresses that `POST /users` refuses. The dashboard
146        // creates users through this route. There is no self to exclude on a create, which is
147        // what the empty objectId means here.
148        crate::routes::users::validate_user_identity(state, rc, &body, "").await?;
149        crate::routes::users::prepare_user_write(&mut body, true).await?;
150    }
151    let ctx = rc.ctx(state.storage());
152    let res = parse_rust_rest::create(&ctx, class_name, body)
153        .await
154        // Same relabelling the update path does, and `_User` only: a collision on the unique index
155        // is 202 or 203 to a client, not a bare 137.
156        .map_err(|e| {
157            if class_name == crate::routes::users::USER_CLASS {
158                crate::routes::users::map_duplicate(e)
159            } else {
160                e
161            }
162        })?;
163
164    let mut out = json!({
165        "objectId": res.object_id,
166        "createdAt": res.created_at.to_iso(),
167    });
168    merge_echo(&mut out, res.echoed, rc, class_name);
169    Ok(out)
170}
171
172pub async fn update_core(
173    state: &AppState,
174    rc: &RequestContext,
175    authority: &Authority,
176    class_name: &str,
177    object_id: &str,
178    body: &Json,
179) -> Result<Json, ParseError> {
180    parse_rust_rest::enforce_class_security(
181        class_name,
182        authority.is_privileged(),
183        "update",
184        rc.options.error_detail,
185    )?;
186    let mut body = decode_body(body, parse_rust_core::op::OpPath::Update)?;
187    let is_user = class_name == crate::routes::users::USER_CLASS;
188
189    // Whether this write changes the password, decided before `prepare_user_write` replaces the
190    // key with its hash. **Only a string counts**, because only a string is a password: a
191    // `{"password": null}` body previously read as "no password" to the hasher and as "a password
192    // change" to the followup below, so it revoked every session and issued a replacement while
193    // leaving the old password working. The policy check refuses that body outright now, and this
194    // stays narrow so the two cannot disagree again.
195    let changes_password = is_user
196        && matches!(
197            body.get("password"),
198            Some(parse_rust_core::FieldWrite::Value(ParseValue::String(_)))
199        );
200
201    if is_user {
202        crate::routes::users::enforce_user_update_policy(&body, rc, authority, object_id)?;
203        crate::routes::users::validate_user_identity(state, rc, &body, object_id).await?;
204        crate::routes::users::force_owner_into_acl(&mut body, object_id, authority.is_privileged());
205        crate::routes::users::prepare_user_write(&mut body, false).await?;
206    }
207    let ctx = rc.ctx(state.storage());
208    let res = parse_rust_rest::update(&ctx, class_name, object_id, body)
209        .await
210        // **`_User` only.** The relabelling turns a duplicate-key error into 202 or 203 by reading
211        // the index name, and an ordinary class is free to carry its own unique index called
212        // `username_1`. Applying it everywhere reported somebody else's collision as
213        // `Account already exists for this username.`, where upstream leaves a non-`_User`
214        // collision as 137.
215        .map_err(|e| {
216            if is_user {
217                crate::routes::users::map_duplicate(e)
218            } else {
219                e
220            }
221        })?;
222
223    let mut out = json!({ "updatedAt": res.updated_at.to_iso() });
224    merge_echo(&mut out, res.echoed, rc, class_name);
225
226    // **A password change revokes every session and, for a non-master caller, mints a replacement**
227    // (`RestWrite.js:1192-1211`). Both halves matter and they are not symmetric: revoking is what
228    // makes a password change mean anything, and the new token is what stops the caller logging
229    // themselves out by changing their own password. Master gets the revocation and no new token,
230    // because upstream gates `generateNewSession` on the caller not being master.
231    //
232    // Runs after the write, as upstream's `handleFollowup` does. A failure here leaves the password
233    // changed and the old sessions alive, which is the safe direction to fail in only because the
234    // caller can retry; it is not silent, because the error reaches the client.
235    if changes_password {
236        parse_rust_auth::revoke_all_for_user(state.storage(), object_id).await?;
237        if !authority.is_privileged() {
238            let session = parse_rust_auth::create_session(
239                state.storage(),
240                &state.config().session,
241                parse_rust_auth::NewSession {
242                    user_object_id: object_id,
243                    // **No `createdWith`.** `setCreatedWith` computes `login` only when an auth
244                    // provider is in storage and `signup` only on a create; a password update is
245                    // neither, so it returns before setting anything and upstream's replacement
246                    // session carries no such column (`RestWrite.js:860-870`). Writing
247                    // `{"action":"login"}` here would be visible through `/sessions/me` and would
248                    // describe a login that did not happen.
249                    created_with: None,
250                    installation_id: rc.installation_id.as_deref(),
251                },
252            )
253            .await?;
254            out["sessionToken"] = json!(session.session_token);
255        }
256    }
257    Ok(out)
258}
259
260pub async fn delete_core(
261    state: &AppState,
262    rc: &RequestContext,
263    authority: &Authority,
264    class_name: &str,
265    object_id: &str,
266) -> Result<Json, ParseError> {
267    parse_rust_rest::enforce_class_security(
268        class_name,
269        authority.is_privileged(),
270        "delete",
271        rc.options.error_detail,
272    )?;
273    let ctx = rc.ctx(state.storage());
274    parse_rust_rest::delete(&ctx, class_name, object_id).await?;
275    // Upstream answers an empty object, not 204.
276    Ok(json!({}))
277}
278
279/// Fold the post-write value of any operation the request carried into the response.
280///
281/// `protectedFieldsSaveResponseExempt` decides whether a protected field survives that fold. It
282/// defaults to `true` (`Options/Definitions.js:507-512`), which is the pass-through case; set to
283/// `false` the echo is stripped the same way a query result is.
284fn merge_echo(out: &mut Json, echoed: ParseMap, rc: &RequestContext, class_name: &str) {
285    if echoed.is_empty() {
286        return;
287    }
288    let mut echoed = echoed;
289    if !rc.save_response_exempt && !rc.is_master() {
290        if let Some(plan) = parse_rust_rest::clp::plan_protected_fields(
291            class_name,
292            rc.snapshot.clp(class_name),
293            &rc.scope,
294            None,
295            &rc.options,
296        ) {
297            for field in plan.strip {
298                echoed.shift_remove(&field);
299            }
300        }
301    }
302    let Json::Object(map) = out else { return };
303    if let Json::Object(rendered) = body_of(&echoed) {
304        for (key, value) in rendered {
305            map.insert(key, value);
306        }
307    }
308}
309
310/// `DELETE /sessions/:objectId`, which is narrower than an ordinary class delete.
311///
312/// `rest.del` reads the row first for `_Session` and then re-checks the owner explicitly
313/// (`rest.js:181-197`), because `_Session` rows carry no ACL and the ordinary write constraint
314/// therefore excludes nothing. A miss is `Object not found for delete.`, which is that path's
315/// message rather than the class router's `Object not found.`
316pub async fn delete_session_core(
317    state: &AppState,
318    rc: &RequestContext,
319    object_id: &str,
320) -> Result<Json, ParseError> {
321    let mut where_ = ParsedWhere::default();
322    where_.push(ParsedClause::Field(Constraint::equal(
323        "objectId",
324        ParseValue::String(object_id.to_string()),
325    )));
326
327    let ctx = rc.ctx(state.storage());
328    let rows = parse_rust_rest::find(
329        &ctx,
330        SESSION_CLASS,
331        where_,
332        FindOptions {
333            limit: Some(1),
334            ..Default::default()
335        },
336    )
337    .await?;
338    if rows.is_empty() {
339        return Err(ParseError::new(
340            ErrorCode::ObjectNotFound,
341            "Object not found for delete.",
342        ));
343    }
344
345    // The delete itself is master-scoped, because the narrowing above has already established
346    // that this row belongs to the caller and `_Session` carries no `_wperm` for the ordinary
347    // write constraint to match.
348    let schema = rc.snapshot.get_or_default(SESSION_CLASS);
349    let query = parse_rust_storage::Query::from_constraints(vec![Constraint::equal(
350        "objectId",
351        ParseValue::String(object_id.to_string()),
352    )]);
353    state.storage().delete(&schema, &query).await?;
354    Ok(json!({}))
355}