diff --git a/Methodology Library/iREC/Policies/iRec 10 Recipient (1684757087.809526003).policy b/Methodology Library/iREC/Policies/iRec 10 Recipient (1684757087.809526003).policy new file mode 100644 index 000000000..367af71f3 Binary files /dev/null and b/Methodology Library/iREC/Policies/iRec 10 Recipient (1684757087.809526003).policy differ diff --git a/Methodology Library/iREC/Policies/iRec 10 Source (1684756995.238994037).policy b/Methodology Library/iREC/Policies/iRec 10 Source (1684756995.238994037).policy new file mode 100644 index 000000000..d5b6ef62f Binary files /dev/null and b/Methodology Library/iREC/Policies/iRec 10 Source (1684756995.238994037).policy differ diff --git a/common/src/database-modules/database-server.ts b/common/src/database-modules/database-server.ts index 239229c14..243a8aa91 100644 --- a/common/src/database-modules/database-server.ts +++ b/common/src/database-modules/database-server.ts @@ -22,7 +22,8 @@ import { PolicyModule, Tag, TagCache, - Contract as ContractCollection + Contract as ContractCollection, + ExternalDocument } from '../entity'; import { Binary } from 'bson'; import { @@ -77,6 +78,7 @@ export class DatabaseServer { this.classMap.set(SplitDocuments, 'SplitDocuments'); this.classMap.set(Tag, 'Tag'); this.classMap.set(TagCache, 'TagCache'); + this.classMap.set(ExternalDocument, 'ExternalDocument'); } /** @@ -1361,6 +1363,69 @@ export class DatabaseServer { }); } + /** + * Get External Topic + * @param policyId + * @param blockId + * @param userId + * + * @virtual + */ + public async getExternalTopic( + policyId: string, + blockId: string, + userId: string + ): Promise { + return await this.findOne(ExternalDocument, { + where: { + policyId: { $eq: policyId }, + blockId: { $eq: blockId }, + owner: { $eq: userId } + } + }); + } + + /** + * Create External Topic + * @param row + * + * @virtual + */ + public async createExternalTopic(row: any): Promise { + const item = this.create(ExternalDocument, row); + return await this.save(ExternalDocument, item); + } + + /** + * Update External Topic + * @param row + * + * @virtual + */ + public async updateExternalTopic(item: ExternalDocument): Promise { + return await this.save(ExternalDocument, item); + } + + /** + * Get Active External Topic + * @param policyId + * @param blockId + * + * @virtual + */ + public async getActiveExternalTopics( + policyId: string, + blockId: string + ): Promise { + return await this.find(ExternalDocument, { + where: { + policyId: { $eq: policyId }, + blockId: { $eq: blockId }, + active: { $eq: true } + } + }); + } + /** * Create tag * @param tag diff --git a/common/src/entity/dry-run.ts b/common/src/entity/dry-run.ts index f316389ee..a796ee476 100644 --- a/common/src/entity/dry-run.ts +++ b/common/src/entity/dry-run.ts @@ -308,7 +308,7 @@ export class DryRun extends BaseEntity { /** * IRI */ - @Property({ nullable: true, type: 'unknown'}) + @Property({ nullable: true, type: 'unknown' }) iri?: any; /** @@ -551,6 +551,60 @@ export class DryRun extends BaseEntity { @Property({ nullable: true }) sourceDocumentId?: ObjectId; + /** + * Document Topic Id + */ + @Property({ nullable: true, type: 'unknown' }) + documentTopicId?: any; + + /** + * Policy Topic Id + */ + @Property({ nullable: true, type: 'unknown' }) + policyTopicId?: any; + + /** + * Document Message + */ + @Property({ nullable: true, type: 'unknown' }) + documentMessage?: any; + + /** + * Policy Message + */ + @Property({ nullable: true, type: 'unknown' }) + policyMessage?: any; + + /** + * Policy Instance Message + */ + @Property({ nullable: true, type: 'unknown' }) + policyInstanceMessage?: any; + + /** + * Schemas + */ + @Property({ nullable: true, type: 'unknown' }) + schemas?: any; + + /** + * Schema Id + */ + @Property({ nullable: true, type: 'unknown' }) + schemaId?: any; + + /** + * Last Message + */ + @Property({ nullable: true, type: 'unknown' }) + lastMessage?: any; + + /** + * Last Update + */ + @Property({ nullable: true, type: 'unknown' }) + lastUpdate?: any; + /** * Default document values */ @@ -590,9 +644,9 @@ export class DryRun extends BaseEntity { if ( (typeof fieldValue === 'string' && fieldValue.length < - (+process.env - .DOCUMENT_CACHE_FIELD_LIMIT || - 100)) || + (+process.env + .DOCUMENT_CACHE_FIELD_LIMIT || + 100)) || typeof fieldValue === 'number' ) { ObjSet(newDocument, field, fieldValue); diff --git a/common/src/entity/external-document.ts b/common/src/entity/external-document.ts new file mode 100644 index 000000000..ca86b651a --- /dev/null +++ b/common/src/entity/external-document.ts @@ -0,0 +1,117 @@ +import { BeforeCreate, Entity, Property } from '@mikro-orm/core'; +import { BaseEntity } from '../models'; + +/** + * Artifact collection + */ +@Entity() +export class ExternalDocument extends BaseEntity { + /** + * Block UUID + */ + @Property({ nullable: true }) + blockId?: string; + + /** + * Policy id + */ + @Property({ + nullable: true, + index: true, + }) + policyId?: string; + + /** + * User + */ + @Property({ nullable: true }) + owner?: string; + + /** + * Document Topic Id + */ + @Property({ nullable: true }) + documentTopicId?: string; + + /** + * Policy Topic Id + */ + @Property({ nullable: true }) + policyTopicId?: string; + + /** + * Instance Topic Id + */ + @Property({ nullable: true }) + instanceTopicId?: string; + + /** + * Document Message + */ + @Property({ nullable: true, type: 'unknown' }) + documentMessage?: any; + + /** + * Policy Message + */ + @Property({ nullable: true, type: 'unknown' }) + policyMessage?: any; + + /** + * Policy Instance Message + */ + @Property({ nullable: true, type: 'unknown' }) + policyInstanceMessage?: any; + + /** + * Schemas + */ + @Property({ nullable: true, type: 'unknown' }) + schemas?: any[]; + + /** + * Schema + */ + @Property({ nullable: true, type: 'unknown' }) + schema?: any; + + /** + * Schema Id + */ + @Property({ nullable: true }) + schemaId?: string; + + /** + * Status + */ + @Property({ nullable: true }) + active?: boolean; + + /** + * Last Message + */ + @Property({ nullable: true }) + lastMessage?: string; + + /** + * Last Update + */ + @Property({ nullable: true }) + lastUpdate?: string; + + /** + * Status + */ + @Property({ nullable: true }) + status?: string; + + /** + * Default document values + */ + @BeforeCreate() + setDefaults() { + this.lastMessage = this.lastMessage || ''; + this.lastUpdate = this.lastUpdate || ''; + this.active = this.active || false; + } +} diff --git a/common/src/entity/index.ts b/common/src/entity/index.ts index b9290e8a5..0cdaf2895 100644 --- a/common/src/entity/index.ts +++ b/common/src/entity/index.ts @@ -24,4 +24,5 @@ export * from './token'; export * from './topic'; export * from './vc-document'; export * from './vp-document'; -export * from './theme'; \ No newline at end of file +export * from './theme'; +export * from './external-document'; \ No newline at end of file diff --git a/common/src/entity/vc-document.ts b/common/src/entity/vc-document.ts index 5665e1965..cdec7d763 100644 --- a/common/src/entity/vc-document.ts +++ b/common/src/entity/vc-document.ts @@ -10,7 +10,6 @@ import { Property, Enum, BeforeCreate, - Unique, OnLoad, BeforeUpdate, AfterDelete, @@ -27,10 +26,6 @@ import ObjSet from 'lodash.set'; * VC documents collection */ @Entity() -@Unique({ - properties: ['hash'], - options: { partialFilterExpression: { hash: { $type: 'string' } } }, -}) export class VcDocument extends BaseEntity implements IVCDocument { /** * Document owner @@ -64,7 +59,7 @@ export class VcDocument extends BaseEntity implements IVCDocument { */ @Property({ nullable: true, - // index: true + index: true }) hash?: string; diff --git a/common/src/hedera-modules/message/message-server.ts b/common/src/hedera-modules/message/message-server.ts index e29717103..635864a8b 100644 --- a/common/src/hedera-modules/message/message-server.ts +++ b/common/src/hedera-modules/message/message-server.ts @@ -227,7 +227,7 @@ export class MessageServer { message.setLang(MessageServer.lang); const time = await this.messageStartLog('Hedera'); const buffer = message.toMessage(); - const id = await new Workers().addRetryableTask({ + const timestamp = await new Workers().addRetryableTask({ type: WorkerTaskType.SEND_HEDERA, data: { topicId: this.topicId, @@ -241,7 +241,7 @@ export class MessageServer { } }, 10); await this.messageEndLog(time, 'Hedera'); - message.setId(id); + message.setId(timestamp); message.setTopicId(this.topicId); return message; } @@ -336,6 +336,8 @@ export class MessageServer { new Logger().info(`getTopicMessage, ${timeStamp}, ${topicId}, ${message}`, ['GUARDIAN_SERVICE']); const result = MessageServer.fromMessage(message, type); + result.setAccount(message.payer_account_id); + result.setIndex(message.sequence_number); result.setId(timeStamp); result.setTopicId(topicId); return result; @@ -346,12 +348,18 @@ export class MessageServer { * @param topicId * @param type * @param action + * @param timestamp * @private */ - private async getTopicMessages(topicId: string | TopicId, type?: MessageType, action?: MessageAction): Promise { + private async getTopicMessages( + topicId: string | TopicId, + type?: MessageType, + action?: MessageAction, + timestamp?: string + ): Promise { const { operatorId, operatorKey, dryRun } = this.clientOptions; - if(!topicId) { + if (!topicId) { throw new Error(`Invalid Topic Id`); } @@ -363,7 +371,8 @@ export class MessageServer { operatorId, operatorKey, dryRun, - topic + topic, + timestamp } }, 10); @@ -380,6 +389,8 @@ export class MessageServer { filter = filter && item.action === action; } if (filter) { + item.setAccount(message.payer_account_id); + item.setIndex(message.sequence_number); item.setId(message.id); item.setTopicId(topic); result.push(item); @@ -470,6 +481,55 @@ export class MessageServer { return await this.loadIPFS(message); } + /** + * Load documents + * @param message + */ + public async loadDocuments(messages: T[]): Promise { + for (const message of messages) { + const urls = message.getUrls(); + const documents: any[] = []; + for (const url of urls) { + const doc = await this.getFile(url.cid, message.responseType); + documents.push(doc); + } + await message.loadDocuments(documents, this.clientOptions.operatorKey); + } + return messages; + } + + /** + * Load document + * @param message + */ + public static async loadDocument(message: T, cryptoKey?: string): Promise { + const urls = message.getUrls(); + const documents: any[] = []; + for (const url of urls) { + const doc = await IPFS.getFile(url.cid, message.responseType); + documents.push(doc); + } + await message.loadDocuments(documents, cryptoKey); + return message; + } + + /** + * Load documents + * @param message + */ + public static async loadDocuments(messages: T[], cryptoKey?: string): Promise { + for (const message of messages) { + const urls = message.getUrls(); + const documents: any[] = []; + for (const url of urls) { + const doc = await IPFS.getFile(url.cid, message.responseType); + documents.push(doc); + } + await message.loadDocuments(documents, cryptoKey); + } + return messages; + } + /** * Find topic * @param messageId @@ -496,4 +556,88 @@ export class MessageServer { return null; } } + + /** + * Get messages + * @param topicId + * @param type + * @param action + */ + public static async getMessages( + topicId: string | TopicId, + type?: MessageType, + action?: MessageAction, + timestamp?: string + ): Promise { + if (!topicId) { + throw new Error(`Invalid Topic Id`); + } + const topic = topicId.toString(); + const workers = new Workers(); + const messages = await workers.addRetryableTask({ + type: WorkerTaskType.GET_TOPIC_MESSAGES, + data: { + topic, + timestamp + } + }, 10); + new Logger().info(`getTopicMessages, ${topic}`, ['GUARDIAN_SERVICE']); + const result: Message[] = []; + for (const message of messages) { + try { + const item = MessageServer.fromMessage(message.message); + let filter = true; + if (type) { + filter = filter && item.type === type; + } + if (action) { + filter = filter && item.action === action; + } + if (filter) { + item.setAccount(message.payer_account_id); + item.setIndex(message.sequence_number); + item.setId(message.id); + item.setTopicId(topic); + result.push(item); + } + } catch (error) { + continue; + } + } + return result as T[]; + } + + /** + * Get messages + * @param topicId + */ + public static async getTopic(topicId: string | TopicId): Promise { + if (!topicId) { + throw new Error(`Invalid Topic Id`); + } + const topic = topicId.toString(); + const workers = new Workers(); + const message = await workers.addRetryableTask({ + type: WorkerTaskType.GET_TOPIC_MESSAGE_BY_INDEX, + data: { + topic, + index: 1 + } + }, 10); + new Logger().info(`getTopic, ${topic}`, ['GUARDIAN_SERVICE']); + try { + const json = JSON.parse(message.message); + if (json.type === MessageType.Topic) { + const item = TopicMessage.fromMessageObject(json); + item.setAccount(message.payer_account_id); + item.setIndex(message.sequence_number); + item.setId(message.id); + item.setTopicId(topic); + return item; + } + return null; + } catch (error) { + return null; + } + } } diff --git a/common/src/hedera-modules/message/message.ts b/common/src/hedera-modules/message/message.ts index c60e46618..97e4e9c84 100644 --- a/common/src/hedera-modules/message/message.ts +++ b/common/src/hedera-modules/message/message.ts @@ -49,6 +49,14 @@ export abstract class Message { * Message type */ public type: MessageType; + /** + * Payer + */ + public payer: string; + /** + * Index + */ + public index: string | number; /** * Response type @@ -157,6 +165,22 @@ export abstract class Message { this.topicId = topicId; } + /** + * Set payer + * @param payer + */ + public setAccount(payer: string): void { + this.payer = payer; + } + + /** + * Set index + * @param index + */ + public setIndex(index: string | number): void { + this.index = index; + } + /** * Get URL */ diff --git a/common/src/helpers/migration.ts b/common/src/helpers/migration.ts index 58bc519b2..b48df89b5 100644 --- a/common/src/helpers/migration.ts +++ b/common/src/helpers/migration.ts @@ -1,12 +1,24 @@ import { MikroORM } from '@mikro-orm/core'; +import { MongoDriver } from '@mikro-orm/mongodb'; /** * Define migration process - * @param initalConfig Config + * @param initConfig Config */ -export async function Migration(initalConfig: any) { - const orm = await MikroORM.init(initalConfig); - const migrator = orm.getMigrator(); - await migrator.up(); - await orm.close(true); +export async function Migration(initConfig: any, migrations?: string[]) { + const orm = await MikroORM.init(initConfig); + const migrator = orm.getMigrator(); + const executedMigrations = await migrator.getExecutedMigrations(); + const executeOldMigrations = async (name: string) => { + if (!executedMigrations.some((migration) => migration.name === name)) { + await migrator.up(name); + } + }; + if (migrations) { + for (const name of migrations) { + await executeOldMigrations(name); + } + } + await migrator.up(); + return orm; }; \ No newline at end of file diff --git a/frontend/src/app/modules/policy-engine/policy-configuration/common-property/common-property.component.ts b/frontend/src/app/modules/policy-engine/policy-configuration/common-property/common-property.component.ts index 620fd4085..2333d6ad0 100644 --- a/frontend/src/app/modules/policy-engine/policy-configuration/common-property/common-property.component.ts +++ b/frontend/src/app/modules/policy-engine/policy-configuration/common-property/common-property.component.ts @@ -85,6 +85,8 @@ export class CommonPropertyComponent implements OnInit { } this.childrenBlocks = this.allBlocks.filter(item => item.parent === this.data?.id); this.schemas = moduleVariables?.schemas || []; + } else if(this.property.type === 'Schemas') { + this.schemas = moduleVariables?.schemas || []; } if (this.property.type !== 'Group' && this.property.type !== 'Array') { if (this.property.default && !this.data.hasOwnProperty(this.property.name)) { diff --git a/frontend/src/app/modules/policy-engine/policy-configuration/policy-tree/policy-tree.component.ts b/frontend/src/app/modules/policy-engine/policy-configuration/policy-tree/policy-tree.component.ts index e728a49b5..c8af82de9 100644 --- a/frontend/src/app/modules/policy-engine/policy-configuration/policy-tree/policy-tree.component.ts +++ b/frontend/src/app/modules/policy-engine/policy-configuration/policy-tree/policy-tree.component.ts @@ -1,4 +1,4 @@ -import { Component, ComponentFactoryResolver, ElementRef, EventEmitter, Input, OnInit, Output, SimpleChanges, ViewChild, ViewContainerRef } from '@angular/core'; +import { Component, ComponentFactoryResolver, ElementRef, EventEmitter, HostListener, Input, OnInit, Output, SimpleChanges, ViewChild, ViewContainerRef } from '@angular/core'; import { FlatBlockNode } from '../../structures/tree-model/block-node'; import { CdkDropList } from '@angular/cdk/drag-drop'; import { PolicyBlockModel, BlocLine, BlockRect, EventCanvas, PolicyModel, PolicyModuleModel } from '../../structures'; @@ -67,6 +67,7 @@ export class PolicyTreeComponent implements OnInit { private _allCollapse: string = '2'; private _visibleMoveActions: string = '0'; + private _resizeTimer: any = null; constructor( private registeredService: RegisteredService, @@ -605,7 +606,19 @@ export class PolicyTreeComponent implements OnInit { return { type, data }; } - public blockStyle(node: FlatBlockNode):any { + public blockStyle(node: FlatBlockNode): any { return this.themeService.getStyle(node.node); } + + @HostListener('window:resize', ['$event']) + public onResize(event:any) { + if(this._resizeTimer) { + clearTimeout(this._resizeTimer); + this._resizeTimer = null; + } + this._resizeTimer = setTimeout(() => { + this._resizeTimer = null; + this.render(); + }, 200); + } } diff --git a/frontend/src/app/modules/policy-engine/policy-engine.module.ts b/frontend/src/app/modules/policy-engine/policy-engine.module.ts index 5cb121fbd..28cd69f4b 100644 --- a/frontend/src/app/modules/policy-engine/policy-engine.module.ts +++ b/frontend/src/app/modules/policy-engine/policy-engine.module.ts @@ -95,6 +95,7 @@ import { NewPolicyDialog } from './helpers/new-policy-dialog/new-policy-dialog.c import { PolicySettingsComponent } from './policy-configuration/policy-settings/policy-settings.component'; import { ImportFileDialog } from './helpers/import-file-dialog/import-file-dialog.component'; import { NewThemeDialog } from './helpers/new-theme-dialog/new-theme-dialog.component'; +import { ExternalTopicBlockComponent } from './policy-viewer/blocks/external-topic-block/external-topic-block.component'; import { UploadDocumentBlockComponent } from './policy-viewer/blocks/upload-document-block/upload-document-block.component'; @NgModule({ @@ -175,7 +176,8 @@ import { UploadDocumentBlockComponent } from './policy-viewer/blocks/upload-docu NewPolicyDialog, PolicySettingsComponent, ImportFileDialog, - NewThemeDialog + NewThemeDialog, + ExternalTopicBlockComponent ], imports: [ CommonModule, diff --git a/frontend/src/app/modules/policy-engine/policy-viewer/blocks/external-topic-block/external-topic-block.component.css b/frontend/src/app/modules/policy-engine/policy-viewer/blocks/external-topic-block/external-topic-block.component.css new file mode 100644 index 000000000..bc43569e7 --- /dev/null +++ b/frontend/src/app/modules/policy-engine/policy-viewer/blocks/external-topic-block/external-topic-block.component.css @@ -0,0 +1,208 @@ +.loading { + background: #fff; + position: fixed; + z-index: 99; + top: var(--header-height-policy); + left: 0; + bottom: 0; + right: 0; + display: flex; + align-items: center; + justify-items: center; + justify-content: center; + align-content: center; +} + +.sub-loading { + background: #fff; + position: absolute; + z-index: 9; + top: 0; + left: 0; + bottom: 0; + right: 0; + display: flex; + align-items: center; + justify-items: center; + justify-content: center; + align-content: center; + flex-direction: column; +} + +.page-content { + width: 800px; + min-height: 500px; + margin: 40px auto; + padding: 10px 10px 10px 10px; + border: 1px solid #c3c3c3; + border-radius: 4px; + position: relative; +} + +.form-content { + min-height: 350px; + width: 100%; + padding: 20px 0px; + position: relative; +} + +.form-buttons { + width: 100%; + position: relative; + height: 50px; + text-align: end; +} + +.form-buttons button { + margin-left: 18px; +} + +.content { + position: relative; +} + +.page { + width: 650px; + min-height: 150px; + margin: 40px auto; + padding: 10px; + border: 1px solid #c3c3c3; + border-radius: 4px; + position: relative; +} + +.form { + background: #fff; + color: #404040; + margin-bottom: 30px; + padding: 10px 35px; + display: flex; + flex-direction: column; +} + +.field { + display: flex; + flex-direction: row; + padding: 10px 20px; +} + +.field-name { + display: table-cell; + font-weight: bold; + color: #686868; + border-top: 2px solid #fff; + padding-top: 2px; + padding-bottom: 2px; + box-sizing: border-box; + min-width: 150px; + height: 30px; +} + +.field-value { + display: table-cell; + border-top: 2px solid #fff; + padding-top: 4px; + padding-bottom: 4px; + padding-right: 36px; + padding-left: 8px; + min-width: 240px; + position: relative; +} + +.schema-status-NOT_VERIFIED { + font-weight: 500; + text-align: end; +} + +.schema-status-INCOMPATIBLE { + color: #f44336; + font-weight: 500; + text-align: end; +} + +.schema-status-COMPATIBLE { + color: #4caf50; + font-weight: 500; + text-align: end; +} + +.form-link { + color: #0000ee; + width: 110px; + display: inline-block; + cursor: pointer; + padding: 10px 4px; +} + +.schema-row { + display: grid; + grid-template-columns: 40px auto 150px; + height: 40px; +} + +.schema-row>div { + padding: 4px; +} + +.synchronization { + padding: 0px 30px 0px 0px; + height: 24px; + position: relative; + width: auto; + width: fit-content; +} + +.synchronization-time { + width: auto; +} + +.synchronization-update { + position: absolute; + top: -3px; + right: 0px; + display: block; + color: #005edf; + cursor: pointer; + height: 24px; + width: 24px; + line-height: 24px; + border-radius: 4px; +} + +.synchronization-update mat-icon { + position: absolute; + pointer-events: none; + font-size: 22px; + height: 22px; + width: 22px; + left: 1px; + top: 1px; +} + +.synchronization-update:hover { + background: #f1f1f1; +} + +.text { + padding-bottom: 16px; + padding-left: 15px; +} + +.text.note { + padding-bottom: 16px; +} + +.mat-form-field { + width: 100%; + padding-left: 15px; + box-sizing: border-box; +} + +.error-text { + padding-bottom: 24px; + font-size: 20px; +} + +.loading-text { + font-size: 18px; +} \ No newline at end of file diff --git a/frontend/src/app/modules/policy-engine/policy-viewer/blocks/external-topic-block/external-topic-block.component.html b/frontend/src/app/modules/policy-engine/policy-viewer/blocks/external-topic-block/external-topic-block.component.html new file mode 100644 index 000000000..947721824 --- /dev/null +++ b/frontend/src/app/modules/policy-engine/policy-viewer/blocks/external-topic-block/external-topic-block.component.html @@ -0,0 +1,229 @@ +
+ +
+ +
+ + + description + + + +
+
+
+ Enter Hedera topic ID which contains messages about ingress VC documents. +
+ + * Topic Id + + +
+
+ +
+
+
+ + +
+
+ Selected topic belongs to the following policy: +
+
+
Policy Topic Id
+
{{policyTopicId}}
+
+
+
Policy Name
+
{{policyInstanceMessage.name}}
+
+
+
Policy Version
+
{{policyInstanceMessage.version}}
+
+
+
Instance Topic Id
+
{{instanceTopicId}}
+
+
+
Document Topic
+
{{documentMessage.name}}
+
+
+
Document Topic Id
+
{{documentTopicId}}
+
+
+
+ + +
+
+ + +
+
+
+ Select the ingress documents schema. Note: selected schema must be compatible with the current + policy. +
+
+ Only documents conforming to the schema would be accepted into the system. +
+ +
+
+ +
+
{{s.name}}
+
Need + verification
+
Incompatible +
+
Verified
+
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
Policy Topic Id
+
{{policyTopicId}}
+
+
+
Policy Name
+
{{policyInstanceMessage.name}}
+
+
+
Policy Version
+
{{policyInstanceMessage.version}}
+
+
+
Instance Topic Id
+
{{instanceTopicId}}
+
+
+
Document Topic
+
{{documentMessage.name}}
+
+
+
Document Topic Id
+
{{documentTopicId}}
+
+
+
Schema
+
{{schema.name}}
+
+
+
+ + +
+
+
+
+ +
+
+
Processing...
+
+
+ +
+
+
Search...
+
+
+ +
+
+
Verification...
+
+
+ +
+
+
+ An unexpected error occurred. +
+
+ +
+
+
+ +
+
+
+ Successfully registered source topic. Target documents will be loaded accordingly to the schedule. +
+
+ Note: manual refreshes can be trigged as required. +
+
+
Policy Topic Id
+
{{policyTopicId}}
+
+
+
Policy Name
+
{{policyInstanceMessage.name}}
+
+
+
Policy Version
+
{{policyInstanceMessage.version}}
+
+
+
Instance Topic Id
+
{{instanceTopicId}}
+
+
+
Document Topic
+
{{documentMessage.name}}
+
+
+
Document Topic Id
+
{{documentTopicId}}
+
+
+
Schema
+
{{schema.name}}
+
+
+
Last Update
+
+
+
{{lastUpdate}}
+
+ - +
+
+ sync +
+
+ +
+
+
+
\ No newline at end of file diff --git a/frontend/src/app/modules/policy-engine/policy-viewer/blocks/external-topic-block/external-topic-block.component.ts b/frontend/src/app/modules/policy-engine/policy-viewer/blocks/external-topic-block/external-topic-block.component.ts new file mode 100644 index 000000000..4fa3ed45e --- /dev/null +++ b/frontend/src/app/modules/policy-engine/policy-viewer/blocks/external-topic-block/external-topic-block.component.ts @@ -0,0 +1,222 @@ +import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; +import { FormBuilder, Validators } from '@angular/forms'; +import { PolicyEngineService } from 'src/app/services/policy-engine.service'; +import { PolicyHelper } from 'src/app/services/policy-helper.service'; +import { WebSocketService } from 'src/app/services/web-socket.service'; + +/** + * Component for display block of 'external-topic' type. + */ +@Component({ + selector: 'external-topic-block', + templateUrl: './external-topic-block.component.html', + styleUrls: ['./external-topic-block.component.css'] +}) +export class ExternalTopicBlockComponent implements OnInit { + @Input('id') id!: string; + @Input('policyId') policyId!: string; + @Input('static') static!: any; + + loading: boolean = true; + socket: any; + + public documentTopicId: string | null = null; + public instanceTopicId: string | null = null; + public policyTopicId: string | null = null; + public documentMessage: any = null; + public policyInstanceMessage: any = null; + public policyMessage: any = null; + public schemas: any[] | null = null; + public schema: any = null; + public lastUpdate: string | null = null; + public status: string | null = null; + public stepIndex: number = 0; + public completed: boolean[] = []; + public editable: boolean[] = []; + + public topicForm = this.fb.group({ + topicId: ['0.0.4605481', Validators.required], + }); + public schemaForm = this.fb.group({ + schemaId: ['', Validators.required], + }); + + constructor( + private policyEngineService: PolicyEngineService, + private wsService: WebSocketService, + private policyHelper: PolicyHelper, + private fb: FormBuilder, + private changeDetector: ChangeDetectorRef, + ) { + this.documentTopicId = null; + this.instanceTopicId = null; + this.policyTopicId = null; + this.documentMessage = null; + this.policyInstanceMessage = null; + this.policyMessage = null; + this.schemas = null; + this.schema = null; + this.lastUpdate = null; + } + + ngOnInit(): void { + if (!this.static) { + this.socket = this.wsService.blockSubscribe(this.onUpdate.bind(this)); + } + this.loadData(); + } + + ngOnDestroy(): void { + if (this.socket) { + this.socket.unsubscribe(); + } + } + + onUpdate(blocks: string[]): void { + if (Array.isArray(blocks) && blocks.includes(this.id)) { + this.loadData(); + } + } + + loadData() { + this.loading = true; + this.status = null; + if (this.static) { + this.setData(this.static); + setTimeout(() => { + this.loading = false; + }, 500); + } else { + this.policyEngineService.getBlockData(this.id, this.policyId).subscribe((data: any) => { + this.setData(data); + setTimeout(() => { + this.loading = false; + }, 1000); + }, (e) => { + console.error(e.error); + this.loading = false; + }); + } + } + + setData(data: any) { + if (data) { + this.status = data.status || 'NEED_TOPIC'; + this.documentTopicId = data.documentTopicId; + this.instanceTopicId = data.instanceTopicId; + this.policyTopicId = data.policyTopicId; + this.documentMessage = data.documentMessage; + this.policyInstanceMessage = data.policyInstanceMessage; + this.policyMessage = data.policyMessage; + this.schemas = data.schemas || []; + this.schema = data.schema; + this.lastUpdate = data.lastUpdate; + switch (this.status) { + case 'NEED_TOPIC': + this.topicForm.reset(); + this.stepIndex = 0; + this.completed = [false, false, false, false, false]; + this.editable = [true, false, false, false, false]; + break; + case 'NEED_SCHEMA': + this.schemaForm.reset(); + const index = this.schemas?.findIndex(s=>s.status === 'INCOMPATIBLE' || s.status === 'COMPATIBLE'); + this.stepIndex = index === -1 ? 1 : 2; + this.completed = [true, true, false, false, false]; + this.editable = [false, true, true, false, false]; + break; + case 'FREE': + this.stepIndex = 3; + this.completed = [true, true, true, true, true]; + this.editable = [false, false, false, false, false]; + break; + default: + this.stepIndex = 0; + this.completed = [false, false, false, false, false]; + this.editable = [false, false, false, false, false]; + break; + } + } else { + this.status = null; + this.documentTopicId = null; + this.instanceTopicId = null; + this.policyTopicId = null; + this.documentMessage = null; + this.policyInstanceMessage = null; + this.policyMessage = null; + this.schemas = null; + this.schema = null; + this.lastUpdate = null; + this.stepIndex = 0; + } + } + + public setTopic() { + this.loading = true; + const form = this.topicForm.value; + const data = { + operation: 'SetTopic', + value: form?.topicId + }; + this.policyEngineService.setBlockData(this.id, this.policyId, data).subscribe(() => { + this.loadData(); + }, (e) => { + console.error(e.error); + this.loading = false; + }); + } + + public setSchema() { + this.loading = true; + const form = this.schemaForm.value; + const data = { + operation: 'SetSchema', + value: form?.schemaId + }; + this.policyEngineService.setBlockData(this.id, this.policyId, data).subscribe(() => { + this.loadData(); + }, (e) => { + console.error(e.error); + this.loading = false; + }); + } + + public onRefresh() { + this.loading = true; + const data = { + operation: 'LoadDocuments' + }; + this.policyEngineService.setBlockData(this.id, this.policyId, data).subscribe(() => { + this.loadData(); + }, (e) => { + console.error(e.error); + this.loading = false; + }); + } + + public restart() { + this.loading = true; + const data = { + operation: 'Restart' + }; + this.policyEngineService.setBlockData(this.id, this.policyId, data).subscribe(() => { + this.loadData(); + }, (e) => { + console.error(e.error); + this.loading = false; + }); + } + + public verificationAll() { + this.loading = true; + const data = { + operation: 'VerificationSchemas' + }; + this.policyEngineService.setBlockData(this.id, this.policyId, data).subscribe(() => { + this.loadData(); + }, (e) => { + console.error(e.error); + this.loading = false; + }); + } +} diff --git a/frontend/src/app/modules/policy-engine/policy-viewer/blocks/request-document-block/request-document-block.component.css b/frontend/src/app/modules/policy-engine/policy-viewer/blocks/request-document-block/request-document-block.component.css index 51c620885..7879ca981 100644 --- a/frontend/src/app/modules/policy-engine/policy-viewer/blocks/request-document-block/request-document-block.component.css +++ b/frontend/src/app/modules/policy-engine/policy-viewer/blocks/request-document-block/request-document-block.component.css @@ -65,7 +65,7 @@ margin: 0; padding: 0; padding-bottom: 70px; - max-height: calc(70 * var(--vh)) !important; + max-height: calc(65 * var(--vh)) !important; position: relative; overflow: visible; } diff --git a/frontend/src/app/modules/policy-engine/policy-viewer/blocks/token-confirmation-block/token-confirmation-block.component.html b/frontend/src/app/modules/policy-engine/policy-viewer/blocks/token-confirmation-block/token-confirmation-block.component.html index 866250eac..0aca1e58e 100644 --- a/frontend/src/app/modules/policy-engine/policy-viewer/blocks/token-confirmation-block/token-confirmation-block.component.html +++ b/frontend/src/app/modules/policy-engine/policy-viewer/blocks/token-confirmation-block/token-confirmation-block.component.html @@ -26,7 +26,6 @@ I have {{action === 'associate' ? 'associated' : action === 'dissociate' ? 'dissociated' : action}} manually - diff --git a/frontend/src/app/modules/policy-engine/services/blocks-information.ts b/frontend/src/app/modules/policy-engine/services/blocks-information.ts index c185f0878..b2eae1b14 100644 --- a/frontend/src/app/modules/policy-engine/services/blocks-information.ts +++ b/frontend/src/app/modules/policy-engine/services/blocks-information.ts @@ -40,6 +40,9 @@ import { MultiSignBlockComponent } from '../policy-viewer/blocks/multi-sign-bloc import { CreateTokenConfigComponent } from '../policy-configuration/blocks/tokens/create-token-config/create-token-config.component'; import { CreateTokenBlockComponent } from '../policy-viewer/blocks/create-token-block/create-token-block.component'; import { HttpRequestConfigComponent } from '../policy-configuration/blocks/main/http-request-config/http-request-config.component'; +import { ExternalTopicBlockComponent } from '../policy-viewer/blocks/external-topic-block/external-topic-block.component'; +import { UploadDocumentBlockComponent } from '../policy-viewer/blocks/upload-document-block/upload-document-block.component'; + import { BlockType, BlockGroup, @@ -48,7 +51,6 @@ import { IBlockSetting } from "../structures"; import { TagsManagerBlockComponent } from '../policy-viewer/blocks/tags-manager-block/tags-manager-block.component'; -import { UploadDocumentBlockComponent } from '../policy-viewer/blocks/upload-document-block/upload-document-block.component'; const Container: IBlockSetting = { type: BlockType.Container, @@ -87,7 +89,8 @@ const Container: IBlockSetting = { { type: BlockType.DocumentValidatorBlock }, { type: BlockType.MultiSignBlock }, { type: BlockType.CreateToken }, - { type: BlockType.SplitBlock } + { type: BlockType.SplitBlock }, + { type: BlockType.ExternalTopic } ] } @@ -128,7 +131,8 @@ const Step: IBlockSetting = { { type: BlockType.DocumentValidatorBlock }, { type: BlockType.MultiSignBlock }, { type: BlockType.CreateToken }, - { type: BlockType.SplitBlock } + { type: BlockType.SplitBlock }, + { type: BlockType.ExternalTopic } ] } @@ -334,6 +338,19 @@ const ExternalData: IBlockSetting = { }] } +const ExternalTopic: IBlockSetting = { + type: BlockType.ExternalTopic, + icon: 'cloud', + group: BlockGroup.Documents, + header: BlockHeaders.UIComponents, + factory: ExternalTopicBlockComponent, + property: null, + allowedChildren: [{ + type: BlockType.DocumentValidatorBlock, + group: BlockGroup.UnGrouped + }] +} + const AggregateDocument: IBlockSetting = { type: BlockType.AggregateDocument, icon: 'calendar_month', @@ -629,5 +646,6 @@ export default [ ReportItem, HistoryAddon, SelectiveAttributes, - TagManager + TagManager, + ExternalTopic ]; diff --git a/frontend/src/app/modules/policy-engine/structures/types/block-type.type.ts b/frontend/src/app/modules/policy-engine/structures/types/block-type.type.ts index bf83fc48d..4230295e1 100644 --- a/frontend/src/app/modules/policy-engine/structures/types/block-type.type.ts +++ b/frontend/src/app/modules/policy-engine/structures/types/block-type.type.ts @@ -40,4 +40,5 @@ export enum BlockType { SelectiveAttributes = 'selectiveAttributes', Module = 'module', TagsManager = 'tagsManager', + ExternalTopic = 'externalTopicBlock', } diff --git a/frontend/src/app/modules/policy-engine/themes/default.ts b/frontend/src/app/modules/policy-engine/themes/default.ts index 3589d5377..281b32be6 100644 --- a/frontend/src/app/modules/policy-engine/themes/default.ts +++ b/frontend/src/app/modules/policy-engine/themes/default.ts @@ -27,7 +27,8 @@ export const defaultTheme = { 'policyRolesBlock', 'interfaceStepBlock', 'tagsManager', - 'tokenConfirmationBlock' + 'tokenConfirmationBlock', + 'externalTopicBlock' ] }, { diff --git a/frontend/src/app/modules/schema-engine/document-view/document-view.component.html b/frontend/src/app/modules/schema-engine/document-view/document-view.component.html index 528ae36fc..3efda30bd 100644 --- a/frontend/src/app/modules/schema-engine/document-view/document-view.component.html +++ b/frontend/src/app/modules/schema-engine/document-view/document-view.component.html @@ -2,7 +2,7 @@
-
+
Id
diff --git a/frontend/src/app/modules/schema-engine/document-view/document-view.component.ts b/frontend/src/app/modules/schema-engine/document-view/document-view.component.ts index 98a75b51d..f97d83583 100644 --- a/frontend/src/app/modules/schema-engine/document-view/document-view.component.ts +++ b/frontend/src/app/modules/schema-engine/document-view/document-view.component.ts @@ -32,6 +32,10 @@ export class DocumentViewComponent implements OnInit { } ngOnInit(): void { + if(!this.document) { + return; + } + this.issuerOptions = []; this.proofJson = this.document.proof ? JSON.stringify(this.document.proof, null, 4) diff --git a/frontend/src/app/modules/schema-engine/vc-dialog/vc-dialog.component.css b/frontend/src/app/modules/schema-engine/vc-dialog/vc-dialog.component.css index b89ee2e42..2a8175e7d 100644 --- a/frontend/src/app/modules/schema-engine/vc-dialog/vc-dialog.component.css +++ b/frontend/src/app/modules/schema-engine/vc-dialog/vc-dialog.component.css @@ -8,13 +8,14 @@ margin-bottom: 8px; font-family: monospace; cursor: text; - height: 640px; + height: 630px; width: 780px; overflow: auto; border: 1px solid #eee; border-radius: 4px; padding: 10px; box-sizing: border-box; + margin-top: 10px; } .content { @@ -26,6 +27,7 @@ .dialog-content { overflow: hidden; + width: 100%; } .view-toggle { diff --git a/frontend/src/app/modules/schema-engine/vc-dialog/vc-dialog.component.ts b/frontend/src/app/modules/schema-engine/vc-dialog/vc-dialog.component.ts index 80ca0a0a7..a791727b0 100644 --- a/frontend/src/app/modules/schema-engine/vc-dialog/vc-dialog.component.ts +++ b/frontend/src/app/modules/schema-engine/vc-dialog/vc-dialog.component.ts @@ -41,10 +41,14 @@ export class VCViewerDialog { toggle } = this.data; this.title = title; - this.json = JSON.stringify((document), null, 4); + this.json = document ? JSON.stringify((document), null, 4) : ''; this.document = document; this.type = type || 'JSON'; this.toggle = toggle !== false; + if(!this.document) { + this.type = 'JSON'; + this.toggle = false; + } this.isVcDocument = false; this.isVpDocument = false; diff --git a/guardian-service/src/api/artifact.service.ts b/guardian-service/src/api/artifact.service.ts index 8f4068813..0dd36f857 100644 --- a/guardian-service/src/api/artifact.service.ts +++ b/guardian-service/src/api/artifact.service.ts @@ -73,6 +73,7 @@ export async function artifactAPI(): Promise { otherOptions.limit = _pageSize; otherOptions.offset = _pageIndex * _pageSize; } else { + otherOptions.orderBy = { createDate: 'DESC' }; otherOptions.limit = 100; } diff --git a/guardian-service/src/api/contract.service.ts b/guardian-service/src/api/contract.service.ts index 82e0c551e..79cc92db5 100644 --- a/guardian-service/src/api/contract.service.ts +++ b/guardian-service/src/api/contract.service.ts @@ -55,6 +55,7 @@ export async function contractAPI( otherOptions.limit = Math.min(100, _pageSize); otherOptions.offset = _pageIndex * _pageSize; } else { + otherOptions.orderBy = { createDate: 'DESC' }; otherOptions.limit = 100; } @@ -109,6 +110,7 @@ export async function contractAPI( otherOptions.limit = Math.min(100, _pageSize); otherOptions.offset = _pageIndex * _pageSize; } else { + otherOptions.orderBy = { createDate: 'DESC' }; otherOptions.limit = 100; } diff --git a/guardian-service/src/api/documents.service.ts b/guardian-service/src/api/documents.service.ts index 49769a7b6..5e406b30f 100644 --- a/guardian-service/src/api/documents.service.ts +++ b/guardian-service/src/api/documents.service.ts @@ -1,8 +1,5 @@ import { - DidDocumentStatus, - DocumentSignature, - DocumentStatus, IDidObject, IVCDocument, MessageAPI, @@ -16,8 +13,7 @@ import { DidDocument, VcDocument, VpDocument, - Policy, - VcHelper + Policy } from '@guardian/common'; /** * Connect to the message broker methods of working with VC, VP and DID Documents @@ -32,38 +28,6 @@ export async function documentsAPI( vpDocumentRepository: DataBaseHelper, policyRepository: DataBaseHelper, ): Promise { - const getDIDOperation = (operation: DidDocumentStatus) => { - switch (operation) { - case DidDocumentStatus.CREATE: - return DidDocumentStatus.CREATE; - case DidDocumentStatus.DELETE: - return DidDocumentStatus.DELETE; - case DidDocumentStatus.UPDATE: - return DidDocumentStatus.UPDATE; - case DidDocumentStatus.FAILED: - return DidDocumentStatus.FAILED; - default: - return DidDocumentStatus.NEW; - } - } - - const getVCOperation = (operation: DocumentStatus) => { - switch (operation) { - case DocumentStatus.ISSUE: - return DocumentStatus.ISSUE; - case DocumentStatus.RESUME: - return DocumentStatus.RESUME; - case DocumentStatus.REVOKE: - return DocumentStatus.REVOKE; - case DocumentStatus.SUSPEND: - return DocumentStatus.SUSPEND; - case DocumentStatus.FAILED: - return DocumentStatus.FAILED; - default: - return DocumentStatus.NEW; - } - } - /** * Return DID Documents by DID * @@ -111,116 +75,6 @@ export async function documentsAPI( } }); - /** - * Create or update DID Documents - * - * @param {IDidDocument} payload - document - * @param {string} [payload.did] - did - * @param {string} [payload.operation] - document status - * - * @returns {IDidDocument} - new DID Document - */ - ApiResponse(MessageAPI.SET_DID_DOCUMENT, async (msg) => { - if (msg.did && msg.operation) { - const did = msg.did; - const operation = msg.operation; - const item = await didDocumentRepository.findOne({ did }); - if (item) { - item.status = getDIDOperation(operation); - const result: IDidObject = await didDocumentRepository.save(item); - return new MessageResponse(result); - } else { - return new MessageError('Document not found'); - } - } else if (Array.isArray(msg)) { - const result = [] - for (const documentObject of msg) { - result.push(await didDocumentRepository.save(documentObject)); - } - return new MessageResponse(result); - } - else { - const result: IDidObject = await didDocumentRepository.save(msg); - return new MessageResponse(result); - } - }); - - /** - * Create or update VC Documents - * - * @param {IVCDocument} payload - document - * @param {string} [payload.hash] - hash - * @param {string} [payload.operation] - document status - * - * @returns {IVCDocument} - new VC Document - */ - ApiResponse(MessageAPI.SET_VC_DOCUMENT, async (msg) => { - let result: IVCDocument; - - const hash = msg.hash; - if (hash) { - result = await vcDocumentRepository.findOne({ hash }); - } - - if (result) { - const operation = msg.operation; - if (operation) { - result.hederaStatus = getVCOperation(operation); - } - - const assignedTo = msg.assignedTo; - if (assignedTo) { - result.assignedTo = assignedTo; - } - - const type = msg.type; - if (type) { - result.type = type; - } - - const option = msg.option; - if (option) { - result.option = option; - } - } - - if (!result) { - if (msg.document) { - result = vcDocumentRepository.create(msg as VcDocument); - } else { - return new MessageError('Invalid document'); - } - } - - let verify: boolean; - try { - const VCHelper = new VcHelper(); - const res = await VCHelper.verifySchema(result.document); - verify = res.ok; - if (verify) { - verify = await VCHelper.verifyVC(result.document); - } - } catch (error) { - verify = false; - } - result.signature = verify ? DocumentSignature.VERIFIED : DocumentSignature.INVALID; - - result = await vcDocumentRepository.save(result); - return new MessageResponse(result); - }); - - /** - * Create new VP Document - * - * @param {IVPDocument} payload - document - * - * @returns {IVPDocument} - new VP Document - */ - ApiResponse(MessageAPI.SET_VP_DOCUMENT, async (msg) => { - const result: any = await vpDocumentRepository.save(msg); - return new MessageResponse(result); - }); - /** * Return VP Documents * @@ -263,7 +117,8 @@ export async function documentsAPI( } } else { const [items, count] = await vpDocumentRepository.findAndCount(null, { - limit: 100 + limit: 100, + orderBy: { createDate: 'DESC' } }); return new MessageResponse({ items, count }); } diff --git a/guardian-service/src/api/module.service.ts b/guardian-service/src/api/module.service.ts index 7a6cd3f1d..54e4cfd8f 100644 --- a/guardian-service/src/api/module.service.ts +++ b/guardian-service/src/api/module.service.ts @@ -261,6 +261,7 @@ export async function modulesAPI(): Promise { otherOptions.limit = _pageSize; otherOptions.offset = _pageIndex * _pageSize; } else { + otherOptions.orderBy = { createDate: 'DESC' }; otherOptions.limit = 100; } diff --git a/guardian-service/src/api/schema.service.ts b/guardian-service/src/api/schema.service.ts index 1c3272da8..4387c4cbb 100644 --- a/guardian-service/src/api/schema.service.ts +++ b/guardian-service/src/api/schema.service.ts @@ -183,6 +183,7 @@ export async function schemaAPI(): Promise { otherOptions.limit = Math.min(100, _pageSize); otherOptions.offset = _pageIndex * _pageSize; } else { + otherOptions.orderBy = { createDate: 'DESC' }; otherOptions.limit = 100; } @@ -548,6 +549,9 @@ export async function schemaAPI(): Promise { otherOptions.orderBy = { createDate: 'DESC' }; otherOptions.limit = _pageSize; otherOptions.offset = _pageIndex * _pageSize; + } else { + otherOptions.orderBy = { createDate: 'DESC' }; + otherOptions.limit = 100; } const [items, count] = await DatabaseServer.getSchemasAndCount(filter, otherOptions); return new MessageResponse({ diff --git a/guardian-service/src/api/trust-chain.service.ts b/guardian-service/src/api/trust-chain.service.ts index 3a9f3ed89..9b8cebe57 100644 --- a/guardian-service/src/api/trust-chain.service.ts +++ b/guardian-service/src/api/trust-chain.service.ts @@ -281,7 +281,7 @@ export async function trustChainAPI( const vpDocument = HVpDocument.fromJsonTree(root.document); const vcpDocument = vpDocument.getVerifiableCredential(0); const hashVc = vcpDocument.toCredentialHash(); - const vc = await vcDocumentRepository.findOne({ hash: hashVc }); + const vc = await vcDocumentRepository.findOne({ hash: hashVc, policyId }); await getParents(chain, vc, {}, policyId); await getPolicyInfo(chain, policyId); return new MessageResponse(chain); diff --git a/guardian-service/src/app.ts b/guardian-service/src/app.ts index 9ce370561..d1bb8c52e 100644 --- a/guardian-service/src/app.ts +++ b/guardian-service/src/app.ts @@ -38,8 +38,6 @@ import { } from '@guardian/common'; import { ApplicationStates, WorkerTaskType } from '@guardian/interfaces'; import { AccountId, PrivateKey, TopicId } from '@hashgraph/sdk'; -import { MikroORM } from '@mikro-orm/core'; -import { MongoDriver } from '@mikro-orm/mongodb'; import { ipfsAPI } from '@api/ipfs.service'; import { artifactAPI } from '@api/artifact.service'; import { sendKeysToVault } from '@helpers/send-keys-to-vault'; @@ -66,19 +64,21 @@ Promise.all([ path: 'dist/migrations', transactional: false }, - entities - }), - MikroORM.init({ - ...COMMON_CONNECTION_CONFIG, driverOptions: { useUnifiedTopology: true }, ensureIndexes: true, entities - }), + }, [ + 'v2-4-0', + 'v2-7-0', + 'v2-9-0', + 'v2-11-0', + 'v2-12-0' + ]), MessageBrokerChannel.connect('GUARDIANS_SERVICE') ]).then(async values => { - const [_, db, cn] = values; + const [db, cn] = values; DataBaseHelper.orm = db; DataBaseHelper.gridFS = new GridFSBucket( db.em.getDriver().getConnection().getDb() @@ -166,7 +166,7 @@ Promise.all([ } catch (error) { await new Logger().warn( 'HEDERA_CUSTOM_MIRROR_NODES field in settings: ' + - error.message, + error.message, ['GUARDIAN_SERVICE'] ); console.warn(error); diff --git a/guardian-service/src/migrations/v2-12-0.ts b/guardian-service/src/migrations/v2-12-0.ts index 7fb1eb956..9746f53e4 100644 --- a/guardian-service/src/migrations/v2-12-0.ts +++ b/guardian-service/src/migrations/v2-12-0.ts @@ -8,6 +8,7 @@ export class ReleaseMigration extends Migration { * Up migration */ async up(): Promise { + await this.updateVcIndexDocument(); await this.retireRequestDocument(); } @@ -47,4 +48,19 @@ export class ReleaseMigration extends Migration { } ); } + + /** + * Update index + */ + async updateVcIndexDocument() { + const vcDocumentCollection = this.getCollection('VcDocument'); + const listIndexes = vcDocumentCollection.listIndexes(); + while (await listIndexes.hasNext()) { + const index = await listIndexes.next(); + if (index.key.hash !== undefined) { + await vcDocumentCollection.dropIndex(index.name); + } + } + await vcDocumentCollection.createIndex({ hash: 1 }, { sparse: true }); + } } diff --git a/guardian-service/src/policy-engine/block-about.ts b/guardian-service/src/policy-engine/block-about.ts index 33736f3fb..893b7ebbf 100644 --- a/guardian-service/src/policy-engine/block-about.ts +++ b/guardian-service/src/policy-engine/block-about.ts @@ -970,6 +970,31 @@ export const BlockAbout = { 'input': null, 'output': null, 'defaultEvent': false + }, + 'externalTopicBlock': { + 'label': 'External Topic', + 'title': 'Add \'External Topic\' Block', + 'post': true, + 'get': true, + 'children': 'Special', + 'control': 'Server', + 'input': [ + 'TimerEvent', + ], + 'output': [ + 'RunEvent', + 'RefreshEvent', + 'ErrorEvent' + ], + 'defaultEvent': true, + 'properties': [ + { + 'name': 'schema', + 'label': 'Schema', + 'title': 'Schema', + 'type': 'Schemas' + }, + ] } } diff --git a/guardian-service/src/policy-engine/block-validators/block-validator.ts b/guardian-service/src/policy-engine/block-validators/block-validator.ts index e6fa6364e..d019ff5db 100644 --- a/guardian-service/src/policy-engine/block-validators/block-validator.ts +++ b/guardian-service/src/policy-engine/block-validators/block-validator.ts @@ -43,6 +43,7 @@ import { TokenConfirmationBlock } from './blocks/token-confirmation-block'; import { ModuleValidator } from './module-validator'; import { ModuleBlock } from './blocks/module'; import { TagsManagerBlock } from './blocks/tag-manager'; +import { ExternalTopicBlock } from './blocks/external-topic-block'; export const validators = [ InterfaceDocumentActionBlock, @@ -84,7 +85,8 @@ export const validators = [ TokenActionBlock, TokenConfirmationBlock, ModuleBlock, - TagsManagerBlock + TagsManagerBlock, + ExternalTopicBlock ]; /** diff --git a/guardian-service/src/policy-engine/block-validators/blocks/external-topic-block.ts b/guardian-service/src/policy-engine/block-validators/blocks/external-topic-block.ts new file mode 100644 index 000000000..90594a8e4 --- /dev/null +++ b/guardian-service/src/policy-engine/block-validators/blocks/external-topic-block.ts @@ -0,0 +1,33 @@ +import { BlockValidator, IBlockProp } from '@policy-engine/block-validators'; + +/** + * External topic block + */ +export class ExternalTopicBlock { + /** + * Block type + */ + public static readonly blockType: string = 'externalTopicBlock'; + + /** + * Validate block options + * @param validator + * @param config + */ + public static async validate(validator: BlockValidator, ref: IBlockProp): Promise { + try { + if (ref.options.schema) { + if (typeof ref.options.schema !== 'string') { + validator.addError('Option "schema" must be a string'); + return; + } + if (await validator.schemaNotExist(ref.options.schema)) { + validator.addError(`Schema with id "${ref.options.schema}" does not exist`); + return; + } + } + } catch (error) { + validator.addError(`Unhandled exception ${validator.getErrorMessage(error)}`); + } + } +} diff --git a/guardian-service/src/policy-engine/policy-engine.service.ts b/guardian-service/src/policy-engine/policy-engine.service.ts index 405cd2123..e593bfd3c 100644 --- a/guardian-service/src/policy-engine/policy-engine.service.ts +++ b/guardian-service/src/policy-engine/policy-engine.service.ts @@ -302,6 +302,9 @@ export class PolicyEngineService { otherOptions.orderBy = { createDate: 'DESC' }; otherOptions.limit = _pageSize; otherOptions.offset = _pageIndex * _pageSize; + } else { + otherOptions.orderBy = { createDate: 'DESC' }; + otherOptions.limit = 100; } const [policies, count] = await DatabaseServer.getPoliciesAndCount(filter, otherOptions); diff --git a/guardian-service/tests/api/documents.service.test.js b/guardian-service/tests/api/documents.service.test.js index b05bd0a91..c2e8b4e6f 100644 --- a/guardian-service/tests/api/documents.service.test.js +++ b/guardian-service/tests/api/documents.service.test.js @@ -144,38 +144,23 @@ describe('Documents Service API', function () { }) - // it('Get VC Documents', async function () { - // await documentsAPIModule.documentsAPI(channel, getMongoRepositoryMock(DidDocument), getMongoRepositoryMock(VcDocument), getMongoRepositoryMock(VpDocument)); - // const data = await methods['get-vc-documents'](); + // it('Set DID Documents', async function () { + // await documentsAPIModule.documentsAPI(getMongoRepositoryMock(DidDocument), getMongoRepositoryMock(VcDocument), getMongoRepositoryMock(VpDocument)); + // const data = await methods['set-did-document']({ did: 'test' }); // assert.equal(data.code, 200); // assert.equal(typeof data.body === 'object', true); // }) - it('Get DID Documents', async function () { - await documentsAPIModule.documentsAPI(getMongoRepositoryMock(DidDocument), getMongoRepositoryMock(VcDocument), getMongoRepositoryMock(VpDocument)); - const data = await methods['set-did-document']({ did: 'test' }); - assert.equal(data.code, 200); - assert.equal(typeof data.body === 'object', true); - - }) - - it('Set VC Documents', async function () { - await documentsAPIModule.documentsAPI(getMongoRepositoryMock(DidDocument), getMongoRepositoryMock(VcDocument), getMongoRepositoryMock(VpDocument)); - const data = await methods['set-vc-document']({ hash: 'test' }); - assert.equal(data.code, 200); - assert.equal(typeof data.body === 'object', true); - }) - - it('Set VP Documents', async function () { - await documentsAPIModule.documentsAPI(getMongoRepositoryMock(DidDocument), getMongoRepositoryMock(VcDocument), getMongoRepositoryMock(VpDocument)); - const data = await methods['set-vp-document'](); - assert.equal(data.code, 200); - assert.equal(typeof data.body === 'object', true); - }) + // it('Set VC Documents', async function () { + // await documentsAPIModule.documentsAPI(getMongoRepositoryMock(DidDocument), getMongoRepositoryMock(VcDocument), getMongoRepositoryMock(VpDocument)); + // const data = await methods['set-vc-document']({ hash: 'test' }); + // assert.equal(data.code, 200); + // assert.equal(typeof data.body === 'object', true); + // }) // it('Set VP Documents', async function () { - // await documentsAPIModule.documentsAPI(channel, getMongoRepositoryMock(DidDocument), getMongoRepositoryMock(VcDocument), getMongoRepositoryMock(VpDocument)); - // const data = await methods['get-vp-documents'](); + // await documentsAPIModule.documentsAPI(getMongoRepositoryMock(DidDocument), getMongoRepositoryMock(VcDocument), getMongoRepositoryMock(VpDocument)); + // const data = await methods['set-vp-document'](); // assert.equal(data.code, 200); // assert.equal(typeof data.body === 'object', true); // }) diff --git a/interfaces/src/type/workers.type.ts b/interfaces/src/type/workers.type.ts index 6090ed170..764346dc1 100644 --- a/interfaces/src/type/workers.type.ts +++ b/interfaces/src/type/workers.type.ts @@ -23,6 +23,7 @@ export enum WorkerTaskType { CHECK_ACCOUNT = 'check-account', GET_TOPIC_MESSAGE = 'get-topic-message', GET_TOPIC_MESSAGES = 'get-topic-messages', + GET_TOPIC_MESSAGE_BY_INDEX = 'get-topic-message-by-index', CREATE_CONTRACT = 'create-contract', ADD_CONTRACT_USER = 'add-contract-user', CHECK_STATUS = 'check-status', diff --git a/policy-service/src/policy-engine/block-validators/block-validator.ts b/policy-service/src/policy-engine/block-validators/block-validator.ts index e6fa6364e..d019ff5db 100644 --- a/policy-service/src/policy-engine/block-validators/block-validator.ts +++ b/policy-service/src/policy-engine/block-validators/block-validator.ts @@ -43,6 +43,7 @@ import { TokenConfirmationBlock } from './blocks/token-confirmation-block'; import { ModuleValidator } from './module-validator'; import { ModuleBlock } from './blocks/module'; import { TagsManagerBlock } from './blocks/tag-manager'; +import { ExternalTopicBlock } from './blocks/external-topic-block'; export const validators = [ InterfaceDocumentActionBlock, @@ -84,7 +85,8 @@ export const validators = [ TokenActionBlock, TokenConfirmationBlock, ModuleBlock, - TagsManagerBlock + TagsManagerBlock, + ExternalTopicBlock ]; /** diff --git a/policy-service/src/policy-engine/block-validators/blocks/external-topic-block.ts b/policy-service/src/policy-engine/block-validators/blocks/external-topic-block.ts new file mode 100644 index 000000000..90594a8e4 --- /dev/null +++ b/policy-service/src/policy-engine/block-validators/blocks/external-topic-block.ts @@ -0,0 +1,33 @@ +import { BlockValidator, IBlockProp } from '@policy-engine/block-validators'; + +/** + * External topic block + */ +export class ExternalTopicBlock { + /** + * Block type + */ + public static readonly blockType: string = 'externalTopicBlock'; + + /** + * Validate block options + * @param validator + * @param config + */ + public static async validate(validator: BlockValidator, ref: IBlockProp): Promise { + try { + if (ref.options.schema) { + if (typeof ref.options.schema !== 'string') { + validator.addError('Option "schema" must be a string'); + return; + } + if (await validator.schemaNotExist(ref.options.schema)) { + validator.addError(`Schema with id "${ref.options.schema}" does not exist`); + return; + } + } + } catch (error) { + validator.addError(`Unhandled exception ${validator.getErrorMessage(error)}`); + } + } +} diff --git a/policy-service/src/policy-engine/blocks/external-data-block.ts b/policy-service/src/policy-engine/blocks/external-data-block.ts index a2c46a9d3..fc5ae94b0 100644 --- a/policy-service/src/policy-engine/blocks/external-data-block.ts +++ b/policy-service/src/policy-engine/blocks/external-data-block.ts @@ -26,7 +26,7 @@ import { ExternalDocuments, ExternalEvent, ExternalEventType } from '@policy-eng title: `Add 'External Data' Block`, post: true, get: false, - children: ChildrenType.None, + children: ChildrenType.Special, control: ControlType.Server, input: null, output: [ diff --git a/policy-service/src/policy-engine/blocks/external-topic-block.ts b/policy-service/src/policy-engine/blocks/external-topic-block.ts new file mode 100644 index 000000000..a5aba9c9e --- /dev/null +++ b/policy-service/src/policy-engine/blocks/external-topic-block.ts @@ -0,0 +1,906 @@ +import { CronJob } from 'cron'; +import { ActionCallback, EventBlock } from '@policy-engine/helpers/decorators'; +import { IVC, Schema, SchemaField, SchemaHelper, TopicType } from '@guardian/interfaces'; +import { PolicyComponentsUtils } from '@policy-engine/policy-components-utils'; +import { PolicyInputEventType, PolicyOutputEventType } from '@policy-engine/interfaces'; +import { ChildrenType, ControlType, PropertyType } from '@policy-engine/interfaces/block-about'; +import { AnyBlockType, IPolicyAddonBlock, IPolicyDocument, IPolicyValidatorBlock } from '@policy-engine/policy-engine.interface'; +import { BlockActionError } from '@policy-engine/errors'; +import { IPolicyUser } from '@policy-engine/policy-user'; +import { IHederaAccount, PolicyUtils } from '@policy-engine/helpers/utils'; +import { + VcDocument as VcDocumentCollection, + MessageServer, + MessageAction, + SchemaMessage, + UrlType, + PolicyMessage, + TopicMessage, + ExternalDocument, + MessageType, + VCMessage, + VcHelper, + IPFS, +} from '@guardian/common'; +import { + ExternalDocuments, + ExternalEvent, + ExternalEventType +} from '@policy-engine/interfaces/external-event'; + +/** + * Search Topic Result + */ +interface TopicResult { + /** + * Topic count + */ + count?: number; + /** + * Policy Schemas + */ + schemas?: SchemaMessage[]; + /** + * Policy Version + */ + instance?: PolicyMessage; + /** + * Search Topic Result + */ + root?: TopicMessage; + /** + * Policy Instance Topic + */ + instanceTopic?: TopicMessage; + /** + * Policy Topic + */ + policyTopic?: TopicMessage; +} + +/** + * Task Status + */ +enum TaskStatus { + NeedTopic = 'NEED_TOPIC', + NeedSchema = 'NEED_SCHEMA', + Free = 'FREE', + Search = 'SEARCH', + Verification = 'VERIFICATION', + Processing = 'PROCESSING', + Error = 'ERROR' +} + +/** + * Schema Status + */ +enum SchemaStatus { + NotVerified = 'NOT_VERIFIED', + Incompatible = 'INCOMPATIBLE', + Compatible = 'COMPATIBLE', +} + +/** + * External topic block + */ +@EventBlock({ + blockType: 'externalTopicBlock', + commonBlock: false, + about: { + label: 'External Topic', + title: `Add 'External Topic' Block`, + post: true, + get: true, + children: ChildrenType.Special, + control: ControlType.UI, + input: [ + PolicyInputEventType.TimerEvent + ], + output: [ + PolicyOutputEventType.RunEvent, + PolicyOutputEventType.RefreshEvent, + PolicyOutputEventType.ErrorEvent + ], + defaultEvent: true, + properties: [{ + name: 'schema', + label: 'Schema', + title: 'Schema', + type: PropertyType.Schemas + }] + }, + variables: [ + { path: 'options.schema', alias: 'schema', type: 'Schema' } + ] +}) +export class ExternalTopicBlock { + /** + * Schema + * @private + */ + private schema: Schema | null; + + /** + * Cron job + * @private + */ + private job: CronJob; + + /** + * After init callback + */ + protected afterInit() { + this.job = new CronJob(`0 0 * * *`, () => { + this.run().then(); + }, null, false, 'UTC'); + this.job.start(); + } + + /** + * Block destructor + */ + protected destroy() { + if (this.job) { + this.job.stop(); + } + } + + /** + * Get Validators + */ + protected getValidators(): IPolicyValidatorBlock[] { + const ref = PolicyComponentsUtils.GetBlockRef(this); + const validators: IPolicyValidatorBlock[] = []; + for (const child of ref.children) { + if (child.blockClassName === 'ValidatorBlock') { + validators.push(child as IPolicyValidatorBlock); + } + } + return validators; + } + + /** + * Validate Documents + * @param user + * @param state + */ + protected async validateDocuments(user: IPolicyUser, state: any): Promise { + const validators = this.getValidators(); + for (const validator of validators) { + const error = await validator.run({ + type: null, + inputType: null, + outputType: null, + policyId: null, + source: null, + sourceId: null, + target: null, + targetId: null, + user, + data: state + }); + if (error) { + return error; + } + } + return null; + } + + /** + * Update user state + * @private + */ + private updateStatus(ref: AnyBlockType, item: ExternalDocument, user: IPolicyUser) { + ref.updateBlock({ status: item.status }, user); + } + + /** + * Get Schema Fields + * @param document + * @private + */ + private getSchemaFields(document: any): SchemaField[] { + try { + if (typeof document === 'string') { + document = JSON.parse(document); + } + return SchemaHelper.parseFields(document, null, null, false); + } catch (error) { + return null; + } + } + + /** + * Compare Schema Fields + * @param f1 + * @param f2 + * @private + */ + private compareFields(f1: SchemaField, f2: SchemaField): boolean { + if ( + f1.name !== f2.name || + f1.title !== f2.title || + f1.description !== f2.description || + f1.required !== f2.required || + f1.isArray !== f2.isArray || + f1.isRef !== f2.isRef + ) { + return false; + } + if (f1.isRef) { + return true; + } else { + return ( + f1.type === f2.type && + f1.format === f2.format && + f1.pattern === f2.pattern && + f1.unit === f2.unit && + f1.unitSystem === f2.unitSystem && + f1.customType === f2.customType + ); + } + // remoteLink?: string; + // enum?: string[]; + } + + /** + * Compare Schemas + * @param extension + * @param base + * @private + */ + private ifExtendFields(extension: SchemaField[], base: SchemaField[]): boolean { + try { + if (!extension || !base) { + return false; + } + const map = new Map(); + for (const f of extension) { + map.set(f.name, f); + } + for (const baseField of base) { + const extensionField = map.get(baseField.name) + if (!extensionField) { + return false; + } + if (!this.compareFields(baseField, extensionField)) { + return false; + } + if (baseField.isRef) { + if (!this.ifExtendFields(extensionField.fields, baseField.fields)) { + return false; + } + } + } + return true; + } catch (error) { + return false; + } + } + + /** + * Get Schema + * @private + */ + private async getSchema(): Promise { + const ref = PolicyComponentsUtils.GetBlockRef(this); + if (!ref.options.schema) { + return null; + } + if (!this.schema) { + const schema = await ref.databaseServer.getSchemaByIRI(ref.options.schema, ref.topicId); + this.schema = schema ? new Schema(schema) : null; + if (!this.schema) { + throw new BlockActionError('Waiting for schema', ref.blockType, ref.uuid); + } + } + return this.schema; + } + + /** + * Load and Verify Schema + * @param item + * @private + */ + private async verification(item: any): Promise { + try { + const schema = await this.getSchema(); + if (!schema) { + item.status = SchemaStatus.Incompatible; + return; + } + if (!item) { + item.status = SchemaStatus.Incompatible; + return; + } + const document = await IPFS.getFile(item.cid, 'str'); + const base = this.getSchemaFields(schema.document); + const extension = this.getSchemaFields(document); + const verified = this.ifExtendFields(extension, base); + item.status = verified ? SchemaStatus.Compatible : SchemaStatus.Incompatible; + } catch (error) { + item.status = SchemaStatus.Incompatible; + return; + } + } + + /** + * Get user task + * @param user + * @private + */ + private async getUser(user: IPolicyUser): Promise { + const ref = PolicyComponentsUtils.GetBlockRef(this); + let item = await ref.databaseServer.getExternalTopic(ref.policyId, ref.uuid, user.did); + if (!item) { + item = await ref.databaseServer.createExternalTopic({ + policyId: ref.policyId, + blockId: ref.uuid, + owner: user.did, + documentTopicId: '', + policyTopicId: '', + instanceTopicId: '', + documentMessage: null, + policyMessage: null, + policyInstanceMessage: null, + schemas: [], + schema: null, + schemaId: null, + active: false, + lastMessage: '', + lastUpdate: '', + status: TaskStatus.NeedTopic + }); + } + return item; + } + + /** + * Search Policy Topic + * @param topicId + * @param topicTree + * @private + */ + private async searchTopic(topicId: string, topicTree: TopicResult = {}): Promise { + const ref = PolicyComponentsUtils.GetBlockRef(this); + if (topicTree.count) { + topicTree.count++; + } else { + topicTree.count = 1; + } + if (topicTree.count > 20) { + throw new BlockActionError('Max attempts of 20 was reached for request: Get topic info', ref.blockType, ref.uuid); + } + const topicMessage = await MessageServer.getTopic(topicId); + if (!topicTree.root) { + if (topicMessage && ( + topicMessage.messageType === TopicType.InstancePolicyTopic || + topicMessage.messageType === TopicType.DynamicTopic + )) { + topicTree.root = topicMessage; + } else { + throw new BlockActionError('Invalid topic', ref.blockType, ref.uuid); + } + } + if (topicMessage) { + if (topicMessage.messageType === TopicType.PolicyTopic) { + if (!topicTree.instanceTopic) { + throw new BlockActionError('Invalid topic', ref.blockType, ref.uuid); + } + topicTree.policyTopic = topicMessage; + const messages: any[] = await MessageServer.getMessages(topicId); + topicTree.schemas = messages.filter((m: SchemaMessage) => + m.action === MessageAction.PublishSchema); + topicTree.instance = messages.find((m: PolicyMessage) => + m.action === MessageAction.PublishPolicy && + m.instanceTopicId === topicTree.instanceTopic.topicId); + return topicTree; + } else if (topicMessage.messageType === TopicType.InstancePolicyTopic) { + topicTree.instanceTopic = topicMessage; + return await this.searchTopic(topicMessage.parentId, topicTree); + } else if (topicMessage.messageType === TopicType.DynamicTopic) { + return await this.searchTopic(topicMessage.parentId, topicTree); + } + } + throw new BlockActionError('Invalid topic', ref.blockType, ref.uuid); + } + + /** + * Select Topic + * @param item + * @param topicId + * @param user + * @private + */ + private async addTopic( + item: ExternalDocument, + topicId: string, + user: IPolicyUser + ): Promise { + const ref = PolicyComponentsUtils.GetBlockRef(this); + try { + const topicTree = await this.searchTopic(topicId); + const topic = topicTree.root; + const policy = topicTree.policyTopic; + const instance = topicTree.instance; + const list = []; + for (const schema of topicTree.schemas) { + list.push({ + id: schema.getContextUrl(UrlType.url), + name: schema.name, + cid: schema.getDocumentUrl(UrlType.cid), + status: SchemaStatus.NotVerified + }); + } + item.status = TaskStatus.NeedSchema; + item.documentTopicId = topic.topicId?.toString(); + item.policyTopicId = policy.topicId?.toString(); + item.instanceTopicId = instance.instanceTopicId?.toString(); + item.documentMessage = topic.toMessageObject(); + item.policyMessage = policy.toMessageObject(); + item.policyInstanceMessage = instance.toMessageObject(); + item.schemas = list; + item.active = false; + item.lastMessage = ''; + item.lastUpdate = ''; + await ref.databaseServer.updateExternalTopic(item); + this.updateStatus(ref, item, user); + } catch (error) { + item.status = TaskStatus.Error; + ref.databaseServer.updateExternalTopic(item); + ref.error(`setData: ${PolicyUtils.getErrorMessage(error)}`); + } + } + + /** + * Verify Schema + * @param item + * @param schema + * @param user + * @private + */ + private async verificationSchema( + item: ExternalDocument, + schema: any, + user: IPolicyUser + ): Promise { + const ref = PolicyComponentsUtils.GetBlockRef(this); + try { + await this.verification(schema); + item.status = TaskStatus.NeedSchema; + await ref.databaseServer.updateExternalTopic(item); + this.updateStatus(ref, item, user); + } catch (error) { + ref.error(`setData: ${PolicyUtils.getErrorMessage(error)}`); + item.status = TaskStatus.Error; + await ref.databaseServer.updateExternalTopic(item); + this.updateStatus(ref, item, user); + } + } + + /** + * Verify Schemas + * @param item + * @param schemas + * @param user + * @private + */ + private async verificationSchemas( + item: ExternalDocument, + schemas: any[], + user: IPolicyUser + ): Promise { + const ref = PolicyComponentsUtils.GetBlockRef(this); + try { + for (const schema of schemas) { + if (schema.status === SchemaStatus.NotVerified) { + await this.verification(schema); + } + } + item.status = TaskStatus.NeedSchema; + await ref.databaseServer.updateExternalTopic(item); + this.updateStatus(ref, item, user); + } catch (error) { + ref.error(`setData: ${PolicyUtils.getErrorMessage(error)}`); + item.status = TaskStatus.Error; + await ref.databaseServer.updateExternalTopic(item); + this.updateStatus(ref, item, user); + } + } + + /** + * Select Schema + * @param item + * @param schema + * @param user + * @private + */ + private async setSchema( + item: ExternalDocument, + schema: any, + user: IPolicyUser + ): Promise { + const ref = PolicyComponentsUtils.GetBlockRef(this); + try { + await this.verification(schema); + if (schema.status === SchemaStatus.Compatible) { + item.status = TaskStatus.Free; + item.schemaId = schema.id; + item.schema = schema; + item.active = true; + } else { + item.status = TaskStatus.NeedSchema; + } + await ref.databaseServer.updateExternalTopic(item); + this.updateStatus(ref, item, user); + } catch (error) { + ref.error(`setData: ${PolicyUtils.getErrorMessage(error)}`); + item.status = TaskStatus.Error; + await ref.databaseServer.updateExternalTopic(item); + this.updateStatus(ref, item, user); + } + } + + /** + * Verify VC document + * @param item + * @param document + * @private + */ + private async checkDocument(item: ExternalDocument, document: IVC): Promise { + if (!document) { + return 'Invalid document'; + } + + if ( + !Array.isArray(document['@context']) || + document['@context'].indexOf(item.schemaId) === -1 + ) { + return 'Invalid schema'; + } + + let verify: boolean; + try { + const VCHelper = new VcHelper(); + const res = await VCHelper.verifySchema(document); + verify = res.ok; + if (verify) { + verify = await VCHelper.verifyVC(document); + } + } catch (error) { + verify = false; + } + + if (!verify) { + return 'Invalid proof'; + } + + return null; + } + + /** + * Check message and create document + * @param ref + * @param item + * @param hederaAccount + * @param user + * @param message + * @private + */ + private async checkMessage( + ref: AnyBlockType, + item: ExternalDocument, + hederaAccount: IHederaAccount, + user: IPolicyUser, + message: VCMessage + ): Promise { + const documentRef = await this.getRelationships(ref, user); + + if (message.type !== MessageType.VCDocument) { + return; + } + + if (message.payer !== hederaAccount.hederaAccountId) { + return; + } + + await MessageServer.loadDocument(message, hederaAccount.hederaAccountKey); + + const document: IVC = message.getDocument(); + const error = await this.checkDocument(item, document); + if (error) { + return; + } + + const result: IPolicyDocument = PolicyUtils.createPolicyDocument(ref, user, document); + result.schema = ref.options.schema; + if (documentRef) { + PolicyUtils.setDocumentRef(result, documentRef); + } + if (result.relationships) { + result.relationships.push(message.getId()); + } else { + result.relationships = [message.getId()]; + } + + const state = { data: result }; + ref.triggerEvents(PolicyOutputEventType.RunEvent, user, state); + ref.triggerEvents(PolicyOutputEventType.ReleaseEvent, user, null); + ref.triggerEvents(PolicyOutputEventType.RefreshEvent, user, state); + PolicyComponentsUtils.ExternalEventFn(new ExternalEvent(ExternalEventType.Run, ref, user, { + documents: ExternalDocuments(result) + })); + } + + /** + * Load messages by user + * @param item + * @param user + * @private + */ + private async receiveData(item: ExternalDocument, user: IPolicyUser): Promise { + const ref = PolicyComponentsUtils.GetBlockRef(this); + const hederaAccount = await PolicyUtils.getHederaAccount(ref, item.owner); + const messages: VCMessage[] = await MessageServer.getMessages( + item.documentTopicId, + null, + null, + item.lastMessage + ); + for (const message of messages) { + await this.checkMessage(ref, item, hederaAccount, user, message); + item.lastMessage = message.id; + await ref.databaseServer.updateExternalTopic(item); + } + } + + /** + * Load documents + * @param item + * @private + */ + private async runByUser(item: ExternalDocument): Promise { + const ref = PolicyComponentsUtils.GetBlockRef(this); + + item.status = TaskStatus.Processing; + await ref.databaseServer.updateExternalTopic(item); + + const user = await PolicyUtils.createPolicyUser(ref, item.owner); + this.updateStatus(ref, item, user); + try { + await this.receiveData(item, user); + item.status = TaskStatus.Free; + item.lastUpdate = (new Date()).toISOString(); + await ref.databaseServer.updateExternalTopic(item); + } catch (error) { + item.status = TaskStatus.Free; + await ref.databaseServer.updateExternalTopic(item); + ref.error(`setData: ${PolicyUtils.getErrorMessage(error)}`); + } + this.updateStatus(ref, item, user); + } + + /** + * Get Relationships + * @param ref + * @param refId + */ + private async getRelationships(ref: AnyBlockType, user: IPolicyUser): Promise { + try { + for (const child of ref.children) { + if (child.blockClassName === 'SourceAddon') { + const childData = await (child as IPolicyAddonBlock).getFromSource(user, null); + if (childData && childData.length) { + return childData[0]; + } + } + } + return null; + } catch (error) { + ref.error(PolicyUtils.getErrorMessage(error)); + return null; + } + } + + /** + * Tick cron + */ + public async run() { + const ref = PolicyComponentsUtils.GetBlockRef(this); + const items = await ref.databaseServer.getActiveExternalTopics(ref.policyId, ref.uuid); + for (const item of items) { + if (item.status === TaskStatus.Free) { + await this.runByUser(item); + } + } + } + + /** + * Set block data + * @param user + * @param _data + */ + @ActionCallback({ + output: [ + PolicyOutputEventType.RunEvent, + PolicyOutputEventType.RefreshEvent + ] + }) + public async setData(user: IPolicyUser, data: any): Promise { + const ref = PolicyComponentsUtils.GetBlockRef(this); + ref.log(`setData`); + + if (!user?.did) { + throw new BlockActionError('User have no any did', ref.blockType, ref.uuid); + } + + const { operation, value } = data; + + try { + const item = await this.getUser(user); + switch (operation) { + case 'SetTopic': { + if (!value) { + throw new BlockActionError('Invalid value', ref.blockType, ref.uuid); + } + + if (item.status !== TaskStatus.NeedTopic) { + throw new BlockActionError('Topic already set', ref.blockType, ref.uuid); + } + + item.status = TaskStatus.Search; + await ref.databaseServer.updateExternalTopic(item); + + this.addTopic(item, value, user); + break; + } + case 'VerificationSchema': { + if (!value) { + throw new BlockActionError('Invalid value', ref.blockType, ref.uuid); + } + + if (item.status === TaskStatus.NeedTopic) { + throw new BlockActionError('Topic not set.', ref.blockType, ref.uuid); + } + + if (item.status !== TaskStatus.NeedSchema) { + throw new BlockActionError('Schema already set', ref.blockType, ref.uuid); + } + + if (!item.schemas) { + throw new BlockActionError('Schema not found', ref.blockType, ref.uuid); + } + + const schema = item.schemas.find(s => s.id === value); + if (!schema) { + throw new BlockActionError('Schema not found', ref.blockType, ref.uuid); + } + + item.status = TaskStatus.Verification; + await ref.databaseServer.updateExternalTopic(item); + + this.verificationSchema(item, schema, user); + break; + } + case 'VerificationSchemas': { + if (item.status === TaskStatus.NeedTopic) { + throw new BlockActionError('Topic not set.', ref.blockType, ref.uuid); + } + + if (item.status !== TaskStatus.NeedSchema) { + throw new BlockActionError('Schema already set', ref.blockType, ref.uuid); + } + + if (!item.schemas) { + throw new BlockActionError('Schema not found', ref.blockType, ref.uuid); + } + + item.status = TaskStatus.Verification; + await ref.databaseServer.updateExternalTopic(item); + + this.verificationSchemas(item, item.schemas, user); + break; + } + case 'SetSchema': { + if (!value) { + throw new BlockActionError('Invalid value', ref.blockType, ref.uuid); + } + + if (item.status === TaskStatus.NeedTopic) { + throw new BlockActionError('Topic not set.', ref.blockType, ref.uuid); + } + + if (item.status !== TaskStatus.NeedSchema) { + throw new BlockActionError('Schema already set', ref.blockType, ref.uuid); + } + + if (!item.schemas) { + throw new BlockActionError('Schema not found', ref.blockType, ref.uuid); + } + + const schema = item.schemas.find(s => s.id === value); + if (!schema) { + throw new BlockActionError('Schema not found', ref.blockType, ref.uuid); + } + + item.status = TaskStatus.Verification; + await ref.databaseServer.updateExternalTopic(item); + + this.setSchema(item, schema, user); + break; + } + case 'LoadDocuments': { + if (item.status !== TaskStatus.Free) { + throw new BlockActionError('Process already started', ref.blockType, ref.uuid); + } + + item.status = TaskStatus.Processing; + await ref.databaseServer.updateExternalTopic(item); + + this.runByUser(item).then(null, (error) => { + item.status = TaskStatus.Error; + ref.databaseServer.updateExternalTopic(item); + ref.error(`setData: ${PolicyUtils.getErrorMessage(error)}`); + }); + break; + } + case 'Restart': { + if ( + item.status !== TaskStatus.NeedTopic && + item.status !== TaskStatus.NeedSchema && + item.status !== TaskStatus.Error + ) { + throw new BlockActionError('Invalid status', ref.blockType, ref.uuid); + } + item.documentTopicId = ''; + item.policyTopicId = ''; + item.instanceTopicId = ''; + item.documentMessage = ''; + item.policyMessage = ''; + item.policyInstanceMessage = ''; + item.schemas = []; + item.schema = null; + item.schemaId = null; + item.active = false; + item.lastMessage = ''; + item.lastUpdate = ''; + item.status = TaskStatus.NeedTopic; + await ref.databaseServer.updateExternalTopic(item); + break; + } + + default: { + throw new BlockActionError('Invalid operation', ref.blockType, ref.uuid); + } + } + } catch (error) { + ref.error(`setData: ${PolicyUtils.getErrorMessage(error)}`); + throw new BlockActionError(error, ref.blockType, ref.uuid); + } + } + + /** + * Get block data + * @param user + */ + public async getData(user: IPolicyUser): Promise { + const ref = PolicyComponentsUtils.GetBlockRef(this); + const item = await ref.databaseServer.getExternalTopic(ref.policyId, ref.uuid, user.did); + if (item) { + return { + documentTopicId: item.documentTopicId, + policyTopicId: item.policyTopicId, + instanceTopicId: item.instanceTopicId, + documentMessage: item.documentMessage, + policyMessage: item.policyMessage, + policyInstanceMessage: item.policyInstanceMessage, + schemas: item.schemas, + schema: item.schema, + lastUpdate: item.lastUpdate, + status: item.status + }; + } else { + return {}; + } + } +} \ No newline at end of file diff --git a/policy-service/src/policy-engine/blocks/index.ts b/policy-service/src/policy-engine/blocks/index.ts index e2bcd232a..3c37a8f89 100644 --- a/policy-service/src/policy-engine/blocks/index.ts +++ b/policy-service/src/policy-engine/blocks/index.ts @@ -40,4 +40,5 @@ export { HistoryAddon } from './history-addon'; export { SelectiveAttributes } from './selective-attributes-addon'; export { ModuleBlock } from './module'; export { TagsManagerBlock } from './tag-manager'; +export { ExternalTopicBlock } from './external-topic-block'; export { UploadVcDocumentBlock } from './upload-vc-document-block'; diff --git a/policy-service/src/policy-engine/blocks/request-vc-document-block.ts b/policy-service/src/policy-engine/blocks/request-vc-document-block.ts index 1642562dc..5c3ff3f3f 100644 --- a/policy-service/src/policy-engine/blocks/request-vc-document-block.ts +++ b/policy-service/src/policy-engine/blocks/request-vc-document-block.ts @@ -248,8 +248,6 @@ export class RequestVcDocumentBlock { ref.error(`setData: ${PolicyUtils.getErrorMessage(error)}`); throw new BlockActionError(error, ref.blockType, ref.uuid); } - - return {}; } /** diff --git a/policy-service/src/policy-engine/blocks/send-to-guardian-block.ts b/policy-service/src/policy-engine/blocks/send-to-guardian-block.ts index df7e2cd6b..e101e2599 100644 --- a/policy-service/src/policy-engine/blocks/send-to-guardian-block.ts +++ b/policy-service/src/policy-engine/blocks/send-to-guardian-block.ts @@ -86,6 +86,7 @@ export class SendToGuardianBlock { if (document.hash) { old = await ref.databaseServer.getVcDocument({ where: { + policyId: { $eq: ref.policyId }, hash: { $eq: document.hash }, hederaStatus: { $not: { $eq: DocumentStatus.REVOKE } } } @@ -138,6 +139,7 @@ export class SendToGuardianBlock { if (document.hash) { old = await ref.databaseServer.getVpDocument({ where: { + policyId: { $eq: ref.policyId }, hash: { $eq: document.hash }, hederaStatus: { $not: { $eq: DocumentStatus.REVOKE } } } @@ -258,7 +260,8 @@ export class SendToGuardianBlock { } else { old.messageIds = [document.messageId]; } - return await ref.databaseServer.updateVC(old); + await ref.databaseServer.updateVC(old); + return document; } else { if (Array.isArray(document.messageIds)) { document.messageIds.push(document.messageId); @@ -288,7 +291,8 @@ export class SendToGuardianBlock { } else { old.messageIds = [document.messageId]; } - return await ref.databaseServer.updateDid(old); + await ref.databaseServer.updateDid(old); + return document; } else { if (Array.isArray(document.messageIds)) { document.messageIds.push(document.messageId); @@ -316,7 +320,8 @@ export class SendToGuardianBlock { } else { old.messageIds = [document.messageId]; } - return await ref.databaseServer.updateVP(old); + await ref.databaseServer.updateVP(old); + return document; } else { if (Array.isArray(document.messageIds)) { document.messageIds.push(document.messageId); diff --git a/policy-service/src/policy-engine/helpers/utils.ts b/policy-service/src/policy-engine/helpers/utils.ts index 0fbc12f63..d70552605 100644 --- a/policy-service/src/policy-engine/helpers/utils.ts +++ b/policy-service/src/policy-engine/helpers/utils.ts @@ -852,6 +852,21 @@ export class PolicyUtils { return await TopicConfig.fromObject(topic, !ref.dryRun); } + /** + * Create Policy User + * @param ref + * @param did + */ + public static async createPolicyUser(ref: AnyBlockType, did: string): Promise { + const user = new PolicyUser(did); + if (ref.dryRun) { + const virtualUser = await ref.databaseServer.getVirtualUser(did); + user.setVirtualUser(virtualUser); + } + const group = await ref.databaseServer.getActiveGroupByUser(ref.policyId, did); + return user.setGroup(group); + } + /** * Get Policy User * @param ref @@ -1092,11 +1107,13 @@ export class PolicyUtils { * @param document */ public static createPolicyDocument(ref: AnyBlockType, owner: IPolicyUser, document: any): IPolicyDocument { - document.policyId = ref.policyId; - document.tag = ref.tag; - document.owner = owner.did; - document.group = owner.group; - return document; + return { + policyId: ref.policyId, + tag: ref.tag, + document, + owner: owner.did, + group: owner.group + }; } /** diff --git a/worker-service/src/api/helpers/hedera-sdk-helper.ts b/worker-service/src/api/helpers/hedera-sdk-helper.ts index f141cb4ad..5dda11409 100644 --- a/worker-service/src/api/helpers/hedera-sdk-helper.ts +++ b/worker-service/src/api/helpers/hedera-sdk-helper.ts @@ -161,6 +161,20 @@ export class HederaSDKHelper { } } + /** + * Set Network + * @param networkOptions + * @private + */ + public static setNetwork(networkOptions: NetworkOptions) { + Environment.setNetwork(networkOptions.network); + Environment.setLocalNodeAddress(networkOptions.localNodeAddress); + Environment.setLocalNodeProtocol(networkOptions.localNodeProtocol); + Environment.setNodes(networkOptions.nodes); + Environment.setMirrorNodes(networkOptions.mirrorNodes); + return HederaSDKHelper; + } + /** * Transaction starting * @param id @@ -908,16 +922,7 @@ export class HederaSDKHelper { * @returns Message */ @timeout(HederaSDKHelper.MAX_TIMEOUT) - public async getTopicMessage(timeStamp: string): Promise<{ - /** - * Topic ID - */ - topicId: string, - /** - * Message - */ - message: string - }> { + public static async getTopicMessage(timeStamp: string): Promise { const res = await axios.get( `${Environment.HEDERA_MESSAGE_API}/${timeStamp}`, { responseType: 'json' } @@ -926,9 +931,11 @@ export class HederaSDKHelper { throw new Error(`Invalid message '${timeStamp}'`); } const buffer = Buffer.from(res.data.message, 'base64').toString(); - const topicId = res.data.topic_id; return { - topicId, + id: res.data.consensus_timestamp, + payer_account_id: res.data.payer_account_id, + sequence_number: res.data.sequence_number, + topicId: res.data.topic_id, message: buffer } } @@ -936,21 +943,19 @@ export class HederaSDKHelper { /** * Returns topic messages * @param topicId Topic identifier + * @param startTimestamp start timestamp * @returns Messages */ @timeout(HederaSDKHelper.MAX_TIMEOUT) - public async getTopicMessages(topicId: string): Promise<{ - /** - * ID - */ - id: string, - /** - * Message - */ - message: string - }[]> { + public static async getTopicMessages( + topicId: string, + startTimestamp?: string + ): Promise { let goNext = true; let url = `${Environment.HEDERA_TOPIC_API}${topicId}/messages`; + if (startTimestamp) { + url += `?timestamp=gt:${startTimestamp}`; + } const result = []; const p = { params: { limit: Number.MAX_SAFE_INTEGER }, @@ -972,11 +977,11 @@ export class HederaSDKHelper { for (const m of messages) { const buffer = Buffer.from(m.message, 'base64').toString(); - const id = m.consensus_timestamp; - const payer_account_id = m.payer_account_id; result.push({ - id, - payer_account_id, + id: m.consensus_timestamp, + payer_account_id: m.payer_account_id, + sequence_number: m.sequence_number, + topicId: m.topic_id, message: buffer }); } @@ -990,6 +995,29 @@ export class HederaSDKHelper { return result; } + /** + * Returns topic message + * @param topicId Topic identifier + * @param index message index + * @returns Message + */ + @timeout(HederaSDKHelper.MAX_TIMEOUT) + public static async getTopicMessageByIndex(topicId: string, index: number): Promise { + const url = `${Environment.HEDERA_TOPIC_API}${topicId}/messages/${index}`; + const res = await axios.get(url, { responseType: 'json' }); + if (!res || !res.data || !res.data.message) { + throw new Error(`Invalid message. TopicId: '${topicId}', index: '${index}'`); + } + const buffer = Buffer.from(res.data.message, 'base64').toString(); + return { + id: res.data.consensus_timestamp, + payer_account_id: res.data.payer_account_id, + sequence_number: res.data.sequence_number, + topicId: res.data.topic_id, + message: buffer + } + } + /** * Execute and receipt * @param client @@ -1076,7 +1104,7 @@ export class HederaSDKHelper { count < 10 && errorMessage && errorMessage.indexOf(HederaResponseCode.DUPLICATE_TRANSACTION) > - -1 + -1 ) { return await this.receiptQuery(client, transactionId, count++); } @@ -1166,7 +1194,7 @@ export class HederaSDKHelper { count < 10 && errorMessage && errorMessage.indexOf(HederaResponseCode.DUPLICATE_TRANSACTION) > - -1 + -1 ) { return await this.recordQuery(client, transactionId, count++); } diff --git a/worker-service/src/api/worker.ts b/worker-service/src/api/worker.ts index 19bc75b60..701c60259 100644 --- a/worker-service/src/api/worker.ts +++ b/worker-service/src/api/worker.ts @@ -635,30 +635,26 @@ export class Worker extends NatsService { } case WorkerTaskType.GET_TOPIC_MESSAGE: { - const { - operatorId, - operatorKey, - dryRun, - timeStamp - } = task.data; - const client = new HederaSDKHelper(operatorId, operatorKey, dryRun, networkOptions); - result.data = await client.getTopicMessage(timeStamp); - client.destroy(); - + const { timeStamp } = task.data; + result.data = await HederaSDKHelper + .setNetwork(networkOptions) + .getTopicMessage(timeStamp); break; } case WorkerTaskType.GET_TOPIC_MESSAGES: { - const { - operatorId, - operatorKey, - dryRun, - topic - } = task.data; - const client = new HederaSDKHelper(operatorId, operatorKey, dryRun, networkOptions); - result.data = await client.getTopicMessages(topic); - client.destroy(); + const { topic, timestamp } = task.data; + result.data = await HederaSDKHelper + .setNetwork(networkOptions) + .getTopicMessages(topic, timestamp); + break; + } + case WorkerTaskType.GET_TOPIC_MESSAGE_BY_INDEX: { + const { topic, index } = task.data; + result.data = await HederaSDKHelper + .setNetwork(networkOptions) + .getTopicMessageByIndex(topic, index); break; }