bugfix: gate manga PATCH and cover endpoints on uploader (0.34.1)
PATCH /mangas/:id, PUT /mangas/:id/cover and DELETE /mangas/:id/cover took the current user but never compared it against the row's uploaded_by. Any signed-in user could overwrite or clear any manga's metadata and cover. Add require_can_edit gate: non-NULL uploaded_by must match the caller; legacy NULL rows stay open until an admin role lands (per migration 0011 historical-data note). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -566,3 +566,78 @@ async fn patch_requires_authentication(pool: PgPool) {
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
/// A signed-in user who didn't upload the manga must not be able to
|
||||
/// PATCH it. Without the uploader-gate this returned 200 — see
|
||||
/// REVIEW.md "manga PATCH / cover endpoints don't check ownership".
|
||||
#[sqlx::test(migrations = "./migrations")]
|
||||
async fn patch_forbidden_for_non_uploader(pool: PgPool) {
|
||||
let h = common::harness(pool);
|
||||
let (_, owner_cookie) = common::register_user(&h.app).await;
|
||||
let (_, intruder_cookie) = common::register_user(&h.app).await;
|
||||
|
||||
let created = create_manga(&h.app, &owner_cookie, json!({ "title": "Mine" })).await;
|
||||
let id = id_of(&created);
|
||||
|
||||
let resp = h
|
||||
.app
|
||||
.oneshot(common::patch_json_with_cookie(
|
||||
&format!("/api/v1/mangas/{id}"),
|
||||
json!({ "status": "completed" }),
|
||||
&intruder_cookie,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
/// Owner can still edit their own manga (regression guard for the
|
||||
/// authz fix).
|
||||
#[sqlx::test(migrations = "./migrations")]
|
||||
async fn patch_allowed_for_uploader(pool: PgPool) {
|
||||
let h = common::harness(pool);
|
||||
let (_, cookie) = common::register_user(&h.app).await;
|
||||
let created = create_manga(&h.app, &cookie, json!({ "title": "Owned" })).await;
|
||||
let id = id_of(&created);
|
||||
let resp = h
|
||||
.app
|
||||
.oneshot(common::patch_json_with_cookie(
|
||||
&format!("/api/v1/mangas/{id}"),
|
||||
json!({ "status": "completed" }),
|
||||
&cookie,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
/// Legacy rows with `uploaded_by IS NULL` (created before migration
|
||||
/// 0011) remain editable by any signed-in user. Without this carve-out
|
||||
/// the historical-data note in 0011 would be broken.
|
||||
#[sqlx::test(migrations = "./migrations")]
|
||||
async fn patch_allowed_on_legacy_null_uploader(pool: PgPool) {
|
||||
let h = common::harness(pool.clone());
|
||||
let (_, cookie) = common::register_user(&h.app).await;
|
||||
let created = create_manga(&h.app, &cookie, json!({ "title": "Legacy" })).await;
|
||||
let id = id_of(&created);
|
||||
|
||||
// Simulate a row uploaded before the column existed: clear
|
||||
// uploaded_by directly via SQL.
|
||||
sqlx::query("UPDATE mangas SET uploaded_by = NULL WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (_, other_cookie) = common::register_user(&h.app).await;
|
||||
let resp = h
|
||||
.app
|
||||
.oneshot(common::patch_json_with_cookie(
|
||||
&format!("/api/v1/mangas/{id}"),
|
||||
json!({ "status": "completed" }),
|
||||
&other_cookie,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user