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
This commit is contained in:
Quick
2026-05-26 20:04:54 -04:00
parent 9f48ae2d8b
commit c152298ad3
6 changed files with 36 additions and 6 deletions
+2 -2
View File
@@ -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 = ""
}
+16 -2
View File
@@ -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)
}
+15
View File
@@ -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 {
+1
View File
@@ -40,6 +40,7 @@ describe("initializeAuthSession", () => {
username: "admin",
email: "admin@example.com",
role: "admin",
permissions: [],
download_allowed: true,
impersonation: null,
});
+1 -1
View File
@@ -346,7 +346,7 @@ export default function EpisodeContent({ item }: { item: ItemDetail & { type: "e
/>
<div className="page-shell space-y-12 py-10 sm:space-y-14">
{isAdmin && <MediaLocations title="Media locations" versions={item.versions} />}
{canCurateMetadata && <MediaLocations title="Media locations" versions={item.versions} />}
{/* More Episodes carousel — most useful, so show first */}
{siblingsLoading ? (
+1 -1
View File
@@ -289,7 +289,7 @@ export default function MovieContent({ item }: { item: ItemDetail & { type: "mov
/>
<div className="page-shell space-y-12 py-10 sm:space-y-14">
{isAdmin && <MediaLocations title="Media locations" versions={item.versions} />}
{canCurateMetadata && <MediaLocations title="Media locations" versions={item.versions} />}
{item.cast && item.cast.length > 0 && (
<div>