From 07ec40ae24cfa68184e865c006e9745b973cf34c Mon Sep 17 00:00:00 2001 From: tada5hi Date: Tue, 27 Feb 2024 10:46:23 +0100 Subject: [PATCH] feat: restrict update/delete operator by bucket(-file) owner --- .../src/domains/bucket-file/index.ts | 1 + .../src/domains/bucket-file/utils.ts | 14 ++++++++++ .../src/domains/bucket/index.ts | 1 + .../src/domains/bucket/utils.ts | 14 ++++++++++ .../bucket-file/handlers/delete.ts | 27 +++++++++++++------ .../controllers/bucket-file/handlers/read.ts | 10 ++++--- .../controllers/bucket/handlers/delete.ts | 20 +++++++------- .../controllers/bucket/handlers/update.ts | 24 +++++++++-------- 8 files changed, 80 insertions(+), 31 deletions(-) create mode 100644 packages/server-storage/src/domains/bucket-file/utils.ts create mode 100644 packages/server-storage/src/domains/bucket/utils.ts diff --git a/packages/server-storage/src/domains/bucket-file/index.ts b/packages/server-storage/src/domains/bucket-file/index.ts index 2794a1faf..c7ec45716 100644 --- a/packages/server-storage/src/domains/bucket-file/index.ts +++ b/packages/server-storage/src/domains/bucket-file/index.ts @@ -6,3 +6,4 @@ */ export * from './entity'; +export * from './utils'; diff --git a/packages/server-storage/src/domains/bucket-file/utils.ts b/packages/server-storage/src/domains/bucket-file/utils.ts new file mode 100644 index 000000000..5750b2e8d --- /dev/null +++ b/packages/server-storage/src/domains/bucket-file/utils.ts @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2024. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import type { Actor } from '../actor'; +import type { BucketFileEntity } from './entity'; + +export function isBucketFileOwnedByActor(entity: BucketFileEntity, actor: Actor) { + return entity.actor_type === actor.type && + entity.actor_id === actor.id; +} diff --git a/packages/server-storage/src/domains/bucket/index.ts b/packages/server-storage/src/domains/bucket/index.ts index 2794a1faf..c7ec45716 100644 --- a/packages/server-storage/src/domains/bucket/index.ts +++ b/packages/server-storage/src/domains/bucket/index.ts @@ -6,3 +6,4 @@ */ export * from './entity'; +export * from './utils'; diff --git a/packages/server-storage/src/domains/bucket/utils.ts b/packages/server-storage/src/domains/bucket/utils.ts new file mode 100644 index 000000000..640045594 --- /dev/null +++ b/packages/server-storage/src/domains/bucket/utils.ts @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2024. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import type { Actor } from '../actor'; +import type { BucketEntity } from './entity'; + +export function isBucketOwnedByActor(entity: BucketEntity, actor: Actor) { + return entity.actor_type === actor.type && + entity.actor_id === actor.id; +} diff --git a/packages/server-storage/src/http/controllers/bucket-file/handlers/delete.ts b/packages/server-storage/src/http/controllers/bucket-file/handlers/delete.ts index 49ab4aabe..f350e0460 100644 --- a/packages/server-storage/src/http/controllers/bucket-file/handlers/delete.ts +++ b/packages/server-storage/src/http/controllers/bucket-file/handlers/delete.ts @@ -12,17 +12,17 @@ import type { Request, Response } from 'routup'; import { sendAccepted, useRequestParam } from 'routup'; import { useDataSource } from 'typeorm-extension'; import { useMinio } from '../../../../core'; -import { BucketFileEntity } from '../../../../domains'; +import { + BucketFileEntity, + getActorFromRequest, + isBucketFileOwnedByActor, + isBucketOwnedByActor, +} from '../../../../domains'; import { useRequestEnv } from '../../../request'; export async function executeBucketFileRouteDeleteHandler(req: Request, res: Response) : Promise { const id = useRequestParam(req, 'id'); - const ability = useRequestEnv(req, 'ability'); - if (!ability.has(PermissionID.BUCKET_DROP)) { - throw new ForbiddenError(); - } - const dataSource = await useDataSource(); const repository = dataSource.getRepository(BucketFileEntity); const entity = await repository.findOne({ @@ -36,8 +36,19 @@ export async function executeBucketFileRouteDeleteHandler(req: Request, res: Res throw new NotFoundError(); } - if (!isRealmResourceWritable(useRequestEnv(req, 'realm'), entity.realm_id)) { - throw new ForbiddenError(); + const actor = getActorFromRequest(req); + if ( + !isBucketOwnedByActor(entity.bucket, actor) && + !isBucketFileOwnedByActor(entity, actor) + ) { + const ability = useRequestEnv(req, 'ability'); + if (!ability.has(PermissionID.BUCKET_EDIT)) { + throw new ForbiddenError(); + } + + if (!isRealmResourceWritable(useRequestEnv(req, 'realm'), entity.realm_id)) { + throw new ForbiddenError(); + } } const minio = useMinio(); diff --git a/packages/server-storage/src/http/controllers/bucket-file/handlers/read.ts b/packages/server-storage/src/http/controllers/bucket-file/handlers/read.ts index af7585871..2a9b5dc4c 100644 --- a/packages/server-storage/src/http/controllers/bucket-file/handlers/read.ts +++ b/packages/server-storage/src/http/controllers/bucket-file/handlers/read.ts @@ -20,7 +20,7 @@ export async function executeBucketFileRouteGetOneHandler(req: Request, res: Res const dataSource = await useDataSource(); const repository = dataSource.getRepository(BucketFileEntity); - const query = repository.createQueryBuilder('bucket') + const query = repository.createQueryBuilder('bucketFile') .where('bucketFile.id = :id', { id }); applyQuery(query, useRequestQuery(req), { @@ -29,6 +29,7 @@ export async function executeBucketFileRouteGetOneHandler(req: Request, res: Res default: [ 'id', 'name', + 'path', 'directory', 'size', 'hash', @@ -37,6 +38,7 @@ export async function executeBucketFileRouteGetOneHandler(req: Request, res: Res 'realm_id', 'actor_type', 'actor_id', + 'bucket_id', ], }, relations: { @@ -57,14 +59,15 @@ export async function executeBucketFileRouteGetManyHandler(req: Request, res: Re const dataSource = await useDataSource(); const repository = dataSource.getRepository(BucketFileEntity); - const query = repository.createQueryBuilder('bucket'); + const query = repository.createQueryBuilder('bucketFile'); const { pagination } = applyQuery(query, useRequestQuery(req), { - defaultAlias: 'bucket', + defaultAlias: 'bucketFile', fields: { default: [ 'id', 'name', + 'path', 'directory', 'size', 'hash', @@ -73,6 +76,7 @@ export async function executeBucketFileRouteGetManyHandler(req: Request, res: Re 'realm_id', 'actor_type', 'actor_id', + 'bucket_id', ], }, relations: { diff --git a/packages/server-storage/src/http/controllers/bucket/handlers/delete.ts b/packages/server-storage/src/http/controllers/bucket/handlers/delete.ts index e0c9eeb6e..5c72383b1 100644 --- a/packages/server-storage/src/http/controllers/bucket/handlers/delete.ts +++ b/packages/server-storage/src/http/controllers/bucket/handlers/delete.ts @@ -12,27 +12,29 @@ import type { Request, Response } from 'routup'; import { sendAccepted, useRequestParam } from 'routup'; import { useDataSource } from 'typeorm-extension'; import { useMinio } from '../../../../core'; -import { BucketEntity } from '../../../../domains'; +import { BucketEntity, getActorFromRequest, isBucketOwnedByActor } from '../../../../domains'; import { useRequestEnv } from '../../../request'; export async function executeBucketRouteDeleteHandler(req: Request, res: Response) : Promise { const id = useRequestParam(req, 'id'); - const ability = useRequestEnv(req, 'ability'); - if (!ability.has(PermissionID.BUCKET_DROP)) { - throw new ForbiddenError(); - } - const dataSource = await useDataSource(); const repository = dataSource.getRepository(BucketEntity); const entity = await repository.findOneBy({ id }); - if (!entity) { throw new NotFoundError(); } - if (!isRealmResourceWritable(useRequestEnv(req, 'realm'), entity.realm_id)) { - throw new ForbiddenError(); + const actor = getActorFromRequest(req); + if (!isBucketOwnedByActor(entity, actor)) { + const ability = useRequestEnv(req, 'ability'); + if (!ability.has(PermissionID.BUCKET_DROP)) { + throw new ForbiddenError(); + } + + if (!isRealmResourceWritable(useRequestEnv(req, 'realm'), entity.realm_id)) { + throw new ForbiddenError(); + } } const { id: entityId, name: entityName } = entity; diff --git a/packages/server-storage/src/http/controllers/bucket/handlers/update.ts b/packages/server-storage/src/http/controllers/bucket/handlers/update.ts index 4a5a6e955..91057a527 100644 --- a/packages/server-storage/src/http/controllers/bucket/handlers/update.ts +++ b/packages/server-storage/src/http/controllers/bucket/handlers/update.ts @@ -12,33 +12,35 @@ import type { Request, Response } from 'routup'; import { sendAccepted, useRequestParam } from 'routup'; import { useDataSource } from 'typeorm-extension'; import { useMinio } from '../../../../core'; -import { BucketEntity } from '../../../../domains'; +import { BucketEntity, getActorFromRequest, isBucketOwnedByActor } from '../../../../domains'; import { useRequestEnv } from '../../../request'; import { runBucketValidation } from '../utils/validation'; export async function executeBucketRouteUpdateHandler(req: Request, res: Response) : Promise { - const id = useRequestParam(req, 'id'); - - const ability = useRequestEnv(req, 'ability'); - if (!ability.has(PermissionID.BUCKET_EDIT)) { - throw new ForbiddenError(); - } - const result = await runBucketValidation(req, 'update'); if (!result.data) { return sendAccepted(res); } + const id = useRequestParam(req, 'id'); + const dataSource = await useDataSource(); const repository = dataSource.getRepository(BucketEntity); let entity = await repository.findOneBy({ id }); - if (!entity) { throw new NotFoundError(); } - if (!isRealmResourceWritable(useRequestEnv(req, 'realm'), entity.realm_id)) { - throw new ForbiddenError(); + const actor = getActorFromRequest(req); + if (!isBucketOwnedByActor(entity, actor)) { + const ability = useRequestEnv(req, 'ability'); + if (!ability.has(PermissionID.BUCKET_EDIT)) { + throw new ForbiddenError(); + } + + if (!isRealmResourceWritable(useRequestEnv(req, 'realm'), entity.realm_id)) { + throw new ForbiddenError(); + } } entity = repository.merge(entity, result.data);