Limit asset access to owner

This commit is contained in:
Matthias Rupp
2022-11-20 18:14:40 +01:00
parent e01e4e6530
commit 0e02bbed85
3 changed files with 46 additions and 13 deletions
@@ -43,6 +43,7 @@ export interface IAssetRepository {
userId: string, userId: string,
checkDuplicateAssetDto: CheckExistingAssetsDto, checkDuplicateAssetDto: CheckExistingAssetsDto,
): Promise<CheckExistingAssetsResponseDto>; ): Promise<CheckExistingAssetsResponseDto>;
countByIdAndUser(assetId: string, userId: string): Promise<number>;
} }
export const ASSET_REPOSITORY = 'ASSET_REPOSITORY'; export const ASSET_REPOSITORY = 'ASSET_REPOSITORY';
@@ -343,4 +344,13 @@ export class AssetRepository implements IAssetRepository {
}); });
return new CheckExistingAssetsResponseDto(existingAssets.map((a) => a.deviceAssetId)); 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, Header,
Put, Put,
UploadedFiles, UploadedFiles,
HttpException,
HttpStatus
} from '@nestjs/common'; } from '@nestjs/common';
import { Authenticated } from '../../decorators/authenticated.decorator'; import { Authenticated } from '../../decorators/authenticated.decorator';
import { AssetService } from './asset.service'; import { AssetService } from './asset.service';
@@ -86,10 +88,12 @@ export class AssetController {
@Get('/download/:assetId') @Get('/download/:assetId')
async downloadFile( async downloadFile(
@GetAuthUser() authUser: AuthUserDto,
@Response({ passthrough: true }) res: Res, @Response({ passthrough: true }) res: Res,
@Query(new ValidationPipe({ transform: true })) query: ServeFileDto, @Query(new ValidationPipe({ transform: true })) query: ServeFileDto,
@Param('assetId') assetId: string, @Param('assetId') assetId: string,
): Promise<any> { ): Promise<any> {
await this.assetService.checkAssetsAccess(authUser, [assetId]);
return this.assetService.downloadFile(query, assetId, res); return this.assetService.downloadFile(query, assetId, res);
} }
@@ -110,21 +114,25 @@ export class AssetController {
@Get('/file/:assetId') @Get('/file/:assetId')
@Header('Cache-Control', 'max-age=300') @Header('Cache-Control', 'max-age=300')
async serveFile( async serveFile(
@GetAuthUser() authUser: AuthUserDto,
@Headers() headers: Record<string, string>, @Headers() headers: Record<string, string>,
@Response({ passthrough: true }) res: Res, @Response({ passthrough: true }) res: Res,
@Query(new ValidationPipe({ transform: true })) query: ServeFileDto, @Query(new ValidationPipe({ transform: true })) query: ServeFileDto,
@Param('assetId') assetId: string, @Param('assetId') assetId: string,
): Promise<any> { ): Promise<any> {
await this.assetService.checkAssetsAccess(authUser, [assetId]);
return this.assetService.serveFile(assetId, query, res, headers); return this.assetService.serveFile(assetId, query, res, headers);
} }
@Get('/thumbnail/:assetId') @Get('/thumbnail/:assetId')
@Header('Cache-Control', 'max-age=300') @Header('Cache-Control', 'max-age=300')
async getAssetThumbnail( async getAssetThumbnail(
@GetAuthUser() authUser: AuthUserDto,
@Response({ passthrough: true }) res: Res, @Response({ passthrough: true }) res: Res,
@Param('assetId') assetId: string, @Param('assetId') assetId: string,
@Query(new ValidationPipe({ transform: true })) query: GetAssetThumbnailDto, @Query(new ValidationPipe({ transform: true })) query: GetAssetThumbnailDto,
): Promise<any> { ): Promise<any> {
await this.assetService.checkAssetsAccess(authUser, [assetId]);
return this.assetService.getAssetThumbnail(assetId, query, res); return this.assetService.getAssetThumbnail(assetId, query, res);
} }
@@ -195,7 +203,8 @@ export class AssetController {
@GetAuthUser() authUser: AuthUserDto, @GetAuthUser() authUser: AuthUserDto,
@Param('assetId') assetId: string, @Param('assetId') assetId: string,
): Promise<AssetResponseDto> { ): 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, @Param('assetId') assetId: string,
@Body() dto: UpdateAssetDto, @Body() dto: UpdateAssetDto,
): Promise<AssetResponseDto> { ): 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('/') @Delete('/')
@@ -215,17 +225,19 @@ export class AssetController {
@GetAuthUser() authUser: AuthUserDto, @GetAuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) assetIds: DeleteAssetDto, @Body(ValidationPipe) assetIds: DeleteAssetDto,
): Promise<DeleteAssetResponseDto[]> { ): Promise<DeleteAssetResponseDto[]> {
await this.assetService.checkAssetsAccess(authUser, assetIds.ids, true);
const deleteAssetList: AssetResponseDto[] = []; const deleteAssetList: AssetResponseDto[] = [];
for (const id of assetIds.ids) { for (const id of assetIds.ids) {
const assets = await this.assetService.getAssetById(authUser, id); const assets = await this.assetService.getAssetById(id);
if (!assets) { if (!assets) {
continue; continue;
} }
deleteAssetList.push(assets); deleteAssetList.push(assets);
if (assets.livePhotoVideoId) { if (assets.livePhotoVideoId) {
const livePhotoVideo = await this.assetService.getAssetById(authUser, assets.livePhotoVideoId); const livePhotoVideo = await this.assetService.getAssetById(assets.livePhotoVideoId);
if (livePhotoVideo) { if (livePhotoVideo) {
deleteAssetList.push(livePhotoVideo); deleteAssetList.push(livePhotoVideo);
assetIds.ids = [...assetIds.ids, livePhotoVideo.id]; 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) => { result.forEach((res) => {
deleteAssetList.filter((a) => a.id == res.id && res.status == DeleteAssetStatusEnum.SUCCESS); deleteAssetList.filter((a) => a.id == res.id && res.status == DeleteAssetStatusEnum.SUCCESS);
@@ -221,22 +221,18 @@ export class AssetService {
return assets.map((asset) => mapAsset(asset)); 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); const asset = await this._assetRepository.getById(assetId);
return mapAsset(asset); 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); const asset = await this._assetRepository.getById(assetId);
if (!asset) { if (!asset) {
throw new BadRequestException('Asset not found'); 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); const updatedAsset = await this._assetRepository.update(asset, dto);
return mapAsset(updatedAsset); 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 result: DeleteAssetResponseDto[] = [];
const target = assetIds.ids; const target = assetIds.ids;
for (const assetId of target) { for (const assetId of target) {
const res = await this.assetRepository.delete({ const res = await this.assetRepository.delete({
id: assetId, id: assetId,
userId: authUser.id,
}); });
if (res.affected) { if (res.affected) {
@@ -631,4 +626,20 @@ export class AssetService {
getAssetCountByUserId(authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> { getAssetCountByUserId(authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
return this._assetRepository.getAssetCountByUserId(authUser.id); 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();
}
}
} }