Skip to main content

parse_rust_server/routes/
classes.rs

1//! The five `/classes` verbs.
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
7use axum::extract::{Path, Query, State};
8use axum::response::{IntoResponse, Response};
9use axum::Json;
10use parse_rust_core::{ErrorCode, ParseError, ParseMap, ParseValue};
11use parse_rust_rest::AclScope;
12use parse_rust_storage::QueryOptions;
13use serde_json::{json, Value as Json_};
14use std::collections::HashMap;
15
16use crate::auth::Authority;
17use crate::response::ParseErrorResponse;
18use crate::state::AppState;
19
20/// Map request authority onto an ACL scope.
21///
22/// Master and maintenance are unrestricted. A session token would produce
23/// `AclScope::User`, but session resolution lands with `_User`; until then a client-key request
24/// is anonymous, which is the fail-closed direction.
25fn scope_for(authority: &Authority, state: &AppState) -> Result<AclScope, ParseError> {
26    Ok(match authority {
27        Authority::Master | Authority::Maintenance => AclScope::Unrestricted,
28        Authority::Client { session_token } => match session_token {
29            // A token that does not resolve is an error, not anonymity. See `AppState`.
30            Some(token) => state.scope_for_session(token)?,
31            None => AclScope::Anonymous,
32        },
33    })
34}
35
36/// Convert a Parse-format map into a JSON response body.
37///
38/// Strips every `_`-prefixed key first. This is the single audit point for "nothing internal
39/// reaches a client", and it runs on every response body this router produces.
40fn body_of(row: &ParseMap) -> Json_ {
41    let row = parse_rust_rest::to_response_body(row);
42    serde_json::from_str(&ParseValue::Object(row).to_json()).unwrap_or(Json_::Null)
43}
44
45fn err(e: ParseError) -> Response {
46    ParseErrorResponse(e).into_response()
47}
48
49/// Decode a JSON request body into Parse values.
50fn decode_body(value: Json_) -> Result<ParseMap, ParseError> {
51    let map = match parse_rust_core::classify(value)? {
52        ParseValue::Object(map) => map,
53        _ => return Err(ParseError::invalid_json("body must be an object")),
54    };
55    // A client must not supply a server-internal column. Without this, a caller could write its
56    // own `_rperm` and grant itself read access to a row.
57    parse_rust_rest::reject_reserved_keys(&map)?;
58    Ok(map)
59}
60
61pub async fn create(
62    State(state): State<AppState>,
63    authority: Authority,
64    Path(class_name): Path<String>,
65    Json(body): Json<Json_>,
66) -> Response {
67    if let Err(e) = enforce_class_security(&class_name, &authority, "create") {
68        return err(e);
69    }
70    let scope = match scope_for(&authority, &state) {
71        Ok(s) => s,
72        Err(e) => return err(e),
73    };
74    let mut body = match decode_body(body) {
75        Ok(b) => b,
76        Err(e) => return err(e),
77    };
78    if class_name == "_User" {
79        if let Err(e) = super::users::hash_user_password(&mut body) {
80            return err(e);
81        }
82        super::users::ensure_user_identity_and_acl(&mut body);
83    }
84    match parse_rust_rest::create(state.storage(), &class_name, body, &scope).await {
85        Ok(res) => (
86            axum::http::StatusCode::CREATED,
87            Json(json!({
88                "objectId": res.object_id,
89                "createdAt": res.created_at.to_iso(),
90            })),
91        )
92            .into_response(),
93        Err(e) => err(e),
94    }
95}
96
97pub async fn find(
98    State(state): State<AppState>,
99    authority: Authority,
100    Path(class_name): Path<String>,
101    Query(params): Query<HashMap<String, String>>,
102) -> Response {
103    if let Err(e) = enforce_class_security(&class_name, &authority, "find") {
104        return err(e);
105    }
106    let scope = match scope_for(&authority, &state) {
107        Ok(s) => s,
108        Err(e) => return err(e),
109    };
110
111    let constraints = match params.get("where") {
112        Some(raw) => match serde_json::from_str::<Json_>(raw) {
113            Ok(v) => match parse_rust_rest::parse_where(&v) {
114                Ok(c) => c,
115                Err(e) => return err(e),
116            },
117            Err(_) => {
118                return err(ParseError::invalid_query(
119                    "where must be valid JSON".to_string(),
120                ))
121            }
122        },
123        None => Vec::new(),
124    };
125
126    // `count=1` asks for the count instead of, or alongside, the results.
127    let wants_count = params.get("count").map(|c| c == "1").unwrap_or(false);
128
129    let options = QueryOptions {
130        // An absent or unparsable `limit` falls back to Parse's default of 100 rather than to
131        // "no limit". `limit=0` is a legitimate request for zero rows, usually paired with
132        // `count=1`, and must not be read as unlimited either.
133        limit: Some(
134            params
135                .get("limit")
136                .and_then(|v| v.parse::<u32>().ok())
137                .unwrap_or(parse_rust_storage::query::DEFAULT_LIMIT),
138        ),
139        skip: params.get("skip").and_then(|v| v.parse().ok()),
140        order: params
141            .get("order")
142            .map(|o| QueryOptions::parse_order(o))
143            .unwrap_or_default(),
144        keys: params
145            .get("keys")
146            .map(|k| k.split(',').map(str::trim).map(str::to_string).collect()),
147    };
148
149    let results = match parse_rust_rest::find(
150        state.storage(),
151        &class_name,
152        constraints.clone(),
153        options,
154        &scope,
155    )
156    .await
157    {
158        Ok(r) => r,
159        Err(e) => return err(e),
160    };
161
162    let mut body = json!({ "results": results.iter().map(body_of).collect::<Vec<_>>() });
163    if wants_count {
164        match parse_rust_rest::count(state.storage(), &class_name, constraints, &scope).await {
165            Ok(n) => {
166                body["count"] = json!(n);
167            }
168            Err(e) => return err(e),
169        }
170    }
171    Json(body).into_response()
172}
173
174pub async fn get(
175    State(state): State<AppState>,
176    authority: Authority,
177    Path((class_name, object_id)): Path<(String, String)>,
178) -> Response {
179    if let Err(e) = enforce_class_security(&class_name, &authority, "get") {
180        return err(e);
181    }
182    let scope = match scope_for(&authority, &state) {
183        Ok(s) => s,
184        Err(e) => return err(e),
185    };
186    match parse_rust_rest::get(state.storage(), &class_name, &object_id, &scope).await {
187        Ok(row) => Json(body_of(&row)).into_response(),
188        Err(e) => err(e),
189    }
190}
191
192pub async fn update(
193    State(state): State<AppState>,
194    authority: Authority,
195    Path((class_name, object_id)): Path<(String, String)>,
196    Json(body): Json<Json_>,
197) -> Response {
198    if let Err(e) = enforce_class_security(&class_name, &authority, "update") {
199        return err(e);
200    }
201    let scope = match scope_for(&authority, &state) {
202        Ok(s) => s,
203        Err(e) => return err(e),
204    };
205    let mut body = match decode_body(body) {
206        Ok(b) => b,
207        Err(e) => return err(e),
208    };
209    if class_name == "_User" {
210        if let Err(e) = super::users::hash_user_password(&mut body) {
211            return err(e);
212        }
213    }
214    match parse_rust_rest::update(state.storage(), &class_name, &object_id, body, &scope).await {
215        Ok(res) => Json(json!({ "updatedAt": res.updated_at.to_iso() })).into_response(),
216        Err(e) => err(e),
217    }
218}
219
220pub async fn delete(
221    State(state): State<AppState>,
222    authority: Authority,
223    Path((class_name, object_id)): Path<(String, String)>,
224) -> Response {
225    if let Err(e) = enforce_class_security(&class_name, &authority, "delete") {
226        return err(e);
227    }
228    let scope = match scope_for(&authority, &state) {
229        Ok(s) => s,
230        Err(e) => return err(e),
231    };
232    match parse_rust_rest::delete(state.storage(), &class_name, &object_id, &scope).await {
233        // Upstream answers an empty object, not 204.
234        Ok(()) => Json(json!({})).into_response(),
235        Err(e) => err(e),
236    }
237}
238
239/// Classes a client may not address through `/classes`.
240///
241/// `_User` has its own router upstream. A non-master write through `/classes` skips signup and
242/// session creation, so it is refused here. Master and maintenance writes are legitimate, but the
243/// handlers still run their password and ACL data through the shared user preparation path.
244///
245/// The master key is exempt, matching upstream: `enforceRoleSecurity` gates non-master callers
246/// only, and the dashboard legitimately reads `_User` through the class routes.
247fn enforce_class_security(
248    class_name: &str,
249    authority: &Authority,
250    operation: &str,
251) -> Result<(), ParseError> {
252    if matches!(authority, Authority::Master | Authority::Maintenance) {
253        return Ok(());
254    }
255    // **`_User` writes are refused. This is a deliberate difference from upstream.**
256    //
257    // Upstream permits `POST /classes/_User` and makes it safe in `RestWrite`, which special-cases
258    // `className === "_User"` regardless of route. The allowed master path above mirrors that by
259    // sharing password and ACL preparation with signup.
260    //
261    // Refusing is the fail-closed choice and costs a client only the ability to create a user
262    // without a session token. Reads are *not* refused: upstream allows them, they are ACL
263    // filtered, and blocking them would break legitimate user queries.
264    let write_only_forbidden =
265        class_name == "_User" && matches!(operation, "create" | "update" | "delete");
266
267    let forbidden = write_only_forbidden
268        || class_name.starts_with("_Join:")
269        || matches!(
270            class_name,
271            "_Session"
272                | "_Role"
273                | "_Installation"
274                | "_JobStatus"
275                | "_PushStatus"
276                | "_Hooks"
277                | "_GlobalConfig"
278                | "_GraphQLConfig"
279                | "_JobSchedule"
280                | "_Audience"
281                | "_Idempotency"
282        );
283    if forbidden {
284        return Err(ParseError::new(
285            ErrorCode::OperationForbidden,
286            format!(
287                "Clients aren't allowed to perform the {operation} operation on the {class_name} collection."
288            ),
289        ));
290    }
291    Ok(())
292}
293
294/// `POST` dispatcher for the collection route.
295///
296/// The JavaScript SDK sends every request as a `POST` and puts the real method in `_method`.
297/// axum matches on the transport method before middleware can rewrite it, so the dispatch happens
298/// here, where it is explicit and testable. See `body_credentials`.
299pub async fn dispatch_collection(
300    state: State<AppState>,
301    authority: Authority,
302    path: Path<String>,
303    query: Query<HashMap<String, String>>,
304    method: Option<axum::Extension<crate::body_credentials::MethodOverride>>,
305    body: Option<Json<Json_>>,
306) -> Response {
307    match method.map(|m| m.0 .0) {
308        Some(m) if m == axum::http::Method::GET => find(state, authority, path, query).await,
309        // No override at all: a genuine POST, which on the collection route is a create.
310        None => match body {
311            Some(b) => create(state, authority, path, b).await,
312            // A body that failed to parse must not become an empty create. `Option<Json<_>>` is
313            // `None` for a malformed or oversized body as well as an absent one, so treating that
314            // as `{}` turned a rejection into a write.
315            None => err(ParseError::invalid_json("body must be a JSON object")),
316        },
317        // An override we do not implement is an error, not a silent fallthrough to create.
318        Some(other) => err(ParseError::new(
319            ErrorCode::CommandUnavailable,
320            format!("unsupported method override: {other}"),
321        )),
322    }
323}
324
325/// `POST` dispatcher for the object route.
326pub async fn dispatch_object(
327    state: State<AppState>,
328    authority: Authority,
329    path: Path<(String, String)>,
330    method: Option<axum::Extension<crate::body_credentials::MethodOverride>>,
331    body: Option<Json<Json_>>,
332) -> Response {
333    // There is no POST verb on an object route. A bare POST with no override used to fall through
334    // to `update`, so an unrelated request could mutate a row.
335    let Some(m) = method.map(|m| m.0 .0) else {
336        return err(ParseError::new(
337            ErrorCode::CommandUnavailable,
338            "POST is not supported on an object route; use PUT, DELETE, or _method",
339        ));
340    };
341    if m == axum::http::Method::GET {
342        return get(state, authority, path).await;
343    }
344    if m == axum::http::Method::DELETE {
345        return delete(state, authority, path).await;
346    }
347    if m != axum::http::Method::PUT {
348        return err(ParseError::new(
349            ErrorCode::CommandUnavailable,
350            format!("unsupported method override: {m}"),
351        ));
352    }
353    match body {
354        Some(b) => update(state, authority, path, b).await,
355        None => err(ParseError::invalid_json("body must be a JSON object")),
356    }
357}