From 486689107bba5d36c7bfdeb797329c09f73753ce Mon Sep 17 00:00:00 2001 From: Henry Tsai Date: Fri, 21 Jun 2024 09:49:19 -0700 Subject: [PATCH] Added convenience PermissionRequest class (#767) Convenience class so that a dev doesn't have to deal with a `RecordsWrite` and understand the schema directly. --- package-lock.json | 4 +- package.json | 2 +- src/index.ts | 2 + src/protocols/permission-grant.ts | 4 -- src/protocols/permission-request.ts | 63 ++++++++++++++++++++++ src/protocols/permissions.ts | 11 +++- tests/protocols/permission-request.spec.ts | 38 +++++++++++++ 7 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 src/protocols/permission-request.ts create mode 100644 tests/protocols/permission-request.spec.ts diff --git a/package-lock.json b/package-lock.json index 1b629472e..4ff17983c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tbd54566975/dwn-sdk-js", - "version": "0.3.8", + "version": "0.3.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tbd54566975/dwn-sdk-js", - "version": "0.3.8", + "version": "0.3.9", "license": "Apache-2.0", "dependencies": { "@ipld/dag-cbor": "9.0.3", diff --git a/package.json b/package.json index 2850cc6fc..bf0e46287 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tbd54566975/dwn-sdk-js", - "version": "0.3.8", + "version": "0.3.9", "description": "A reference implementation of https://identity.foundation/decentralized-web-node/spec/", "repository": { "type": "git", diff --git a/src/index.ts b/src/index.ts index 802848080..726557e37 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,6 +34,8 @@ export { Message } from './core/message.js'; export { MessagesGet, MessagesGetOptions } from './interfaces/messages-get.js'; export { UnionMessageReply } from './core/message-reply.js'; export { MessageStore, MessageStoreOptions } from './types/message-store.js'; +export { PermissionGrant } from './protocols/permission-grant.js'; +export { PermissionRequest } from './protocols/permission-request.js'; export { PermissionsProtocol } from './protocols/permissions.js'; export { PrivateKeySigner } from './utils/private-key-signer.js'; export { Protocols } from './utils/protocols.js'; diff --git a/src/protocols/permission-grant.ts b/src/protocols/permission-grant.ts index 48f5dc328..0c8e1be15 100644 --- a/src/protocols/permission-grant.ts +++ b/src/protocols/permission-grant.ts @@ -1,5 +1,4 @@ import type { DataEncodedRecordsWriteMessage } from '../types/records-types.js'; - import type { PermissionConditions, PermissionGrantData, PermissionScope } from '../types/permission-types.js'; import { Encoder } from '../utils/encoder.js'; @@ -66,9 +65,6 @@ export class PermissionGrant { return permissionGrant; } - /** - * Creates a Permission Grant abstraction for - */ private constructor(message: DataEncodedRecordsWriteMessage) { // properties derived from the generic DWN message properties this.id = message.recordId; diff --git a/src/protocols/permission-request.ts b/src/protocols/permission-request.ts new file mode 100644 index 000000000..ac4035daf --- /dev/null +++ b/src/protocols/permission-request.ts @@ -0,0 +1,63 @@ +import type { DataEncodedRecordsWriteMessage } from '../types/records-types.js'; +import type { PermissionConditions, PermissionRequestData, PermissionScope } from '../types/permission-types.js'; + +import { Encoder } from '../utils/encoder.js'; +import { Message } from '../core/message.js'; + + +/** + * A class representing a Permission Request for a more convenient abstraction. + */ +export class PermissionRequest { + + /** + * The ID of the permission request, which is the record ID DWN message. + */ + public readonly id: string; + + /** + * The requester for of the permission. + */ + public readonly requester: string; + + /** + * Optional string that communicates what the requested grant would be used for. + */ + public readonly description?: string; + + /** + * Whether the requested grant is delegated or not. + * If `true`, the `requestor` will be able to act as the grantor of the permission within the scope of the requested grant. + */ + public readonly delegated?: boolean; + + /** + * The scope of the allowed access. + */ + public readonly scope: PermissionScope; + + /** + * Optional conditions that must be met when the requested grant is used. + */ + public readonly conditions?: PermissionConditions; + + public static async parse(message: DataEncodedRecordsWriteMessage): Promise { + const permissionRequest = new PermissionRequest(message); + return permissionRequest; + } + + private constructor(message: DataEncodedRecordsWriteMessage) { + // properties derived from the generic DWN message properties + this.id = message.recordId; + this.requester = Message.getSigner(message)!; + + // properties from the data payload itself. + const permissionRequestEncodedData = message.encodedData; + const permissionRequestData = Encoder.base64UrlToObject(permissionRequestEncodedData) as PermissionRequestData; + this.delegated = permissionRequestData.delegated; + this.description = permissionRequestData.description; + this.scope = permissionRequestData.scope; + this.conditions = permissionRequestData.conditions; + } +} + diff --git a/src/protocols/permissions.ts b/src/protocols/permissions.ts index 399283db3..ab9f6d4ef 100644 --- a/src/protocols/permissions.ts +++ b/src/protocols/permissions.ts @@ -165,7 +165,8 @@ export class PermissionsProtocol { public static async createRequest(options: PermissionRequestCreateOptions): Promise<{ recordsWrite: RecordsWrite, permissionRequestData: PermissionRequestData, - permissionRequestBytes: Uint8Array + permissionRequestBytes: Uint8Array, + dataEncodedMessage: DataEncodedRecordsWriteMessage, }> { if (this.isRecordPermissionScope(options.scope) && options.scope.protocol === undefined) { @@ -204,10 +205,16 @@ export class PermissionsProtocol { tags : permissionTags, }); + const dataEncodedMessage: DataEncodedRecordsWriteMessage = { + ...recordsWrite.message, + encodedData: Encoder.bytesToBase64Url(permissionRequestBytes) + }; + return { recordsWrite, permissionRequestData, - permissionRequestBytes + permissionRequestBytes, + dataEncodedMessage }; } diff --git a/tests/protocols/permission-request.spec.ts b/tests/protocols/permission-request.spec.ts new file mode 100644 index 000000000..54705faaf --- /dev/null +++ b/tests/protocols/permission-request.spec.ts @@ -0,0 +1,38 @@ +import type { RecordsPermissionScope } from '../../src/types/permission-types.js'; + +import chaiAsPromised from 'chai-as-promised'; +import sinon from 'sinon'; +import chai, { expect } from 'chai'; + +import { Jws } from '../../src/utils/jws.js'; +import { DwnInterfaceName, DwnMethodName, PermissionRequest, PermissionsProtocol, TestDataGenerator } from '../../src/index.js'; + +chai.use(chaiAsPromised); + +describe('PermissionRequest', () => { + afterEach(() => { + // restores all fakes, stubs, spies etc. not restoring causes a memory leak. + // more info here: https://sinonjs.org/releases/v13/general-setup/ + sinon.restore(); + }); + + it('should parse a permission request message into a PermissionRequest', async () => { + const alice = await TestDataGenerator.generateDidKeyPersona(); + const scope: RecordsPermissionScope = { + interface : DwnInterfaceName.Records, + method : DwnMethodName.Query, + protocol : 'https://example.com/protocol/test' + }; + + const permissionRequest = await PermissionsProtocol.createRequest({ + signer : Jws.createSigner(alice), + delegated : true, + scope + }); + + const parsedPermissionRequest = await PermissionRequest.parse(permissionRequest.dataEncodedMessage); + expect (parsedPermissionRequest.id).to.equal(permissionRequest.dataEncodedMessage.recordId); + expect (parsedPermissionRequest.delegated).to.equal(true); + expect (parsedPermissionRequest.scope).to.deep.equal(scope); + }); +});