feat(books): filter drawer .2 — combinator + chips + Author/Series/Publisher/Language/Tag facets (bookshelf-lj69z.2) #1129
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "bd-bookshelf-lj69z.2"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
/books/facetsendpointappendFacetPredicatesFacetChip/buildFacetChips) shown in the controls strip; each chip has a pre-builtRemoveURLbook_metadata.publisher,book_metadata.language,book_tag.name,author.nameadded in migration 0047Test plan
make testpasses (unit + coverage gate 100%)make coveragegate green — zero uncovered statement blocksgo build -tags e2e ./e2e/...compiles cleanlye2e/browser/journey_filter_drawer_test.go— "Journey: Filter Drawer — combinator + chips" (4 It-steps)Screenshot
Screenshots of the drawer with combinator + chips applied will be uploaded by the go-rod harness during the CI e2e run.
Closes bead bookshelf-lj69z.2, bookshelf-kv9ug, bookshelf-kommq on merge.
Security Review — PR #1129 (filter drawer .2: 5 new facets + AND/OR/NOT combinator)
Scope: multi-user / library scoping / IDOR (the #1 risk given slice .5's history), combinator bypass, SQL injection, input bounds, migration.
Library scoping (the .5 IDOR class) — CLEAN
Traced every one of the 5 new facet count queries (
FacetAuthorCounts,FacetSeriesCounts,FacetPublisherCounts,FacetLanguageCounts,FacetTagCountsininternal/books/facet_store.go):book band appliesb.library_id IN (?…)bound from session-resolveduserLibraryIDs, with the same fail-closed guard as the existingFacetFormatCounts(userLibraryIDs != nil && len==0 → [];nil= admin/unrestricted).appendScopePredicates, whoseAuthorID/TagID/CategoryID/SeriesName/ShelfIDare allEXISTS(… WHERE …book_id = b.id …)correlated tob.id, so a request-suppliedauthor_id/tag_idfrom another library only intersects the user's own scoped rows — no cross-library counts.?live in WHERE/EXISTS fragments, and args are appended in the same order the predicates are assembled (library args, then scope args). MagicJoins/MagicArgs handling is identical to the already-fixed existing functions.Combinator (AND/OR/NOT) — CLEAN, cannot bypass scope
appendFacetPredicates(internal/books/store.go) is called after the unconditional top-level library scope (b.library_id IN (…), or fail-closed1=0whenUserLibraryIDsis non-nil empty). All three combinator branches emit a single WHERE predicate AND-ed with that library scope.NOT (g1 OR g2 …)only negates the facet EXISTS predicates — the libraryINscope is still AND-ed, so a NOT group cannot enumerate or leak foreign authors/tags/series; it merely excludes matching rows from the user's own scoped set.Injection / allowlist — CLEAN
and/or/not) validated in the handler; anything else →ErrValidation.?; no string concatenation of user values into SQL.Input bounds — CLEAN
Handler caps each of
fauthor/fseries/fpublisher/flanguage/ftagat 50 values and per-value length (1000 chars; 50 for language); count queriesLIMIT 100.Migration 0047 — CLEAN
Empty DDL (index-analysis placeholder). No new columns → no Grimmory schema violation.
Templates / JS — CLEAN
Facet chip
Label/RemoveURLrendered via context-aware{{.}}(auto-escaped); notemplate.HTML/| safe. JS controller rebuilds a same-origin URL from checkbox names/values andlocation.assigns — noinnerHTML, no inlinestyle=.Findings
[MAJOR] internal/books/service.go:105-128 — new list-filter facets + combinator are parsed but never applied (dead filters)
The 5 new fields (
FacetAuthors/FacetSeries/FacetPublishers/FacetLanguages/FacetTags) andFacetCombinatorare parsed + validated inListHandlerand set onFilter, and the store predicates (buildAuthorGroup…buildTagGroup, combinator switch) are fully implemented — but theFilter → ListBooksFilteredParamsmapping inservice.gostill stops atFacetGenresand does NOT copy the six new fields (git grep 'FacetAuthors:'in non-test code returns nothing). Result: selecting any of the 5 new facets or a combinator on the book list silently does nothing. This is a correctness/broken-feature bug, not a security leak (the well-scoped predicates never execute). Fix: addFacetAuthors: filter.FacetAuthors, … FacetCombinator: filter.FacetCombinatorto theListBooksFilteredParamsliteral inservice.go.[MINOR] internal/books/facet_handler.go:174 — parseInt64Param loosens numeric validation
parseInt64Paramusesfmt.Sscanf(s, "%d", &v), which accepts trailing garbage (e.g."12abc"→ 12, nil err) where the previousstrconv.ParseIntrejected it. Not exploitable — these scope IDs are parameterized and intersected with the user's library scope, so a malformed/foreign value yields zero rows — but it weakens boundary validation. Fix: revert tostrconv.ParseInt(s, 10, 64).REVIEW VERDICT: 0 blocker, 1 major, 1 minor
Filter Drawer screenshot (filter-drawer-chips-combinator)
Books list filter drawer — Genre facet expanded, filter applied.
Filter Drawer screenshot (filter-drawer-chip-removed)
Books list filter drawer — Genre facet expanded, filter applied.
Filter Drawer screenshot (filter-drawer-genre-expanded)
Books list filter drawer — Genre facet expanded, filter applied.
Filter Drawer screenshot (filter-drawer-fantasy-filtered)
Books list filter drawer — Genre facet expanded, filter applied.
b8e37afe5e4885d5284a