Skip to content

feat(server): make smart list visibility an admin capability - #152

Merged
everpcpc merged 2 commits into
masterfrom
smart-filter
Oct 6, 2026
Merged

everpcpc merged 2 commits into
masterfrom
smart-filter

Conversation

@dyphire

@dyphire dyphire commented Oct 6, 2026

Copy link
Copy Markdown
Member

A regular user's smart lists are always PRIVATE: non-admins cannot set visibility or share targets on create or PATCH, and GET share-targets now requires admin — matching komga's admin-only /api/v2/users, since exposing every user's email to all authenticated users is not acceptable. Admins can still publish PUBLIC lists and share SHARED lists; everyone authenticated can read those, but only the owner or an admin can modify or delete them (require_owner_or_admin gate on PATCH/DELETE, closing the sharee-write hole). The visibility test is rewritten around the admin-only model.

A regular user's smart lists are always PRIVATE: non-admins cannot set
visibility or share targets on create or PATCH, and GET share-targets
now requires admin — matching komga's admin-only /api/v2/users, since
exposing every user's email to all authenticated users is not
acceptable. Admins can still publish PUBLIC lists and share SHARED
lists; everyone authenticated can read those, but only the owner or an
admin can modify or delete them (require_owner_or_admin gate on
PATCH/DELETE, closing the sharee-write hole). The visibility test is
rewritten around the admin-only model.
@dyphire
dyphire requested a review from everpcpc October 6, 2026 09:12

@everpcpc everpcpc left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed; fmt/clippy/nextest (1095 tests) are green. The model is coherent and closes both holes from the previous round: visibility as an admin capability matches the kmweb wizard's gating, and admin-only share-targets resolves the email-directory exposure the same way komga's /api/v2/users does. The rewritten test pins the new contract well — the old version exercised these exact paths with 200s, so the 400/403 assertions are real regression coverage.

One gap and two nits:

1. openapi.json wasn't updated for the semantics change. The document still describes share-targets as a plain "minimal user directory" with no admin note, and the create/PATCH descriptions don't say visibility/sharing is admin-only. Since #150 established that kmrs extensions are documented there, this behavior change should land in the document too — otherwise the spec contradicts the server.

2. Create/PATCH asymmetry on explicit PRIVATE. Create accepts a non-admin body with visibility: "PRIVATE" (it equals the default), but PATCH rejects any non-admin body where visibility is merely present (is_some()). A client that always serializes the field works on create and 400s on PATCH. Either allow the explicit PRIVATE no-op on PATCH too, or reject it on create for symmetry.

3. 400 vs 403. "only admins may set visibility" is an authorization failure, and the codebase's require_admin answers those with 403 (ApiError::forbidden). 400 reads as "your request is malformed", which it isn't. Minor, but the tests now pin the less precise code.

Cross-repo follow-ups (not this PR's scope, flagging so they don't get lost):

  • kmworks/kmweb#123: SmartListDialog queries shareTargets() unconditionally — with this change, every non-admin opening the wizard gets a background 403 (retry noise in the console). The query should be enabled: admin.
  • The website's server/enhancements/smart-lists page describes visibility as owner-writable and share-targets as open; I'll send a small docs PR once this merges.

…nt the admin rule

- create now rejects the mere presence of visibility/sharedWithUserIds
  from non-admins, same as PATCH (an explicit PRIVATE no longer slips
  through), and both gates answer 403 instead of 400
- openapi.json: share-targets and the visibility/share fields carry the
  admin-only note
@everpcpc

everpcpc commented Oct 6, 2026

Copy link
Copy Markdown
Member

Taking over the PR to land the review fixes directly — pushed 3aee57b9:

  • 403 instead of 400: both admin gates now answer ApiError::forbidden, matching the require_admin semantics elsewhere (attempting a capability you don't have is not a malformed request).
  • Create/PATCH symmetry: SmartListCreationDto.visibility/sharedWithUserIds are Option now, and create rejects their mere presence from non-admins — an explicit visibility: "PRIVATE" no longer slips through create while PATCH rejects it. The rule is uniform: non-admins must not send either field at all. The visibility_scopes_who_sees_a_list test covers the explicit-PRIVATE case, and all gate assertions moved to 403 (they fail against the previous revision, so the regression coverage is real).
  • openapi.json: share-targets is documented as admin-only (like komga's /api/v2/users), the create/PATCH descriptions state that visibility and sharing are admin capabilities, and the two schema fields carry admin only notes.

fmt / clippy / nextest (1095 tests) all green locally.

The two cross-repo items remain: kmworks/kmweb#123 should gate the shareTargets() query with enabled: admin, and I'll send the website docs touch-up once this merges.

@everpcpc

everpcpc commented Oct 6, 2026

Copy link
Copy Markdown
Member

Holistic design review of the whole smart-lists feature — #150 (merged) plus this PR — as it now stands. CI: lint/test/ubuntu/docker green, macos/windows builds still running.

Design verdict

The final shape is coherent. The core decisions all hold up:

  • Live evaluation, no stored membership — per-user conditions (read status, restrictions, library authorization) are correct by construction, and the items endpoints reuse the exact /list query path, so a smart list can never drift from what the same search would return.
  • State isolation — definitions, shares, and covers live in kmrs.sqlite only; the main database schema stays byte-for-byte Java, so the drop-in promise is untouched. OpenAPI tags everything as a kmrs extension and webhooks keep komga's event vocabulary.
  • Canonicalized storage — search documents are validated against the target at write time and re-serialized, so the stored JSON is always re-validatable and the DTO round-trips the parsed form.
  • The permission model after this PR is the simplest one that fits komga's idioms:
action regular user sharee any user (PUBLIC) admin
create (always PRIVATE) ✓ ✓ ✓ ✓ (any visibility)
read a list own only ✓ ✓ ✓ all (+?owner=)
PATCH/DELETE own only 403 403 ✓ any
set visibility / share targets 403 403 403 ✓
cover writes own only 403 403 own only
share-targets directory 403 403 403 ✓

Strangers always get 404, so existence never leaks. For regular users the feature is "saved searches"; the curated PUBLIC/SHARED lists are necessarily admin-owned, which matches how komga gates user-visible administration everywhere else.

Design consequences worth stating for the record (accepted, not blockers)

  1. An admin publishing a user's list takes visibility out of that owner's hands — the owner can't revert it (admin gate) but can always delete the list, so nobody is stuck hosting a public list they don't want.
  2. SSE scopes are owner-only — sharees of PUBLIC/SHARED lists receive no SmartListChanged/SmartListThumbnailChanged, so their clients go stale until refetch (covers self-heal via the 60s cache header). Fine for v1; widening the scope is a deliberate follow-up if it ever bothers anyone.
  3. Orphaned PUBLIC lists (owner deleted) evaluate their mosaic from an unrestricted context — the accepted owner-view trade-off widens slightly; admins can delete orphans from the ?owner= view.
  4. Transitional state: lists a non-admin managed to publish/share while feat(server): user-owned smart lists with live filter evaluation #150 was merged but this PR was not keep their scope; the owner just can't change it anymore (admin can). No migration needed, but worth knowing.

Open items across the feature (outside this PR)

  • feat: smart list browsing, wizard editing and cover management kmweb#123 — the match-all round-trip bug in searchToFilterState is the one real functional bug left in the feature area (verified: editing a list whose filter uses match-all silently degrades it to match-any on save, lossy: false). Also there: the admin "Manage posters" 403 trap, the unfiltered SERIES_FILTER_GROUPS, and gating shareTargets() with enabled: admin (now required, since this PR makes the endpoint 403 for non-admins).
  • Website server/enhancements/smart-lists needs the visibility section updated to the admin model once this merges (mine).

@everpcpc
everpcpc merged commit f340680 into master Oct 6, 2026
6 checks passed
@everpcpc
everpcpc deleted the smart-filter branch October 6, 2026 10:26
everpcpc pushed a commit to kmworks/kmweb that referenced this pull request Oct 6, 2026
Surface the kmrs smart-list API (kmworks/kmrs#150, kmworks/kmrs#152): user-owned search filters created and edited through a two-step wizard (details, then filters inline), evaluated live by the server.

- smartListsApi + SmartList DTOs; SSE wiring for the smart-list events,
  including read-progress invalidation so items and the continue target
  stay fresh
- new 'entities' filter group kind (libraries / read lists / collections)
  with multi-select and is/is-not membership; referential options in the
  wizard follow the selected scope
- /smart-lists browse page and /smart-lists/:id detail pages for both
  targets, reusing the browse grid/filter bar; page-side filters post as an
  overlay body; owner-scoped edit/delete (new isOwner permission idiom),
  covers owner-only (admins no longer get a dead Manage posters entry on
  others' lists); adminOnly filter groups hidden from non-admins; read/peek
  entry points on book lists
- two-step create/edit wizard reusing the filter building blocks inline
  (no drawer overlay), with a debounced live match-count preview, admin-only
  visibility and share-target picking, and a lossy warning when a stored
  document contains conditions the editor cannot represent
- stored-filter round-tripping verified lossless across the builder
  vocabulary: group mode tracked through the same condition→group mapping
  as values, single-group documents recognized (whole-condition same-key
  anyOf/allOf and year-pair runs), year-pair bounds arithmetic fixed, and
  unrepresentable constructs (mixed is/isNot per group, repeated bare keys)
  flagged instead of silently rewritten
- PosterManager gains the 'smartlist' kind (upload/select/delete covers,
  generated mosaic listed alongside uploads)
- smart-list cards use the generated/selected cover with cache busting
- i18n: smartlists namespace in all 10 locales plus nav/filter/poster keys
- bump version to 0.11.8
everpcpc added a commit to kmworks/website that referenced this pull request Oct 6, 2026
Aligns the smart-lists enhancement page with kmworks/kmrs#152: visibility and sharing are admin capabilities (a regular user's lists are always private), share-targets is admin-only like komga's /api/v2/users, and writes split into owner-or-admin (rename/filter/scope/delete) vs owner-only (covers).
everpcpc added a commit that referenced this pull request Oct 6, 2026
Patch bump covering the smart-lists feature line: #150 (smart lists), #152 (admin-gated visibility).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants