Compare commits

...

1 Commits

Author SHA1 Message Date
Timon 26714f6bfe fix(server): prevent locked assets from leaking to partners (#28652)
* fix(server): prevent locked assets from leaking to partners

* fix tests
2026-05-27 17:33:49 -04:00
5 changed files with 35 additions and 12 deletions
+9 -5
View File
@@ -75,7 +75,7 @@ export class SearchService extends BaseService {
const page = dto.page ?? 1;
const size = dto.size || 250;
const userIds = await this.getUserIdsToSearch(auth);
const userIds = await this.getUserIdsToSearch(auth, dto.visibility);
const { hasNextPage, items } = await this.searchRepository.searchMetadata(
{ page, size },
{
@@ -103,7 +103,7 @@ export class SearchService extends BaseService {
requireElevatedPermission(auth);
}
const userIds = await this.getUserIdsToSearch(auth);
const userIds = await this.getUserIdsToSearch(auth, dto.visibility);
const items = await this.searchRepository.searchRandom(dto.size || 250, { ...dto, userIds });
return items.map((item) => mapAsset(item, { auth }));
}
@@ -113,7 +113,7 @@ export class SearchService extends BaseService {
requireElevatedPermission(auth);
}
const userIds = await this.getUserIdsToSearch(auth);
const userIds = await this.getUserIdsToSearch(auth, dto.visibility);
const items = await this.searchRepository.searchLargeAssets(dto.size || 250, { ...dto, userIds });
return items.map((item) => mapAsset(item, { auth }));
}
@@ -128,7 +128,7 @@ export class SearchService extends BaseService {
throw new BadRequestException('Smart search is not enabled');
}
const userIds = this.getUserIdsToSearch(auth);
const userIds = this.getUserIdsToSearch(auth, dto.visibility);
let embedding;
if (dto.query) {
const key = machineLearning.clip.modelName + dto.query + dto.language;
@@ -202,7 +202,11 @@ export class SearchService extends BaseService {
}
}
private async getUserIdsToSearch(auth: AuthDto): Promise<string[]> {
private async getUserIdsToSearch(auth: AuthDto, visibility?: AssetVisibility): Promise<string[]> {
// Locked assets are personal. Never include partner IDs, regardless of A's elevated session.
if (visibility === AssetVisibility.Locked) {
return [auth.user.id];
}
const partnerIds = await getMyPartnerIds({
userId: auth.user.id,
repository: this.partnerRepository,
@@ -204,5 +204,16 @@ describe(TimelineService.name, () => {
}),
).rejects.toThrow(BadRequestException);
});
it('should throw an error if withPartners is true and visibility is locked', async () => {
await expect(
sut.getTimeBucket(authStub.adminWithElevatedPermission, {
timeBucket: 'bucket',
visibility: AssetVisibility.Locked,
withPartners: true,
userId: authStub.adminWithElevatedPermission.user.id,
}),
).rejects.toThrow(BadRequestException);
});
});
});
+3 -2
View File
@@ -71,13 +71,14 @@ export class TimelineService extends BaseService {
}
if (dto.withPartners) {
const requestedLocked = dto.visibility === AssetVisibility.Locked;
const requestedArchived = dto.visibility === AssetVisibility.Archive || dto.visibility === undefined;
const requestedFavorite = dto.isFavorite === true || dto.isFavorite === false;
const requestedTrash = dto.isTrashed === true;
if (requestedArchived || requestedFavorite || requestedTrash) {
if (requestedLocked || requestedArchived || requestedFavorite || requestedTrash) {
throw new BadRequestException(
'withPartners is only supported for non-archived, non-trashed, non-favorited assets',
'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets',
);
}
}
+7
View File
@@ -41,6 +41,13 @@ export const authStub = {
id: 'token-id',
} as AuthSession,
}),
adminWithElevatedPermission: Object.freeze<AuthDto>({
user: authUser.admin,
session: {
id: 'token-id-elevated',
hasElevatedPermission: true,
} as AuthSession,
}),
adminSharedLink: Object.freeze({
user: authUser.admin,
sharedLink: {
@@ -51,13 +51,13 @@ describe(TimelineService.name, () => {
const response1 = sut.getTimeBuckets(auth, { withPartners: true, visibility: AssetVisibility.Archive });
await expect(response1).rejects.toBeInstanceOf(BadRequestException);
await expect(response1).rejects.toThrow(
'withPartners is only supported for non-archived, non-trashed, non-favorited assets',
'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets',
);
const response2 = sut.getTimeBuckets(auth, { withPartners: true });
await expect(response2).rejects.toBeInstanceOf(BadRequestException);
await expect(response2).rejects.toThrow(
'withPartners is only supported for non-archived, non-trashed, non-favorited assets',
'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets',
);
});
@@ -67,13 +67,13 @@ describe(TimelineService.name, () => {
const response1 = sut.getTimeBuckets(auth, { withPartners: true, isFavorite: false });
await expect(response1).rejects.toBeInstanceOf(BadRequestException);
await expect(response1).rejects.toThrow(
'withPartners is only supported for non-archived, non-trashed, non-favorited assets',
'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets',
);
const response2 = sut.getTimeBuckets(auth, { withPartners: true, isFavorite: true });
await expect(response2).rejects.toBeInstanceOf(BadRequestException);
await expect(response2).rejects.toThrow(
'withPartners is only supported for non-archived, non-trashed, non-favorited assets',
'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets',
);
});
@@ -83,7 +83,7 @@ describe(TimelineService.name, () => {
const response = sut.getTimeBuckets(auth, { withPartners: true, isTrashed: true });
await expect(response).rejects.toBeInstanceOf(BadRequestException);
await expect(response).rejects.toThrow(
'withPartners is only supported for non-archived, non-trashed, non-favorited assets',
'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets',
);
});