fix(library): scope show + scan-status by user library access (bookshelf-q14o) #644
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "bd-bookshelf-q14o"
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
GET /libraries/{id}(showHandler) andGET /libraries/{id}/scan-status(scanStatusHandler) were gated only by AuthMiddleware — any authenticated user could fetch library detail (including filesystem paths) and scan-status for any library ID, even libraries not in theiruser_library_mapping.Changes
showHandler: resolve user's library IDs viaGetUserLibraryIDs(stubUserID), returnErrNotFoundif the requested ID is not in the set. Fail-closed on empty set.scanStatusHandlerwith matching pattern.slicesinhandler.go(already used inhealth_handler.go).handler_test.goandscan_handler_test.gotests to provideGetUserLibraryIDsstub inDepswhere needed.GetUserLibraryIDsreturns error (→ 500), empty access set (→ 404 fail-closed).bookshelf-evu5as follow-up for the still-unscopedGET /librarieslist endpoint.Mirrors the pattern established by
healthHandlerin #633.Test Plan
GetUserLibraryIDs)make testgreenmake coveragegreen (100% gate passes)Closes bead bookshelf-q14o on merge.
GET /libraries/{id} and GET /libraries/{id}/scan-status were gated only by AuthMiddleware — any authenticated user could fetch detail (including library filesystem paths) and scan-status for any library ID, even libraries not in their user_library_mapping. Add an ownership check to both handlers: resolve the user's library IDs via GetUserLibraryIDs (stubUserID=1 until real auth lands, bookshelf-4op), then return ErrNotFound if the requested ID is not in the set — fail-closed on empty set too. Mirrors the pattern established by healthHandler (#633). Also adds tests for the access-denied, GetUserLibraryIDs-error, and empty-set cases for both endpoints. Follow-up bookshelf-evu5 tracks the still-unscoped GET /libraries list.CODE REVIEW: APPROVED
Phase 0: DEMO Verification
No DEMO block is present in the bead. The spec is a pure ownership/scoping fix with no interactive demo surface (the change is a security gate, not a new feature). The review proceeds on the diff.
Phase 1: Spec Compliance
All stated requirements verified:
showHandlerownership check fires beforehandler.WantsJSONbranch — both JSON and HTML paths are gated. The HTML redirect path (http.Redirect(w, r, "/", http.StatusSeeOther)) is only reachable afterslices.Containspasses. Confirmed.scanStatusHandlerownership check fires befored.GetScanStatus— no scan data is returned before the gate. Confirmed.fmt.Errorf("library show: library %d: %w", id, middleware.ErrNotFound)wrapsmiddleware.ErrNotFound, same as a genuinely missing library. Confirmed.slices.Contains([]int64{}, id)is always false. Confirmed by dedicated"empty set"test contexts in both test files.stubUserIDcomes frominternal/library/bulk_handler.go:17(const stubUserID = int64(1)) — a package constant, not from a request body or query param. Confirmed.healthHandlerininternal/library/health_handler.goexactly: sameslices.Containsguard, samemiddleware.ErrNotFoundwrap, samestubUserIDreference, samevar (...) / libraryIDstop-of-block declaration.GetUserLibraryIDsdep wired intoDepsstruct and used correctly — no interface leakage.GET /librarieslist is correctly out of scope and not flagged.Phase 2: Code Quality
Conventions:
varblock at top of handler function withlibraryIDs []int64declared before use — correct per project style.d.GetUserLibraryIDsinjected viaDeps, not a global or interface. Correct.slices.Containsimport added cleanly.Test conventions:
stubUserLibraryIDshelper defined once inhandler_test.goat package scope, reused across both test files (same package). Clean.Contexthas its ownBeforeEachwith a completeDepsstruct, no shared mutable state leaking between contexts.Itblock has exactly oneExpect. Correct.BeforeEachdoes setup;JustBeforeEachdoes invocation (server + request). Correct separation.GetUserLibraryIDsis not needed (parseIDfails before the ownership check).Security:
slices.Containsgate in either handler.No issues found.
REVIEW VERDICT: 0 blocker, 0 major, 0 minor
SECURITY REVIEW — PR #644 (bookshelf-q14o)
Scope: IDOR fix on
GET /libraries/{id}(showHandler) andGET /libraries/{id}/scan-status(scanStatusHandler).Findings
[MAJOR] internal/library/handler.go — Write handlers lack GetUserLibraryIDs ownership check (out-of-scope but noted)
updateHandler(PUT/PATCH),deleteHandler,addPathHandler,deletePathHandler,editModalHandler, andtriggerScanHandler(POST /scan) operate on an arbitrary library ID taken from the URL path without callingGetUserLibraryIDs. They are gated by theManipulateLibrarypermission middleware (403 on lacking the permission flag), but that is a capability gate, not an ownership gate — a user withpermission_manipulate_librarycan update, delete, or trigger a scan on any library they do not own. This is the same class of IDOR as the one this PR fixes on the read endpoints.This is pre-existing debt, not introduced by this PR. However the PR description says the read endpoints "expose filesystem paths" — the write endpoints modify or delete those libraries, which is at least equally sensitive. Flagging as MAJOR so a follow-up bead is filed; it should not block merging THIS diff which is a net improvement.
Suggested fix: apply the same
GetUserLibraryIDs+slices.Containsguard to each write handler before the mutation call, following the pattern established here.This PR — verified clean on:
WantsJSONbranch (line ~140), so both the JSON response path and the HTML redirect path are guarded. Neither branch can reachd.Getwithout passing the check. ✓d.GetScanStatus(line ~672). ✓stubUserIDis a package-level constantint64(1)defined inbulk_handler.go:17— not from path/query/body. ✓slices.Contains([], id)returns false →ErrNotFound. Test "when the user has no library access (empty set)" asserts 404. ✓fmt.Errorf("...: %w", middleware.ErrNotFound)— identical error class as a missing record. The middleware translates both to 404 with no distinguishing body. ✓GetUserLibraryIDserror returns a wrapped error (500), not a data leak. Test "when GetUserLibraryIDs returns an error" asserts 500. ✓Contextblocks per endpoint (lacks-access→404, empty-set→404, db-error→500). Existing happy-path tests updated withstubUserLibraryIDs(id)so they still pass the check. ✓/after the ownership check — the redirect is gated, not a bypass. ✓REVIEW VERDICT: 0 blocker, 1 major, 0 minor
dab1f1ff0cc4859ddc4fc4859ddc4f4bd8e4cb28