mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	refactor(server): access checks (#2776)
* refactor(server): access checks * chore: simply asset module
This commit is contained in:
		
							parent
							
								
									61d74263d9
								
							
						
					
					
						commit
						f04e47803c
					
				@ -2,6 +2,8 @@ export const IAccessRepository = 'IAccessRepository';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export interface IAccessRepository {
 | 
					export interface IAccessRepository {
 | 
				
			||||||
  hasPartnerAccess(userId: string, partnerId: string): Promise<boolean>;
 | 
					  hasPartnerAccess(userId: string, partnerId: string): Promise<boolean>;
 | 
				
			||||||
 | 
					  hasAlbumAssetAccess(userId: string, assetId: string): Promise<boolean>;
 | 
				
			||||||
 | 
					  hasOwnerAssetAccess(userId: string, assetId: string): Promise<boolean>;
 | 
				
			||||||
  hasPartnerAssetAccess(userId: string, assetId: string): Promise<boolean>;
 | 
					  hasPartnerAssetAccess(userId: string, assetId: string): Promise<boolean>;
 | 
				
			||||||
  hasSharedLinkAssetAccess(userId: string, assetId: string): Promise<boolean>;
 | 
					  hasSharedLinkAssetAccess(userId: string, assetId: string): Promise<boolean>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
import { AlbumEntity, AssetEntity } from '@app/infra/entities';
 | 
					 | 
				
			||||||
import { dataSource } from '@app/infra/database.config';
 | 
					import { dataSource } from '@app/infra/database.config';
 | 
				
			||||||
 | 
					import { AlbumEntity, AssetEntity } from '@app/infra/entities';
 | 
				
			||||||
import { Injectable } from '@nestjs/common';
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
import { Repository } from 'typeorm';
 | 
					import { Repository } from 'typeorm';
 | 
				
			||||||
@ -12,7 +12,6 @@ export interface IAlbumRepository {
 | 
				
			|||||||
  removeAssets(album: AlbumEntity, removeAssets: RemoveAssetsDto): Promise<number>;
 | 
					  removeAssets(album: AlbumEntity, removeAssets: RemoveAssetsDto): Promise<number>;
 | 
				
			||||||
  addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AddAssetsResponseDto>;
 | 
					  addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AddAssetsResponseDto>;
 | 
				
			||||||
  updateThumbnails(): Promise<number | undefined>;
 | 
					  updateThumbnails(): Promise<number | undefined>;
 | 
				
			||||||
  getSharedWithUserAlbumCount(userId: string, assetId: string): Promise<number>;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const IAlbumRepository = 'IAlbumRepository';
 | 
					export const IAlbumRepository = 'IAlbumRepository';
 | 
				
			||||||
@ -130,25 +129,4 @@ export class AlbumRepository implements IAlbumRepository {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return result.affected;
 | 
					    return result.affected;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  async getSharedWithUserAlbumCount(userId: string, assetId: string): Promise<number> {
 | 
					 | 
				
			||||||
    return this.albumRepository.count({
 | 
					 | 
				
			||||||
      where: [
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          ownerId: userId,
 | 
					 | 
				
			||||||
          assets: {
 | 
					 | 
				
			||||||
            id: assetId,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          sharedUsers: {
 | 
					 | 
				
			||||||
            id: userId,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          assets: {
 | 
					 | 
				
			||||||
            id: assetId,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -6,15 +6,9 @@ import { AlbumEntity, AssetEntity } from '@app/infra/entities';
 | 
				
			|||||||
import { AlbumRepository, IAlbumRepository } from './album-repository';
 | 
					import { AlbumRepository, IAlbumRepository } from './album-repository';
 | 
				
			||||||
import { DownloadModule } from '../../modules/download/download.module';
 | 
					import { DownloadModule } from '../../modules/download/download.module';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ALBUM_REPOSITORY_PROVIDER = {
 | 
					 | 
				
			||||||
  provide: IAlbumRepository,
 | 
					 | 
				
			||||||
  useClass: AlbumRepository,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@Module({
 | 
					@Module({
 | 
				
			||||||
  imports: [TypeOrmModule.forFeature([AlbumEntity, AssetEntity]), DownloadModule],
 | 
					  imports: [TypeOrmModule.forFeature([AlbumEntity, AssetEntity]), DownloadModule],
 | 
				
			||||||
  controllers: [AlbumController],
 | 
					  controllers: [AlbumController],
 | 
				
			||||||
  providers: [AlbumService, ALBUM_REPOSITORY_PROVIDER],
 | 
					  providers: [AlbumService, { provide: IAlbumRepository, useClass: AlbumRepository }],
 | 
				
			||||||
  exports: [ALBUM_REPOSITORY_PROVIDER],
 | 
					 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class AlbumModule {}
 | 
					export class AlbumModule {}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,13 +1,12 @@
 | 
				
			|||||||
import { AlbumService } from './album.service';
 | 
					import { AlbumResponseDto, ICryptoRepository, ISharedLinkRepository, mapUser } from '@app/domain';
 | 
				
			||||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
 | 
					 | 
				
			||||||
import { NotFoundException, ForbiddenException } from '@nestjs/common';
 | 
					 | 
				
			||||||
import { AlbumEntity, UserEntity } from '@app/infra/entities';
 | 
					import { AlbumEntity, UserEntity } from '@app/infra/entities';
 | 
				
			||||||
import { AlbumResponseDto, ICryptoRepository, mapUser } from '@app/domain';
 | 
					import { ForbiddenException, NotFoundException } from '@nestjs/common';
 | 
				
			||||||
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
 | 
					 | 
				
			||||||
import { IAlbumRepository } from './album-repository';
 | 
					 | 
				
			||||||
import { DownloadService } from '../../modules/download/download.service';
 | 
					 | 
				
			||||||
import { ISharedLinkRepository } from '@app/domain';
 | 
					 | 
				
			||||||
import { newCryptoRepositoryMock, newSharedLinkRepositoryMock, userEntityStub } from '@test';
 | 
					import { newCryptoRepositoryMock, newSharedLinkRepositoryMock, userEntityStub } from '@test';
 | 
				
			||||||
 | 
					import { AuthUserDto } from '../../decorators/auth-user.decorator';
 | 
				
			||||||
 | 
					import { DownloadService } from '../../modules/download/download.service';
 | 
				
			||||||
 | 
					import { IAlbumRepository } from './album-repository';
 | 
				
			||||||
 | 
					import { AlbumService } from './album.service';
 | 
				
			||||||
 | 
					import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe('Album service', () => {
 | 
					describe('Album service', () => {
 | 
				
			||||||
  let sut: AlbumService;
 | 
					  let sut: AlbumService;
 | 
				
			||||||
@ -98,7 +97,6 @@ describe('Album service', () => {
 | 
				
			|||||||
      get: jest.fn(),
 | 
					      get: jest.fn(),
 | 
				
			||||||
      removeAssets: jest.fn(),
 | 
					      removeAssets: jest.fn(),
 | 
				
			||||||
      updateThumbnails: jest.fn(),
 | 
					      updateThumbnails: jest.fn(),
 | 
				
			||||||
      getSharedWithUserAlbumCount: jest.fn(),
 | 
					 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sharedLinkRepositoryMock = newSharedLinkRepositoryMock();
 | 
					    sharedLinkRepositoryMock = newSharedLinkRepositoryMock();
 | 
				
			||||||
 | 
				
			|||||||
@ -39,7 +39,6 @@ export interface IAssetRepository {
 | 
				
			|||||||
  getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]>;
 | 
					  getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]>;
 | 
				
			||||||
  getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise<AssetCheck[]>;
 | 
					  getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise<AssetCheck[]>;
 | 
				
			||||||
  getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]>;
 | 
					  getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]>;
 | 
				
			||||||
  countByIdAndUser(assetId: string, userId: string): Promise<number>;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const IAssetRepository = 'IAssetRepository';
 | 
					export const IAssetRepository = 'IAssetRepository';
 | 
				
			||||||
@ -329,15 +328,6 @@ export class AssetRepository implements IAssetRepository {
 | 
				
			|||||||
    return assets.map((asset) => asset.deviceAssetId);
 | 
					    return assets.map((asset) => asset.deviceAssetId);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  countByIdAndUser(assetId: string, ownerId: string): Promise<number> {
 | 
					 | 
				
			||||||
    return this.assetRepository.count({
 | 
					 | 
				
			||||||
      where: {
 | 
					 | 
				
			||||||
        id: assetId,
 | 
					 | 
				
			||||||
        ownerId,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private getAssetCount(items: any): AssetCountByUserIdResponseDto {
 | 
					  private getAssetCount(items: any): AssetCountByUserIdResponseDto {
 | 
				
			||||||
    const assetCountByUserId = new AssetCountByUserIdResponseDto();
 | 
					    const assetCountByUserId = new AssetCountByUserIdResponseDto();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -5,22 +5,14 @@ import { TypeOrmModule } from '@nestjs/typeorm';
 | 
				
			|||||||
import { AssetEntity, ExifEntity } from '@app/infra/entities';
 | 
					import { AssetEntity, ExifEntity } from '@app/infra/entities';
 | 
				
			||||||
import { AssetRepository, IAssetRepository } from './asset-repository';
 | 
					import { AssetRepository, IAssetRepository } from './asset-repository';
 | 
				
			||||||
import { DownloadModule } from '../../modules/download/download.module';
 | 
					import { DownloadModule } from '../../modules/download/download.module';
 | 
				
			||||||
import { AlbumModule } from '../album/album.module';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const ASSET_REPOSITORY_PROVIDER = {
 | 
					 | 
				
			||||||
  provide: IAssetRepository,
 | 
					 | 
				
			||||||
  useClass: AssetRepository,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Module({
 | 
					@Module({
 | 
				
			||||||
  imports: [
 | 
					  imports: [
 | 
				
			||||||
    //
 | 
					    //
 | 
				
			||||||
    TypeOrmModule.forFeature([AssetEntity, ExifEntity]),
 | 
					    TypeOrmModule.forFeature([AssetEntity, ExifEntity]),
 | 
				
			||||||
    DownloadModule,
 | 
					    DownloadModule,
 | 
				
			||||||
    AlbumModule,
 | 
					 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  controllers: [AssetController],
 | 
					  controllers: [AssetController],
 | 
				
			||||||
  providers: [AssetService, ASSET_REPOSITORY_PROVIDER],
 | 
					  providers: [AssetService, { provide: IAssetRepository, useClass: AssetRepository }],
 | 
				
			||||||
  exports: [ASSET_REPOSITORY_PROVIDER],
 | 
					 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class AssetModule {}
 | 
					export class AssetModule {}
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,6 @@ import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group
 | 
				
			|||||||
import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
 | 
					import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
 | 
				
			||||||
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
 | 
					import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
 | 
				
			||||||
import { DownloadService } from '../../modules/download/download.service';
 | 
					import { DownloadService } from '../../modules/download/download.service';
 | 
				
			||||||
import { AlbumRepository, IAlbumRepository } from '../album/album-repository';
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  IAccessRepository,
 | 
					  IAccessRepository,
 | 
				
			||||||
  ICryptoRepository,
 | 
					  ICryptoRepository,
 | 
				
			||||||
@ -29,7 +28,7 @@ import {
 | 
				
			|||||||
  sharedLinkStub,
 | 
					  sharedLinkStub,
 | 
				
			||||||
} from '@test';
 | 
					} from '@test';
 | 
				
			||||||
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
 | 
					import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
 | 
				
			||||||
import { BadRequestException } from '@nestjs/common';
 | 
					import { BadRequestException, ForbiddenException } from '@nestjs/common';
 | 
				
			||||||
import { when } from 'jest-when';
 | 
					import { when } from 'jest-when';
 | 
				
			||||||
import { AssetRejectReason, AssetUploadAction } from './response-dto/asset-check-response.dto';
 | 
					import { AssetRejectReason, AssetUploadAction } from './response-dto/asset-check-response.dto';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -134,7 +133,6 @@ describe('AssetService', () => {
 | 
				
			|||||||
  let a: Repository<AssetEntity>; // TO BE DELETED AFTER FINISHED REFACTORING
 | 
					  let a: Repository<AssetEntity>; // TO BE DELETED AFTER FINISHED REFACTORING
 | 
				
			||||||
  let accessMock: jest.Mocked<IAccessRepository>;
 | 
					  let accessMock: jest.Mocked<IAccessRepository>;
 | 
				
			||||||
  let assetRepositoryMock: jest.Mocked<IAssetRepository>;
 | 
					  let assetRepositoryMock: jest.Mocked<IAssetRepository>;
 | 
				
			||||||
  let albumRepositoryMock: jest.Mocked<IAlbumRepository>;
 | 
					 | 
				
			||||||
  let downloadServiceMock: jest.Mocked<Partial<DownloadService>>;
 | 
					  let downloadServiceMock: jest.Mocked<Partial<DownloadService>>;
 | 
				
			||||||
  let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
 | 
					  let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
 | 
				
			||||||
  let cryptoMock: jest.Mocked<ICryptoRepository>;
 | 
					  let cryptoMock: jest.Mocked<ICryptoRepository>;
 | 
				
			||||||
@ -160,13 +158,8 @@ describe('AssetService', () => {
 | 
				
			|||||||
      getAssetCountByUserId: jest.fn(),
 | 
					      getAssetCountByUserId: jest.fn(),
 | 
				
			||||||
      getArchivedAssetCountByUserId: jest.fn(),
 | 
					      getArchivedAssetCountByUserId: jest.fn(),
 | 
				
			||||||
      getExistingAssets: jest.fn(),
 | 
					      getExistingAssets: jest.fn(),
 | 
				
			||||||
      countByIdAndUser: jest.fn(),
 | 
					 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    albumRepositoryMock = {
 | 
					 | 
				
			||||||
      getSharedWithUserAlbumCount: jest.fn(),
 | 
					 | 
				
			||||||
    } as unknown as jest.Mocked<AlbumRepository>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    downloadServiceMock = {
 | 
					    downloadServiceMock = {
 | 
				
			||||||
      downloadArchive: jest.fn(),
 | 
					      downloadArchive: jest.fn(),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
@ -180,7 +173,6 @@ describe('AssetService', () => {
 | 
				
			|||||||
    sut = new AssetService(
 | 
					    sut = new AssetService(
 | 
				
			||||||
      accessMock,
 | 
					      accessMock,
 | 
				
			||||||
      assetRepositoryMock,
 | 
					      assetRepositoryMock,
 | 
				
			||||||
      albumRepositoryMock,
 | 
					 | 
				
			||||||
      a,
 | 
					      a,
 | 
				
			||||||
      downloadServiceMock as DownloadService,
 | 
					      downloadServiceMock as DownloadService,
 | 
				
			||||||
      sharedLinkRepositoryMock,
 | 
					      sharedLinkRepositoryMock,
 | 
				
			||||||
@ -203,13 +195,13 @@ describe('AssetService', () => {
 | 
				
			|||||||
      const dto: CreateAssetsShareLinkDto = { assetIds: [asset1.id] };
 | 
					      const dto: CreateAssetsShareLinkDto = { assetIds: [asset1.id] };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      assetRepositoryMock.getById.mockResolvedValue(asset1);
 | 
					      assetRepositoryMock.getById.mockResolvedValue(asset1);
 | 
				
			||||||
      assetRepositoryMock.countByIdAndUser.mockResolvedValue(1);
 | 
					      accessMock.hasOwnerAssetAccess.mockResolvedValue(true);
 | 
				
			||||||
      sharedLinkRepositoryMock.create.mockResolvedValue(sharedLinkStub.valid);
 | 
					      sharedLinkRepositoryMock.create.mockResolvedValue(sharedLinkStub.valid);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await expect(sut.createAssetsSharedLink(authStub.user1, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
 | 
					      await expect(sut.createAssetsSharedLink(authStub.user1, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
 | 
					      expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
 | 
				
			||||||
      expect(assetRepositoryMock.countByIdAndUser).toHaveBeenCalledWith(asset1.id, authStub.user1.id);
 | 
					      expect(accessMock.hasOwnerAssetAccess).toHaveBeenCalledWith(authStub.user1.id, asset1.id);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -383,7 +375,7 @@ describe('AssetService', () => {
 | 
				
			|||||||
  describe('deleteAll', () => {
 | 
					  describe('deleteAll', () => {
 | 
				
			||||||
    it('should return failed status when an asset is missing', async () => {
 | 
					    it('should return failed status when an asset is missing', async () => {
 | 
				
			||||||
      assetRepositoryMock.get.mockResolvedValue(null);
 | 
					      assetRepositoryMock.get.mockResolvedValue(null);
 | 
				
			||||||
      assetRepositoryMock.countByIdAndUser.mockResolvedValue(1);
 | 
					      accessMock.hasOwnerAssetAccess.mockResolvedValue(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await expect(sut.deleteAll(authStub.user1, { ids: ['asset1'] })).resolves.toEqual([
 | 
					      await expect(sut.deleteAll(authStub.user1, { ids: ['asset1'] })).resolves.toEqual([
 | 
				
			||||||
        { id: 'asset1', status: 'FAILED' },
 | 
					        { id: 'asset1', status: 'FAILED' },
 | 
				
			||||||
@ -395,7 +387,7 @@ describe('AssetService', () => {
 | 
				
			|||||||
    it('should return failed status a delete fails', async () => {
 | 
					    it('should return failed status a delete fails', async () => {
 | 
				
			||||||
      assetRepositoryMock.get.mockResolvedValue({ id: 'asset1' } as AssetEntity);
 | 
					      assetRepositoryMock.get.mockResolvedValue({ id: 'asset1' } as AssetEntity);
 | 
				
			||||||
      assetRepositoryMock.remove.mockRejectedValue('delete failed');
 | 
					      assetRepositoryMock.remove.mockRejectedValue('delete failed');
 | 
				
			||||||
      assetRepositoryMock.countByIdAndUser.mockResolvedValue(1);
 | 
					      accessMock.hasOwnerAssetAccess.mockResolvedValue(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await expect(sut.deleteAll(authStub.user1, { ids: ['asset1'] })).resolves.toEqual([
 | 
					      await expect(sut.deleteAll(authStub.user1, { ids: ['asset1'] })).resolves.toEqual([
 | 
				
			||||||
        { id: 'asset1', status: 'FAILED' },
 | 
					        { id: 'asset1', status: 'FAILED' },
 | 
				
			||||||
@ -405,7 +397,7 @@ describe('AssetService', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should delete a live photo', async () => {
 | 
					    it('should delete a live photo', async () => {
 | 
				
			||||||
      assetRepositoryMock.countByIdAndUser.mockResolvedValue(1);
 | 
					      accessMock.hasOwnerAssetAccess.mockResolvedValue(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await expect(sut.deleteAll(authStub.user1, { ids: [assetEntityStub.livePhotoStillAsset.id] })).resolves.toEqual([
 | 
					      await expect(sut.deleteAll(authStub.user1, { ids: [assetEntityStub.livePhotoStillAsset.id] })).resolves.toEqual([
 | 
				
			||||||
        { id: assetEntityStub.livePhotoStillAsset.id, status: 'SUCCESS' },
 | 
					        { id: assetEntityStub.livePhotoStillAsset.id, status: 'SUCCESS' },
 | 
				
			||||||
@ -454,7 +446,7 @@ describe('AssetService', () => {
 | 
				
			|||||||
        .calledWith(asset2.id)
 | 
					        .calledWith(asset2.id)
 | 
				
			||||||
        .mockResolvedValue(asset2 as AssetEntity);
 | 
					        .mockResolvedValue(asset2 as AssetEntity);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      assetRepositoryMock.countByIdAndUser.mockResolvedValue(1);
 | 
					      accessMock.hasOwnerAssetAccess.mockResolvedValue(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await expect(sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'] })).resolves.toEqual([
 | 
					      await expect(sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'] })).resolves.toEqual([
 | 
				
			||||||
        { id: 'asset1', status: 'SUCCESS' },
 | 
					        { id: 'asset1', status: 'SUCCESS' },
 | 
				
			||||||
@ -499,7 +491,7 @@ describe('AssetService', () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  describe('downloadFile', () => {
 | 
					  describe('downloadFile', () => {
 | 
				
			||||||
    it('should download a single file', async () => {
 | 
					    it('should download a single file', async () => {
 | 
				
			||||||
      assetRepositoryMock.countByIdAndUser.mockResolvedValue(1);
 | 
					      accessMock.hasOwnerAssetAccess.mockResolvedValue(true);
 | 
				
			||||||
      assetRepositoryMock.get.mockResolvedValue(_getAsset_1());
 | 
					      assetRepositoryMock.get.mockResolvedValue(_getAsset_1());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.downloadFile(authStub.admin, 'id_1');
 | 
					      await sut.downloadFile(authStub.admin, 'id_1');
 | 
				
			||||||
@ -535,4 +527,60 @@ describe('AssetService', () => {
 | 
				
			|||||||
      expect(assetRepositoryMock.getAssetsByChecksums).toHaveBeenCalledWith(authStub.admin.id, [file1, file2]);
 | 
					      expect(assetRepositoryMock.getAssetsByChecksums).toHaveBeenCalledWith(authStub.admin.id, [file1, file2]);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('getAssetById', () => {
 | 
				
			||||||
 | 
					    it('should allow owner access', async () => {
 | 
				
			||||||
 | 
					      accessMock.hasOwnerAssetAccess.mockResolvedValue(true);
 | 
				
			||||||
 | 
					      assetRepositoryMock.getById.mockResolvedValue(assetEntityStub.image);
 | 
				
			||||||
 | 
					      await sut.getAssetById(authStub.admin, assetEntityStub.image.id);
 | 
				
			||||||
 | 
					      expect(accessMock.hasOwnerAssetAccess).toHaveBeenCalledWith(authStub.admin.id, assetEntityStub.image.id);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should allow shared link access', async () => {
 | 
				
			||||||
 | 
					      accessMock.hasSharedLinkAssetAccess.mockResolvedValue(true);
 | 
				
			||||||
 | 
					      assetRepositoryMock.getById.mockResolvedValue(assetEntityStub.image);
 | 
				
			||||||
 | 
					      await sut.getAssetById(authStub.adminSharedLink, assetEntityStub.image.id);
 | 
				
			||||||
 | 
					      expect(accessMock.hasSharedLinkAssetAccess).toHaveBeenCalledWith(
 | 
				
			||||||
 | 
					        authStub.adminSharedLink.sharedLinkId,
 | 
				
			||||||
 | 
					        assetEntityStub.image.id,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should allow partner sharing access', async () => {
 | 
				
			||||||
 | 
					      accessMock.hasOwnerAssetAccess.mockResolvedValue(false);
 | 
				
			||||||
 | 
					      accessMock.hasPartnerAssetAccess.mockResolvedValue(true);
 | 
				
			||||||
 | 
					      assetRepositoryMock.getById.mockResolvedValue(assetEntityStub.image);
 | 
				
			||||||
 | 
					      await sut.getAssetById(authStub.admin, assetEntityStub.image.id);
 | 
				
			||||||
 | 
					      expect(accessMock.hasPartnerAssetAccess).toHaveBeenCalledWith(authStub.admin.id, assetEntityStub.image.id);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should allow shared album access', async () => {
 | 
				
			||||||
 | 
					      accessMock.hasOwnerAssetAccess.mockResolvedValue(false);
 | 
				
			||||||
 | 
					      accessMock.hasPartnerAssetAccess.mockResolvedValue(false);
 | 
				
			||||||
 | 
					      accessMock.hasAlbumAssetAccess.mockResolvedValue(true);
 | 
				
			||||||
 | 
					      assetRepositoryMock.getById.mockResolvedValue(assetEntityStub.image);
 | 
				
			||||||
 | 
					      await sut.getAssetById(authStub.admin, assetEntityStub.image.id);
 | 
				
			||||||
 | 
					      expect(accessMock.hasAlbumAssetAccess).toHaveBeenCalledWith(authStub.admin.id, assetEntityStub.image.id);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should throw an error for no access', async () => {
 | 
				
			||||||
 | 
					      accessMock.hasOwnerAssetAccess.mockResolvedValue(false);
 | 
				
			||||||
 | 
					      accessMock.hasPartnerAssetAccess.mockResolvedValue(false);
 | 
				
			||||||
 | 
					      accessMock.hasSharedLinkAssetAccess.mockResolvedValue(false);
 | 
				
			||||||
 | 
					      accessMock.hasAlbumAssetAccess.mockResolvedValue(false);
 | 
				
			||||||
 | 
					      await expect(sut.getAssetById(authStub.admin, assetEntityStub.image.id)).rejects.toBeInstanceOf(
 | 
				
			||||||
 | 
					        ForbiddenException,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      expect(assetRepositoryMock.getById).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should throw an error for an invalid shared link', async () => {
 | 
				
			||||||
 | 
					      accessMock.hasSharedLinkAssetAccess.mockResolvedValue(false);
 | 
				
			||||||
 | 
					      await expect(sut.getAssetById(authStub.adminSharedLink, assetEntityStub.image.id)).rejects.toBeInstanceOf(
 | 
				
			||||||
 | 
					        ForbiddenException,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      expect(accessMock.hasOwnerAssetAccess).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(assetRepositoryMock.getById).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -53,7 +53,6 @@ import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-res
 | 
				
			|||||||
import { ICryptoRepository, IJobRepository } from '@app/domain';
 | 
					import { ICryptoRepository, IJobRepository } from '@app/domain';
 | 
				
			||||||
import { DownloadService } from '../../modules/download/download.service';
 | 
					import { DownloadService } from '../../modules/download/download.service';
 | 
				
			||||||
import { DownloadDto } from './dto/download-library.dto';
 | 
					import { DownloadDto } from './dto/download-library.dto';
 | 
				
			||||||
import { IAlbumRepository } from '../album/album-repository';
 | 
					 | 
				
			||||||
import { SharedLinkCore } from '@app/domain';
 | 
					import { SharedLinkCore } from '@app/domain';
 | 
				
			||||||
import { ISharedLinkRepository } from '@app/domain';
 | 
					import { ISharedLinkRepository } from '@app/domain';
 | 
				
			||||||
import { DownloadFilesDto } from './dto/download-files.dto';
 | 
					import { DownloadFilesDto } from './dto/download-files.dto';
 | 
				
			||||||
@ -85,7 +84,6 @@ export class AssetService {
 | 
				
			|||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    @Inject(IAccessRepository) private accessRepository: IAccessRepository,
 | 
					    @Inject(IAccessRepository) private accessRepository: IAccessRepository,
 | 
				
			||||||
    @Inject(IAssetRepository) private _assetRepository: IAssetRepository,
 | 
					    @Inject(IAssetRepository) private _assetRepository: IAssetRepository,
 | 
				
			||||||
    @Inject(IAlbumRepository) private _albumRepository: IAlbumRepository,
 | 
					 | 
				
			||||||
    @InjectRepository(AssetEntity)
 | 
					    @InjectRepository(AssetEntity)
 | 
				
			||||||
    private assetRepository: Repository<AssetEntity>,
 | 
					    private assetRepository: Repository<AssetEntity>,
 | 
				
			||||||
    private downloadService: DownloadService,
 | 
					    private downloadService: DownloadService,
 | 
				
			||||||
@ -567,32 +565,33 @@ export class AssetService {
 | 
				
			|||||||
    const sharedLinkId = authUser.sharedLinkId;
 | 
					    const sharedLinkId = authUser.sharedLinkId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const assetId of assetIds) {
 | 
					    for (const assetId of assetIds) {
 | 
				
			||||||
      // Step 1: Check if asset is part of a public shared
 | 
					 | 
				
			||||||
      if (sharedLinkId) {
 | 
					      if (sharedLinkId) {
 | 
				
			||||||
        const canAccess = await this.accessRepository.hasSharedLinkAssetAccess(sharedLinkId, assetId);
 | 
					        const canAccess = await this.accessRepository.hasSharedLinkAssetAccess(sharedLinkId, assetId);
 | 
				
			||||||
        if (canAccess) {
 | 
					        if (canAccess) {
 | 
				
			||||||
          continue;
 | 
					          continue;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } else {
 | 
					
 | 
				
			||||||
        // Step 2: Check if user owns asset
 | 
					        throw new ForbiddenException();
 | 
				
			||||||
        if ((await this._assetRepository.countByIdAndUser(assetId, authUser.id)) == 1) {
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const isOwner = await this.accessRepository.hasOwnerAssetAccess(authUser.id, assetId);
 | 
				
			||||||
 | 
					      if (isOwner) {
 | 
				
			||||||
        continue;
 | 
					        continue;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Step 3: Check if any partner owns the asset
 | 
					      if (mustBeOwner) {
 | 
				
			||||||
        const canAccess = await this.accessRepository.hasPartnerAssetAccess(authUser.id, assetId);
 | 
					        throw new ForbiddenException();
 | 
				
			||||||
        if (canAccess) {
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const isPartnerShared = await this.accessRepository.hasPartnerAssetAccess(authUser.id, assetId);
 | 
				
			||||||
 | 
					      if (isPartnerShared) {
 | 
				
			||||||
        continue;
 | 
					        continue;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Avoid additional checks if ownership is required
 | 
					      const isAlbumShared = await this.accessRepository.hasAlbumAssetAccess(authUser.id, assetId);
 | 
				
			||||||
        if (!mustBeOwner) {
 | 
					      if (isAlbumShared) {
 | 
				
			||||||
          // Step 2: Check if asset is part of an album shared with me
 | 
					 | 
				
			||||||
          if ((await this._albumRepository.getSharedWithUserAlbumCount(authUser.id, assetId)) > 0) {
 | 
					 | 
				
			||||||
        continue;
 | 
					        continue;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      throw new ForbiddenException();
 | 
					      throw new ForbiddenException();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,12 @@
 | 
				
			|||||||
import { IAccessRepository } from '@app/domain';
 | 
					import { IAccessRepository } from '@app/domain';
 | 
				
			||||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
import { Repository } from 'typeorm';
 | 
					import { Repository } from 'typeorm';
 | 
				
			||||||
import { PartnerEntity, SharedLinkEntity } from '../entities';
 | 
					import { AlbumEntity, AssetEntity, PartnerEntity, SharedLinkEntity } from '../entities';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class AccessRepository implements IAccessRepository {
 | 
					export class AccessRepository implements IAccessRepository {
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
 | 
					    @InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
 | 
				
			||||||
 | 
					    @InjectRepository(AlbumEntity) private albumRepository: Repository<AlbumEntity>,
 | 
				
			||||||
    @InjectRepository(PartnerEntity) private partnerRepository: Repository<PartnerEntity>,
 | 
					    @InjectRepository(PartnerEntity) private partnerRepository: Repository<PartnerEntity>,
 | 
				
			||||||
    @InjectRepository(SharedLinkEntity) private sharedLinkRepository: Repository<SharedLinkEntity>,
 | 
					    @InjectRepository(SharedLinkEntity) private sharedLinkRepository: Repository<SharedLinkEntity>,
 | 
				
			||||||
  ) {}
 | 
					  ) {}
 | 
				
			||||||
@ -18,6 +20,36 @@ export class AccessRepository implements IAccessRepository {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  hasAlbumAssetAccess(userId: string, assetId: string): Promise<boolean> {
 | 
				
			||||||
 | 
					    return this.albumRepository.exist({
 | 
				
			||||||
 | 
					      where: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          ownerId: userId,
 | 
				
			||||||
 | 
					          assets: {
 | 
				
			||||||
 | 
					            id: assetId,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          sharedUsers: {
 | 
				
			||||||
 | 
					            id: userId,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          assets: {
 | 
				
			||||||
 | 
					            id: assetId,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  hasOwnerAssetAccess(userId: string, assetId: string): Promise<boolean> {
 | 
				
			||||||
 | 
					    return this.assetRepository.exist({
 | 
				
			||||||
 | 
					      where: {
 | 
				
			||||||
 | 
					        id: assetId,
 | 
				
			||||||
 | 
					        ownerId: userId,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  hasPartnerAssetAccess(userId: string, assetId: string): Promise<boolean> {
 | 
					  hasPartnerAssetAccess(userId: string, assetId: string): Promise<boolean> {
 | 
				
			||||||
    return this.partnerRepository.exist({
 | 
					    return this.partnerRepository.exist({
 | 
				
			||||||
      where: {
 | 
					      where: {
 | 
				
			||||||
 | 
				
			|||||||
@ -124,8 +124,8 @@ export class AlbumRepository implements IAlbumRepository {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async hasAsset(id: string, assetId: string): Promise<boolean> {
 | 
					  hasAsset(id: string, assetId: string): Promise<boolean> {
 | 
				
			||||||
    const count = await this.repository.count({
 | 
					    return this.repository.exist({
 | 
				
			||||||
      where: {
 | 
					      where: {
 | 
				
			||||||
        id,
 | 
					        id,
 | 
				
			||||||
        assets: {
 | 
					        assets: {
 | 
				
			||||||
@ -136,8 +136,6 @@ export class AlbumRepository implements IAlbumRepository {
 | 
				
			|||||||
        assets: true,
 | 
					        assets: true,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					 | 
				
			||||||
    return Boolean(count);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async create(album: Partial<AlbumEntity>): Promise<AlbumEntity> {
 | 
					  async create(album: Partial<AlbumEntity>): Promise<AlbumEntity> {
 | 
				
			||||||
 | 
				
			|||||||
@ -3,6 +3,8 @@ import { IAccessRepository } from '@app/domain';
 | 
				
			|||||||
export const newAccessRepositoryMock = (): jest.Mocked<IAccessRepository> => {
 | 
					export const newAccessRepositoryMock = (): jest.Mocked<IAccessRepository> => {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    hasPartnerAccess: jest.fn(),
 | 
					    hasPartnerAccess: jest.fn(),
 | 
				
			||||||
 | 
					    hasAlbumAssetAccess: jest.fn(),
 | 
				
			||||||
 | 
					    hasOwnerAssetAccess: jest.fn(),
 | 
				
			||||||
    hasPartnerAssetAccess: jest.fn(),
 | 
					    hasPartnerAssetAccess: jest.fn(),
 | 
				
			||||||
    hasSharedLinkAssetAccess: jest.fn(),
 | 
					    hasSharedLinkAssetAccess: jest.fn(),
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user