fix(server): restrict partner timeline requests to timeline assets (#32565)
* fix(server): restrict partner timeline requests to timeline assets * test(server): update medium timeline tests for consolidated check (cherry picked from commit e9fb180f466a687cebda9b7e17cef403de4b81fa)
This commit is contained in:
@@ -160,7 +160,7 @@ describe(TimelineService.name, () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if withParners is true and visibility true or undefined', async () => {
|
||||
it('should throw an error if withParners is true and visibility is archive or hidden', async () => {
|
||||
await expect(
|
||||
sut.getTimeBucket(authStub.admin, {
|
||||
timeBucket: 'bucket',
|
||||
@@ -173,13 +173,78 @@ describe(TimelineService.name, () => {
|
||||
await expect(
|
||||
sut.getTimeBucket(authStub.admin, {
|
||||
timeBucket: 'bucket',
|
||||
visibility: undefined,
|
||||
visibility: AssetVisibility.Hidden,
|
||||
withPartners: true,
|
||||
userId: authStub.admin.user.id,
|
||||
}),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('should default to timeline visibility if withPartners is true and visibility is undefined', async () => {
|
||||
const json = `[{ id: ['asset-id'] }]`;
|
||||
mocks.asset.getTimeBucket.mockResolvedValue({ assets: json });
|
||||
mocks.partner.getAll.mockResolvedValue([]);
|
||||
|
||||
await expect(
|
||||
sut.getTimeBucket(authStub.admin, {
|
||||
timeBucket: 'bucket',
|
||||
withPartners: true,
|
||||
userId: authStub.admin.user.id,
|
||||
}),
|
||||
).resolves.toEqual(json);
|
||||
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith(
|
||||
'bucket',
|
||||
expect.objectContaining({ visibility: AssetVisibility.Timeline, userIds: [authStub.admin.user.id] }),
|
||||
authStub.admin,
|
||||
);
|
||||
});
|
||||
|
||||
it("should default to timeline visibility for another user's timeline", async () => {
|
||||
const json = `[{ id: ['asset-id'] }]`;
|
||||
mocks.asset.getTimeBucket.mockResolvedValue({ assets: json });
|
||||
mocks.access.timeline.checkPartnerAccess.mockResolvedValue(new Set(['partner-id']));
|
||||
|
||||
await expect(sut.getTimeBucket(authStub.admin, { timeBucket: 'bucket', userId: 'partner-id' })).resolves.toEqual(
|
||||
json,
|
||||
);
|
||||
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith(
|
||||
'bucket',
|
||||
{ timeBucket: 'bucket', visibility: AssetVisibility.Timeline, userIds: ['partner-id'] },
|
||||
authStub.admin,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw an error for another user's archived, hidden, trashed or favorite assets", async () => {
|
||||
mocks.access.timeline.checkPartnerAccess.mockResolvedValue(new Set(['partner-id']));
|
||||
|
||||
for (const dto of [
|
||||
{ visibility: AssetVisibility.Archive },
|
||||
{ visibility: AssetVisibility.Hidden },
|
||||
{ isTrashed: true },
|
||||
{ isFavorite: true },
|
||||
{ isFavorite: false },
|
||||
]) {
|
||||
await expect(
|
||||
sut.getTimeBucket(authStub.admin, { timeBucket: 'bucket', userId: 'partner-id', ...dto }),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
}
|
||||
|
||||
expect(mocks.asset.getTimeBucket).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should throw an error for another user's locked assets even with an elevated session", async () => {
|
||||
mocks.access.timeline.checkPartnerAccess.mockResolvedValue(new Set(['partner-id']));
|
||||
|
||||
await expect(
|
||||
sut.getTimeBucket(authStub.adminWithElevatedPermission, {
|
||||
timeBucket: 'bucket',
|
||||
userId: 'partner-id',
|
||||
visibility: AssetVisibility.Locked,
|
||||
}),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
expect(mocks.asset.getTimeBucket).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw an error if withParners is true and isFavorite is either true or false', async () => {
|
||||
await expect(
|
||||
sut.getTimeBucket(authStub.admin, {
|
||||
|
||||
@@ -57,11 +57,15 @@ export class TimelineService extends BaseService {
|
||||
|
||||
if (dto.userId) {
|
||||
await this.requireAccess({ auth, permission: Permission.TimelineRead, ids: [dto.userId] });
|
||||
if (dto.visibility === AssetVisibility.Archive) {
|
||||
await this.requireAccess({ auth, permission: Permission.ArchiveRead, ids: [dto.userId] });
|
||||
}
|
||||
if (dto.visibility === AssetVisibility.Locked && dto.userId !== auth.user.id) {
|
||||
throw new BadRequestException("You may not access another user's locked timeline");
|
||||
}
|
||||
|
||||
const includesOtherUsers = dto.withPartners || (dto.userId && dto.userId !== auth.user.id);
|
||||
if (includesOtherUsers) {
|
||||
dto.visibility ??= AssetVisibility.Timeline;
|
||||
if (dto.visibility !== AssetVisibility.Timeline || dto.isTrashed || dto.isFavorite !== undefined) {
|
||||
throw new BadRequestException(
|
||||
'Only non-archived, non-trashed, non-favorited, non-locked assets can be requested for other users',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,19 +76,5 @@ export class TimelineService extends BaseService {
|
||||
if (isMetadataHidden(auth)) {
|
||||
dto.withCoordinates = false;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unicorn/prefer-early-return
|
||||
if (dto.withPartners) {
|
||||
const isRequestedLocked = dto.visibility === AssetVisibility.Locked;
|
||||
const isRequestedArchived = dto.visibility === AssetVisibility.Archive || dto.visibility === undefined;
|
||||
const isRequestedFavorite = dto.isFavorite === true || dto.isFavorite === false;
|
||||
const isRequestedTrash = dto.isTrashed === true;
|
||||
|
||||
if (isRequestedLocked || isRequestedArchived || isRequestedFavorite || isRequestedTrash) {
|
||||
throw new BadRequestException(
|
||||
'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,29 +51,35 @@ 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, non-locked assets',
|
||||
'Only non-archived, non-trashed, non-favorited, non-locked assets can be requested for other users',
|
||||
);
|
||||
|
||||
const response2 = sut.getTimeBuckets(auth, { withPartners: true });
|
||||
const response2 = sut.getTimeBuckets(auth, { withPartners: true, visibility: AssetVisibility.Hidden });
|
||||
await expect(response2).rejects.toBeInstanceOf(BadRequestException);
|
||||
await expect(response2).rejects.toThrow(
|
||||
'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets',
|
||||
'Only non-archived, non-trashed, non-favorited, non-locked assets can be requested for other users',
|
||||
);
|
||||
});
|
||||
|
||||
it('should default to timeline visibility if time bucket is requested with partners assets', async () => {
|
||||
const { sut } = setup();
|
||||
const auth = factory.auth();
|
||||
await expect(sut.getTimeBuckets(auth, { withPartners: true })).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should return error if time bucket is requested with partners asset and favorite', async () => {
|
||||
const { sut } = setup();
|
||||
const auth = factory.auth();
|
||||
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, non-locked assets',
|
||||
'Only non-archived, non-trashed, non-favorited, non-locked assets can be requested for other users',
|
||||
);
|
||||
|
||||
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, non-locked assets',
|
||||
'Only non-archived, non-trashed, non-favorited, non-locked assets can be requested for other users',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -83,7 +89,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, non-locked assets',
|
||||
'Only non-archived, non-trashed, non-favorited, non-locked assets can be requested for other users',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -96,7 +102,9 @@ describe(TimelineService.name, () => {
|
||||
const auth = factory.auth({ user, session: { hasElevatedPermission: true } });
|
||||
|
||||
const response = sut.getTimeBuckets(auth, { userId: partner.id, visibility: AssetVisibility.Locked });
|
||||
await expect(response).rejects.toThrow("You may not access another user's locked timeline");
|
||||
await expect(response).rejects.toThrow(
|
||||
'Only non-archived, non-trashed, non-favorited, non-locked assets can be requested for other users',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not allow access for unrelated shared links', async () => {
|
||||
|
||||
Reference in New Issue
Block a user