refactor(vis): H4.5 steps 4-5 — statistics and the three reader gates on the canonical resolver; core goes package-level

VisibleLibrarySet becomes a package-level function taking the queries
(db-only services like statistics reach it without new wiring); the
LibraryService method stays as the ergonomic entry point. Statistics'
ids-shaper and the reader pages' three hand-rolled membership loops
(empty-rows fallback then linear scans) now delegate; the gates keep
their 403-on-invisible semantics (>null set membership, never silent
pass).

Census addendum from this pass: two OPDS sites call
GetUserVisibleLibraries directly but consume FULL ROWS (per-library
folder enumeration) — a third legitimate pattern alongside set/ids and
resolveLibrary's active-selection; documented, untouched. The library
LIST endpoint and its SSR data helper are row consumers too, by design.
This commit is contained in:
John O'Keefe committed 2026-10-17 17:35:43 -04:00
1 parent 8dbaa8f810
commit 454938c31f
3 files changed
+26 -38

No files matched your search

+13 -24
View File
@@ -78,19 +78,14 @@ func (h *ReaderHandler) ShowReader(c *echo.Context) error {
} }
userData := user.(database.Users) userData := user.(database.Users)
// Get user's visible libraries // Family-lens gate (H4.5 canonical resolver): the media item's
libraries, err := h.libraryService.GetUserVisibleLibraries(c.Request().Context(), userData.ID) // library must be in the requester's visible set — 403, never a
// silent pass.
visibleSet, _, err := h.libraryService.VisibleLibrarySet(c.Request().Context(), userData.ID)
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to check library access"}) return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to check library access"})
} }
// Check if media item's library is in visible list visible := visibleSet[uuid.UUID(mediaItem.LibraryID.Bytes).String()]
visible := false
for _, lib := range libraries {
if lib.ID == mediaItem.LibraryID {
visible = true
break
}
}
if !visible { if !visible {
return c.JSON(http.StatusForbidden, map[string]string{"error": "Access denied to this library"}) return c.JSON(http.StatusForbidden, map[string]string{"error": "Access denied to this library"})
} }
@@ -172,19 +167,14 @@ func (h *ReaderHandler) GetPage(c *echo.Context) error {
} }
userData := user.(database.Users) userData := user.(database.Users)
// Get user's visible libraries // Family-lens gate (H4.5 canonical resolver): the media item's
libraries, err := h.libraryService.GetUserVisibleLibraries(c.Request().Context(), userData.ID) // library must be in the requester's visible set — 403, never a
// silent pass.
visibleSet, _, err := h.libraryService.VisibleLibrarySet(c.Request().Context(), userData.ID)
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to check library access"}) return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to check library access"})
} }
// Check if media item's library is in visible list visible := visibleSet[uuid.UUID(mediaItem.LibraryID.Bytes).String()]
visible := false
for _, lib := range libraries {
if lib.ID == mediaItem.LibraryID {
visible = true
break
}
}
if !visible { if !visible {
return c.JSON(http.StatusForbidden, map[string]string{"error": "Access denied to this library"}) return c.JSON(http.StatusForbidden, map[string]string{"error": "Access denied to this library"})
} }
@@ -225,15 +215,14 @@ func (h *ReaderHandler) ensureItemVisible(c *echo.Context, mediaItem *database.M
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "User not authenticated"}) return c.JSON(http.StatusUnauthorized, map[string]string{"error": "User not authenticated"})
} }
userData := user.(database.Users) userData := user.(database.Users)
libraries, err := h.libraryService.GetUserVisibleLibraries(c.Request().Context(), userData.ID) // Family-lens gate (H4.5 canonical resolver).
visibleSet, _, err := h.libraryService.VisibleLibrarySet(c.Request().Context(), userData.ID)
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to check library access"}) return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to check library access"})
} }
for _, lib := range libraries { if visibleSet[uuid.UUID(mediaItem.LibraryID.Bytes).String()] {
if lib.ID == mediaItem.LibraryID {
return nil return nil
} }
}
return c.JSON(http.StatusForbidden, map[string]string{"error": "Access denied to this library"}) return c.JSON(http.StatusForbidden, map[string]string{"error": "Access denied to this library"})
} }
+10 -3
View File
@@ -174,7 +174,8 @@ func (s *LibraryService) SetLibraryVisibility(ctx context.Context, userID, libra
return &visibility, nil return &visibility, nil
} }
// GetUserVisibleLibraries retrieves all libraries visible to a user // GetUserVisibleLibraries retrieves all libraries visible to a user.
// VisibleLibrarySet is THE canonical library-visibility resolver (H4.5): // VisibleLibrarySet is THE canonical library-visibility resolver (H4.5):
// every endpoint that asks "which libraries can this user see" funnels // every endpoint that asks "which libraries can this user see" funnels
// here. The family-lens rule itself lives in the // here. The family-lens rule itself lives in the
@@ -182,8 +183,8 @@ func (s *LibraryService) SetLibraryVisibility(ctx context.Context, userID, libra
// default-allow: an admin hides a library, everyone else still sees it). // default-allow: an admin hides a library, everyone else still sees it).
// Callers adapt their own identity source (JWT user object, device // Callers adapt their own identity source (JWT user object, device
// token, SSR session); this core takes the resolved user id. // token, SSR session); this core takes the resolved user id.
func (s *LibraryService) VisibleLibrarySet(ctx context.Context, userID pgtype.UUID) (map[string]bool, []pgtype.UUID, error) { func VisibleLibrarySet(ctx context.Context, db *database.Queries, userID pgtype.UUID) (map[string]bool, []pgtype.UUID, error) {
rows, err := s.GetUserVisibleLibraries(ctx, userID) rows, err := db.GetUserVisibleLibraries(ctx, userID)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("failed to check library access: %w", err) return nil, nil, fmt.Errorf("failed to check library access: %w", err)
} }
@@ -199,6 +200,12 @@ func (s *LibraryService) VisibleLibrarySet(ctx context.Context, userID pgtype.UU
return set, ids, nil return set, ids, nil
} }
// VisibleLibrarySet is the LibraryService entry point for the
// package-level core (see the core's doc).
func (s *LibraryService) VisibleLibrarySet(ctx context.Context, userID pgtype.UUID) (map[string]bool, []pgtype.UUID, error) {
return VisibleLibrarySet(ctx, s.db, userID)
}
func (s *LibraryService) GetUserVisibleLibraries(ctx context.Context, userID pgtype.UUID) ([]database.GetUserVisibleLibrariesRow, error) { func (s *LibraryService) GetUserVisibleLibraries(ctx context.Context, userID pgtype.UUID) ([]database.GetUserVisibleLibrariesRow, error) {
return s.db.GetUserVisibleLibraries(ctx, userID) return s.db.GetUserVisibleLibraries(ctx, userID)
} }
+3 -11
View File
@@ -188,18 +188,10 @@ type SessionSummary struct {
// userVisibleLibraryIDs loads the family-lens set for this user (empty → // userVisibleLibraryIDs loads the family-lens set for this user (empty →
// no visible-library filter, i.e. everything is visible by default). // no visible-library filter, i.e. everything is visible by default).
// userVisibleLibraryIDs delegates to the canonical resolver (H4.5).
func (s *StatisticsService) userVisibleLibraryIDs(ctx context.Context, userID pgtype.UUID) ([]pgtype.UUID, error) { func (s *StatisticsService) userVisibleLibraryIDs(ctx context.Context, userID pgtype.UUID) ([]pgtype.UUID, error) {
rows, err := s.db.GetUserVisibleLibraries(ctx, userID) _, ids, err := VisibleLibrarySet(ctx, s.db, userID)
if err != nil { return ids, err
return nil, fmt.Errorf("failed to check library access: %w", err)
}
ids := make([]pgtype.UUID, 0, len(rows))
for _, lib := range rows {
if lib.ID.Valid {
ids = append(ids, lib.ID)
}
}
return ids, nil
} }
// ErrLibraryNotVisible is returned by LibraryUsage when the caller asked // ErrLibraryNotVisible is returned by LibraryUsage when the caller asked