fix(feed): a malformed saved view cannot break the autotag cleanup, and a foreign private tag reads as missing
E32 review round 1 (B5, B6).
- The autotag orphan cleanup reads every user's feed views. A view whose tag pick was not a list
("tags": 5) raised for everyone's job, True passed as tag id 1, and ~66k ids pushed the NOT IN
past psycopg's 65535 bind parameters. The views are now read defensively and the kept ids
filtered in Python; the saved-views route holds filters.tags to a list of at most 500 ids, on
create and on update.
- tags_by_category resolved any tag id, so another user's private tag landed in its real category
and the feed summary answered differently than for an id that does not exist, which told the
caller which foreign ids exist. It is owner-scoped now (pre-existing; S2's missing-tag branch
made the difference visible).
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, field_validator
|
||||
from sqlalchemy import func, select, update
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -53,11 +53,30 @@ def list_views(
|
||||
return [_serialize(v) for v in rows]
|
||||
|
||||
|
||||
# A feed view's tag pick is read by more than the feed: the autotag job keeps every tag a view names
|
||||
# (sync/autotag.py). Held here to the shape the feed sends — a list of tag ids, at most this many — so
|
||||
# a hand-made body (`"tags": 5`, or 60k ids) can't break that job for every user (E32 review).
|
||||
_MAX_VIEW_TAGS = 500
|
||||
|
||||
|
||||
def _check_filters(filters: dict | None) -> dict | None:
|
||||
tags = (filters or {}).get("tags")
|
||||
if tags is None:
|
||||
return filters
|
||||
if not isinstance(tags, list) or len(tags) > _MAX_VIEW_TAGS:
|
||||
raise ValueError(f"filters.tags must be a list of at most {_MAX_VIEW_TAGS} tag ids")
|
||||
if any(type(t) is not int for t in tags): # `type`, not isinstance: True is not a tag id
|
||||
raise ValueError("filters.tags must hold tag ids")
|
||||
return filters
|
||||
|
||||
|
||||
class CreateViewIn(BaseModel):
|
||||
name: str
|
||||
filters: dict
|
||||
source: str = "feed"
|
||||
|
||||
_filters = field_validator("filters")(_check_filters)
|
||||
|
||||
|
||||
@router.post("")
|
||||
def create_view(
|
||||
@@ -98,6 +117,8 @@ class UpdateViewIn(BaseModel):
|
||||
filters: dict | None = None
|
||||
is_default: bool | None = None
|
||||
|
||||
_filters = field_validator("filters")(_check_filters)
|
||||
|
||||
|
||||
@router.patch("/{view_id}")
|
||||
def update_view(
|
||||
|
||||
Reference in New Issue
Block a user