Keep a feed's items and files to the people who subscribe to it (#129)
Routes that take a feed or an enclosure id did not check who was asking. Anyone signed in could
read any feed's items through GET /api/feeds/{id}/entries, a paid feed's included, with the
addresses of its files, which can carry the subscriber's key: the Directory leaves such feeds
out for that reason, and this route handed them back to whoever guessed the id, a slug of the
title. In production it answered 25 items of a feed the asking account does not subscribe to.
/media/{id} served any downloaded file by its sequential id, POST /api/enclosures/{id}/download
and /api/feeds/{id}/download-latest queued any feed's downloads, and DELETE
/api/enclosures/{id}?force=true deleted any file.
Each now answers 404, "you do not subscribe to that feed", unless the person subscribes to it.
A feed inside an OPML has a subscription row of its own for everyone subscribed to the OPML, so
that holds for those feeds too. Found while adding the Directory's feed page (#128), which has
its own route that answers only for listed feeds and carries no files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
22
src/web.rs
22
src/web.rs
@@ -1325,9 +1325,22 @@ async fn entries(
|
||||
user: crate::db::User,
|
||||
Query(page): Query<Page>,
|
||||
) -> Result<Json<EntryPage>, ApiError> {
|
||||
subscribed(&state, &user, &id).await?;
|
||||
entry_page(&state, user.id, Some(&id), &page).await
|
||||
}
|
||||
|
||||
/// A feed's items and files are for the people who subscribe to it (#129). Anyone signed in
|
||||
/// could read any feed's by its id, a paid one's included, with its enclosure addresses, which
|
||||
/// can carry the subscriber's key, and fetch, queue or delete its files by theirs. A feed in
|
||||
/// an OPML has a subscription of its own for everyone subscribed to the OPML, so this holds
|
||||
/// for those too. Not found, as for any other feed that is not yours.
|
||||
async fn subscribed(state: &WebState, user: &crate::db::User, feed_id: &str) -> Result<(), ApiError> {
|
||||
match state.ctx.db.subscription(user.id, feed_id).await? {
|
||||
Some(_) => Ok(()),
|
||||
None => Err(ApiError::not_found("you do not subscribe to that feed")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Every subscribed feed's items together, newest first: All Subscriptions.
|
||||
async fn all_entries(
|
||||
State(state): State<WebState>,
|
||||
@@ -1676,12 +1689,14 @@ async fn set_flags(
|
||||
async fn download_now(
|
||||
State(state): State<WebState>,
|
||||
Path(id): Path<i64>,
|
||||
user: crate::db::User,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
let enc = state
|
||||
.ctx
|
||||
.db
|
||||
.enclosure(id).await?
|
||||
.ok_or_else(|| anyhow::anyhow!("no enclosure {id}"))?;
|
||||
subscribed(&state, &user, &enc.feed_id).await?;
|
||||
if enc.path.is_some() {
|
||||
return Ok(StatusCode::NO_CONTENT); // Already here.
|
||||
}
|
||||
@@ -1711,6 +1726,7 @@ async fn delete_file(
|
||||
.db
|
||||
.enclosure(id).await?
|
||||
.ok_or_else(|| anyhow::anyhow!("no enclosure {id}"))?;
|
||||
subscribed(&state, &user, &enc.feed_id).await?;
|
||||
|
||||
// There is one copy of the file: deleting it deletes everyone's. Say so before doing
|
||||
// it, once, and let them decide.
|
||||
@@ -1828,11 +1844,15 @@ fn changed_feed(ev: &Event) -> Option<&str> {
|
||||
async fn media(
|
||||
State(state): State<WebState>,
|
||||
Path(id): Path<i64>,
|
||||
user: crate::db::User,
|
||||
req: Request,
|
||||
) -> Response {
|
||||
let Ok(Some(enc)) = state.ctx.db.enclosure(id).await else {
|
||||
return (StatusCode::NOT_FOUND, "no such enclosure").into_response();
|
||||
};
|
||||
if let Err(e) = subscribed(&state, &user, &enc.feed_id).await {
|
||||
return e.into_response();
|
||||
}
|
||||
let Some(path) = enc.path else {
|
||||
return (StatusCode::NOT_FOUND, "not downloaded").into_response();
|
||||
};
|
||||
@@ -1962,8 +1982,10 @@ fn five() -> i64 {
|
||||
async fn download_latest(
|
||||
State(state): State<WebState>,
|
||||
Path(id): Path<String>,
|
||||
user: crate::db::User,
|
||||
Json(body): Json<HowMany>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
subscribed(&state, &user, &id).await?;
|
||||
let ids = state.ctx.db.undownloaded(&id, body.count.clamp(1, 100)).await?;
|
||||
for enc in &ids {
|
||||
state.ctx.db.requeue(*enc).await?;
|
||||
|
||||
Reference in New Issue
Block a user