diff --git a/persistence/album_repository.go b/persistence/album_repository.go index 08bc80039..5fe5a7bc3 100644 --- a/persistence/album_repository.go +++ b/persistence/album_repository.go @@ -217,7 +217,8 @@ func (r *albumRepository) UpdateExternalInfo(al *model.Album) error { func (r *albumRepository) selectAlbum(options ...model.QueryOptions) SelectBuilder { sql := r.newSelect(options...).Columns("album.*") - return r.withAnnotation(sql, "album.id") + sql = r.withAnnotation(sql, "album.id") + return r.applyLibraryFilter(sql) } func (r *albumRepository) Get(id string) (*model.Album, error) { diff --git a/persistence/folder_repository.go b/persistence/folder_repository.go index 02b272134..45ba168d6 100644 --- a/persistence/folder_repository.go +++ b/persistence/folder_repository.go @@ -61,8 +61,9 @@ func newFolderRepository(ctx context.Context, db dbx.Builder) model.FolderReposi } func (r folderRepository) selectFolder(options ...model.QueryOptions) SelectBuilder { - return r.newSelect(options...).Columns("folder.*", "library.path as library_path"). + sql := r.newSelect(options...).Columns("folder.*", "library.path as library_path"). Join("library on library.id = folder.library_id") + return r.applyLibraryFilter(sql) } func (r folderRepository) Get(id string) (*model.Folder, error) { diff --git a/persistence/mediafile_repository.go b/persistence/mediafile_repository.go index dd22b1413..ed74436d3 100644 --- a/persistence/mediafile_repository.go +++ b/persistence/mediafile_repository.go @@ -137,7 +137,8 @@ func (r *mediaFileRepository) selectMediaFile(options ...model.QueryOptions) Sel sql := r.newSelect(options...).Columns("media_file.*", "library.path as library_path"). LeftJoin("library on media_file.library_id = library.id") sql = r.withAnnotation(sql, "media_file.id") - return r.withBookmark(sql, "media_file.id") + sql = r.withBookmark(sql, "media_file.id") + return r.applyLibraryFilter(sql) } func (r *mediaFileRepository) Get(id string) (*model.MediaFile, error) { diff --git a/persistence/sql_base_repository.go b/persistence/sql_base_repository.go index ea22389a2..be7848636 100644 --- a/persistence/sql_base_repository.go +++ b/persistence/sql_base_repository.go @@ -199,6 +199,29 @@ func (r sqlRepository) applyFilters(sq SelectBuilder, options ...model.QueryOpti return sq } +// applyLibraryFilter adds library filtering to queries for tables that have a library_id column +// This ensures users only see content from libraries they have access to +func (r sqlRepository) applyLibraryFilter(sq SelectBuilder) SelectBuilder { + user := loggedUser(r.ctx) + + // Admin users see all content + if user.IsAdmin { + return sq + } + + // Get user's accessible library IDs + userID := userId(r.ctx) + if userID == invalidUserId { + // No user context - return empty result set + return sq.Where(Eq{"1": "0"}) + } + + // Use subquery to filter by user's library access + // This approach doesn't require DataStore in context + return sq.Where(Expr(r.tableName+".library_id IN ("+ + "SELECT ul.library_id FROM user_library ul WHERE ul.user_id = ?)", userID)) +} + func (r sqlRepository) seedKey() string { // Seed keys must be all lowercase, or else SQLite3 will encode it, making it not match the seed // used in the query. Hashing the user ID and converting it to a hex string will do the trick diff --git a/scanner/scanner_test.go b/scanner/scanner_test.go index 6bb74997f..4d35f767c 100644 --- a/scanner/scanner_test.go +++ b/scanner/scanner_test.go @@ -71,6 +71,16 @@ var _ = Describe("Scanner", Ordered, func() { } ds.MockedMediaFile = mfRepo + // Create the admin user in the database to match the context + adminUser := model.User{ + ID: "123", + UserName: "admin", + Name: "Admin User", + IsAdmin: true, + NewPassword: "password", + } + Expect(ds.User(ctx).Put(&adminUser)).To(Succeed()) + s = scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(), core.NewPlaylists(ds), metrics.NewNoopInstance())