mirror of
https://github.com/immich-app/immich.git
synced 2026-02-09 03:08:53 +03:00
Limit asset access to owner
This commit is contained in:
@@ -43,6 +43,7 @@ export interface IAssetRepository {
|
||||
userId: string,
|
||||
checkDuplicateAssetDto: CheckExistingAssetsDto,
|
||||
): Promise<CheckExistingAssetsResponseDto>;
|
||||
countByIdAndUser(assetId: string, userId: string): Promise<number>;
|
||||
}
|
||||
|
||||
export const ASSET_REPOSITORY = 'ASSET_REPOSITORY';
|
||||
@@ -343,4 +344,13 @@ export class AssetRepository implements IAssetRepository {
|
||||
});
|
||||
return new CheckExistingAssetsResponseDto(existingAssets.map((a) => a.deviceAssetId));
|
||||
}
|
||||
|
||||
async countByIdAndUser(assetId: string, userId: string): Promise<number> {
|
||||
return await this.assetRepository.count({
|
||||
where: {
|
||||
id: assetId,
|
||||
userId
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
Header,
|
||||
Put,
|
||||
UploadedFiles,
|
||||
HttpException,
|
||||
HttpStatus
|
||||
} from '@nestjs/common';
|
||||
import { Authenticated } from '../../decorators/authenticated.decorator';
|
||||
import { AssetService } from './asset.service';
|
||||
@@ -86,10 +88,12 @@ export class AssetController {
|
||||
|
||||
@Get('/download/:assetId')
|
||||
async downloadFile(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
@Response({ passthrough: true }) res: Res,
|
||||
@Query(new ValidationPipe({ transform: true })) query: ServeFileDto,
|
||||
@Param('assetId') assetId: string,
|
||||
): Promise<any> {
|
||||
await this.assetService.checkAssetsAccess(authUser, [assetId]);
|
||||
return this.assetService.downloadFile(query, assetId, res);
|
||||
}
|
||||
|
||||
@@ -110,21 +114,25 @@ export class AssetController {
|
||||
@Get('/file/:assetId')
|
||||
@Header('Cache-Control', 'max-age=300')
|
||||
async serveFile(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
@Headers() headers: Record<string, string>,
|
||||
@Response({ passthrough: true }) res: Res,
|
||||
@Query(new ValidationPipe({ transform: true })) query: ServeFileDto,
|
||||
@Param('assetId') assetId: string,
|
||||
): Promise<any> {
|
||||
await this.assetService.checkAssetsAccess(authUser, [assetId]);
|
||||
return this.assetService.serveFile(assetId, query, res, headers);
|
||||
}
|
||||
|
||||
@Get('/thumbnail/:assetId')
|
||||
@Header('Cache-Control', 'max-age=300')
|
||||
async getAssetThumbnail(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
@Response({ passthrough: true }) res: Res,
|
||||
@Param('assetId') assetId: string,
|
||||
@Query(new ValidationPipe({ transform: true })) query: GetAssetThumbnailDto,
|
||||
): Promise<any> {
|
||||
await this.assetService.checkAssetsAccess(authUser, [assetId]);
|
||||
return this.assetService.getAssetThumbnail(assetId, query, res);
|
||||
}
|
||||
|
||||
@@ -195,7 +203,8 @@ export class AssetController {
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
@Param('assetId') assetId: string,
|
||||
): Promise<AssetResponseDto> {
|
||||
return await this.assetService.getAssetById(authUser, assetId);
|
||||
await this.assetService.checkAssetsAccess(authUser, [assetId]);
|
||||
return await this.assetService.getAssetById(assetId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -207,7 +216,8 @@ export class AssetController {
|
||||
@Param('assetId') assetId: string,
|
||||
@Body() dto: UpdateAssetDto,
|
||||
): Promise<AssetResponseDto> {
|
||||
return await this.assetService.updateAssetById(authUser, assetId, dto);
|
||||
await this.assetService.checkAssetsAccess(authUser, [assetId], true);
|
||||
return await this.assetService.updateAssetById(assetId, dto);
|
||||
}
|
||||
|
||||
@Delete('/')
|
||||
@@ -215,17 +225,19 @@ export class AssetController {
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
@Body(ValidationPipe) assetIds: DeleteAssetDto,
|
||||
): Promise<DeleteAssetResponseDto[]> {
|
||||
await this.assetService.checkAssetsAccess(authUser, assetIds.ids, true);
|
||||
|
||||
const deleteAssetList: AssetResponseDto[] = [];
|
||||
|
||||
for (const id of assetIds.ids) {
|
||||
const assets = await this.assetService.getAssetById(authUser, id);
|
||||
const assets = await this.assetService.getAssetById(id);
|
||||
if (!assets) {
|
||||
continue;
|
||||
}
|
||||
deleteAssetList.push(assets);
|
||||
|
||||
if (assets.livePhotoVideoId) {
|
||||
const livePhotoVideo = await this.assetService.getAssetById(authUser, assets.livePhotoVideoId);
|
||||
const livePhotoVideo = await this.assetService.getAssetById(assets.livePhotoVideoId);
|
||||
if (livePhotoVideo) {
|
||||
deleteAssetList.push(livePhotoVideo);
|
||||
assetIds.ids = [...assetIds.ids, livePhotoVideo.id];
|
||||
@@ -233,7 +245,7 @@ export class AssetController {
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.assetService.deleteAssetById(authUser, assetIds);
|
||||
const result = await this.assetService.deleteAssetById(assetIds);
|
||||
|
||||
result.forEach((res) => {
|
||||
deleteAssetList.filter((a) => a.id == res.id && res.status == DeleteAssetStatusEnum.SUCCESS);
|
||||
|
||||
@@ -221,22 +221,18 @@ export class AssetService {
|
||||
return assets.map((asset) => mapAsset(asset));
|
||||
}
|
||||
|
||||
public async getAssetById(authUser: AuthUserDto, assetId: string): Promise<AssetResponseDto> {
|
||||
public async getAssetById(assetId: string): Promise<AssetResponseDto> {
|
||||
const asset = await this._assetRepository.getById(assetId);
|
||||
|
||||
return mapAsset(asset);
|
||||
}
|
||||
|
||||
public async updateAssetById(authUser: AuthUserDto, assetId: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
|
||||
public async updateAssetById(assetId: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
|
||||
const asset = await this._assetRepository.getById(assetId);
|
||||
if (!asset) {
|
||||
throw new BadRequestException('Asset not found');
|
||||
}
|
||||
|
||||
if (authUser.id !== asset.userId) {
|
||||
throw new ForbiddenException('Not the owner');
|
||||
}
|
||||
|
||||
const updatedAsset = await this._assetRepository.update(asset, dto);
|
||||
|
||||
return mapAsset(updatedAsset);
|
||||
@@ -485,14 +481,13 @@ export class AssetService {
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteAssetById(authUser: AuthUserDto, assetIds: DeleteAssetDto): Promise<DeleteAssetResponseDto[]> {
|
||||
public async deleteAssetById(assetIds: DeleteAssetDto): Promise<DeleteAssetResponseDto[]> {
|
||||
const result: DeleteAssetResponseDto[] = [];
|
||||
|
||||
const target = assetIds.ids;
|
||||
for (const assetId of target) {
|
||||
const res = await this.assetRepository.delete({
|
||||
id: assetId,
|
||||
userId: authUser.id,
|
||||
});
|
||||
|
||||
if (res.affected) {
|
||||
@@ -631,4 +626,20 @@ export class AssetService {
|
||||
getAssetCountByUserId(authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
|
||||
return this._assetRepository.getAssetCountByUserId(authUser.id);
|
||||
}
|
||||
|
||||
async checkAssetsAccess(authUser: AuthUserDto, assetIds: string[], mustBeOwner: boolean = false) {
|
||||
for (let assetId of assetIds) {
|
||||
// Step 1: Check if user owns asset
|
||||
if (await this._assetRepository.countByIdAndUser(assetId, authUser.id) == 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Avoid additional checks if ownership is required
|
||||
if (!mustBeOwner) {
|
||||
|
||||
}
|
||||
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user