mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-30 06:57:57 +01:00
feat: enhance artist library filtering based on user permissions and optimize library ID retrieval
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
9eb89a9650
commit
739856cda4
5 changed files with 716 additions and 600 deletions
|
|
@ -2,6 +2,8 @@ package model
|
|||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/utils/slice"
|
||||
)
|
||||
|
||||
type Library struct {
|
||||
|
|
@ -31,6 +33,10 @@ const (
|
|||
|
||||
type Libraries []Library
|
||||
|
||||
func (l Libraries) IDs() []int {
|
||||
return slice.Map(l, func(lib Library) int { return lib.ID })
|
||||
}
|
||||
|
||||
type LibraryRepository interface {
|
||||
Get(id int) (*Library, error)
|
||||
// GetPath returns the path of the library with the given ID.
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ type User struct {
|
|||
UpdatedAt time.Time `structs:"updated_at" json:"updatedAt"`
|
||||
|
||||
// Library associations (many-to-many relationship)
|
||||
Libraries []Library `structs:"-" json:"libraries,omitempty"`
|
||||
Libraries Libraries `structs:"-" json:"libraries,omitempty"`
|
||||
|
||||
// This is only available on the backend, and it is never sent over the wire
|
||||
Password string `structs:"-" json:"-"`
|
||||
|
|
@ -41,15 +41,7 @@ func (u User) HasLibraryAccess(libraryID int) bool {
|
|||
|
||||
// AccessibleLibraryIDs returns a slice of library IDs that the user has access to
|
||||
func (u User) AccessibleLibraryIDs() []int {
|
||||
if len(u.Libraries) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ids := make([]int, len(u.Libraries))
|
||||
for i, lib := range u.Libraries {
|
||||
ids[i] = lib.ID
|
||||
}
|
||||
return ids
|
||||
return u.Libraries.IDs()
|
||||
}
|
||||
|
||||
func (u User) FilteredLibraries(libraryIds []int) Libraries {
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ var _ = Describe("User", func() {
|
|||
It("returns nil when user has no libraries", func() {
|
||||
user.Libraries = nil
|
||||
ids := user.AccessibleLibraryIDs()
|
||||
Expect(ids).To(BeNil())
|
||||
Expect(ids).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -118,7 +119,7 @@ func NewArtistRepository(ctx context.Context, db dbx.Builder) model.ArtistReposi
|
|||
"starred": booleanFilter,
|
||||
"role": roleFilter,
|
||||
"missing": booleanFilter,
|
||||
"library_id": artistLibraryFilter,
|
||||
"library_id": r.artistLibraryFilter,
|
||||
})
|
||||
r.setSortMappings(map[string]string{
|
||||
"name": "order_artist_name",
|
||||
|
|
@ -145,11 +146,51 @@ func roleFilter(_ string, role any) Sqlizer {
|
|||
}
|
||||
|
||||
// artistLibraryFilter handles filtering artists by library_id through the library_artist junction table
|
||||
func artistLibraryFilter(_ string, value any) Sqlizer {
|
||||
// It checks the logged-in user's permissions and returns a Sqlizer that can be used in a query.
|
||||
// If the user is an admin, it returns a condition that always evaluates to true.
|
||||
// If the value is nil, it uses the user's accessible library IDs.
|
||||
func (r *artistRepository) artistLibraryFilter(_ string, value any) Sqlizer {
|
||||
user := loggedUser(r.ctx)
|
||||
|
||||
// Admin users see all content
|
||||
if user.IsAdmin {
|
||||
return Eq{"1": 1} // Always true, no filtering needed
|
||||
}
|
||||
|
||||
var accessibleLibraryIDs []int
|
||||
if value == nil {
|
||||
accessibleLibraryIDs = user.AccessibleLibraryIDs()
|
||||
} else {
|
||||
// Convert various input types to []int
|
||||
var libIds []int
|
||||
switch v := value.(type) {
|
||||
case []int:
|
||||
libIds = v
|
||||
case []string:
|
||||
for _, str := range v {
|
||||
if id, err := strconv.Atoi(str); err == nil {
|
||||
libIds = append(libIds, id)
|
||||
}
|
||||
}
|
||||
case int:
|
||||
libIds = []int{v}
|
||||
case string:
|
||||
if id, err := strconv.Atoi(v); err == nil {
|
||||
libIds = []int{id}
|
||||
}
|
||||
}
|
||||
|
||||
accessibleLibraryIDs = user.FilteredLibraries(libIds).IDs()
|
||||
if len(accessibleLibraryIDs) == 0 {
|
||||
// User has no library access - return empty result set
|
||||
return Eq{"1": "0"}
|
||||
}
|
||||
}
|
||||
|
||||
// Use a subquery to find artists that belong to the specified library
|
||||
subquery := Select("1").From("library_artist la").Where(And{
|
||||
Expr("la.artist_id = artist.id"),
|
||||
Eq{"la.library_id": value},
|
||||
Eq{"la.library_id": accessibleLibraryIDs},
|
||||
})
|
||||
return Expr("EXISTS (?)", subquery)
|
||||
}
|
||||
|
|
@ -157,22 +198,8 @@ func artistLibraryFilter(_ string, value any) Sqlizer {
|
|||
// applyArtistLibraryFilter adds library filtering based on user permissions for artists
|
||||
// This ensures users only see artists from libraries they have access to
|
||||
func (r *artistRepository) applyArtistLibraryFilter(sq SelectBuilder) SelectBuilder {
|
||||
user := loggedUser(r.ctx)
|
||||
|
||||
// Admin users see all content
|
||||
if user.IsAdmin {
|
||||
return sq
|
||||
}
|
||||
|
||||
// Get user's accessible library IDs using User model method
|
||||
accessibleLibraryIDs := user.AccessibleLibraryIDs()
|
||||
if len(accessibleLibraryIDs) == 0 {
|
||||
// User has no library access - return empty result set
|
||||
return sq.Where(Eq{"1": "0"})
|
||||
}
|
||||
|
||||
// Use existing artistLibraryFilter with the user's accessible library IDs
|
||||
libFilter := artistLibraryFilter("library_id", accessibleLibraryIDs)
|
||||
libFilter := r.artistLibraryFilter("library_id", nil)
|
||||
return sq.Where(libFilter)
|
||||
}
|
||||
|
||||
|
|
@ -190,26 +217,10 @@ func (r *artistRepository) CountAll(options ...model.QueryOptions) (int64, error
|
|||
}
|
||||
|
||||
func (r *artistRepository) Exists(id string) (bool, error) {
|
||||
// For Exists, we need to check if the artist exists AND the user has access to it
|
||||
user := loggedUser(r.ctx)
|
||||
|
||||
// Admin users see all content
|
||||
if user.IsAdmin {
|
||||
return r.exists(Eq{"artist.id": id})
|
||||
}
|
||||
|
||||
// Get user's accessible library IDs using User model method
|
||||
accessibleLibraryIDs := user.AccessibleLibraryIDs()
|
||||
if len(accessibleLibraryIDs) == 0 {
|
||||
// User has no library access - return false
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Check if artist exists AND user has access to it via library permissions
|
||||
libFilter := artistLibraryFilter("library_id", accessibleLibraryIDs)
|
||||
condition := And{
|
||||
Eq{"artist.id": id},
|
||||
libFilter,
|
||||
r.artistLibraryFilter("library_id", nil),
|
||||
}
|
||||
|
||||
return r.exists(condition)
|
||||
|
|
@ -293,7 +304,7 @@ func (r *artistRepository) GetIndex(includeMissing bool, libraryIds []int, roles
|
|||
}
|
||||
}
|
||||
|
||||
libFilter := artistLibraryFilter("library_id", libraryIds)
|
||||
libFilter := r.artistLibraryFilter("library_id", libraryIds)
|
||||
if options.Filters == nil {
|
||||
options.Filters = libFilter
|
||||
} else {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue