diff --git a/server/src/domain/access/access.core.ts b/server/src/domain/access/access.core.ts index d829139a95..c47b2acd24 100644 --- a/server/src/domain/access/access.core.ts +++ b/server/src/domain/access/access.core.ts @@ -179,6 +179,48 @@ export class AccessCore { case Permission.ALBUM_REMOVE_ASSET: return this.repository.album.checkOwnerAccess(authUser.id, ids); + + case Permission.ASSET_UPLOAD: + return this.repository.library.checkOwnerAccess(authUser.id, ids); + + case Permission.ARCHIVE_READ: + return ids.has(authUser.id) ? new Set([authUser.id]) : new Set(); + + case Permission.AUTH_DEVICE_DELETE: + return this.repository.authDevice.checkOwnerAccess(authUser.id, ids); + + case Permission.TIMELINE_READ: { + const isOwner = ids.has(authUser.id) ? new Set([authUser.id]) : new Set(); + const isPartner = await this.repository.timeline.checkPartnerAccess(authUser.id, setDifference(ids, isOwner)); + return setUnion(isOwner, isPartner); + } + + case Permission.TIMELINE_DOWNLOAD: + return ids.has(authUser.id) ? new Set([authUser.id]) : new Set(); + + case Permission.LIBRARY_READ: { + const isOwner = await this.repository.library.checkOwnerAccess(authUser.id, ids); + const isPartner = await this.repository.library.checkPartnerAccess(authUser.id, setDifference(ids, isOwner)); + return setUnion(isOwner, isPartner); + } + + case Permission.LIBRARY_UPDATE: + return this.repository.library.checkOwnerAccess(authUser.id, ids); + + case Permission.LIBRARY_DELETE: + return this.repository.library.checkOwnerAccess(authUser.id, ids); + + case Permission.PERSON_READ: + return this.repository.person.checkOwnerAccess(authUser.id, ids); + + case Permission.PERSON_WRITE: + return this.repository.person.checkOwnerAccess(authUser.id, ids); + + case Permission.PERSON_MERGE: + return this.repository.person.checkOwnerAccess(authUser.id, ids); + + case Permission.PARTNER_UPDATE: + return this.repository.partner.checkUpdateAccess(authUser.id, ids); } const allowedIds = new Set(); @@ -240,45 +282,6 @@ export class AccessCore { (await this.repository.asset.hasPartnerAccess(authUser.id, id)) ); - case Permission.ASSET_UPLOAD: - return this.repository.library.hasOwnerAccess(authUser.id, id); - - case Permission.ARCHIVE_READ: - return authUser.id === id; - - case Permission.AUTH_DEVICE_DELETE: - return this.repository.authDevice.hasOwnerAccess(authUser.id, id); - - case Permission.TIMELINE_READ: - return authUser.id === id || (await this.repository.timeline.hasPartnerAccess(authUser.id, id)); - - case Permission.TIMELINE_DOWNLOAD: - return authUser.id === id; - - case Permission.LIBRARY_READ: - return ( - (await this.repository.library.hasOwnerAccess(authUser.id, id)) || - (await this.repository.library.hasPartnerAccess(authUser.id, id)) - ); - - case Permission.LIBRARY_UPDATE: - return this.repository.library.hasOwnerAccess(authUser.id, id); - - case Permission.LIBRARY_DELETE: - return this.repository.library.hasOwnerAccess(authUser.id, id); - - case Permission.PERSON_READ: - return this.repository.person.hasOwnerAccess(authUser.id, id); - - case Permission.PERSON_WRITE: - return this.repository.person.hasOwnerAccess(authUser.id, id); - - case Permission.PERSON_MERGE: - return this.repository.person.hasOwnerAccess(authUser.id, id); - - case Permission.PARTNER_UPDATE: - return this.repository.partner.hasUpdateAccess(authUser.id, id); - default: return false; } diff --git a/server/src/domain/asset/asset.service.spec.ts b/server/src/domain/asset/asset.service.spec.ts index 0baddd953e..28a138254c 100644 --- a/server/src/domain/asset/asset.service.spec.ts +++ b/server/src/domain/asset/asset.service.spec.ts @@ -559,7 +559,7 @@ describe(AssetService.name, () => { }); it('should return a list of archives (userId)', async () => { - accessMock.library.hasOwnerAccess.mockResolvedValue(true); + accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([authStub.admin.id])); assetMock.getByUserId.mockResolvedValue({ items: [assetStub.image, assetStub.video], hasNextPage: false, @@ -575,7 +575,7 @@ describe(AssetService.name, () => { }); it('should split archives by size', async () => { - accessMock.library.hasOwnerAccess.mockResolvedValue(true); + accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([authStub.admin.id])); assetMock.getByUserId.mockResolvedValue({ items: [ diff --git a/server/src/domain/auth/auth.service.spec.ts b/server/src/domain/auth/auth.service.spec.ts index a815e22d1f..7ece7bed85 100644 --- a/server/src/domain/auth/auth.service.spec.ts +++ b/server/src/domain/auth/auth.service.spec.ts @@ -395,11 +395,11 @@ describe('AuthService', () => { describe('logoutDevice', () => { it('should logout the device', async () => { - accessMock.authDevice.hasOwnerAccess.mockResolvedValue(true); + accessMock.authDevice.checkOwnerAccess.mockResolvedValue(new Set(['token-1'])); await sut.logoutDevice(authStub.user1, 'token-1'); - expect(accessMock.authDevice.hasOwnerAccess).toHaveBeenCalledWith(authStub.user1.id, 'token-1'); + expect(accessMock.authDevice.checkOwnerAccess).toHaveBeenCalledWith(authStub.user1.id, new Set(['token-1'])); expect(userTokenMock.delete).toHaveBeenCalledWith('token-1'); }); }); diff --git a/server/src/domain/library/library.service.spec.ts b/server/src/domain/library/library.service.spec.ts index 3d7d68736f..c7e15e9600 100644 --- a/server/src/domain/library/library.service.spec.ts +++ b/server/src/domain/library/library.service.spec.ts @@ -58,7 +58,7 @@ describe(LibraryService.name, () => { ctime: new Date('2023-01-01'), } as Stats); - accessMock.library.hasOwnerAccess.mockResolvedValue(true); + accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([authStub.admin.id])); sut = new LibraryService( accessMock, diff --git a/server/src/domain/person/person.service.spec.ts b/server/src/domain/person/person.service.spec.ts index 3a4ac6b6db..b210a9165e 100644 --- a/server/src/domain/person/person.service.spec.ts +++ b/server/src/domain/person/person.service.spec.ts @@ -183,105 +183,101 @@ describe(PersonService.name, () => { describe('getById', () => { it('should require person.read permission', async () => { personMock.getById.mockResolvedValue(personStub.withName); - accessMock.person.hasOwnerAccess.mockResolvedValue(false); await expect(sut.getById(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should throw a bad request when person is not found', async () => { personMock.getById.mockResolvedValue(null); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.getById(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should get a person by id', async () => { personMock.getById.mockResolvedValue(personStub.withName); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.getById(authStub.admin, 'person-1')).resolves.toEqual(responseDto); expect(personMock.getById).toHaveBeenCalledWith('person-1'); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); }); describe('getThumbnail', () => { it('should require person.read permission', async () => { personMock.getById.mockResolvedValue(personStub.noName); - accessMock.person.hasOwnerAccess.mockResolvedValue(false); await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); expect(storageMock.createReadStream).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should throw an error when personId is invalid', async () => { personMock.getById.mockResolvedValue(null); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(NotFoundException); expect(storageMock.createReadStream).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should throw an error when person has no thumbnail', async () => { personMock.getById.mockResolvedValue(personStub.noThumbnail); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(NotFoundException); expect(storageMock.createReadStream).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should serve the thumbnail', async () => { personMock.getById.mockResolvedValue(personStub.noName); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await sut.getThumbnail(authStub.admin, 'person-1'); expect(storageMock.createReadStream).toHaveBeenCalledWith('/path/to/thumbnail.jpg', 'image/jpeg'); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); }); describe('getAssets', () => { it('should require person.read permission', async () => { personMock.getAssets.mockResolvedValue([assetStub.image, assetStub.video]); - accessMock.person.hasOwnerAccess.mockResolvedValue(false); await expect(sut.getAssets(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); expect(personMock.getAssets).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it("should return a person's assets", async () => { personMock.getAssets.mockResolvedValue([assetStub.image, assetStub.video]); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await sut.getAssets(authStub.admin, 'person-1'); expect(personMock.getAssets).toHaveBeenCalledWith('person-1'); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); }); describe('update', () => { it('should require person.write permission', async () => { personMock.getById.mockResolvedValue(personStub.noName); - accessMock.person.hasOwnerAccess.mockResolvedValue(false); await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).rejects.toBeInstanceOf( BadRequestException, ); expect(personMock.update).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should throw an error when personId is invalid', async () => { personMock.getById.mockResolvedValue(null); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).rejects.toBeInstanceOf( BadRequestException, ); expect(personMock.update).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it("should update a person's name", async () => { personMock.getById.mockResolvedValue(personStub.noName); personMock.update.mockResolvedValue(personStub.withName); personMock.getAssets.mockResolvedValue([assetStub.image]); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).resolves.toEqual(responseDto); @@ -291,14 +287,14 @@ describe(PersonService.name, () => { name: JobName.SEARCH_INDEX_ASSET, data: { ids: [assetStub.image.id] }, }); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it("should update a person's date of birth", async () => { personMock.getById.mockResolvedValue(personStub.noBirthDate); personMock.update.mockResolvedValue(personStub.withBirthDate); personMock.getAssets.mockResolvedValue([assetStub.image]); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.update(authStub.admin, 'person-1', { birthDate: new Date('1976-06-30') })).resolves.toEqual({ id: 'person-1', @@ -311,14 +307,14 @@ describe(PersonService.name, () => { expect(personMock.getById).toHaveBeenCalledWith('person-1'); expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: new Date('1976-06-30') }); expect(jobMock.queue).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should update a person visibility', async () => { personMock.getById.mockResolvedValue(personStub.hidden); personMock.update.mockResolvedValue(personStub.withName); personMock.getAssets.mockResolvedValue([assetStub.image]); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.update(authStub.admin, 'person-1', { isHidden: false })).resolves.toEqual(responseDto); @@ -328,7 +324,7 @@ describe(PersonService.name, () => { name: JobName.SEARCH_INDEX_ASSET, data: { ids: [assetStub.image.id] }, }); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it("should update a person's thumbnailPath", async () => { @@ -336,7 +332,7 @@ describe(PersonService.name, () => { personMock.update.mockResolvedValue(personStub.withName); personMock.getFacesByIds.mockResolvedValue([faceStub.face1]); accessMock.asset.hasOwnerAccess.mockResolvedValue(true); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect( sut.update(authStub.admin, 'person-1', { featureFaceAssetId: faceStub.face1.assetId }), @@ -351,31 +347,31 @@ describe(PersonService.name, () => { }, ]); expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: 'person-1' } }); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should throw an error when the face feature assetId is invalid', async () => { personMock.getById.mockResolvedValue(personStub.withName); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.update(authStub.admin, 'person-1', { featureFaceAssetId: '-1' })).rejects.toThrow( BadRequestException, ); expect(personMock.update).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); }); describe('updateAll', () => { it('should throw an error when personId is invalid', async () => { personMock.getById.mockResolvedValue(null); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect( sut.updatePeople(authStub.admin, { people: [{ id: 'person-1', name: 'Person 1' }] }), ).resolves.toEqual([{ error: BulkIdErrorReason.UNKNOWN, id: 'person-1', success: false }]); expect(personMock.update).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); }); @@ -652,7 +648,6 @@ describe(PersonService.name, () => { personMock.getById.mockResolvedValueOnce(personStub.mergePerson); personMock.prepareReassignFaces.mockResolvedValue([]); personMock.delete.mockResolvedValue(personStub.mergePerson); - accessMock.person.hasOwnerAccess.mockResolvedValue(false); await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).rejects.toBeInstanceOf( BadRequestException, @@ -663,7 +658,7 @@ describe(PersonService.name, () => { expect(personMock.reassignFaces).not.toHaveBeenCalled(); expect(personMock.delete).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should merge two people', async () => { @@ -671,7 +666,8 @@ describe(PersonService.name, () => { personMock.getById.mockResolvedValueOnce(personStub.mergePerson); personMock.prepareReassignFaces.mockResolvedValue([]); personMock.delete.mockResolvedValue(personStub.mergePerson); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1'])); + accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2'])); await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([ { id: 'person-2', success: true }, @@ -691,14 +687,15 @@ describe(PersonService.name, () => { name: JobName.PERSON_DELETE, data: { id: personStub.mergePerson.id }, }); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should delete conflicting faces before merging', async () => { personMock.getById.mockResolvedValue(personStub.primaryPerson); personMock.getById.mockResolvedValue(personStub.mergePerson); personMock.prepareReassignFaces.mockResolvedValue([assetStub.image.id]); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1'])); + accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2'])); await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([ { id: 'person-2', success: true }, @@ -713,25 +710,26 @@ describe(PersonService.name, () => { name: JobName.SEARCH_REMOVE_FACE, data: { assetId: assetStub.image.id, personId: personStub.mergePerson.id }, }); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should throw an error when the primary person is not found', async () => { personMock.getById.mockResolvedValue(null); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).rejects.toBeInstanceOf( BadRequestException, ); expect(personMock.delete).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should handle invalid merge ids', async () => { personMock.getById.mockResolvedValueOnce(personStub.primaryPerson); personMock.getById.mockResolvedValueOnce(null); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1'])); + accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2'])); await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([ { id: 'person-2', success: false, error: BulkIdErrorReason.NOT_FOUND }, @@ -740,7 +738,7 @@ describe(PersonService.name, () => { expect(personMock.prepareReassignFaces).not.toHaveBeenCalled(); expect(personMock.reassignFaces).not.toHaveBeenCalled(); expect(personMock.delete).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should handle an error reassigning faces', async () => { @@ -748,14 +746,15 @@ describe(PersonService.name, () => { personMock.getById.mockResolvedValue(personStub.mergePerson); personMock.prepareReassignFaces.mockResolvedValue([assetStub.image.id]); personMock.reassignFaces.mockRejectedValue(new Error('update failed')); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1'])); + accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2'])); await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([ { id: 'person-2', success: false, error: BulkIdErrorReason.UNKNOWN }, ]); expect(personMock.delete).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); }); @@ -763,16 +762,15 @@ describe(PersonService.name, () => { it('should get correct number of person', async () => { personMock.getById.mockResolvedValue(personStub.primaryPerson); personMock.getStatistics.mockResolvedValue(statistics); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.getStatistics(authStub.admin, 'person-1')).resolves.toEqual({ assets: 3 }); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should require person.read permission', async () => { personMock.getById.mockResolvedValue(personStub.primaryPerson); - accessMock.person.hasOwnerAccess.mockResolvedValue(false); await expect(sut.getStatistics(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); }); }); diff --git a/server/src/domain/repositories/access.repository.ts b/server/src/domain/repositories/access.repository.ts index e1ea27ce6d..7736fd890f 100644 --- a/server/src/domain/repositories/access.repository.ts +++ b/server/src/domain/repositories/access.repository.ts @@ -14,7 +14,7 @@ export interface IAccessRepository { }; authDevice: { - hasOwnerAccess(userId: string, deviceId: string): Promise; + checkOwnerAccess(userId: string, deviceIds: Set): Promise>; }; album: { @@ -24,19 +24,19 @@ export interface IAccessRepository { }; library: { - hasOwnerAccess(userId: string, libraryId: string): Promise; - hasPartnerAccess(userId: string, partnerId: string): Promise; + checkOwnerAccess(userId: string, libraryIds: Set): Promise>; + checkPartnerAccess(userId: string, partnerIds: Set): Promise>; }; timeline: { - hasPartnerAccess(userId: string, partnerId: string): Promise; + checkPartnerAccess(userId: string, partnerIds: Set): Promise>; }; person: { - hasOwnerAccess(userId: string, personId: string): Promise; + checkOwnerAccess(userId: string, personIds: Set): Promise>; }; partner: { - hasUpdateAccess(userId: string, partnerId: string): Promise; + checkUpdateAccess(userId: string, partnerIds: Set): Promise>; }; } diff --git a/server/src/immich/api-v1/asset/asset.service.spec.ts b/server/src/immich/api-v1/asset/asset.service.spec.ts index cc2102766c..11173b55fa 100644 --- a/server/src/immich/api-v1/asset/asset.service.spec.ts +++ b/server/src/immich/api-v1/asset/asset.service.spec.ts @@ -130,7 +130,7 @@ describe('AssetService', () => { const dto = _getCreateAssetDto(); assetRepositoryMock.create.mockResolvedValue(assetEntity); - accessMock.library.hasOwnerAccess.mockResolvedValue(true); + accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!])); await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: false, id: 'id_1' }); @@ -150,7 +150,7 @@ describe('AssetService', () => { assetRepositoryMock.create.mockRejectedValue(error); assetRepositoryMock.getAssetsByChecksums.mockResolvedValue([_getAsset_1()]); - accessMock.library.hasOwnerAccess.mockResolvedValue(true); + accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!])); await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: true, id: 'id_1' }); @@ -167,7 +167,7 @@ describe('AssetService', () => { assetRepositoryMock.create.mockResolvedValueOnce(assetStub.livePhotoMotionAsset); assetRepositoryMock.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset); - accessMock.library.hasOwnerAccess.mockResolvedValue(true); + accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!])); await expect( sut.uploadFile(authStub.user1, dto, fileStub.livePhotoStill, fileStub.livePhotoMotion), diff --git a/server/src/infra/repositories/access.repository.ts b/server/src/infra/repositories/access.repository.ts index fb0b865fb6..b23c559a61 100644 --- a/server/src/infra/repositories/access.repository.ts +++ b/server/src/infra/repositories/access.repository.ts @@ -62,33 +62,52 @@ export class AccessRepository implements IAccessRepository { }); }, }; + library = { - hasOwnerAccess: (userId: string, libraryId: string): Promise => { - return this.libraryRepository.exist({ - where: { - id: libraryId, - ownerId: userId, - }, - }); + checkOwnerAccess: async (userId: string, libraryIds: Set): Promise> => { + if (libraryIds.size === 0) { + return new Set(); + } + + return this.libraryRepository + .find({ + select: { id: true }, + where: { + id: In([...libraryIds]), + ownerId: userId, + }, + }) + .then((libraries) => new Set(libraries.map((library) => library.id))); }, - hasPartnerAccess: (userId: string, partnerId: string): Promise => { - return this.partnerRepository.exist({ - where: { - sharedWithId: userId, - sharedById: partnerId, - }, - }); + + checkPartnerAccess: async (userId: string, partnerIds: Set): Promise> => { + if (partnerIds.size === 0) { + return new Set(); + } + + return this.partnerRepository + .createQueryBuilder('partner') + .select('partner.sharedById') + .where('partner.sharedById IN (:...partnerIds)', { partnerIds: [...partnerIds] }) + .andWhere('partner.sharedWithId = :userId', { userId }) + .getMany() + .then((partners) => new Set(partners.map((partner) => partner.sharedById))); }, }; timeline = { - hasPartnerAccess: (userId: string, partnerId: string): Promise => { - return this.partnerRepository.exist({ - where: { - sharedWithId: userId, - sharedById: partnerId, - }, - }); + checkPartnerAccess: async (userId: string, partnerIds: Set): Promise> => { + if (partnerIds.size === 0) { + return new Set(); + } + + return this.partnerRepository + .createQueryBuilder('partner') + .select('partner.sharedById') + .where('partner.sharedById IN (:...partnerIds)', { partnerIds: [...partnerIds] }) + .andWhere('partner.sharedWithId = :userId', { userId }) + .getMany() + .then((partners) => new Set(partners.map((partner) => partner.sharedById))); }, }; @@ -198,13 +217,20 @@ export class AccessRepository implements IAccessRepository { }; authDevice = { - hasOwnerAccess: (userId: string, deviceId: string): Promise => { - return this.tokenRepository.exist({ - where: { - userId, - id: deviceId, - }, - }); + checkOwnerAccess: async (userId: string, deviceIds: Set): Promise> => { + if (deviceIds.size === 0) { + return new Set(); + } + + return this.tokenRepository + .find({ + select: { id: true }, + where: { + userId, + id: In([...deviceIds]), + }, + }) + .then((tokens) => new Set(tokens.map((token) => token.id))); }, }; @@ -264,24 +290,36 @@ export class AccessRepository implements IAccessRepository { }; person = { - hasOwnerAccess: (userId: string, personId: string): Promise => { - return this.personRepository.exist({ - where: { - id: personId, - ownerId: userId, - }, - }); + checkOwnerAccess: async (userId: string, personIds: Set): Promise> => { + if (personIds.size === 0) { + return new Set(); + } + + return this.personRepository + .find({ + select: { id: true }, + where: { + id: In([...personIds]), + ownerId: userId, + }, + }) + .then((persons) => new Set(persons.map((person) => person.id))); }, }; partner = { - hasUpdateAccess: (userId: string, partnerId: string): Promise => { - return this.partnerRepository.exist({ - where: { - sharedById: partnerId, - sharedWithId: userId, - }, - }); + checkUpdateAccess: async (userId: string, partnerIds: Set): Promise> => { + if (partnerIds.size === 0) { + return new Set(); + } + + return this.partnerRepository + .createQueryBuilder('partner') + .select('partner.sharedById') + .where('partner.sharedById IN (:...partnerIds)', { partnerIds: [...partnerIds] }) + .andWhere('partner.sharedWithId = :userId', { userId }) + .getMany() + .then((partners) => new Set(partners.map((partner) => partner.sharedById))); }, }; } diff --git a/server/test/repositories/access.repository.mock.ts b/server/test/repositories/access.repository.mock.ts index eceb25812e..f495d800e0 100644 --- a/server/test/repositories/access.repository.mock.ts +++ b/server/test/repositories/access.repository.mock.ts @@ -36,24 +36,24 @@ export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock => }, authDevice: { - hasOwnerAccess: jest.fn(), + checkOwnerAccess: jest.fn().mockResolvedValue(new Set()), }, library: { - hasOwnerAccess: jest.fn(), - hasPartnerAccess: jest.fn(), + checkOwnerAccess: jest.fn().mockResolvedValue(new Set()), + checkPartnerAccess: jest.fn().mockResolvedValue(new Set()), }, timeline: { - hasPartnerAccess: jest.fn(), + checkPartnerAccess: jest.fn().mockResolvedValue(new Set()), }, person: { - hasOwnerAccess: jest.fn(), + checkOwnerAccess: jest.fn().mockResolvedValue(new Set()), }, partner: { - hasUpdateAccess: jest.fn(), + checkUpdateAccess: jest.fn().mockResolvedValue(new Set()), }, }; };