From c152298ad3ff043b130d7fad875e63d280dd6dd5 Mon Sep 17 00:00:00 2001 From: Quick <31828688+Quick104@users.noreply.github.com> Date: Tue, 26 May 2026 20:04:54 -0400 Subject: [PATCH] fix(auth): gate media file paths on metadata curation permission - Allow curators (not just admins) to view media file paths and locations - Apply library access filter to file-level access checks --- internal/api/handlers/catalog_resources.go | 4 ++-- internal/api/handlers/items.go | 18 ++++++++++++++++-- internal/catalog/access_filter.go | 15 +++++++++++++++ web/src/hooks/useAuth.test.ts | 1 + web/src/pages/ItemDetail/EpisodeContent.tsx | 2 +- web/src/pages/ItemDetail/MovieContent.tsx | 2 +- 6 files changed, 36 insertions(+), 6 deletions(-) diff --git a/internal/api/handlers/catalog_resources.go b/internal/api/handlers/catalog_resources.go index 404e9d42..61d3ea58 100644 --- a/internal/api/handlers/catalog_resources.go +++ b/internal/api/handlers/catalog_resources.go @@ -78,7 +78,7 @@ func (h *CatalogResourceHandler) HandleGetItemVersions(w http.ResponseWriter, r return } - if !requestIsAdmin(r) { + if !h.items.requestCanViewFilePaths(r) { for i := range detail.Versions { detail.Versions[i].FilePath = "" } @@ -502,7 +502,7 @@ func (h *CatalogResourceHandler) enrichItemDetail(r *http.Request, detail *catal applyEffectiveEditionPreference(detail.SeasonUserData, &detail.EffectiveVersionEditionKey) } - if !requestIsAdmin(r) { + if !h.items.requestCanViewFilePaths(r) { for i := range detail.Versions { detail.Versions[i].FilePath = "" } diff --git a/internal/api/handlers/items.go b/internal/api/handlers/items.go index 0b8114cb..ffa4ffe5 100644 --- a/internal/api/handlers/items.go +++ b/internal/api/handlers/items.go @@ -1258,7 +1258,21 @@ func isNotFound(err error) bool { errors.Is(err, catalog.ErrSeasonNotFound) } -func requestIsAdmin(r *http.Request) bool { +func (h *ItemsHandler) requestCanViewFilePaths(r *http.Request) bool { claims := apimw.GetClaims(r.Context()) - return claims != nil && claims.Role == "admin" + if claims == nil { + return false + } + if claims.Role == "admin" { + return true + } + if h == nil || h.UserRepo == nil { + return false + } + user, err := h.UserRepo.GetByID(r.Context(), claims.UserID) + if err != nil { + slog.WarnContext(r.Context(), "checking file path visibility permissions", "user_id", claims.UserID, "error", err) + return false + } + return auth.HasEffectivePermission(user, auth.PermissionMetadataCuration) } diff --git a/internal/catalog/access_filter.go b/internal/catalog/access_filter.go index f58ace5b..0f1aa09b 100644 --- a/internal/catalog/access_filter.go +++ b/internal/catalog/access_filter.go @@ -51,9 +51,24 @@ func FileAllowedByAccess(file *models.MediaFile, filter AccessFilter) bool { if file == nil { return false } + if filter.AllowedLibraryIDs != nil && !intInSlice(file.MediaFolderID, filter.AllowedLibraryIDs) { + return false + } + if len(filter.DisabledLibraryIDs) > 0 && intInSlice(file.MediaFolderID, filter.DisabledLibraryIDs) { + return false + } return access.QualityAllowed(file.Resolution, filter.MaxPlaybackQuality) } +func intInSlice(value int, values []int) bool { + for _, candidate := range values { + if candidate == value { + return true + } + } + return false +} + // FilterMediaFilesByAccess drops file versions that exceed the viewer's // effective quality ceiling. func FilterMediaFilesByAccess(files []*models.MediaFile, filter AccessFilter) []*models.MediaFile { diff --git a/web/src/hooks/useAuth.test.ts b/web/src/hooks/useAuth.test.ts index 7607c208..b0a04934 100644 --- a/web/src/hooks/useAuth.test.ts +++ b/web/src/hooks/useAuth.test.ts @@ -40,6 +40,7 @@ describe("initializeAuthSession", () => { username: "admin", email: "admin@example.com", role: "admin", + permissions: [], download_allowed: true, impersonation: null, }); diff --git a/web/src/pages/ItemDetail/EpisodeContent.tsx b/web/src/pages/ItemDetail/EpisodeContent.tsx index 18df321d..a408bb64 100644 --- a/web/src/pages/ItemDetail/EpisodeContent.tsx +++ b/web/src/pages/ItemDetail/EpisodeContent.tsx @@ -346,7 +346,7 @@ export default function EpisodeContent({ item }: { item: ItemDetail & { type: "e />
- {isAdmin && } + {canCurateMetadata && } {/* More Episodes carousel — most useful, so show first */} {siblingsLoading ? ( diff --git a/web/src/pages/ItemDetail/MovieContent.tsx b/web/src/pages/ItemDetail/MovieContent.tsx index 44892cdf..f8a63430 100644 --- a/web/src/pages/ItemDetail/MovieContent.tsx +++ b/web/src/pages/ItemDetail/MovieContent.tsx @@ -289,7 +289,7 @@ export default function MovieContent({ item }: { item: ItemDetail & { type: "mov />
- {isAdmin && } + {canCurateMetadata && } {item.cast && item.cast.length > 0 && (