diff --git a/packages/core-http-kit/src/client/module.ts b/packages/core-http-kit/src/client/module.ts index cba251b08..a2cb42cae 100644 --- a/packages/core-http-kit/src/client/module.ts +++ b/packages/core-http-kit/src/client/module.ts @@ -11,6 +11,7 @@ import { AnalysisAPI, AnalysisBucketFileAPI, AnalysisLogAPI, + AnalysisPermissionAPI, MasterImageAPI, MasterImageGroupAPI, NodeAPI, @@ -48,6 +49,8 @@ export class Client extends BaseClient { public readonly analysisNode : TrainStationAPI; + public readonly analysisPermission : AnalysisPermissionAPI; + public readonly service : ServiceAPI; constructor(config: RequestBaseOptions) { @@ -65,6 +68,7 @@ export class Client extends BaseClient { this.analysisBucketFile = new AnalysisBucketFileAPI({ client: this }); this.analysisLog = new AnalysisLogAPI({ client: this }); this.analysisNode = new TrainStationAPI({ client: this }); + this.analysisPermission = new AnalysisPermissionAPI({ client: this }); this.service = new ServiceAPI({ client: this }); this.on(HookName.RESPONSE_ERROR, ((error) => { diff --git a/packages/core-http-kit/src/domains/analysis-permission/index.ts b/packages/core-http-kit/src/domains/analysis-permission/index.ts new file mode 100644 index 000000000..6e2714384 --- /dev/null +++ b/packages/core-http-kit/src/domains/analysis-permission/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2021-2024. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +export * from './module'; diff --git a/packages/core-http-kit/src/domains/analysis-permission/module.ts b/packages/core-http-kit/src/domains/analysis-permission/module.ts new file mode 100644 index 000000000..a87228ef6 --- /dev/null +++ b/packages/core-http-kit/src/domains/analysis-permission/module.ts @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2021-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 { BuildInput } from 'rapiq'; +import { buildQuery } from 'rapiq'; +import type { AnalysisPermission } from '@privateaim/core-kit'; +import { BaseAPI } from '../base'; +import type { CollectionResourceResponse, SingleResourceResponse } from '../types-base'; + +export class AnalysisPermissionAPI extends BaseAPI { + async getMany(options?: BuildInput): Promise> { + const { data: response } = await this.client.get(`analysis-permissions${buildQuery(options)}`); + return response; + } + + async getOne(id: AnalysisPermission['id']): Promise> { + const { data: response } = await this.client.get(`analysis-permissions/${id}`); + + return response; + } + + async delete(id: AnalysisPermission['id']): Promise> { + const { data: response } = await this.client.delete(`analysis-permissions/${id}`); + + return response; + } + + async update(id: AnalysisPermission['id'], data: Partial): Promise> { + const { data: response } = await this.client.post(`analysis-permissions/${id}`, data); + + return response; + } + + async create(data: Partial): Promise> { + const { data: response } = await this.client.post('analysis-permissions', data); + + return response; + } +} diff --git a/packages/core-http-kit/src/domains/index.ts b/packages/core-http-kit/src/domains/index.ts index 474caa0d1..88db5158a 100644 --- a/packages/core-http-kit/src/domains/index.ts +++ b/packages/core-http-kit/src/domains/index.ts @@ -16,5 +16,6 @@ export * from './analysis'; export * from './analysis-bucket-file'; export * from './analysis-log'; export * from './analysis-node'; +export * from './analysis-permission'; export * from './service'; export * from './types-base'; diff --git a/packages/core-kit/src/domains/analysis-permission/entity.ts b/packages/core-kit/src/domains/analysis-permission/entity.ts new file mode 100644 index 000000000..cdec77224 --- /dev/null +++ b/packages/core-kit/src/domains/analysis-permission/entity.ts @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022-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 { PermissionRelation, Realm } from '@authup/core-kit'; +import type { Analysis } from '../analysis'; +import type { DomainType } from '../constants'; +import type { DomainEventBaseContext } from '../types-base'; + +export interface AnalysisPermission extends PermissionRelation { + id: string; + + // ------------------------------------------------------------------ + + analysis_id: Analysis['id']; + + analysis: Analysis; + + // ------------------------------------------------------------------ + + analysis_realm: Realm | null; + + analysis_realm_id: Realm['id'] | null; + + // ------------------------------------------------------------------ + + created_at: Date | string; + + updated_at: Date | string; +} + +export type AnalysisPermissionEventContext = DomainEventBaseContext & { + type: `${DomainType.ANALYSIS_PERMISSION}`, + data: AnalysisPermission +}; diff --git a/packages/core-kit/src/domains/analysis-permission/index.ts b/packages/core-kit/src/domains/analysis-permission/index.ts new file mode 100644 index 000000000..56ae8ad19 --- /dev/null +++ b/packages/core-kit/src/domains/analysis-permission/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2021-2024. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +export * from './entity'; diff --git a/packages/core-kit/src/domains/constants.ts b/packages/core-kit/src/domains/constants.ts index e1b20ca01..52ef34a04 100644 --- a/packages/core-kit/src/domains/constants.ts +++ b/packages/core-kit/src/domains/constants.ts @@ -13,12 +13,12 @@ export enum DomainType { REGISTRY = 'registry', REGISTRY_PROJECT = 'registryProject', NODE = 'node', - SERVICE = 'service', ANALYSIS = 'analysis', ANALYSIS_BUCKET = 'analysisBucket', ANALYSIS_BUCKET_FILE = 'analysisBucketFile', ANALYSIS_LOG = 'analysisLog', ANALYSIS_NODE = 'analysisNode', + ANALYSIS_PERMISSION = 'analysisPermission', } export enum DomainSubType { diff --git a/packages/core-kit/src/domains/index.ts b/packages/core-kit/src/domains/index.ts index 8cedd0b56..525991fd1 100644 --- a/packages/core-kit/src/domains/index.ts +++ b/packages/core-kit/src/domains/index.ts @@ -10,6 +10,7 @@ export * from './analysis-bucket'; export * from './analysis-bucket-file'; export * from './analysis-log'; export * from './analysis-node'; +export * from './analysis-permission'; export * from './permission'; export * from './realm'; export * from './master-image-group'; diff --git a/packages/server-core/src/aggregators/authup/entities/index.ts b/packages/server-core/src/aggregators/authup/entities/index.ts index 0b158f9de..4de768bc7 100644 --- a/packages/server-core/src/aggregators/authup/entities/index.ts +++ b/packages/server-core/src/aggregators/authup/entities/index.ts @@ -5,6 +5,7 @@ * view the LICENSE file that was distributed with this source code. */ +export * from './permission'; export * from './realm'; export * from './robot'; export * from './user'; diff --git a/packages/server-core/src/aggregators/authup/entities/permission.ts b/packages/server-core/src/aggregators/authup/entities/permission.ts new file mode 100644 index 000000000..ffc4cedf6 --- /dev/null +++ b/packages/server-core/src/aggregators/authup/entities/permission.ts @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023-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 { PermissionEventContext } from '@authup/core-kit'; +import { useDataSource } from 'typeorm-extension'; +import { AnalysisPermissionEntity } from '../../../domains'; + +export async function handleAuthupPermissionEvent(context: PermissionEventContext) { + if (context.event !== 'deleted') { + return; + } + + const dataSource = await useDataSource(); + const repository = dataSource.getRepository(AnalysisPermissionEntity); + const entities = await repository.findBy({ + permission_id: context.data.id, + }); + + await repository.remove(entities); +} diff --git a/packages/server-core/src/aggregators/authup/module.ts b/packages/server-core/src/aggregators/authup/module.ts index 848b28b09..9453ec2c5 100644 --- a/packages/server-core/src/aggregators/authup/module.ts +++ b/packages/server-core/src/aggregators/authup/module.ts @@ -10,6 +10,7 @@ import { isRedisClientUsable, useLogger, useRedisSubscribeClient } from '@privat import type { Aggregator } from '@privateaim/server-kit'; import { EnvironmentName, useEnv } from '../../config'; import { + handleAuthupPermissionEvent, handleAuthupRealmEvent, handleAuthupRobotEvent, handleAuthupUserEvent, } from './entities'; @@ -22,17 +23,27 @@ export function createAuthupAggregator() : Aggregator { }, }; } + return { start() { const redisSub = useRedisSubscribeClient(); - redisSub.subscribe('realm', 'user', 'robot'); + redisSub.subscribe( + 'permission', + 'realm', + 'user', + 'robot', + ); redisSub.on('message', async (channel, message) => { useLogger().info(`Received event from channel ${channel}`); const event = JSON.parse(message); switch (event.type) { + case DomainType.PERMISSION: { + await handleAuthupPermissionEvent(event); + break; + } case DomainType.REALM: { await handleAuthupRealmEvent(event); break; diff --git a/packages/server-core/src/database/utils/extend.ts b/packages/server-core/src/database/utils/extend.ts index 34fe18afa..0deaa9acc 100644 --- a/packages/server-core/src/database/utils/extend.ts +++ b/packages/server-core/src/database/utils/extend.ts @@ -13,6 +13,7 @@ import { AnalysisEntity, AnalysisLogEntity, AnalysisNodeEntity, + AnalysisPermissionEntity, MasterImageEntity, MasterImageGroupEntity, NodeEntity, @@ -52,6 +53,7 @@ export async function extendDataSourceOptions(options: DataSourceOptions) : Prom AnalysisLogEntity, AnalysisBucketFileEntity, AnalysisNodeEntity, + AnalysisPermissionEntity, ], migrations: [], migrationsTransactionMode: 'each', diff --git a/packages/server-core/src/domains/analysis-permission/entity.ts b/packages/server-core/src/domains/analysis-permission/entity.ts new file mode 100644 index 000000000..1596eed29 --- /dev/null +++ b/packages/server-core/src/domains/analysis-permission/entity.ts @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2021-2024. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; +import type { + Analysis, + AnalysisPermission, +} from '@privateaim/core-kit'; +import type { Permission, Policy, Realm } from '@authup/core-kit'; +import { AnalysisEntity } from '../analysis'; + +@Entity({ name: 'analysis_permissions' }) +export class AnalysisPermissionEntity implements AnalysisPermission { + @PrimaryGeneratedColumn('uuid') + id: string; + + // ------------------------------------------------------------------ + + @CreateDateColumn() + created_at: Date; + + @UpdateDateColumn() + updated_at: Date; + + // ------------------------------------------------------------------ + + @Column() + analysis_id: Analysis['id']; + + @ManyToOne(() => AnalysisEntity, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'analysis_id' }) + analysis: AnalysisEntity; + + // ------------------------------------------------------------------ + + analysis_realm: Realm | null; + + @Column({ type: 'uuid', nullable: true }) + analysis_realm_id: Realm['id'] | null; + + // ------------------------------------------------------------------ + + @Column({ type: 'uuid' }) + permission_id: Permission['id']; + + permission: Permission; + + // ------------------------------------------------------------------ + + @Column({ type: 'uuid', nullable: true }) + policy_id: Policy['id'] | null; + + policy: Policy | null; + + // ------------------------------------------------------------------ + + @Column({ type: 'uuid', nullable: true }) + permission_realm_id: Realm['id'] | null; + + permission_realm: Realm | null; +} diff --git a/packages/server-core/src/domains/analysis-permission/index.ts b/packages/server-core/src/domains/analysis-permission/index.ts new file mode 100644 index 000000000..9983c332a --- /dev/null +++ b/packages/server-core/src/domains/analysis-permission/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2022-2024. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +export * from './entity'; diff --git a/packages/server-core/src/domains/index.ts b/packages/server-core/src/domains/index.ts index e4a9facde..c39b87291 100644 --- a/packages/server-core/src/domains/index.ts +++ b/packages/server-core/src/domains/index.ts @@ -10,6 +10,7 @@ export * from './analysis-bucket'; export * from './analysis-bucket-file'; export * from './analysis-log'; export * from './anaylsis-node'; +export * from './analysis-permission'; export * from './master-image'; export * from './master-image-group'; export * from './node'; diff --git a/packages/server-core/src/http/controllers/core/analysis-permission/handlers/create.ts b/packages/server-core/src/http/controllers/core/analysis-permission/handlers/create.ts new file mode 100644 index 000000000..480ce1bf3 --- /dev/null +++ b/packages/server-core/src/http/controllers/core/analysis-permission/handlers/create.ts @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022-2024. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { ForbiddenError } from '@ebec/http'; +import { PermissionID } from '@privateaim/core-kit'; +import type { Request, Response } from 'routup'; +import { sendCreated } from 'routup'; +import { useDataSource } from 'typeorm-extension'; +import { useRequestEnv } from '@privateaim/server-http-kit'; +import { AnalysisPermissionEntity } from '../../../../../domains'; +import { runAnalysisPermissionValidation } from '../utils'; + +export async function createAnalysisPermissionRouteHandler(req: Request, res: Response) : Promise { + const ability = useRequestEnv(req, 'abilities'); + if (!ability.has(PermissionID.ANALYSIS_EDIT)) { + throw new ForbiddenError(); + } + + const result = await runAnalysisPermissionValidation(req, 'create'); + + const dataSource = await useDataSource(); + const repository = dataSource.getRepository(AnalysisPermissionEntity); + + let entity = repository.create(result.data); + + entity = await repository.save(entity); + + entity.analysis = result.relation.analysis; + + return sendCreated(res, entity); +} diff --git a/packages/server-core/src/http/controllers/core/analysis-permission/handlers/delete.ts b/packages/server-core/src/http/controllers/core/analysis-permission/handlers/delete.ts new file mode 100644 index 000000000..9fc5d39c0 --- /dev/null +++ b/packages/server-core/src/http/controllers/core/analysis-permission/handlers/delete.ts @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2022-2024. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { PermissionID } from '@privateaim/core-kit'; +import { ForbiddenError, NotFoundError } from '@ebec/http'; +import type { Request, Response } from 'routup'; +import { sendAccepted, useRequestParam } from 'routup'; +import { isRealmResourceWritable } from '@authup/core-kit'; +import { useDataSource } from 'typeorm-extension'; +import { useRequestEnv } from '@privateaim/server-http-kit'; +import { AnalysisPermissionEntity } from '../../../../../domains'; + +export async function deleteAnalysisPermissionRouteHandler(req: Request, res: Response) : Promise { + const id = useRequestParam(req, 'id'); + + const ability = useRequestEnv(req, 'abilities'); + if (!ability.has(PermissionID.ANALYSIS_EDIT)) { + throw new ForbiddenError(); + } + + const dataSource = await useDataSource(); + const repository = dataSource.getRepository(AnalysisPermissionEntity); + + const entity = await repository.findOneBy({ id }); + + if (!entity) { + throw new NotFoundError(); + } + + if (!isRealmResourceWritable(useRequestEnv(req, 'realm'), entity.analysis_realm_id)) { + throw new ForbiddenError(); + } + + const { id: entityId } = entity; + + await repository.remove(entity); + + entity.id = entityId; + + return sendAccepted(res, entity); +} diff --git a/packages/server-core/src/http/controllers/core/analysis-permission/handlers/index.ts b/packages/server-core/src/http/controllers/core/analysis-permission/handlers/index.ts new file mode 100644 index 000000000..68b6ddfb1 --- /dev/null +++ b/packages/server-core/src/http/controllers/core/analysis-permission/handlers/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2022-2024. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +export * from './create'; +export * from './delete'; +export * from './read'; +export * from './update'; diff --git a/packages/server-core/src/http/controllers/core/analysis-permission/handlers/read.ts b/packages/server-core/src/http/controllers/core/analysis-permission/handlers/read.ts new file mode 100644 index 000000000..d1097e2ce --- /dev/null +++ b/packages/server-core/src/http/controllers/core/analysis-permission/handlers/read.ts @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2022-2024. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { isAuthupClientUsable, useAuthupClient } from '@privateaim/server-kit'; +import type { RelationsParseOutputElement } from 'rapiq'; +import { parseQueryRelations } from 'rapiq'; +import { useRequestQuery } from '@routup/basic/query'; +import type { Request, Response } from 'routup'; +import { send, useRequestParam } from 'routup'; +import { + applyQuery, applyQueryRelationsParseOutput, useDataSource, +} from 'typeorm-extension'; +import { ForbiddenError, NotFoundError } from '@ebec/http'; +import type { Permission, Policy } from '@authup/core-kit'; +import { isRealmResourceReadable } from '@authup/core-kit'; +import { useRequestEnv } from '@privateaim/server-http-kit'; +import { AnalysisPermissionEntity, onlyRealmWritableQueryResources } from '../../../../../domains'; + +type RelationMapKey = 'analysis' | 'permission' | 'policy'; +type RelationsMap = { + [K in RelationMapKey]?: RelationsParseOutputElement +}; + +function getRelations(req: Request) : RelationsMap { + const relations = parseQueryRelations(useRequestQuery(req, 'include'), { + allowed: ['analysis', 'permission', 'policy'], + }); + + const output : Record = {}; + for (let i = 0; i < relations.length; i++) { + output[relations[i].value] = relations[i]; + } + + return output; +} + +function groupById(input: T[]) : Record { + const output : Record = {}; + + for (let i = 0; i < input.length; i++) { + output[input[i].id] = input[i]; + } + + return output; +} + +export async function getOneAnalysisPermissionRouteHandler(req: Request, res: Response) : Promise { + const id = useRequestParam(req, 'id'); + + const dataSource = await useDataSource(); + const repository = dataSource.getRepository(AnalysisPermissionEntity); + const query = repository.createQueryBuilder('analysisPermission') + .where('analysisPermission.id = :id', { id }); + + const relationsMap = getRelations(req); + if (relationsMap.analysis) { + applyQueryRelationsParseOutput(query, [ + relationsMap.analysis, + ], { + defaultAlias: 'analysisPermission', + }); + } + + const entity = await query.getOne(); + if (!entity) { + throw new NotFoundError(); + } + + if (isAuthupClientUsable()) { + const authupClient = useAuthupClient(); + + if (relationsMap.permission) { + entity.permission = await authupClient.permission.getOne(entity.permission_id); + } + + if (relationsMap.policy) { + // todo: enable when policy api client is defined + // entity.policy = await authupClient.policy.getOne(entity.policy_id); + } + } + + if (!isRealmResourceReadable(useRequestEnv(req, 'realm'), entity.analysis_realm_id)) { + throw new ForbiddenError(); + } + + return send(res, entity); +} + +export async function getManyAnalysisPermissionRouteHandler(req: Request, res: Response) : Promise { + const dataSource = await useDataSource(); + const repository = dataSource.getRepository(AnalysisPermissionEntity); + const query = repository.createQueryBuilder('analysisPermission'); + query.distinctOn(['analysisPermission.id']); + + onlyRealmWritableQueryResources(query, useRequestEnv(req, 'realm'), [ + 'analysisPermission.analysis_realm_id', + ]); + + const { pagination } = applyQuery(query, useRequestQuery(req), { + defaultAlias: 'analysisPermission', + filters: { + allowed: [ + 'permission_id', + 'permission_realm_id', + + 'analysis_id', + 'analysis_realm_id', + 'analysis.id', + 'analysis.name', + ], + }, + pagination: { + maxLimit: 50, + }, + sort: { + allowed: ['created_at', 'updated_at'], + }, + }); + + const relationsMap = getRelations(req); + if (relationsMap.analysis) { + applyQueryRelationsParseOutput(query, [ + relationsMap.analysis, + ], { + defaultAlias: 'analysisPermission', + }); + } + + const [entities, total] = await query.getManyAndCount(); + + if (isAuthupClientUsable()) { + const authupClient = useAuthupClient(); + + let permissionMap : Record = {}; + if (relationsMap.permission) { + const { data: permissions } = await authupClient.permission.getMany({ + filter: { + id: entities.map((entity) => entity.permission_id), + }, + }); + + permissionMap = groupById(permissions); + } + + const policyMap : Record = {}; + if (relationsMap.policy) { + // todo: enable when policy api client is defined + // entity.policy = await authupClient.policy.getOne(entity.policy_id); + } + + for (let i = 0; i < entities.length; i++) { + if ( + entities[i].permission_id && + typeof permissionMap[entities[i].permission_id] !== 'undefined' + ) { + entities[i].permission = permissionMap[entities[i].permission_id]; + } + + if ( + entities[i].policy_id && + typeof policyMap[entities[i].policy_id] !== 'undefined' + ) { + entities[i].policy = policyMap[entities[i].policy_id]; + } + } + } + + return send(res, { + data: entities, + meta: { + total, + ...pagination, + }, + }); +} diff --git a/packages/server-core/src/http/controllers/core/analysis-permission/handlers/update.ts b/packages/server-core/src/http/controllers/core/analysis-permission/handlers/update.ts new file mode 100644 index 000000000..41e39d568 --- /dev/null +++ b/packages/server-core/src/http/controllers/core/analysis-permission/handlers/update.ts @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022-2024. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { ForbiddenError, NotFoundError } from '@ebec/http'; +import { isRealmResourceWritable } from '@authup/core-kit'; +import { PermissionID } from '@privateaim/core-kit'; +import type { Request, Response } from 'routup'; +import { sendAccepted, useRequestParam } from 'routup'; +import { useDataSource } from 'typeorm-extension'; +import { useRequestEnv } from '@privateaim/server-http-kit'; +import { AnalysisPermissionEntity } from '../../../../../domains'; +import { runAnalysisPermissionValidation } from '../utils'; + +export async function updateAnalysisPermissionRouteHandler(req: Request, res: Response) : Promise { + const ability = useRequestEnv(req, 'abilities'); + if (!ability.has(PermissionID.ANALYSIS_EDIT)) { + throw new ForbiddenError(); + } + + const id = useRequestParam(req, 'id'); + + const dataSource = await useDataSource(); + const repository = dataSource.getRepository(AnalysisPermissionEntity); + let entity = await repository.findOneBy({ id }); + if (!entity) { + throw new NotFoundError(); + } + + if (!isRealmResourceWritable(useRequestEnv(req, 'realm'), entity.analysis_realm_id)) { + throw new ForbiddenError(); + } + + const result = await runAnalysisPermissionValidation(req, 'update'); + + entity = repository.merge(entity, result.data); + + entity = await repository.save(entity); + + return sendAccepted(res, entity); +} diff --git a/packages/server-core/src/http/controllers/core/analysis-permission/index.ts b/packages/server-core/src/http/controllers/core/analysis-permission/index.ts new file mode 100644 index 000000000..291a66a37 --- /dev/null +++ b/packages/server-core/src/http/controllers/core/analysis-permission/index.ts @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2021-2024. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { + AnalysisPermission, +} from '@privateaim/core-kit'; + +import { + DBody, DController, DDelete, DGet, DPath, DPost, DRequest, DResponse, DTags, +} from '@routup/decorators'; +import { ForceLoggedInMiddleware } from '@privateaim/server-http-kit'; +import { + createAnalysisPermissionRouteHandler, + deleteAnalysisPermissionRouteHandler, + getManyAnalysisPermissionRouteHandler, + getOneAnalysisPermissionRouteHandler, + updateAnalysisPermissionRouteHandler, +} from './handlers'; + +type PartialAnalysisPermission = Partial; + +@DTags('analysis', 'permission') +@DController('/analysis-permissions') +export class AnalysisPermissionController { + @DGet('', [ForceLoggedInMiddleware]) + async getMany( + @DRequest() req: any, + @DResponse() res: any, + ): Promise { + return await getManyAnalysisPermissionRouteHandler(req, res) as PartialAnalysisPermission[]; + } + + @DGet('/:id', [ForceLoggedInMiddleware]) + async getOne( + @DPath('id') id: string, + @DRequest() req: any, + @DResponse() res: any, + ): Promise { + return await getOneAnalysisPermissionRouteHandler(req, res) as PartialAnalysisPermission | undefined; + } + + @DPost('/:id', [ForceLoggedInMiddleware]) + async edit( + @DPath('id') id: string, + @DBody() data: AnalysisPermission, + @DRequest() req: any, + @DResponse() res: any, + ): Promise { + return await updateAnalysisPermissionRouteHandler(req, res) as PartialAnalysisPermission | undefined; + } + + @DPost('', [ForceLoggedInMiddleware]) + async add( + @DBody() data: PartialAnalysisPermission, + @DRequest() req: any, + @DResponse() res: any, + ): Promise { + return await createAnalysisPermissionRouteHandler(req, res) as PartialAnalysisPermission | undefined; + } + + @DDelete('/:id', [ForceLoggedInMiddleware]) + async drop( + @DPath('id') id: string, + @DRequest() req: any, + @DResponse() res: any, + ): Promise { + return await deleteAnalysisPermissionRouteHandler(req, res) as PartialAnalysisPermission | undefined; + } +} diff --git a/packages/server-core/src/http/controllers/core/analysis-permission/utils/index.ts b/packages/server-core/src/http/controllers/core/analysis-permission/utils/index.ts new file mode 100644 index 000000000..0a6d8bd4c --- /dev/null +++ b/packages/server-core/src/http/controllers/core/analysis-permission/utils/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2022-2024. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +export * from './validation'; diff --git a/packages/server-core/src/http/controllers/core/analysis-permission/utils/validation.ts b/packages/server-core/src/http/controllers/core/analysis-permission/utils/validation.ts new file mode 100644 index 000000000..beba03e48 --- /dev/null +++ b/packages/server-core/src/http/controllers/core/analysis-permission/utils/validation.ts @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2022-2024. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { isClientErrorWithStatusCode } from '@hapic/harbor'; +import { isAuthupClientUsable, useAuthupClient } from '@privateaim/server-kit'; +import { check } from 'express-validator'; +import { BadRequestError } from '@ebec/http'; +import { isRealmResourceWritable } from '@authup/core-kit'; +import type { Request } from 'routup'; +import type { HTTPValidationResult } from '@privateaim/server-http-kit'; +import { + buildHTTPValidationErrorMessage, + createHTTPValidationResult, + extendHTTPValidationResultWithRelation, + useRequestEnv, +} from '@privateaim/server-http-kit'; +import { AnalysisEntity } from '../../../../../domains'; +import type { AnalysisPermissionEntity } from '../../../../../domains'; + +export async function runAnalysisPermissionValidation( + req: Request, + operation: 'create' | 'update', +) : Promise> { + if (operation === 'create') { + await check('analysis_id') + .exists() + .isUUID() + .run(req); + + await check('permission_id') + .exists() + .isUUID() + .run(req); + } + + await check('policy_id') + .isUUID() + .optional({ values: 'null' }) + .run(req); + + const result = createHTTPValidationResult(req); + + // ---------------------------------------------- + + await extendHTTPValidationResultWithRelation(result, AnalysisEntity, { + id: 'analysis_id', + entity: 'analysis', + }); + + if (result.relation.analysis) { + if (!isRealmResourceWritable(useRequestEnv(req, 'realm'), result.relation.analysis.realm_id)) { + throw new BadRequestError(buildHTTPValidationErrorMessage('analysis_id')); + } + + result.data.analysis_realm_id = result.relation.analysis.realm_id; + } + + // ---------------------------------------------- + + if (isAuthupClientUsable()) { + const authup = useAuthupClient(); + + try { + const permission = await authup.permission.getOne(result.data.permission_id); + // todo: is requester permitted to assign permission ?! + result.data.permission_realm_id = permission.realm_id; + } catch (e) { + if (isClientErrorWithStatusCode(e, 404)) { + throw new BadRequestError(buildHTTPValidationErrorMessage('permission_id')); + } + + throw e; + } + // todo: wait for authup implementation + // const policy = await authup.policy.getOne(result.data.policy_id); + // result.data.policy_id = policy.id; + } + + // ---------------------------------------------- + + return result; +} diff --git a/packages/server-core/src/http/router.ts b/packages/server-core/src/http/router.ts index 0fde89f35..e1a3c0d3b 100644 --- a/packages/server-core/src/http/router.ts +++ b/packages/server-core/src/http/router.ts @@ -23,6 +23,7 @@ import { AnalysisBucketController } from './controllers/core/analysis-bucket'; import { AnalysisBucketFileController } from './controllers/core/analysis-bucket-file'; import { AnalysisLogController } from './controllers/core/analysis-log'; import { AnalysisNodeController } from './controllers/core/analysis-node'; +import { AnalysisPermissionController } from './controllers/core/analysis-permission'; import { MasterImageController } from './controllers/core/master-image'; import { MasterImageGroupController } from './controllers/core/master-image-group'; import { NodeController } from './controllers/core/node'; @@ -80,6 +81,7 @@ export function createRouter() : Router { AnalysisBucketFileController, AnalysisLogController, AnalysisNodeController, + AnalysisPermissionController, RootController, diff --git a/packages/server-core/test/unit/http/analysis-permission.ts b/packages/server-core/test/unit/http/analysis-permission.ts new file mode 100644 index 000000000..0eb5a262d --- /dev/null +++ b/packages/server-core/test/unit/http/analysis-permission.ts @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2021-2024. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { extendObject } from '@authup/kit'; +import type { AnalysisPermission } from '@privateaim/core-kit'; +import { isAuthupClientUsable, useAuthupClient } from '@privateaim/server-kit'; +import { + dropTestDatabase, expectPropertiesEqualToSrc, removeDateProperties, useSuperTest, useTestDatabase, +} from '../../utils'; +import { + createSuperTestAnalysis, + createSuperTestProject, +} from '../../utils/domains'; + +describe('src/controllers/core/analysis-permission', () => { + const superTest = useSuperTest(); + + beforeAll(async () => { + await useTestDatabase(); + }); + + afterAll(async () => { + await dropTestDatabase(); + }); + + const attributes : Partial = { + permission_id: '667672f6-1c6b-468f-947f-6370cf18454c', + }; + + it('should create resource', async () => { + const project = await createSuperTestProject(superTest); + expect(project.body.id).toBeDefined(); + + const analysis = await createSuperTestAnalysis(superTest, { + project_id: project.body.id, + }); + expect(analysis.body.id).toBeDefined(); + attributes.analysis_id = analysis.body.id; + + // todo: maybe create authup policy + + if (isAuthupClientUsable()) { + const authup = useAuthupClient(); + const permission = await authup.permission.create({ name: 'analysis_permission' }); + attributes.permission_id = permission.id; + } + + const response = await superTest + .post('/analysis-permissions') + .auth('admin', 'start123') + .send(attributes); + + expect(response.status).toEqual(201); + expect(response.body).toBeDefined(); + + delete response.body.analysis; + delete response.body.node; + + extendObject(attributes, removeDateProperties(response.body)); + }); + + it('should read collection', async () => { + const response = await superTest + .get('/analysis-permissions') + .auth('admin', 'start123'); + + expect(response.status).toEqual(200); + expect(response.body).toBeDefined(); + expect(response.body.data).toBeDefined(); + expect(response.body.data.length).toBeGreaterThanOrEqual(1); + }); + + it('should read resource', async () => { + const response = await superTest + .get(`/analysis-permissions/${attributes.id}`) + .auth('admin', 'start123'); + + expect(response.status).toEqual(200); + expect(response.body).toBeDefined(); + + expectPropertiesEqualToSrc(attributes, response.body); + }); + + it('should delete resource', async () => { + const response = await superTest + .delete(`/analysis-permissions/${attributes.id}`) + .auth('admin', 'start123'); + + expect(response.status).toEqual(202); + }); +});