diff --git a/api-gateway/src/api/service/artifact.ts b/api-gateway/src/api/service/artifact.ts index d08d8bbfed..19419209b5 100644 --- a/api-gateway/src/api/service/artifact.ts +++ b/api-gateway/src/api/service/artifact.ts @@ -1,7 +1,19 @@ import { UserRole } from '@guardian/interfaces'; import { Logger } from '@guardian/common'; import { Guardians } from '@helpers/guardians'; -import { Controller, Delete, Get, HttpCode, HttpException, HttpStatus, Post, Req, Response } from '@nestjs/common'; +import { + Controller, + Delete, + Get, + HttpCode, + HttpException, + HttpStatus, + Post, + Req, + Response, + UploadedFiles, + UseInterceptors, +} from '@nestjs/common'; import { checkPermission } from '@auth/authorization-helper'; import { ApiExtraModels, @@ -12,12 +24,15 @@ import { ApiTags, ApiUnauthorizedResponse, ApiForbiddenResponse, - getSchemaPath + getSchemaPath, + ApiBody, + ApiConsumes } from '@nestjs/swagger'; import { InternalServerErrorDTO } from '@middlewares/validation/schemas/errors'; import { ApiImplicitQuery } from '@nestjs/swagger/dist/decorators/api-implicit-query.decorator'; import { ArtifactDTOItem } from '@middlewares/validation/schemas/artifacts'; import { ApiImplicitParam } from '@nestjs/swagger/dist/decorators/api-implicit-param.decorator'; +import { FilesInterceptor } from '@nestjs/platform-express'; @Controller('artifacts') @ApiTags('artifacts') @@ -127,6 +142,23 @@ export class ArtifactApi { required: true, example: '000000000000000000000001' }) + @ApiConsumes('multipart/form-data') + @ApiBody({ + description: 'Form data with artifacts.', + required: true, + schema: { + type: 'array', + items: { + type: 'object', + properties: { + 'artifacts': { + type: 'string', + format: 'binary', + } + } + } + } + }) @ApiOkResponse({ description: 'Successful operation.', schema: { @@ -149,26 +181,25 @@ export class ArtifactApi { } }) @ApiExtraModels(ArtifactDTOItem, InternalServerErrorDTO) + @UseInterceptors(FilesInterceptor('artifacts')) @HttpCode(HttpStatus.CREATED) - async uploadArtifacts(@Req() req, @Response() res): Promise { + async uploadArtifacts(@Req() req, @UploadedFiles() files): Promise { await checkPermission(UserRole.STANDARD_REGISTRY)(req.user); try { - const files = req.files; if (!files) { throw new HttpException('There are no files to upload', HttpStatus.UNPROCESSABLE_ENTITY) } const owner = req.user.did; const parentId = req.params.parentId; const uploadedArtifacts = []; - const artifacts = Array.isArray(files.artifacts) ? files.artifacts : [files.artifacts]; const guardian = new Guardians(); - for (const artifact of artifacts) { + for (const artifact of files) { if (artifact) { const result = await guardian.uploadArtifact(artifact, owner, parentId); uploadedArtifacts.push(result); } } - return res.status(201).json(uploadedArtifacts); + return uploadedArtifacts; } catch (error) { new Logger().error(error, ['API_GATEWAY']); throw error; diff --git a/api-gateway/src/api/service/policy.ts b/api-gateway/src/api/service/policy.ts index 7e32804ad5..94b1f5725a 100644 --- a/api-gateway/src/api/service/policy.ts +++ b/api-gateway/src/api/service/policy.ts @@ -8,9 +8,44 @@ import { ServiceError } from '@helpers/service-requests-base'; import { TaskManager } from '@helpers/task-manager'; import { Users } from '@helpers/users'; import { InternalServerErrorDTO } from '@middlewares/validation/schemas/errors'; -import { MigrationConfigDTO, PolicyCategoryDTO } from '@middlewares/validation/schemas/policies'; -import { Body, Controller, Delete, Get, HttpCode, HttpException, HttpStatus, Param, Post, Put, Query, Req, Response } from '@nestjs/common'; -import { ApiAcceptedResponse, ApiBody, ApiExtraModels, ApiForbiddenResponse, ApiInternalServerErrorResponse, ApiOkResponse, ApiOperation, ApiParam, ApiQuery, ApiSecurity, ApiTags, ApiUnauthorizedResponse, getSchemaPath } from '@nestjs/swagger'; +import { + MigrationConfigDTO, + PolicyCategoryDTO, +} from '@middlewares/validation/schemas/policies'; +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpException, + HttpStatus, + Param, + Post, + Put, + Query, + Req, + Response, + UploadedFiles, + UseInterceptors, +} from '@nestjs/common'; +import { AnyFilesInterceptor } from '@nestjs/platform-express'; +import { + ApiAcceptedResponse, + ApiBody, + ApiConsumes, + ApiExtraModels, + ApiForbiddenResponse, + ApiInternalServerErrorResponse, + ApiOkResponse, + ApiOperation, + ApiParam, + ApiQuery, + ApiSecurity, + ApiTags, + ApiUnauthorizedResponse, + getSchemaPath, +} from '@nestjs/swagger'; import { ApiImplicitParam } from '@nestjs/swagger/dist/decorators/api-implicit-param.decorator'; import { ApiImplicitQuery } from '@nestjs/swagger/dist/decorators/api-implicit-query.decorator'; @@ -1178,11 +1213,19 @@ export class PolicyApi { const engineService = new PolicyEngine(); const versionOfTopicId = req.query ? req.query.versionOfTopicId : null; try { - const policies = await engineService.importMessage(req.user, req.body.messageId, versionOfTopicId); + const policies = await engineService.importMessage( + req.user, + req.body.messageId, + versionOfTopicId, + req.body.metadata + ); return res.status(201).send(policies); } catch (error) { new Logger().error(error, ['API_GATEWAY']); - throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR); + throw new HttpException( + error.message, + HttpStatus.INTERNAL_SERVER_ERROR + ); } } @@ -1212,14 +1255,29 @@ export class PolicyApi { const messageId = req.body.messageId; const versionOfTopicId = req.query ? req.query.versionOfTopicId : null; const taskManager = new TaskManager(); - const task = taskManager.start(TaskAction.IMPORT_POLICY_MESSAGE, user.id); - RunFunctionAsync(async () => { - const engineService = new PolicyEngine(); - await engineService.importMessageAsync(user, messageId, versionOfTopicId, task); - }, async (error) => { - new Logger().error(error, ['API_GATEWAY']); - taskManager.addError(task.taskId, { code: 500, message: 'Unknown error: ' + error.message }); - }); + const task = taskManager.start( + TaskAction.IMPORT_POLICY_MESSAGE, + user.id + ); + RunFunctionAsync( + async () => { + const engineService = new PolicyEngine(); + await engineService.importMessageAsync( + user, + messageId, + versionOfTopicId, + task, + req.body.metadata + ); + }, + async (error) => { + new Logger().error(error, ['API_GATEWAY']); + taskManager.addError(task.taskId, { + code: 500, + message: 'Unknown error: ' + error.message, + }); + } + ); return res.status(202).send(task); } @@ -1347,6 +1405,91 @@ export class PolicyApi { } } + /** + * Policy import from a zip file with metadata. + */ + @Post('/import/file-metadata') + @Auth( + UserRole.STANDARD_REGISTRY + ) + @ApiSecurity('bearerAuth') + @ApiOperation({ + summary: 'Imports new policy from a zip file with metadata.', + description: 'Imports new policy and all associated artifacts, such as schemas and VCs, from the provided zip file into the local DB.' + ONLY_SR, + }) + @ApiImplicitQuery({ + name: 'versionOfTopicId', + type: String, + description: 'Topic Id', + required: false + }) + @ApiConsumes('multipart/form-data') + @ApiBody({ + description: 'Form data with policy file and metadata.', + required: true, + schema: { + type: 'object', + properties: { + 'policyFile': { + type: 'string', + format: 'binary', + }, + 'metadata': { + type: 'string', + format: 'binary', + } + } + } + }) + @ApiOkResponse({ + description: 'Successful operation.', + schema: { + 'type': 'object' + }, + }) + @ApiUnauthorizedResponse({ + description: 'Unauthorized.', + }) + @ApiForbiddenResponse({ + description: 'Forbidden.', + }) + @ApiInternalServerErrorResponse({ + description: 'Internal server error.', + type: InternalServerErrorDTO + }) + @HttpCode(HttpStatus.CREATED) + @UseInterceptors(AnyFilesInterceptor()) + async importPolicyFromFileWithMetadata( + @AuthUser() user: IAuthUser, + @UploadedFiles() files: any, + @Query('versionOfTopicId') versionOfTopicId, + ): Promise { + try { + const policyFile = files.find( + (item) => item.fieldname === 'policyFile' + ); + if (!policyFile) { + throw new Error('There is no policy file'); + } + const metadata = files.find( + (item) => item.fieldname === 'metadata' + ); + const engineService = new PolicyEngine(); + return await engineService.importFile( + user, + policyFile.buffer, + versionOfTopicId, + metadata?.buffer && JSON.parse(metadata.buffer.toString()) + ); + } catch (error) { + new Logger().error(error, ['API_GATEWAY']); + throw new HttpException( + error.message, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + /** * Policy import from a zip file (async). */ @@ -1405,6 +1548,98 @@ export class PolicyApi { return res.status(202).send(task); } + /** + * Policy import from a zip file with metadata (async). + */ + @Post('/push/import/file-metadata') + @Auth( + UserRole.STANDARD_REGISTRY + ) + @ApiSecurity('bearerAuth') + @ApiOperation({ + summary: 'Imports new policy from a zip file with metadata.', + description: 'Imports new policy and all associated artifacts, such as schemas and VCs, from the provided zip file into the local DB.' + ONLY_SR, + }) + @ApiImplicitQuery({ + name: 'versionOfTopicId', + type: String, + description: 'Topic Id', + required: false + }) + @ApiConsumes('multipart/form-data') + @ApiBody({ + description: 'Form data with policy file and metadata.', + required: true, + schema: { + type: 'object', + properties: { + 'policyFile': { + type: 'string', + format: 'binary', + }, + 'metadata': { + type: 'string', + format: 'binary', + } + } + } + }) + @ApiOkResponse({ + description: 'Successful operation.', + schema: { + 'type': 'object' + }, + }) + @ApiUnauthorizedResponse({ + description: 'Unauthorized.', + }) + @ApiForbiddenResponse({ + description: 'Forbidden.', + }) + @ApiInternalServerErrorResponse({ + description: 'Internal server error.', + type: InternalServerErrorDTO + }) + @HttpCode(HttpStatus.ACCEPTED) + @UseInterceptors(AnyFilesInterceptor()) + async importPolicyFromFileWithMetadataAsync( + @AuthUser() user: IAuthUser, + @UploadedFiles() files: any, + @Query('versionOfTopicId') versionOfTopicId, + ): Promise { + const taskManager = new TaskManager(); + const task = taskManager.start(TaskAction.IMPORT_POLICY_FILE, user.id); + RunFunctionAsync( + async () => { + const policyFile = files.find( + (item) => item.fieldname === 'policyFile' + ); + if (!policyFile) { + throw new Error('There is no policy file'); + } + const metadata = files.find( + (item) => item.fieldname === 'metadata' + ); + const engineService = new PolicyEngine(); + await engineService.importFileAsync( + user, + policyFile.buffer, + versionOfTopicId, + task, + metadata?.buffer && JSON.parse(metadata.buffer.toString()) + ); + }, + async (error) => { + new Logger().error(error, ['API_GATEWAY']); + taskManager.addError(task.taskId, { + code: 500, + message: 'Unknown error: ' + error.message, + }); + } + ); + return task; + } + /** * Policy preview from a zip file. */ diff --git a/api-gateway/src/api/service/tokens.ts b/api-gateway/src/api/service/tokens.ts index 8edc3625d1..9e7f79a8c4 100644 --- a/api-gateway/src/api/service/tokens.ts +++ b/api-gateway/src/api/service/tokens.ts @@ -5,7 +5,18 @@ import { PolicyEngine } from '@helpers/policy-engine'; import { TaskManager } from '@helpers/task-manager'; import { ServiceError } from '@helpers/service-requests-base'; import { prepareValidationResponse } from '@middlewares/validation'; -import { Controller, Delete, Get, HttpCode, HttpException, HttpStatus, Post, Put, Req, Response } from '@nestjs/common'; +import { + Controller, + Delete, + Get, + HttpCode, + HttpException, + HttpStatus, + Post, + Put, + Req, + Response, +} from '@nestjs/common'; import { checkPermission } from '@auth/authorization-helper'; import { ApiInternalServerErrorResponse, @@ -17,8 +28,12 @@ import { ApiTags, ApiBearerAuth, ApiParam, + ApiBody, + ApiSecurity, + ApiUnprocessableEntityResponse, } from '@nestjs/swagger'; import { InternalServerErrorDTO } from '@middlewares/validation/schemas'; +import { Auth } from '@auth/auth.decorator'; /** * Token route @@ -179,6 +194,66 @@ export class TokensApi { return res.status(202).send(task); } + @Put('/') + @Auth( + UserRole.STANDARD_REGISTRY + ) + @ApiSecurity('bearerAuth') + @ApiOperation({ + summary: 'Update token.', + description: 'Update token. Only users with the Standard Registry role are allowed to make the request.', + }) + @ApiBody({ + description: 'Token', + required: true, + schema: { + type: 'object' + } + }) + @ApiOkResponse({ + description: 'Updated token.', + schema: { + 'type': 'object' + }, + }) + @ApiForbiddenResponse({ + description: 'Forbidden.', + }) + @ApiUnprocessableEntityResponse({ + description: 'Unprocessable entity.' + }) + @ApiInternalServerErrorResponse({ + description: 'Internal server error.', + type: InternalServerErrorDTO + }) + @HttpCode(HttpStatus.CREATED) + async updateToken(@Req() req): Promise { + await checkPermission(UserRole.STANDARD_REGISTRY)(req.user); + const user = req.user; + const token = req.body; + + if (!user.did) { + throw new HttpException('User is not registered', HttpStatus.UNPROCESSABLE_ENTITY); + } + + if (!token.tokenId) { + throw new HttpException('The field tokenId is required', HttpStatus.UNPROCESSABLE_ENTITY); + } + + const guardians = new Guardians(); + const tokenObject = await guardians.getTokenById(token.tokenId); + + if (!tokenObject) { + throw new HttpException('Token not found', HttpStatus.NOT_FOUND) + } + + if (tokenObject.owner !== user.did) { + throw new HttpException('Invalid creator.', HttpStatus.FORBIDDEN) + } + + return await guardians.updateToken(token); + } + @Put('/push') @HttpCode(HttpStatus.ACCEPTED) async updateTokenAsync(@Req() req, @Response() res): Promise { diff --git a/api-gateway/src/api/service/tool.ts b/api-gateway/src/api/service/tool.ts index 6e35017aa3..1e2922626b 100644 --- a/api-gateway/src/api/service/tool.ts +++ b/api-gateway/src/api/service/tool.ts @@ -10,11 +10,15 @@ import { Post, Put, Req, - Response + Response, + UploadedFiles, + UseInterceptors } from '@nestjs/common'; import { checkPermission } from '@auth/authorization-helper'; import { TaskAction, UserRole } from '@guardian/interfaces'; import { + ApiBody, + ApiConsumes, ApiForbiddenResponse, ApiInternalServerErrorResponse, ApiOkResponse, @@ -29,6 +33,7 @@ import { TaskManager } from '@helpers/task-manager'; import { ServiceError } from '@helpers/service-requests-base'; import { InternalServerErrorDTO, TaskDTO, ToolDTO } from '@middlewares/validation/schemas'; import { ApiImplicitParam } from '@nestjs/swagger/dist/decorators/api-implicit-param.decorator'; +import { AnyFilesInterceptor } from '@nestjs/platform-express'; const ONLY_SR = ' Only users with the Standard Registry role are allowed to make the request.' @@ -710,6 +715,82 @@ export class ToolsApi { } } + /** + * Import tool from file with metadata + */ + @Post('/import/file-metadata') + @ApiSecurity('bearerAuth') + @ApiOperation({ + summary: 'Imports new tool from a zip file.', + description: 'Imports new tool and all associated artifacts, such as schemas and VCs, from the provided zip file into the local DB.' + ONLY_SR + }) + @ApiOkResponse({ + description: 'Successful operation.', + schema: { + $ref: getSchemaPath(ToolDTO) + } + }) + @ApiConsumes('multipart/form-data') + @ApiBody({ + description: 'Form data with tool file and metadata.', + required: true, + schema: { + type: 'object', + properties: { + 'file': { + type: 'string', + format: 'binary', + }, + 'metadata': { + type: 'string', + format: 'binary', + } + } + } + }) + @ApiUnauthorizedResponse({ + description: 'Unauthorized.', + }) + @ApiForbiddenResponse({ + description: 'Forbidden.', + }) + @ApiInternalServerErrorResponse({ + description: 'Internal server error.', + schema: { + $ref: getSchemaPath(InternalServerErrorDTO) + } + }) + @UseInterceptors(AnyFilesInterceptor()) + @HttpCode(HttpStatus.CREATED) + async toolImportFileWithMetadata( + @Req() req, + @UploadedFiles() files: any + ): Promise { + await checkPermission(UserRole.STANDARD_REGISTRY)(req.user); + const guardian = new Guardians(); + try { + const file = files.find((item) => item.fieldname === 'file'); + if (!file) { + throw new Error('There is no tool file'); + } + const metadata = files.find( + (item) => item.fieldname === 'metadata' + ); + const tool = await guardian.importToolFile( + file.buffer, + req.user.did, + metadata?.buffer && JSON.parse(metadata.buffer.toString()) + ); + return tool; + } catch (error) { + new Logger().error(error, ['API_GATEWAY']); + throw new HttpException( + error.message, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + /** * Import tool from IPFS (Async) */ @@ -760,6 +841,101 @@ export class ToolsApi { } } + /** + * Import tool from file with metadata (Async) + */ + @Post('/push/import/file-metadata') + @ApiSecurity('bearerAuth') + @ApiOperation({ + summary: 'Imports new tool from a zip file.', + description: + 'Imports new tool and all associated artifacts, such as schemas and VCs, from the provided zip file into the local DB.' + + ONLY_SR, + }) + @ApiConsumes('multipart/form-data') + @ApiBody({ + description: 'Form data with tool file and metadata.', + required: true, + schema: { + type: 'object', + properties: { + 'file': { + type: 'string', + format: 'binary', + }, + 'metadata': { + type: 'string', + format: 'binary', + } + } + } + }) + @ApiOkResponse({ + description: 'Successful operation.', + schema: { + $ref: getSchemaPath(TaskDTO), + }, + }) + @ApiUnauthorizedResponse({ + description: 'Unauthorized.', + }) + @ApiForbiddenResponse({ + description: 'Forbidden.', + }) + @ApiInternalServerErrorResponse({ + description: 'Internal server error.', + schema: { + $ref: getSchemaPath(InternalServerErrorDTO), + }, + }) + @UseInterceptors(AnyFilesInterceptor()) + @HttpCode(HttpStatus.ACCEPTED) + async toolImportFileWithMetadataAsync( + @Req() req, + @UploadedFiles() files: any + ): Promise { + await checkPermission(UserRole.STANDARD_REGISTRY)(req.user); + try { + const file = files.find(item => item.fieldname === 'file'); + if (!file) { + throw new Error('There is no tool file'); + } + const metadata = files.find(item => item.fieldname === 'metadata'); + const user = req.user; + const owner = req.user.did; + const guardian = new Guardians(); + const taskManager = new TaskManager(); + const task = taskManager.start( + TaskAction.IMPORT_TOOL_FILE, + user.id + ); + RunFunctionAsync( + async () => { + await guardian.importToolFileAsync( + file.buffer, + owner, + task, + metadata?.buffer && JSON.parse(metadata.buffer.toString()) + ); + }, + async (error) => { + new Logger().error(error, ['API_GATEWAY']); + taskManager.addError(task.taskId, { + code: 500, + message: error.message, + }); + } + ); + return task; + } catch (error) { + new Logger().error(error, ['API_GATEWAY']); + throw new HttpException( + error.message, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + /** * Import tool from IPFS (Async) */ diff --git a/api-gateway/src/app.module.ts b/api-gateway/src/app.module.ts index 429bf0efc5..9142510b61 100644 --- a/api-gateway/src/app.module.ts +++ b/api-gateway/src/app.module.ts @@ -24,7 +24,6 @@ import { TrustChainsApi } from '@api/service/trust-chains'; import { WizardApi } from '@api/service/wizard'; import process from 'process'; import express from 'express'; -import fileUpload from 'express-fileupload'; import hpp from 'hpp'; import { ThemesApi } from '@api/service/themes'; import { BrandingApi } from '@api/service/branding'; @@ -146,7 +145,6 @@ export class AppModule { limit: RAW_REQUEST_LIMIT, type: 'binary/octet-stream' })).forRoutes('*'); - consumer.apply(fileUpload()).forRoutes('*'); consumer.apply(hpp()).forRoutes('*'); } } diff --git a/api-gateway/src/helpers/guardians.ts b/api-gateway/src/helpers/guardians.ts index 1737fce280..294951b5b8 100644 --- a/api-gateway/src/helpers/guardians.ts +++ b/api-gateway/src/helpers/guardians.ts @@ -1,5 +1,5 @@ import { Singleton } from '@helpers/decorators/singleton'; -import { ApplicationStates, CommonSettings, ContractAPI, ContractType, GenerateUUIDv4, IArtifact, IChainItem, IContract, IDidObject, IRetirePool, IRetireRequest, ISchema, IToken, ITokenInfo, IUser, IVCDocument, IVPDocument, MessageAPI, RetireTokenPool, RetireTokenRequest, SchemaNode, SuggestionsOrderPriority } from '@guardian/interfaces'; +import { ApplicationStates, CommonSettings, ContractAPI, ContractType, GenerateUUIDv4, IArtifact, IChainItem, IContract, IDidObject, IRetirePool, IRetireRequest, ISchema, IToken, ITokenInfo, IUser, IVCDocument, IVPDocument, MessageAPI, PolicyToolMetadata, RetireTokenPool, RetireTokenRequest, SchemaNode, SuggestionsOrderPriority } from '@guardian/interfaces'; import { IAuthUser, NatsService } from '@guardian/common'; import { NewTask } from './task-manager'; @@ -170,6 +170,14 @@ export class Guardians extends NatsService { return await this.sendMessage(MessageAPI.SET_TOKEN_ASYNC, { token, owner, task }); } + /** + * Update token + * @param token + */ + public async updateToken(token: IToken | any): Promise { + return await this.sendMessage(MessageAPI.UPDATE_TOKEN, { token }); + } + /** * Async create new token * @param token @@ -1942,9 +1950,10 @@ export class Guardians extends NatsService { * Load tool file for import * @param zip * @param owner + * @param metadata */ - public async importToolFile(zip: any, owner: string) { - return await this.sendMessage(MessageAPI.TOOL_IMPORT_FILE, { zip, owner }); + public async importToolFile(zip: any, owner: string, metadata?: PolicyToolMetadata) { + return await this.sendMessage(MessageAPI.TOOL_IMPORT_FILE, { zip, owner, metadata }); } /** @@ -1979,9 +1988,10 @@ export class Guardians extends NatsService { * @param zip * @param owner * @param task + * @param metadata */ - public async importToolFileAsync(zip: any, owner: string, task: NewTask) { - return await this.sendMessage(MessageAPI.TOOL_IMPORT_FILE_ASYNC, { zip, owner, task }); + public async importToolFileAsync(zip: any, owner: string, task: NewTask, metadata?: PolicyToolMetadata) { + return await this.sendMessage(MessageAPI.TOOL_IMPORT_FILE_ASYNC, { zip, owner, task, metadata }); } /** diff --git a/api-gateway/src/helpers/policy-engine.ts b/api-gateway/src/helpers/policy-engine.ts index 4d27d58083..299218683e 100644 --- a/api-gateway/src/helpers/policy-engine.ts +++ b/api-gateway/src/helpers/policy-engine.ts @@ -1,5 +1,5 @@ import { Singleton } from '@helpers/decorators/singleton'; -import { DocumentType, GenerateUUIDv4, MigrationConfig, PolicyEngineEvents } from '@guardian/interfaces'; +import { DocumentType, GenerateUUIDv4, MigrationConfig, PolicyEngineEvents, PolicyToolMetadata } from '@guardian/interfaces'; import { IAuthUser, NatsService } from '@guardian/common'; import { NewTask } from './task-manager'; @@ -269,9 +269,20 @@ export class PolicyEngine extends NatsService { * @param user * @param zip * @param versionOfTopicId - */ - public async importFile(user: IAuthUser, zip: Buffer, versionOfTopicId?: string) { - return await this.sendMessage(PolicyEngineEvents.POLICY_IMPORT_FILE, { zip, user, versionOfTopicId }); + * @param metadata + */ + public async importFile( + user: IAuthUser, + zip: Buffer, + versionOfTopicId?: string, + metadata?: PolicyToolMetadata + ) { + return await this.sendMessage(PolicyEngineEvents.POLICY_IMPORT_FILE, { + zip, + user, + versionOfTopicId, + metadata, + }); } /** @@ -280,18 +291,38 @@ export class PolicyEngine extends NatsService { * @param zip * @param versionOfTopicId * @param task + * @param metadata */ - public async importFileAsync(user: IAuthUser, zip: Buffer, versionOfTopicId: string, task: NewTask) { - return await this.sendMessage(PolicyEngineEvents.POLICY_IMPORT_FILE_ASYNC, { zip, user, versionOfTopicId, task }); + public async importFileAsync( + user: IAuthUser, + zip: Buffer, + versionOfTopicId: string, + task: NewTask, + metadata?: PolicyToolMetadata + ) { + return await this.sendMessage( + PolicyEngineEvents.POLICY_IMPORT_FILE_ASYNC, + { zip, user, versionOfTopicId, task, metadata } + ); } /** * Import policy from message * @param user * @param messageId + * @param versionOfTopicId + * @param metadata */ - public async importMessage(user: IAuthUser, messageId: string, versionOfTopicId: string) { - return await this.sendMessage(PolicyEngineEvents.POLICY_IMPORT_MESSAGE, { messageId, user, versionOfTopicId }); + public async importMessage( + user: IAuthUser, + messageId: string, + versionOfTopicId: string, + metadata?: PolicyToolMetadata + ) { + return await this.sendMessage( + PolicyEngineEvents.POLICY_IMPORT_MESSAGE, + { messageId, user, versionOfTopicId, metadata } + ); } /** @@ -300,9 +331,19 @@ export class PolicyEngine extends NatsService { * @param messageId * @param versionOfTopicId * @param task - */ - public async importMessageAsync(user: IAuthUser, messageId: string, versionOfTopicId: string, task: NewTask) { - return await this.sendMessage(PolicyEngineEvents.POLICY_IMPORT_MESSAGE_ASYNC, { messageId, user, versionOfTopicId, task }); + * @param metadata + */ + public async importMessageAsync( + user: IAuthUser, + messageId: string, + versionOfTopicId: string, + task: NewTask, + metadata?: PolicyToolMetadata + ) { + return await this.sendMessage( + PolicyEngineEvents.POLICY_IMPORT_MESSAGE_ASYNC, + { messageId, user, versionOfTopicId, task, metadata } + ); } /** diff --git a/frontend/src/app/modules/policy-engine/helpers/preview-policy-dialog/preview-policy-dialog.component.html b/frontend/src/app/modules/policy-engine/helpers/preview-policy-dialog/preview-policy-dialog.component.html index 670f7309b8..a301e93d83 100644 --- a/frontend/src/app/modules/policy-engine/helpers/preview-policy-dialog/preview-policy-dialog.component.html +++ b/frontend/src/app/modules/policy-engine/helpers/preview-policy-dialog/preview-policy-dialog.component.html @@ -67,6 +67,16 @@
Similar Policies
{{ similar }}
+ +
+
Tools
+
+
+ + +
+
+
@@ -97,6 +107,21 @@
Creator
{{ tool.owner }}
+ +
+
Tools
+
{{ tools }}
+
+ +
+
Tools
+
+
+ + +
+
+
@@ -134,7 +159,7 @@
- +
diff --git a/frontend/src/app/modules/policy-engine/helpers/preview-policy-dialog/preview-policy-dialog.component.scss b/frontend/src/app/modules/policy-engine/helpers/preview-policy-dialog/preview-policy-dialog.component.scss index 240a39a45d..16a057c44f 100644 --- a/frontend/src/app/modules/policy-engine/helpers/preview-policy-dialog/preview-policy-dialog.component.scss +++ b/frontend/src/app/modules/policy-engine/helpers/preview-policy-dialog/preview-policy-dialog.component.scss @@ -17,6 +17,14 @@ align-content: center; } +.field-input { + margin: 30px 5px 0 5px; +} + +.input { + width: 100%; +} + .field-value { color: #181818; font-family: Inter, sans-serif; diff --git a/frontend/src/app/modules/policy-engine/helpers/preview-policy-dialog/preview-policy-dialog.component.ts b/frontend/src/app/modules/policy-engine/helpers/preview-policy-dialog/preview-policy-dialog.component.ts index d6d0071940..1c59c7c1ff 100644 --- a/frontend/src/app/modules/policy-engine/helpers/preview-policy-dialog/preview-policy-dialog.component.ts +++ b/frontend/src/app/modules/policy-engine/helpers/preview-policy-dialog/preview-policy-dialog.component.ts @@ -1,4 +1,5 @@ import { Component } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; /** @@ -15,6 +16,7 @@ export class PreviewPolicyDialog { public schemas!: string; public tokens!: string; public tools!: string; + public toolConfigs!: { name: string, messageId: string }[]; public policyGroups!: string; public newVersions: any[] = []; public versionOfTopicId: any; @@ -24,6 +26,8 @@ export class PreviewPolicyDialog { public tool!: any; public xlsx!: any; public errors!: any; + public toolForm?: FormGroup; + public isFile?: boolean; constructor( public ref: DynamicDialogRef, @@ -62,6 +66,18 @@ export class PreviewPolicyDialog { return s.name; }) .join(', '); + + this.toolConfigs = importFile.tools || []; + this.toolForm = new FormGroup({}); + for (const toolConfigs of this.toolConfigs) { + this.toolForm.addControl( + toolConfigs.messageId, + new FormControl(toolConfigs.messageId, [ + Validators.required, + Validators.pattern(/^[0-9]{10}\.[0-9]{9}$/), + ]) + ); + } } if (this.config.data.module) { @@ -70,6 +86,21 @@ export class PreviewPolicyDialog { if (this.config.data.tool) { this.tool = this.config.data.tool?.tool; + this.isFile = this.config.data.isFile; + this.toolConfigs = this.config.data.tool.tools || []; + if (this.isFile) { + this.toolForm = new FormGroup({}); + for (const toolConfigs of this.toolConfigs) { + this.toolForm.addControl( + toolConfigs.messageId, + new FormControl(toolConfigs.messageId, [ + Validators.required, + Validators.pattern(/^[0-9]{10}\.[0-9]{9}$/), + ]) + ); + } + } + this.tools = this.toolConfigs.map((tool) => tool.name).join(', '); } if (this.config.data.xlsx) { @@ -121,12 +152,14 @@ export class PreviewPolicyDialog { onImport() { this.ref.close({ versionOfTopicId: this.versionOfTopicId, + tools: this.toolForm?.value, }); } onNewVersionClick(messageId: string) { this.ref.close({ messageId, + tools: this.toolForm?.value, }); } } diff --git a/frontend/src/app/modules/policy-engine/policies/policies.component.ts b/frontend/src/app/modules/policy-engine/policies/policies.component.ts index 1b5662c52e..3c73b4bb9f 100644 --- a/frontend/src/app/modules/policy-engine/policies/policies.component.ts +++ b/frontend/src/app/modules/policy-engine/policies/policies.component.ts @@ -467,7 +467,9 @@ export class PoliciesComponent implements OnInit { this.loading = true; if (type == 'message') { this.policyEngineService - .pushImportByMessage(data, versionOfTopicId) + .pushImportByMessage(data, versionOfTopicId, { + tools: result.tools + }) .subscribe( (result) => { const { taskId, expectation } = result; @@ -483,7 +485,9 @@ export class PoliciesComponent implements OnInit { ); } else if (type == 'file') { this.policyEngineService - .pushImportByFile(data, versionOfTopicId) + .pushImportByFile(data, versionOfTopicId, { + tools: result.tools + }) .subscribe( (result) => { const { taskId, expectation } = result; diff --git a/frontend/src/app/modules/policy-engine/tools-list/tools-list.component.ts b/frontend/src/app/modules/policy-engine/tools-list/tools-list.component.ts index 1501cf7e4e..acea462f53 100644 --- a/frontend/src/app/modules/policy-engine/tools-list/tools-list.component.ts +++ b/frontend/src/app/modules/policy-engine/tools-list/tools-list.component.ts @@ -154,13 +154,16 @@ export class ToolsListComponent implements OnInit, OnDestroy { styleClass: 'custom-dialog', data: { tool: tool, + isFile: type === 'file' } }); dialogRef.onClose.subscribe(async (result) => { if (result) { if (type === 'message') { this.loading = true; - this.toolsService.importByMessage(data).subscribe( + this.toolsService.importByMessage(data, { + tools: result.tools + }).subscribe( (result) => { this.loadAllTools(); }, (e) => { @@ -168,7 +171,9 @@ export class ToolsListComponent implements OnInit, OnDestroy { }); } else if (type === 'file') { this.loading = true; - this.toolsService.importByFile(data).subscribe( + this.toolsService.importByFile(data, { + tools: result.tools + }).subscribe( (result) => { this.loadAllTools(); }, (e) => { diff --git a/frontend/src/app/services/policy-engine.service.ts b/frontend/src/app/services/policy-engine.service.ts index 3a6077844e..a0fd4ea6f9 100644 --- a/frontend/src/app/services/policy-engine.service.ts +++ b/frontend/src/app/services/policy-engine.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { API_BASE_URL } from './api'; -import { MigrationConfig } from '@guardian/interfaces'; +import { MigrationConfig, PolicyToolMetadata } from '@guardian/interfaces'; /** * Services for working from policy and separate blocks. @@ -116,32 +116,45 @@ export class PolicyEngineService { return this.http.get(`${this.url}/${policyId}/export/message`); } - public importByMessage(messageId: string, versionOfTopicId?: string): Observable { - var query = versionOfTopicId ? `?versionOfTopicId=${versionOfTopicId}` : ''; - return this.http.post(`${this.url}/import/message${query}`, { messageId }); - } - - public pushImportByMessage(messageId: string, versionOfTopicId?: string): Observable<{ taskId: string, expectation: number }> { - var query = versionOfTopicId ? `?versionOfTopicId=${versionOfTopicId}` : ''; - return this.http.post<{ taskId: string, expectation: number }>(`${this.url}/push/import/message${query}`, { messageId }); - } - - public importByFile(policyFile: any, versionOfTopicId?: string): Observable { - var query = versionOfTopicId ? `?versionOfTopicId=${versionOfTopicId}` : ''; - return this.http.post(`${this.url}/import/file${query}`, policyFile, { - headers: { - 'Content-Type': 'binary/octet-stream' - } - }); - } - - public pushImportByFile(policyFile: any, versionOfTopicId?: string): Observable<{ taskId: string, expectation: number }> { - var query = versionOfTopicId ? `?versionOfTopicId=${versionOfTopicId}` : ''; - return this.http.post<{ taskId: string, expectation: number }>(`${this.url}/push/import/file${query}`, policyFile, { - headers: { - 'Content-Type': 'binary/octet-stream' - } - }); + public pushImportByMessage( + messageId: string, + versionOfTopicId?: string, + metadata?: PolicyToolMetadata + ): Observable<{ taskId: string; expectation: number }> { + var query = versionOfTopicId + ? `?versionOfTopicId=${versionOfTopicId}` + : ''; + return this.http.post<{ taskId: string; expectation: number }>( + `${this.url}/push/import/message${query}`, + { messageId, metadata } + ); + } + + public pushImportByFile( + policyFile: any, + versionOfTopicId?: string, + metadata?: PolicyToolMetadata + ): Observable<{ taskId: string; expectation: number }> { + var query = versionOfTopicId + ? `?versionOfTopicId=${versionOfTopicId}` + : ''; + const formData = new FormData(); + formData.append( + 'policyFile', + new Blob([policyFile], { type: 'application/octet-stream' }) + ); + if (metadata) { + formData.append( + 'metadata', + new Blob([JSON.stringify(metadata)], { + type: 'application/json', + }) + ); + } + return this.http.post<{ taskId: string; expectation: number }>( + `${this.url}/push/import/file-metadata${query}`, + formData + ); } public previewByMessage(messageId: string): Observable { diff --git a/frontend/src/app/services/tools.service.ts b/frontend/src/app/services/tools.service.ts index 708532514b..1f8e9236f1 100644 --- a/frontend/src/app/services/tools.service.ts +++ b/frontend/src/app/services/tools.service.ts @@ -2,7 +2,7 @@ import { HttpClient, HttpResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { API_BASE_URL } from './api'; -import { ISchema } from '@guardian/interfaces'; +import { PolicyToolMetadata } from '@guardian/interfaces'; /** * Services for working from tools. @@ -75,28 +75,46 @@ export class ToolsService { }); } - public importByMessage(messageId: string): Observable { - return this.http.post(`${this.url}/import/message`, { messageId }); + public importByMessage(messageId: string, metadata?: { tools: { [key: string]: string }}): Observable { + return this.http.post(`${this.url}/import/message`, { messageId, metadata }); } - public importByFile(file: any): Observable { - return this.http.post(`${this.url}/import/file`, file, { - headers: { - 'Content-Type': 'binary/octet-stream' - } - }); + public importByFile(file: any, metadata?: { tools: { [key: string]: string }}): Observable { + const formData = new FormData(); + formData.append('file', new Blob([file], { type: "application/octet-stream" })); + if (metadata) { + formData.append('metadata', new Blob([JSON.stringify(metadata)], { + type: "application/json", + })); + } + return this.http.post(`${this.url}/import/file-metadata`, formData); } public pushImportByMessage(messageId: string): Observable<{ taskId: string, expectation: number }> { return this.http.post<{ taskId: string, expectation: number }>(`${this.url}/push/import/message`, { messageId }); } - public pushImportByFile(file: any): Observable<{ taskId: string, expectation: number }> { - return this.http.post<{ taskId: string, expectation: number }>(`${this.url}/push/import/file`, file, { - headers: { - 'Content-Type': 'binary/octet-stream' - } - }); + public pushImportByFile( + file: any, + metadata?: PolicyToolMetadata + ): Observable<{ taskId: string; expectation: number }> { + const formData = new FormData(); + formData.append( + 'file', + new Blob([file], { type: 'application/octet-stream' }) + ); + if (metadata) { + formData.append( + 'metadata', + new Blob([JSON.stringify(metadata)], { + type: 'application/json', + }) + ); + } + return this.http.post<{ taskId: string; expectation: number }>( + `${this.url}/push/import/file-metadata`, + formData + ); } public validate(policy: any): Observable { diff --git a/guardian-cli/helpers/policy-publisher.helper.ts b/guardian-cli/helpers/policy-publisher.helper.ts index 7701838607..a34321e749 100644 --- a/guardian-cli/helpers/policy-publisher.helper.ts +++ b/guardian-cli/helpers/policy-publisher.helper.ts @@ -3,15 +3,20 @@ import fs from 'fs'; import WebSocket from 'ws'; import Path from 'path'; +interface Task { + action: string; options?: any; resolve: Function +} export class PolicyPublisher { private _policiesConfig: Map = new Map(); private _accessToken: string; private _refreshToken: string; private _pingInterval; private _updateTokenInterval; - private _tasks: Map = new Map(); + private _tasks: Map< + string, + Task + > = new Map(); private _ws: WebSocket; - private _resolver: Function; private constructor( private _policiesDirectory: string, @@ -58,7 +63,6 @@ export class PolicyPublisher { if (task.action === 'Publish policy') { this.onPolicyPublished(taskId, result.policyId); } - return; } else if (error) { this._tasks.delete(taskId); return; @@ -118,6 +122,7 @@ export class PolicyPublisher { this._tasks.set(taskId, { action, options: task.options, + resolve: task.resolve, }); } catch (error) { this._policiesConfig.delete(task.options.file); @@ -141,15 +146,8 @@ export class PolicyPublisher { } catch (error) { console.error(error); } finally { - this._policiesConfig.delete(task.options.file); this._tasks.delete(publishTaskId); - await this.checkFinish(); - } - } - - private async checkFinish() { - if (this._policiesConfig.size === 0) { - await this.finish(); + task.resolve(); } } @@ -157,9 +155,6 @@ export class PolicyPublisher { if (this._ws) { this._ws?.close(); } - if (this._resolver) { - this._resolver(); - } if (this._pingInterval) { clearInterval(this._pingInterval); } @@ -221,17 +216,11 @@ export class PolicyPublisher { await policyPublisher.authorize(); await policyPublisher.parseConfigFile(configFilePath); await policyPublisher.start(); + await policyPublisher.finish(); } - private start(): Promise { - return new Promise(async (resolve, reject) => { - try { - this._resolver = resolve; - await this.read(this._policiesDirectory); - } catch (error) { - reject(error); - } - }); + private async start(): Promise { + await this.read(this._policiesDirectory); } private async read(dirPath) { @@ -250,13 +239,16 @@ export class PolicyPublisher { await fs.readFileSync(dirPath) ); console.log(`Import policy ${file} is started`); - this._tasks.set(taskId, { - action, - options: { - version, - file, - }, - }); + await new Promise((resolve) => + this._tasks.set(taskId, { + action, + options: { + version, + file, + }, + resolve, + }) + ); } if (stat.isDirectory()) { const dirs = fs.readdirSync(dirPath); diff --git a/guardian-service/src/api/artifact.service.ts b/guardian-service/src/api/artifact.service.ts index f3db21e651..80c7f5ad89 100644 --- a/guardian-service/src/api/artifact.service.ts +++ b/guardian-service/src/api/artifact.service.ts @@ -58,17 +58,17 @@ export async function artifactAPI(): Promise { } } - const extention = getArtifactExtention(msg.artifact.name); + const extention = getArtifactExtention(msg.artifact.originalname); const type = getArtifactType(extention); const artifact = await DatabaseServer.saveArtifact({ - name: msg.artifact.name.split('.')[0], + name: msg.artifact.originalname.split('.')[0], extention, type, policyId: msg.parentId, owner: msg.owner, category } as any); - await DatabaseServer.saveArtifactFile(artifact.uuid, Buffer.from(msg.artifact.data)); + await DatabaseServer.saveArtifactFile(artifact.uuid, Buffer.from(msg.artifact.buffer)); return new MessageResponse(artifact); } catch (error) { new Logger().error(error, ['GUARDIAN_SERVICE']); diff --git a/guardian-service/src/api/helpers/schema-import-export-helper.ts b/guardian-service/src/api/helpers/schema-import-export-helper.ts index a736265dc8..ee3e982fd8 100644 --- a/guardian-service/src/api/helpers/schema-import-export-helper.ts +++ b/guardian-service/src/api/helpers/schema-import-export-helper.ts @@ -180,7 +180,8 @@ export async function importSchemaByFiles( files: ISchema[], topicId: string, notifier: INotifier, - skipGenerateId = false + skipGenerateId = false, + outerSchemasMapping?: { name: string, iri: string }[] ): Promise { notifier.start('Import schemas'); @@ -212,6 +213,19 @@ export async function importSchemaByFiles( file.owner = owner; file.topicId = topicId || 'draft'; file.status = SchemaStatus.DRAFT; + if (file.document?.$defs && outerSchemasMapping) { + for (const def of Object.values(file.document.$defs)) { + if (!def || uuidMap.has(def.$id)) { + continue; + } + const subSchemaMapping = outerSchemasMapping.find( + (item) => item.name === def.title + ); + if (subSchemaMapping) { + uuidMap.set(def.$id, subSchemaMapping.iri); + } + } + } } notifier.info(`Found ${files.length} schemas`); diff --git a/guardian-service/src/api/helpers/tool-import-export-helper.ts b/guardian-service/src/api/helpers/tool-import-export-helper.ts index 882ad051fd..1f7ffa995c 100644 --- a/guardian-service/src/api/helpers/tool-import-export-helper.ts +++ b/guardian-service/src/api/helpers/tool-import-export-helper.ts @@ -20,6 +20,7 @@ import { GenerateUUIDv4, IRootConfig, ModuleStatus, + PolicyToolMetadata, SchemaCategory, SchemaStatus, TagType, @@ -64,7 +65,8 @@ interface ImportResults { */ export async function replaceConfig( tool: PolicyTool, - schemasMap: any[] + schemasMap: any[], + tools: { oldMessageId: string, messageId: string, oldHash: string, newHash?: string }[] ) { if (await DatabaseServer.getTool({ name: tool.name })) { tool.name = tool.name + '_' + Date.now(); @@ -74,6 +76,14 @@ export async function replaceConfig( replaceAllEntities(tool.config, SchemaFields, item.oldIRI, item.newIRI); replaceAllVariables(tool.config, 'Schema', item.oldIRI, item.newIRI); } + + for (const item of tools) { + if (!item.newHash || !item.messageId) { + continue; + } + replaceAllEntities(tool.config, ['messageId'], item.oldMessageId, item.messageId); + replaceAllEntities(tool.config, ['hash'], item.oldHash, item.newHash); + } } /** @@ -85,7 +95,6 @@ export async function replaceConfig( export async function importSubTools( hederaAccount: IRootConfig, messages: { - uuid?: string, name?: string, messageId?: string }[], @@ -116,7 +125,6 @@ export async function importSubTools( } catch (error) { errors.push({ type: 'tool', - hash: message.uuid, name: message.name, messageId: message.messageId, error: 'Invalid tool' @@ -331,7 +339,8 @@ export function importToolErrors(errors: any[]): string { export async function importToolByFile( owner: string, components: IToolComponents, - notifier: INotifier + notifier: INotifier, + metadata?: PolicyToolMetadata ): Promise { notifier.start('Import tool'); @@ -342,6 +351,33 @@ export async function importToolByFile( schemas } = components; + notifier.completedAndStart('Resolve Hedera account'); + const users = new Users(); + const root = await users.getHederaAccount(owner); + + const toolsMapping: { + oldMessageId: string; + messageId: string; + oldHash: string; + newHash?: string; + }[] = []; + if (metadata?.tools) { + // tslint:disable-next-line:no-shadowed-variable + for (const tool of tools) { + if ( + metadata.tools[tool.messageId] && + tool.messageId !== metadata.tools[tool.messageId] + ) { + toolsMapping.push({ + oldMessageId: tool.messageId, + messageId: metadata.tools[tool.messageId], + oldHash: tool.hash, + }); + tool.messageId = metadata.tools[tool.messageId]; + } + } + } + delete tool._id; delete tool.id; delete tool.messageId; @@ -353,10 +389,6 @@ export async function importToolByFile( await updateToolConfig(tool); - notifier.completedAndStart('Resolve Hedera account'); - const users = new Users(); - const root = await users.getHederaAccount(owner); - notifier.completedAndStart('Create topic'); const parent = await TopicConfig.fromObject( await DatabaseServer.getTopicByType(owner, TopicType.UserTopic), true @@ -401,20 +433,41 @@ export async function importToolByFile( const toolsResult = await importSubTools(root, tools, notifier); notifier.sub(true); + for (const toolMapping of toolsMapping) { + const toolByMessageId = toolsResult.tools.find( + // tslint:disable-next-line:no-shadowed-variable + (tool) => tool.messageId === toolMapping.messageId + ); + toolMapping.newHash = toolByMessageId?.hash; + } + + const toolsSchemas = (await DatabaseServer.getSchemas( + { + category: SchemaCategory.TOOL, + // tslint:disable-next-line:no-shadowed-variable + topicId: { $in: toolsResult.tools.map((tool) => tool.topicId) }, + }, + { + fields: ['name', 'iri'], + } + )) as { name: string; iri: string }[]; + // Import Schemas const schemasResult = await importSchemaByFiles( SchemaCategory.TOOL, owner, schemas, tool.topicId, - notifier + notifier, + false, + toolsSchemas ); const schemasMap = schemasResult.schemasMap; notifier.completedAndStart('Saving in DB'); // Replace id - await replaceConfig(tool, schemasMap); + await replaceConfig(tool, schemasMap, toolsMapping); const item = await DatabaseServer.createTool(tool); const _topicRow = await DatabaseServer.getTopicById(topic.topicId); @@ -504,4 +557,4 @@ export async function updateToolConfig(tool: PolicyTool): Promise { tool.tools = list; return tool; -} \ No newline at end of file +} diff --git a/guardian-service/src/api/token.service.ts b/guardian-service/src/api/token.service.ts index 6d2b009959..7c2b428413 100644 --- a/guardian-service/src/api/token.service.ts +++ b/guardian-service/src/api/token.service.ts @@ -603,6 +603,24 @@ export async function tokenAPI(tokenRepository: DataBaseHelper): Promise< return new MessageResponse(task); }); + ApiResponse(MessageAPI.UPDATE_TOKEN, async (msg) => { + try { + const { token } = msg; + if (!msg) { + throw new Error('Invalid Params'); + } + const item = await tokenRepository.findOne({ tokenId: token.tokenId }); + if (!item) { + throw new Error('Token not found'); + } + + return new MessageResponse(await updateToken(item, token, tokenRepository, emptyNotifier())); + } catch (error) { + new Logger().error(error, ['GUARDIAN_SERVICE']); + return new MessageError(error); + } + }); + ApiResponse(MessageAPI.UPDATE_TOKEN_ASYNC, async (msg) => { const { token, task } = msg; const notifier = await initNotifier(task); diff --git a/guardian-service/src/api/tool.service.ts b/guardian-service/src/api/tool.service.ts index 099e32b4d3..5c857c14e7 100644 --- a/guardian-service/src/api/tool.service.ts +++ b/guardian-service/src/api/tool.service.ts @@ -645,12 +645,12 @@ export async function toolsAPI(): Promise { ApiResponse(MessageAPI.TOOL_IMPORT_FILE, async (msg) => { try { - const { zip, owner } = msg; + const { zip, owner, metadata } = msg; if (!zip) { throw new Error('file in body is empty'); } const preview = await ToolImportExport.parseZipFile(Buffer.from(zip.data)); - const { tool, errors } = await importToolByFile(owner, preview, emptyNotifier()); + const { tool, errors } = await importToolByFile(owner, preview, emptyNotifier(), metadata); if (errors?.length) { const message = importToolErrors(errors); new Logger().warn(message, ['GUARDIAN_SERVICE']); @@ -688,14 +688,14 @@ export async function toolsAPI(): Promise { }); ApiResponse(MessageAPI.TOOL_IMPORT_FILE_ASYNC, async (msg) => { - const { zip, owner, task } = msg; + const { zip, owner, task, metadata} = msg; const notifier = await initNotifier(task); RunFunctionAsync(async () => { if (!zip) { throw new Error('file in body is empty'); } const preview = await ToolImportExport.parseZipFile(Buffer.from(zip.data)); - const { tool, errors } = await importToolByFile(owner, preview, notifier); + const { tool, errors } = await importToolByFile(owner, preview, notifier, metadata); if (errors?.length) { const message = importToolErrors(errors); notifier.error(message); diff --git a/guardian-service/src/policy-engine/helpers/policy-import-export-helper.ts b/guardian-service/src/policy-engine/helpers/policy-import-export-helper.ts index 44d62b669f..57ba8f4cdf 100644 --- a/guardian-service/src/policy-engine/helpers/policy-import-export-helper.ts +++ b/guardian-service/src/policy-engine/helpers/policy-import-export-helper.ts @@ -4,6 +4,7 @@ import { ConfigType, GenerateUUIDv4, ModuleStatus, + PolicyToolMetadata, PolicyType, SchemaCategory, SchemaEntity, @@ -89,6 +90,7 @@ export class PolicyImportExportHelper { versionOfTopicId: string, notifier: INotifier, additionalPolicyConfig?: Partial, + metadata?: PolicyToolMetadata, ): Promise<{ /** * New Policy @@ -108,6 +110,32 @@ export class PolicyImportExportHelper { tools } = policyToImport; + const users = new Users(); + notifier.start('Resolve Hedera account'); + const root = await users.getHederaAccount(policyOwner); + + const toolsMapping: { + oldMessageId: string; + messageId: string; + oldHash: string; + newHash?: string; + }[] = []; + if (metadata?.tools) { + for (const tool of tools) { + if ( + metadata.tools[tool.messageId] && + tool.messageId !== metadata.tools[tool.messageId] + ) { + toolsMapping.push({ + oldMessageId: tool.messageId, + messageId: metadata.tools[tool.messageId], + oldHash: tool.hash, + }); + tool.messageId = metadata.tools[tool.messageId]; + } + } + } + delete policy._id; delete policy.id; delete policy.messageId; @@ -126,9 +154,6 @@ export class PolicyImportExportHelper { policy.topicDescription = additionalPolicyConfig?.topicDescription || policy.topicDescription; policy.description = additionalPolicyConfig?.description || policy.description; - const users = new Users(); - notifier.start('Resolve Hedera account'); - const root = await users.getHederaAccount(policyOwner); notifier.completedAndStart('Resolve topic'); const parent = await TopicConfig.fromObject( await DatabaseServer.getTopicByType(policyOwner, TopicType.UserTopic), true @@ -186,17 +211,37 @@ export class PolicyImportExportHelper { const toolsResult = await importSubTools(root, tools, notifier); notifier.sub(false); + for (const toolMapping of toolsMapping) { + const toolByMessageId = toolsResult.tools.find( + // tslint:disable-next-line:no-shadowed-variable + (tool) => tool.messageId === toolMapping.messageId + ); + toolMapping.newHash = toolByMessageId?.hash; + } + // Import Tokens const tokensResult = await importTokensByFiles(policyOwner, tokens, notifier); const tokenMap = tokensResult.tokenMap; + const toolsSchemas = (await DatabaseServer.getSchemas( + { + category: SchemaCategory.TOOL, + topicId: { $in: toolsResult.tools.map((tool) => tool.topicId) }, + }, + { + fields: ['name', 'iri'], + } + )) as { name: string; iri: string }[]; + // Import Schemas const schemasResult = await importSchemaByFiles( SchemaCategory.POLICY, policyOwner, schemas, topicRow.topicId, - notifier + notifier, + false, + toolsSchemas ); const schemasMap = schemasResult.schemasMap; @@ -207,7 +252,13 @@ export class PolicyImportExportHelper { notifier.completedAndStart('Saving in DB'); // Replace id - await PolicyImportExportHelper.replaceConfig(policy, schemasMap, artifactsMap, tokenMap); + await PolicyImportExportHelper.replaceConfig( + policy, + schemasMap, + artifactsMap, + tokenMap, + toolsMapping + ); // Save const model = new DataBaseHelper(Policy).create(policy as Policy); @@ -288,7 +339,8 @@ export class PolicyImportExportHelper { policy: Policy, schemasMap: SchemaImportResult[], artifactsMap: Map, - tokenMap: any[] + tokenMap: any[], + tools: { oldMessageId: string, messageId: string, oldHash: string, newHash?: string }[] ) { if (await new DataBaseHelper(Policy).findOne({ name: policy.name })) { policy.name = policy.name + '_' + Date.now(); @@ -308,6 +360,14 @@ export class PolicyImportExportHelper { replaceAllVariables(policy.config, 'Token', item.oldTokenID, item.newTokenID); } + for (const item of tools) { + if (!item.newHash || !item.messageId) { + continue; + } + replaceAllEntities(policy.config, ['messageId'], item.oldMessageId, item.messageId); + replaceAllEntities(policy.config, ['hash'], item.oldHash, item.newHash); + } + // compatibility with older versions policy = PolicyConverterUtils.PolicyConverter(policy); policy.codeVersion = PolicyConverterUtils.VERSION; diff --git a/guardian-service/src/policy-engine/policy-engine.service.ts b/guardian-service/src/policy-engine/policy-engine.service.ts index d0baf43949..aa93b92050 100644 --- a/guardian-service/src/policy-engine/policy-engine.service.ts +++ b/guardian-service/src/policy-engine/policy-engine.service.ts @@ -926,14 +926,21 @@ export class PolicyEngineService { this.channel.getMessages(PolicyEngineEvents.POLICY_IMPORT_FILE, async (msg) => { try { - const { zip, user, versionOfTopicId } = msg; + const { zip, user, versionOfTopicId, metadata } = msg; if (!zip) { throw new Error('file in body is empty'); } new Logger().info(`Import policy by file`, ['GUARDIAN_SERVICE']); const did = await this.getUserDid(user.username); const policyToImport = await PolicyImportExport.parseZipFile(Buffer.from(zip.data), true); - const result = await PolicyImportExportHelper.importPolicy(policyToImport, did, versionOfTopicId, emptyNotifier()); + const result = await PolicyImportExportHelper.importPolicy( + policyToImport, + did, + versionOfTopicId, + emptyNotifier(), + undefined, + metadata + ); if (result?.errors?.length) { const message = PolicyImportExportHelper.errorsMessage(result.errors); new Logger().warn(message, ['GUARDIAN_SERVICE']); @@ -948,7 +955,7 @@ export class PolicyEngineService { }); this.channel.getMessages(PolicyEngineEvents.POLICY_IMPORT_FILE_ASYNC, async (msg) => { - const { zip, user, versionOfTopicId, task } = msg; + const { zip, user, versionOfTopicId, task, metadata } = msg; const notifier = await initNotifier(task); RunFunctionAsync(async () => { @@ -960,7 +967,14 @@ export class PolicyEngineService { notifier.start('File parsing'); const policyToImport = await PolicyImportExport.parseZipFile(Buffer.from(zip.data), true); notifier.completed(); - const result = await PolicyImportExportHelper.importPolicy(policyToImport, did, versionOfTopicId, notifier); + const result = await PolicyImportExportHelper.importPolicy( + policyToImport, + did, + versionOfTopicId, + notifier, + undefined, + metadata + ); if (result?.errors?.length) { const message = PolicyImportExportHelper.errorsMessage(result.errors); notifier.error(message); @@ -1142,7 +1156,7 @@ export class PolicyEngineService { this.channel.getMessages(PolicyEngineEvents.POLICY_IMPORT_MESSAGE, async (msg) => { try { - const { messageId, user, versionOfTopicId } = msg; + const { messageId, user, versionOfTopicId, metadata } = msg; const did = await this.getUserDid(user.username); if (!messageId) { throw new Error('Policy ID in body is empty'); @@ -1150,7 +1164,7 @@ export class PolicyEngineService { const root = await this.users.getHederaAccount(did); - const result = await this.policyEngine.importPolicyMessage(messageId, did, root, versionOfTopicId, emptyNotifier()); + const result = await this.policyEngine.importPolicyMessage(messageId, did, root, versionOfTopicId, emptyNotifier(), metadata); if (result?.errors?.length) { const message = PolicyImportExportHelper.errorsMessage(result.errors); new Logger().warn(message, ['GUARDIAN_SERVICE']); @@ -1166,7 +1180,7 @@ export class PolicyEngineService { }); this.channel.getMessages(PolicyEngineEvents.POLICY_IMPORT_MESSAGE_ASYNC, async (msg) => { - const { messageId, user, versionOfTopicId, task } = msg; + const { messageId, user, versionOfTopicId, task, metadata } = msg; const notifier = await initNotifier(task); RunFunctionAsync(async () => { @@ -1178,7 +1192,7 @@ export class PolicyEngineService { const did = await this.getUserDid(user.username); const root = await this.users.getHederaAccount(did); notifier.completed(); - const result = await this.policyEngine.importPolicyMessage(messageId, did, root, versionOfTopicId, notifier); + const result = await this.policyEngine.importPolicyMessage(messageId, did, root, versionOfTopicId, notifier, metadata); if (result?.errors?.length) { const message = PolicyImportExportHelper.errorsMessage(result.errors); notifier.error(message); diff --git a/guardian-service/src/policy-engine/policy-engine.ts b/guardian-service/src/policy-engine/policy-engine.ts index f33635bf08..ff29e51535 100644 --- a/guardian-service/src/policy-engine/policy-engine.ts +++ b/guardian-service/src/policy-engine/policy-engine.ts @@ -1,4 +1,4 @@ -import { GenerateUUIDv4, IRootConfig, ModelHelper, NotificationAction, PolicyEvents, PolicyType, Schema, SchemaEntity, SchemaHelper, SchemaStatus, TagType, TopicType } from '@guardian/interfaces'; +import { GenerateUUIDv4, IRootConfig, ModelHelper, NotificationAction, PolicyEvents, PolicyToolMetadata, PolicyType, Schema, SchemaEntity, SchemaHelper, SchemaStatus, TagType, TopicType } from '@guardian/interfaces'; import { Artifact, DataBaseHelper, @@ -973,7 +973,8 @@ export class PolicyEngine extends NatsService { owner: string, hederaAccount: IRootConfig, versionOfTopicId: string, - notifier: INotifier + notifier: INotifier, + metadata?: PolicyToolMetadata ): Promise<{ /** * New Policy @@ -1023,7 +1024,14 @@ export class PolicyEngine extends NatsService { } as any); } notifier.completed(); - return await PolicyImportExportHelper.importPolicy(policyToImport, owner, versionOfTopicId, notifier); + return await PolicyImportExportHelper.importPolicy( + policyToImport, + owner, + versionOfTopicId, + notifier, + undefined, + metadata + ); } /** diff --git a/interfaces/src/interface/index.ts b/interfaces/src/interface/index.ts index 5c8c427ddb..688520e3a0 100644 --- a/interfaces/src/interface/index.ts +++ b/interfaces/src/interface/index.ts @@ -38,3 +38,4 @@ export * from './wizard-config.interface'; export * from './schema-node.interface'; export * from './policy-category-export.interface'; export * from './migration-config.interface'; +export * from './policy-tool-metadata.interface'; diff --git a/interfaces/src/interface/policy-tool-metadata.interface.ts b/interfaces/src/interface/policy-tool-metadata.interface.ts new file mode 100644 index 0000000000..2deeaf3e35 --- /dev/null +++ b/interfaces/src/interface/policy-tool-metadata.interface.ts @@ -0,0 +1,9 @@ +/** + * Policy tool metadata + */ +export interface PolicyToolMetadata { + /** + * Tools mapping + */ + tools?: { [key: string]: string }; +} diff --git a/interfaces/src/type/messages/message-api.type.ts b/interfaces/src/type/messages/message-api.type.ts index ce212ce27b..1a077ca129 100644 --- a/interfaces/src/type/messages/message-api.type.ts +++ b/interfaces/src/type/messages/message-api.type.ts @@ -14,6 +14,7 @@ export enum MessageAPI { SET_TOKEN = 'set-token', SET_TOKEN_ASYNC = 'set-token-async', SET_ACCESS_TOKEN = 'SET_ACCESS_TOKEN', + UPDATE_TOKEN = 'update-token', UPDATE_TOKEN_ASYNC = 'update-token-async', DELETE_TOKEN_ASYNC = 'delete-token-async', IMPORT_TOKENS = 'import-tokens', diff --git a/swagger.yaml b/swagger.yaml index 01e6c97ed1..f55fca6407 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -797,6 +797,19 @@ paths: description: Parent ID schema: type: string + requestBody: + required: true + description: Form data with artifacts. + content: + multipart/form-data: + schema: + type: array + items: + type: object + properties: + artifacts: + type: string + format: binary responses: '200': description: Successful operation. @@ -3327,6 +3340,49 @@ paths: tags: *ref_5 security: - bearerAuth: [] + /tools/import/file-metadata: + post: + operationId: ToolsApi_toolImportFileWithMetadata + summary: Imports new tool from a zip file. + description: >- + Imports new tool and all associated artifacts, such as schemas and VCs, + from the provided zip file into the local DB. Only users with the + Standard Registry role are allowed to make the request. + parameters: [] + requestBody: + required: true + description: Form data with tool file and metadata. + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + metadata: + type: string + format: binary + responses: + '200': + description: Successful operation. + content: + application/json: + schema: + $ref: '#/components/schemas/ToolDTO' + '401': + description: Unauthorized. + '403': + description: Forbidden. + '500': + description: Internal server error. + content: + application/json: + schema: + $ref: '#/components/schemas/InternalServerErrorDTO' + tags: *ref_5 + security: + - bearerAuth: [] /tools/push/import/file: post: operationId: ToolsApi_toolImportFileAsync @@ -3356,6 +3412,49 @@ paths: tags: *ref_5 security: - bearerAuth: [] + /tools/push/import/file-metadata: + post: + operationId: ToolsApi_toolImportFileWithMetadataAsync + summary: Imports new tool from a zip file. + description: >- + Imports new tool and all associated artifacts, such as schemas and VCs, + from the provided zip file into the local DB. Only users with the + Standard Registry role are allowed to make the request. + parameters: [] + requestBody: + required: true + description: Form data with tool file and metadata. + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + metadata: + type: string + format: binary + responses: + '200': + description: Successful operation. + content: + application/json: + schema: + $ref: '#/components/schemas/TaskDTO' + '401': + description: Unauthorized. + '403': + description: Forbidden. + '500': + description: Internal server error. + content: + application/json: + schema: + $ref: '#/components/schemas/InternalServerErrorDTO' + tags: *ref_5 + security: + - bearerAuth: [] /tools/push/import/message: post: operationId: ToolsApi_toolImportMessageAsync @@ -4956,6 +5055,56 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /policies/import/file-metadata: + post: + operationId: PolicyApi_importPolicyFromFileWithMetadata + summary: Imports new policy from a zip file with metadata. + description: >- + Imports new policy and all associated artifacts, such as schemas and + VCs, from the provided zip file into the local DB. Only users with the + Standard Registry role are allowed to make the request. + parameters: + - name: versionOfTopicId + required: false + in: query + description: Topic Id + schema: + type: string + requestBody: + required: true + description: Form data with policy file and metadata. + content: + multipart/form-data: + schema: + type: object + properties: + policyFile: + type: string + format: binary + metadata: + type: string + format: binary + responses: + '200': + description: Successful operation. + content: + application/json: + schema: + type: object + '401': + description: Unauthorized + '403': + description: Forbidden. + '500': + description: Internal server error. + content: + application/json: + schema: + $ref: '#/components/schemas/InternalServerErrorDTO' + tags: *ref_7 + security: + - bearerAuth: [] + - bearer: [] /policies/push/import/file: post: tags: @@ -4996,6 +5145,56 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /policies/push/import/file-metadata: + post: + operationId: PolicyApi_importPolicyFromFileWithMetadataAsync + summary: Imports new policy from a zip file with metadata. + description: >- + Imports new policy and all associated artifacts, such as schemas and + VCs, from the provided zip file into the local DB. Only users with the + Standard Registry role are allowed to make the request. + parameters: + - name: versionOfTopicId + required: false + in: query + description: Topic Id + schema: + type: string + requestBody: + required: true + description: Form data with policy file and metadata. + content: + multipart/form-data: + schema: + type: object + properties: + policyFile: + type: string + format: binary + metadata: + type: string + format: binary + responses: + '200': + description: Successful operation. + content: + application/json: + schema: + type: object + '401': + description: Unauthorized + '403': + description: Forbidden. + '500': + description: Internal server error. + content: + application/json: + schema: + $ref: '#/components/schemas/InternalServerErrorDTO' + tags: *ref_7 + security: + - bearerAuth: [] + - bearer: [] /policies/import/file/preview: post: tags: