Browse Source

chore(server): Use access core for person permissions (#4138)

* use access core for all person methods

* minor fixes, feedback

* reorder assignments

* remove unnecessary permission requirement

* unify naming of tests

* reorder variables
Daniel Dietzler 1 year ago
parent
commit
1a633f3fca

+ 13 - 0
server/src/domain/access/access.core.ts

@@ -23,6 +23,10 @@ export enum Permission {
 
   LIBRARY_READ = 'library.read',
   LIBRARY_DOWNLOAD = 'library.download',
+
+  PERSON_READ = 'person.read',
+  PERSON_WRITE = 'person.write',
+  PERSON_MERGE = 'person.merge',
 }
 
 export class AccessCore {
@@ -167,6 +171,15 @@ export class AccessCore {
       case Permission.LIBRARY_DOWNLOAD:
         return 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);
+
       default:
         return false;
     }

+ 4 - 0
server/src/domain/access/access.repository.ts

@@ -17,4 +17,8 @@ export interface IAccessRepository {
   library: {
     hasPartnerAccess(userId: string, partnerId: string): Promise<boolean>;
   };
+
+  person: {
+    hasOwnerAccess(userId: string, personId: string): Promise<boolean>;
+  };
 }

+ 2 - 2
server/src/domain/person/person.repository.ts

@@ -17,9 +17,9 @@ export interface IPersonRepository {
   getAllWithoutThumbnail(): Promise<PersonEntity[]>;
   getAllForUser(userId: string, options: PersonSearchOptions): Promise<PersonEntity[]>;
   getAllWithoutFaces(): Promise<PersonEntity[]>;
-  getById(userId: string, personId: string): Promise<PersonEntity | null>;
+  getById(personId: string): Promise<PersonEntity | null>;
 
-  getAssets(userId: string, personId: string): Promise<AssetEntity[]>;
+  getAssets(personId: string): Promise<AssetEntity[]>;
   prepareReassignFaces(data: UpdateFacesData): Promise<string[]>;
   reassignFaces(data: UpdateFacesData): Promise<number>;
 

+ 103 - 10
server/src/domain/person/person.service.spec.ts

@@ -1,8 +1,10 @@
 import { BadRequestException, NotFoundException } from '@nestjs/common';
 import {
+  IAccessRepositoryMock,
   assetStub,
   authStub,
   faceStub,
+  newAccessRepositoryMock,
   newJobRepositoryMock,
   newPersonRepositoryMock,
   newStorageRepositoryMock,
@@ -26,18 +28,20 @@ const responseDto: PersonResponseDto = {
 };
 
 describe(PersonService.name, () => {
-  let sut: PersonService;
-  let personMock: jest.Mocked<IPersonRepository>;
+  let accessMock: IAccessRepositoryMock;
   let configMock: jest.Mocked<ISystemConfigRepository>;
-  let storageMock: jest.Mocked<IStorageRepository>;
   let jobMock: jest.Mocked<IJobRepository>;
+  let personMock: jest.Mocked<IPersonRepository>;
+  let storageMock: jest.Mocked<IStorageRepository>;
+  let sut: PersonService;
 
   beforeEach(async () => {
+    accessMock = newAccessRepositoryMock();
     personMock = newPersonRepositoryMock();
     storageMock = newStorageRepositoryMock();
     configMock = newSystemConfigRepositoryMock();
     jobMock = newJobRepositoryMock();
-    sut = new PersonService(personMock, configMock, storageMock, jobMock);
+    sut = new PersonService(accessMock, personMock, configMock, storageMock, jobMock);
   });
 
   it('should be defined', () => {
@@ -93,74 +97,124 @@ 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');
+    });
+
     it('should throw a bad request when person is not found', async () => {
       personMock.getById.mockResolvedValue(null);
+      accessMock.person.hasOwnerAccess.mockResolvedValue(true);
       await expect(sut.getById(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException);
+      expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
     });
 
     it('should get a person by id', async () => {
       personMock.getById.mockResolvedValue(personStub.withName);
+      accessMock.person.hasOwnerAccess.mockResolvedValue(true);
       await expect(sut.getById(authStub.admin, 'person-1')).resolves.toEqual(responseDto);
-      expect(personMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
+      expect(personMock.getById).toHaveBeenCalledWith('person-1');
+      expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, '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');
+    });
+
     it('should throw an error when personId is invalid', async () => {
       personMock.getById.mockResolvedValue(null);
+      accessMock.person.hasOwnerAccess.mockResolvedValue(true);
       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');
     });
 
     it('should throw an error when person has no thumbnail', async () => {
       personMock.getById.mockResolvedValue(personStub.noThumbnail);
+      accessMock.person.hasOwnerAccess.mockResolvedValue(true);
       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');
     });
 
     it('should serve the thumbnail', async () => {
       personMock.getById.mockResolvedValue(personStub.noName);
+      accessMock.person.hasOwnerAccess.mockResolvedValue(true);
       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');
     });
   });
 
   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');
+    });
+
     it("should return a person's assets", async () => {
       personMock.getAssets.mockResolvedValue([assetStub.image, assetStub.video]);
+      accessMock.person.hasOwnerAccess.mockResolvedValue(true);
       await sut.getAssets(authStub.admin, 'person-1');
-      expect(personMock.getAssets).toHaveBeenCalledWith('admin_id', 'person-1');
+      expect(personMock.getAssets).toHaveBeenCalledWith('person-1');
+      expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, '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');
+    });
+
     it('should throw an error when personId is invalid', async () => {
       personMock.getById.mockResolvedValue(null);
+      accessMock.person.hasOwnerAccess.mockResolvedValue(true);
       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');
     });
 
     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);
 
       await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).resolves.toEqual(responseDto);
 
-      expect(personMock.getById).toHaveBeenCalledWith('admin_id', 'person-1');
+      expect(personMock.getById).toHaveBeenCalledWith('person-1');
       expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', name: 'Person 1' });
       expect(jobMock.queue).toHaveBeenCalledWith({
         name: JobName.SEARCH_INDEX_ASSET,
         data: { ids: [assetStub.image.id] },
       });
+      expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, '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);
 
       await expect(sut.update(authStub.admin, 'person-1', { birthDate: new Date('1976-06-30') })).resolves.toEqual({
         id: 'person-1',
@@ -170,35 +224,39 @@ describe(PersonService.name, () => {
         isHidden: false,
       });
 
-      expect(personMock.getById).toHaveBeenCalledWith('admin_id', 'person-1');
+      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');
     });
 
     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);
 
       await expect(sut.update(authStub.admin, 'person-1', { isHidden: false })).resolves.toEqual(responseDto);
 
-      expect(personMock.getById).toHaveBeenCalledWith('admin_id', 'person-1');
+      expect(personMock.getById).toHaveBeenCalledWith('person-1');
       expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', isHidden: false });
       expect(jobMock.queue).toHaveBeenCalledWith({
         name: JobName.SEARCH_INDEX_ASSET,
         data: { ids: [assetStub.image.id] },
       });
+      expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
     });
 
     it("should update a person's thumbnailPath", async () => {
       personMock.getById.mockResolvedValue(personStub.withName);
       personMock.getFaceById.mockResolvedValue(faceStub.face1);
+      accessMock.person.hasOwnerAccess.mockResolvedValue(true);
 
       await expect(
         sut.update(authStub.admin, 'person-1', { featureFaceAssetId: faceStub.face1.assetId }),
       ).resolves.toEqual(responseDto);
 
-      expect(personMock.getById).toHaveBeenCalledWith('admin_id', 'person-1');
+      expect(personMock.getById).toHaveBeenCalledWith('person-1');
       expect(personMock.getFaceById).toHaveBeenCalledWith({
         assetId: faceStub.face1.assetId,
         personId: 'person-1',
@@ -218,25 +276,31 @@ describe(PersonService.name, () => {
           imageWidth: faceStub.face1.imageWidth,
         },
       });
+      expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, '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);
 
       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');
     });
   });
 
   describe('updateAll', () => {
     it('should throw an error when personId is invalid', async () => {
       personMock.getById.mockResolvedValue(null);
+      accessMock.person.hasOwnerAccess.mockResolvedValue(true);
+
       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');
     });
   });
 
@@ -255,11 +319,31 @@ describe(PersonService.name, () => {
   });
 
   describe('mergePerson', () => {
+    it('should require person.write and person.merge permission', async () => {
+      personMock.getById.mockResolvedValueOnce(personStub.primaryPerson);
+      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,
+      );
+
+      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');
+    });
+
     it('should merge two people', async () => {
       personMock.getById.mockResolvedValueOnce(personStub.primaryPerson);
       personMock.getById.mockResolvedValueOnce(personStub.mergePerson);
       personMock.prepareReassignFaces.mockResolvedValue([]);
       personMock.delete.mockResolvedValue(personStub.mergePerson);
+      accessMock.person.hasOwnerAccess.mockResolvedValue(true);
 
       await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([
         { id: 'person-2', success: true },
@@ -276,12 +360,14 @@ describe(PersonService.name, () => {
       });
 
       expect(personMock.delete).toHaveBeenCalledWith(personStub.mergePerson);
+      expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, '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);
 
       await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([
         { id: 'person-2', success: true },
@@ -296,21 +382,25 @@ 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');
     });
 
     it('should throw an error when the primary person is not found', async () => {
       personMock.getById.mockResolvedValue(null);
+      accessMock.person.hasOwnerAccess.mockResolvedValue(true);
 
       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');
     });
 
     it('should handle invalid merge ids', async () => {
       personMock.getById.mockResolvedValueOnce(personStub.primaryPerson);
       personMock.getById.mockResolvedValueOnce(null);
+      accessMock.person.hasOwnerAccess.mockResolvedValue(true);
 
       await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([
         { id: 'person-2', success: false, error: BulkIdErrorReason.NOT_FOUND },
@@ -319,6 +409,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');
     });
 
     it('should handle an error reassigning faces', async () => {
@@ -326,12 +417,14 @@ 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);
 
       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');
     });
   });
 });

+ 26 - 10
server/src/domain/person/person.service.ts

@@ -1,4 +1,5 @@
 import { BadRequestException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
+import { AccessCore, IAccessRepository, Permission } from '../access';
 import { AssetResponseDto, BulkIdErrorReason, BulkIdResponseDto, mapAsset } from '../asset';
 import { AuthUserDto } from '../auth';
 import { mimeTypes } from '../domain.constant';
@@ -18,15 +19,18 @@ import { IPersonRepository, UpdateFacesData } from './person.repository';
 
 @Injectable()
 export class PersonService {
+  private access: AccessCore;
   private configCore: SystemConfigCore;
   readonly logger = new Logger(PersonService.name);
 
   constructor(
+    @Inject(IAccessRepository) private accessRepository: IAccessRepository,
     @Inject(IPersonRepository) private repository: IPersonRepository,
     @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
     @Inject(IStorageRepository) private storageRepository: IStorageRepository,
     @Inject(IJobRepository) private jobRepository: IJobRepository,
   ) {
+    this.access = new AccessCore(accessRepository);
     this.configCore = new SystemConfigCore(configRepository);
   }
 
@@ -48,12 +52,14 @@ export class PersonService {
     };
   }
 
-  getById(authUser: AuthUserDto, id: string): Promise<PersonResponseDto> {
-    return this.findOrFail(authUser, id).then(mapPerson);
+  async getById(authUser: AuthUserDto, id: string): Promise<PersonResponseDto> {
+    await this.access.requirePermission(authUser, Permission.PERSON_READ, id);
+    return this.findOrFail(id).then(mapPerson);
   }
 
   async getThumbnail(authUser: AuthUserDto, id: string): Promise<ImmichReadStream> {
-    const person = await this.repository.getById(authUser.id, id);
+    await this.access.requirePermission(authUser, Permission.PERSON_READ, id);
+    const person = await this.repository.getById(id);
     if (!person || !person.thumbnailPath) {
       throw new NotFoundException();
     }
@@ -62,17 +68,19 @@ export class PersonService {
   }
 
   async getAssets(authUser: AuthUserDto, id: string): Promise<AssetResponseDto[]> {
-    const assets = await this.repository.getAssets(authUser.id, id);
+    await this.access.requirePermission(authUser, Permission.PERSON_READ, id);
+    const assets = await this.repository.getAssets(id);
     return assets.map(mapAsset);
   }
 
   async update(authUser: AuthUserDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
-    let person = await this.findOrFail(authUser, id);
+    await this.access.requirePermission(authUser, Permission.PERSON_WRITE, id);
+    let person = await this.findOrFail(id);
 
     if (dto.name !== undefined || dto.birthDate !== undefined || dto.isHidden !== undefined) {
       person = await this.repository.update({ id, name: dto.name, birthDate: dto.birthDate, isHidden: dto.isHidden });
       if (this.needsSearchIndexUpdate(dto)) {
-        const assets = await this.repository.getAssets(authUser.id, id);
+        const assets = await this.repository.getAssets(id);
         const ids = assets.map((asset) => asset.id);
         await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids } });
       }
@@ -141,14 +149,22 @@ export class PersonService {
 
   async mergePerson(authUser: AuthUserDto, id: string, dto: MergePersonDto): Promise<BulkIdResponseDto[]> {
     const mergeIds = dto.ids;
-    const primaryPerson = await this.findOrFail(authUser, id);
+    await this.access.requirePermission(authUser, Permission.PERSON_WRITE, id);
+    const primaryPerson = await this.findOrFail(id);
     const primaryName = primaryPerson.name || primaryPerson.id;
 
     const results: BulkIdResponseDto[] = [];
 
     for (const mergeId of mergeIds) {
+      const hasPermission = await this.access.hasPermission(authUser, Permission.PERSON_MERGE, mergeId);
+
+      if (!hasPermission) {
+        results.push({ id: mergeId, success: false, error: BulkIdErrorReason.NO_PERMISSION });
+        continue;
+      }
+
       try {
-        const mergePerson = await this.repository.getById(authUser.id, mergeId);
+        const mergePerson = await this.repository.getById(mergeId);
         if (!mergePerson) {
           results.push({ id: mergeId, success: false, error: BulkIdErrorReason.NOT_FOUND });
           continue;
@@ -188,8 +204,8 @@ export class PersonService {
     return dto.name !== undefined || dto.isHidden !== undefined;
   }
 
-  private async findOrFail(authUser: AuthUserDto, id: string) {
-    const person = await this.repository.getById(authUser.id, id);
+  private async findOrFail(id: string) {
+    const person = await this.repository.getById(id);
     if (!person) {
       throw new BadRequestException('Person not found');
     }

+ 13 - 1
server/src/infra/repositories/access.repository.ts

@@ -1,13 +1,14 @@
 import { IAccessRepository } from '@app/domain';
 import { InjectRepository } from '@nestjs/typeorm';
 import { Repository } from 'typeorm';
-import { AlbumEntity, AssetEntity, PartnerEntity, SharedLinkEntity } from '../entities';
+import { AlbumEntity, AssetEntity, PartnerEntity, PersonEntity, SharedLinkEntity } from '../entities';
 
 export class AccessRepository implements IAccessRepository {
   constructor(
     @InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
     @InjectRepository(AlbumEntity) private albumRepository: Repository<AlbumEntity>,
     @InjectRepository(PartnerEntity) private partnerRepository: Repository<PartnerEntity>,
+    @InjectRepository(PersonEntity) private personRepository: Repository<PersonEntity>,
     @InjectRepository(SharedLinkEntity) private sharedLinkRepository: Repository<SharedLinkEntity>,
   ) {}
 
@@ -156,4 +157,15 @@ export class AccessRepository implements IAccessRepository {
       });
     },
   };
+
+  person = {
+    hasOwnerAccess: (userId: string, personId: string): Promise<boolean> => {
+      return this.personRepository.exist({
+        where: {
+          id: personId,
+          ownerId: userId,
+        },
+      });
+    },
+  };
 }

+ 3 - 4
server/src/infra/repositories/person.repository.ts

@@ -86,14 +86,13 @@ export class PersonRepository implements IPersonRepository {
       .getMany();
   }
 
-  getById(ownerId: string, personId: string): Promise<PersonEntity | null> {
-    return this.personRepository.findOne({ where: { id: personId, ownerId } });
+  getById(personId: string): Promise<PersonEntity | null> {
+    return this.personRepository.findOne({ where: { id: personId } });
   }
 
-  getAssets(ownerId: string, personId: string): Promise<AssetEntity[]> {
+  getAssets(personId: string): Promise<AssetEntity[]> {
     return this.assetRepository.find({
       where: {
-        ownerId,
         faces: {
           personId,
         },

+ 5 - 0
server/test/repositories/access.repository.mock.ts

@@ -4,6 +4,7 @@ export interface IAccessRepositoryMock {
   asset: jest.Mocked<IAccessRepository['asset']>;
   album: jest.Mocked<IAccessRepository['album']>;
   library: jest.Mocked<IAccessRepository['library']>;
+  person: jest.Mocked<IAccessRepository['person']>;
 }
 
 export const newAccessRepositoryMock = (): IAccessRepositoryMock => {
@@ -24,5 +25,9 @@ export const newAccessRepositoryMock = (): IAccessRepositoryMock => {
     library: {
       hasPartnerAccess: jest.fn(),
     },
+
+    person: {
+      hasOwnerAccess: jest.fn(),
+    },
   };
 };