feat: honor album access permissions in search endpoints

This commit is contained in:
Daniel Dietzler
2026-06-27 13:13:34 +02:00
parent 6e1143e799
commit 82cad5a965
3 changed files with 74 additions and 16 deletions

View File

@@ -8,9 +8,9 @@ from
inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
where
"asset"."visibility" = $1
and "asset"."fileCreatedAt" >= $2
and "asset_exif"."lensModel" = $3
and "asset"."ownerId" = any ($4::uuid[])
and "asset"."ownerId" = any ($2::uuid[])
and "asset"."fileCreatedAt" >= $3
and "asset_exif"."lensModel" = $4
and "asset"."isFavorite" = $5
and "asset"."deletedAt" is null
order by
@@ -28,9 +28,9 @@ from
inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
where
"asset"."visibility" = $1
and "asset"."fileCreatedAt" >= $2
and "asset_exif"."lensModel" = $3
and "asset"."ownerId" = any ($4::uuid[])
and "asset"."ownerId" = any ($2::uuid[])
and "asset"."fileCreatedAt" >= $3
and "asset_exif"."lensModel" = $4
and "asset"."isFavorite" = $5
and "asset"."deletedAt" is null
@@ -42,9 +42,9 @@ from
inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
where
"asset"."visibility" = $1
and "asset"."fileCreatedAt" >= $2
and "asset_exif"."lensModel" = $3
and "asset"."ownerId" = any ($4::uuid[])
and "asset"."ownerId" = any ($2::uuid[])
and "asset"."fileCreatedAt" >= $3
and "asset_exif"."lensModel" = $4
and "asset"."isFavorite" = $5
and "asset"."deletedAt" is null
order by
@@ -61,9 +61,9 @@ from
inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
where
"asset"."visibility" = $1
and "asset"."fileCreatedAt" >= $2
and "asset_exif"."lensModel" = $3
and "asset"."ownerId" = any ($4::uuid[])
and "asset"."ownerId" = any ($2::uuid[])
and "asset"."fileCreatedAt" >= $3
and "asset_exif"."lensModel" = $4
and "asset"."isFavorite" = $5
and "asset"."deletedAt" is null
and "asset_exif"."fileSizeInByte" > $6
@@ -84,9 +84,9 @@ from
inner join "smart_search" on "asset"."id" = "smart_search"."assetId"
where
"asset"."visibility" = $1
and "asset"."fileCreatedAt" >= $2
and "asset_exif"."lensModel" = $3
and "asset"."ownerId" = any ($4::uuid[])
and "asset"."ownerId" = any ($2::uuid[])
and "asset"."fileCreatedAt" >= $3
and "asset_exif"."lensModel" = $4
and "asset"."isFavorite" = $5
and "asset"."deletedAt" is null
order by

View File

@@ -380,6 +380,27 @@ export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuild
.selectFrom('asset')
.where('asset.visibility', '=', visibility)
.$if(!!options.albumIds && options.albumIds.length > 0, (qb) => inAlbums(qb, options.albumIds!))
.$if(!!options.userIds, (qb) => {
if (options.albumIds && options.albumIds.length > 0) {
return qb.where((eb) =>
eb(
'asset.ownerId',
'in',
eb
.selectFrom('album_user')
.select('album_user.userId')
.where('album_user.albumId', 'in', (eb) =>
eb
.selectFrom('album_user')
.select('album_user.albumId')
.whereRef('album_user.albumId', '=', anyUuid(options.albumIds!))
.where('album_user.userId', '=', options.userIds![0]),
),
),
);
}
return qb.where('asset.ownerId', '=', anyUuid(options.userIds!));
})
.$if(!!options.tagIds && options.tagIds.length > 0, (qb) => hasTags(qb, options.tagIds!))
.$if(options.tagIds === null, (qb) =>
qb.where((eb) => eb.not(eb.exists((eb) => eb.selectFrom('tag_asset').whereRef('assetId', '=', 'asset.id')))),
@@ -431,7 +452,6 @@ export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuild
.$if(!!options.checksum, (qb) => qb.where('asset.checksum', '=', options.checksum!))
.$if(!!options.id, (qb) => qb.where('asset.id', '=', asUuid(options.id!)))
.$if(!!options.libraryId, (qb) => qb.where('asset.libraryId', '=', asUuid(options.libraryId!)))
.$if(!!options.userIds, (qb) => qb.where('asset.ownerId', '=', anyUuid(options.userIds!)))
.$if(!!options.encodedVideoPath, (qb) =>
qb
.innerJoin('asset_file', (join) =>

View File

@@ -1,5 +1,6 @@
import { Kysely } from 'kysely';
import { SearchSuggestionType } from 'src/dtos/search.dto';
import { AlbumUserRole } from 'src/enum';
import { AccessRepository } from 'src/repositories/access.repository';
import { AssetRepository } from 'src/repositories/asset.repository';
import { DatabaseRepository } from 'src/repositories/database.repository';
@@ -110,6 +111,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 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 } });
const response = await sut.searchMetadata(auth, { albumIds: [album.id] });
expect(response.assets.items.length).toBe(0);
});
});
describe('getSearchSuggestions', () => {
it('should filter out empty search suggestions', async () => {
const { sut, ctx } = setup();