Skip to content

fix(api): admins see all libraries in list/browse views#1310

Open
vavallee wants to merge 1 commit into
mainfrom
fix/admin-list-scoping
Open

fix(api): admins see all libraries in list/browse views#1310
vavallee wants to merge 1 commit into
mainfrom
fix/admin-list-scoping

Conversation

@vavallee

Copy link
Copy Markdown
Owner

Root cause (verified against prod)

A multi-admin instance had authors/books split by owner_user_id (user 1 admin, user 2 akadmin — the latter auto-provisioned when SSO was enabled). The list endpoints scoped strictly to the caller's id (AuthorHandler.Listfilter.UserID = auth.UserIDFromContext(ctx)) with no admin bypass, while auth.CheckOwnership already treats admins (and API-key / no-tenancy callers) as able to access any item by id. So admin user 2 could not see user 1's authors/books in lists, and "Add Author" hit the global foreign_id UNIQUE constraint → dead-end "author already exists".

Confirmed on the live DB: author Cory Doctorow (id 41) is owner_user_id = 1; browsing as admin user 2 hid it; owner spread was NULL=25 / u1=13 / u2=14.

Fix

  • auth.ListScopeUserID(ctx) — returns 0 (unscoped) for admin role, disabled tenancy, or API-key callers; the caller's id otherwise. Mirrors CheckOwnership's bypasses.
  • Routed the owner-scoped list/browse handlers through it: authors list, books list, ListByAuthorAndUser, OPDS.
  • db.QueryScopeForIncludingNull — books list now uses (owner = ? OR owner IS NULL) like the authors list, so owner-NULL rows (pre-multi-user / imported without an owner) are visible to a logged-in user instead of hidden. userID == 0 still means unscoped.

Security

No real access widening: admins can already open any item by id via CheckOwnership; this only makes lists consistent with that. Non-admin isolation is preserved and tested — a non-admin sees only their own rows plus unowned, never another non-admin's.

Tests

internal/api/list_scope_test.go, internal/db/list_scope_test.go: admin-sees-everything, non-admin-isolated-plus-global, API-key unscoped, tenancy-disabled unscoped, books-includes-null-owner. Admin-sees-everything fails pre-fix. Full api/db/auth suites pass; targeted scope tests clean under -race (0 data races; the full-package -race only timed out on a constrained box).

🤖 Generated with Claude Code

A multi-admin instance (e.g. an SSO-provisioned second admin) split authors
and books by owner_user_id, but the list endpoints scoped strictly to the
caller's user id with no admin bypass — so one admin could not see another
admin's authors/books, and adding them hit the global foreign_id UNIQUE
constraint as "author already exists". This was inconsistent with
auth.CheckOwnership, which already treats admins (and API-key / no-tenancy
callers) as able to access any item by id.

Add auth.ListScopeUserID — unscoped (0) for admin role, disabled tenancy, or
API-key callers; the caller's id otherwise — and route the owner-scoped
list/browse handlers (authors, books, ListByAuthorAndUser, OPDS) through it.
Also add db.QueryScopeForIncludingNull so the books list matches the authors
list and includes owner-NULL rows (CheckOwnership treats unowned rows as
visible to all), fixing books that pre-date the multi-user migration or were
imported without an owner being hidden from a logged-in user.

Non-admin isolation is preserved and tested: a non-admin still sees only their
own rows plus unowned ones, never another non-admin's.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 59.09091% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/auth/middleware.go 0.00% 6 Missing ⚠️
internal/db/scope.go 71.42% 1 Missing and 1 partial ⚠️
internal/db/books.go 80.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

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.

1 participant