From deeb042a9eeb12687e551cd289827a16ea63b221 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Mon, 29 Jun 2026 22:27:22 +0200 Subject: [PATCH] feat: honor album access permissions in search endpoints (#29352) --- server/src/services/search.service.ts | 9 ++++- .../specs/services/search.service.spec.ts | 39 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts index 37facf325..f7094eba9 100644 --- a/server/src/services/search.service.ts +++ b/server/src/services/search.service.ts @@ -73,9 +73,16 @@ export class SearchService extends BaseService { checksum = Buffer.from(dto.checksum, encoding); } + let userIds: string[] | undefined; + + if (dto.albumIds && dto.albumIds.length > 0) { + await this.requireAccess({ auth, ids: dto.albumIds, permission: Permission.AlbumRead }); + } else { + userIds = await this.getUserIdsToSearch(auth, dto.visibility); + } + const page = dto.page ?? 1; const size = dto.size || 250; - const userIds = await this.getUserIdsToSearch(auth, dto.visibility); const { hasNextPage, items } = await this.searchRepository.searchMetadata( { page, size }, { diff --git a/server/test/medium/specs/services/search.service.spec.ts b/server/test/medium/specs/services/search.service.spec.ts index 4ed8fe8c2..171d97103 100644 --- a/server/test/medium/specs/services/search.service.spec.ts +++ b/server/test/medium/specs/services/search.service.spec.ts @@ -1,6 +1,6 @@ import { Kysely } from 'kysely'; import { SearchSuggestionType } from 'src/dtos/search.dto'; -import { AssetVisibility } from 'src/enum'; +import { AlbumUserRole, AssetVisibility } from 'src/enum'; import { AccessRepository } from 'src/repositories/access.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; import { DatabaseRepository } from 'src/repositories/database.repository'; @@ -139,6 +139,43 @@ describe(SearchService.name, () => { }); }); + describe('albumIds option', () => { + it('should return assets from shared album', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const { user: otherUser } = await ctx.newUser(); + + const { asset } = await ctx.newAsset({ ownerId: otherUser.id }); + const { album } = await ctx.newAlbum({ ownerId: otherUser.id }); + await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); + await ctx.newAlbumUser({ albumId: album.id, userId: user.id, role: AlbumUserRole.Editor }); + + const auth = factory.auth({ user: { id: user.id } }); + + const response = await sut.searchMetadata(auth, { albumIds: [album.id] }); + + expect(response.assets.items.length).toBe(1); + }); + + it('should not return assets for album, a user is not in, when partner sharing is enabled', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const { user: otherUser } = await ctx.newUser(); + + await ctx.newPartner({ sharedById: otherUser.id, sharedWithId: user.id }); + + const { asset } = await ctx.newAsset({ ownerId: otherUser.id }); + const { album } = await ctx.newAlbum({ ownerId: otherUser.id }); + await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); + + const auth = factory.auth({ user: { id: user.id } }); + + await expect(sut.searchMetadata(auth, { albumIds: [album.id] })).rejects.toThrow( + 'Not found or no album.read access', + ); + }); + }); + describe('getSearchSuggestions', () => { it('should filter out empty search suggestions', async () => { const { sut, ctx } = setup();