From ee68791acd0f48d16d17b9a4a22a8a7a0e11730a Mon Sep 17 00:00:00 2001 From: Henry Tsai Date: Wed, 27 Mar 2024 11:39:43 -0700 Subject: [PATCH] Introduced first-class Permissions protocol (#713) This will replace the entire `Permissions` interface. This PR only contains the support of permission life-cycle management through Request, Grant, and Revocation records. The actual replacing/swapping out the existing logic that depends on `Permissions` messages will come in the next PR. --- json-schemas/permission-grant.json | 31 ++ src/core/protocol-authorization.ts | 6 + src/index.ts | 1 + src/interfaces/records-write.ts | 9 +- src/protocols/permissions.ts | 277 ++++++++++++++++++ src/types/permissions-grant-descriptor.ts | 71 ++++- src/types/protocols-types.ts | 1 + tests/features/author-delegated-grant.spec.ts | 2 +- tests/features/owner-delegated-grant.spec.ts | 2 +- tests/features/owner-signature.spec.ts | 2 +- tests/features/permissions.spec.ts | 208 +++++++++++++ tests/test-suite.ts | 2 + 12 files changed, 604 insertions(+), 8 deletions(-) create mode 100644 json-schemas/permission-grant.json create mode 100644 src/protocols/permissions.ts create mode 100644 tests/features/permissions.spec.ts diff --git a/json-schemas/permission-grant.json b/json-schemas/permission-grant.json new file mode 100644 index 000000000..3bbe931bf --- /dev/null +++ b/json-schemas/permission-grant.json @@ -0,0 +1,31 @@ +{ + "$id": "https://identity.foundation/dwn/json-schemas/permission-grant.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": [ + "dateExpires", + "scope" + ], + "properties": { + "dateExpires": { + "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/definitions/date-time" + }, + "description": { + "type": "string" + }, + "delegated": { + "type": "boolean" + }, + "requestId": { + "description": "CID of an associated permission request DWN message", + "type": "string" + }, + "scope": { + "$ref": "https://identity.foundation/dwn/json-schemas/permissions/defs.json#/definitions/scope" + }, + "conditions": { + "$ref": "https://identity.foundation/dwn/json-schemas/permissions/defs.json#/definitions/conditions" + } + } +} \ No newline at end of file diff --git a/src/core/protocol-authorization.ts b/src/core/protocol-authorization.ts index dcf385887..097f0ec82 100644 --- a/src/core/protocol-authorization.ts +++ b/src/core/protocol-authorization.ts @@ -8,6 +8,7 @@ import type { RecordsWriteMessage } from '../types/records-types.js'; import type { ProtocolActionRule, ProtocolDefinition, ProtocolRuleSet, ProtocolsConfigureMessage, ProtocolType, ProtocolTypes } from '../types/protocols-types.js'; import { FilterUtility } from '../utils/filter.js'; +import { PermissionsProtocol } from '../protocols/permissions.js'; import { Records } from '../utils/records.js'; import { RecordsWrite } from '../interfaces/records-write.js'; import { DwnError, DwnErrorCode } from './dwn-error.js'; @@ -250,6 +251,11 @@ export class ProtocolAuthorization { protocolUri: string, messageStore: MessageStore ): Promise { + // if first-class protocol, return the definition from const object directly without going to data store + if (protocolUri === PermissionsProtocol.uri) { + return PermissionsProtocol.definition; + } + // fetch the corresponding protocol definition const query: Filter = { interface : DwnInterfaceName.Protocols, diff --git a/src/index.ts b/src/index.ts index 18401fc05..65f79b2ab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,6 +37,7 @@ export { UnionMessageReply } from './core/message-reply.js'; export { MessageStore, MessageStoreOptions } from './types/message-store.js'; export { PermissionsGrant, PermissionsGrantOptions } from './interfaces/permissions-grant.js'; export { PermissionsRequest, PermissionsRequestOptions } from './interfaces/permissions-request.js'; +export { PermissionsProtocol } from './protocols/permissions.js'; export { PermissionsRevoke, PermissionsRevokeOptions } from './interfaces/permissions-revoke.js'; export { PrivateKeySigner } from './utils/private-key-signer.js'; export { Protocols } from './utils/protocols.js'; diff --git a/src/interfaces/records-write.ts b/src/interfaces/records-write.ts index 072944dc9..a459b44a7 100644 --- a/src/interfaces/records-write.ts +++ b/src/interfaces/records-write.ts @@ -57,12 +57,12 @@ export type RecordsWriteOptions = { dataFormat: string; /** - * Signer of the message. + * The signer of the message, which is commonly the author, but can also be a delegate. */ signer?: Signer; /** - * The delegated grant to sign on behalf of the logical author, which is the grantor (`grantedBy`) of the delegated grant. + * The delegated grant invoked to sign on behalf of the logical author, which is the grantor of the delegated grant. */ delegatedGrant?: DelegatedGrantMessage; @@ -136,8 +136,9 @@ export type CreateFromOptions = { messageTimestamp?: string; datePublished?: string; + /** - * Signer of the message. + * The signer of the message, which is commonly the author, but can also be a delegate. */ signer?: Signer; @@ -487,7 +488,7 @@ export class RecordsWrite implements MessageInterface { } /** - * Signs the RecordsWrite, commonly as author, but can also be a delegate. + * Signs the RecordsWrite, the signer is commonly the author, but can also be a delegate. */ public async sign(options: { signer: Signer, diff --git a/src/protocols/permissions.ts b/src/protocols/permissions.ts new file mode 100644 index 000000000..88956f27f --- /dev/null +++ b/src/protocols/permissions.ts @@ -0,0 +1,277 @@ +import type { ProtocolDefinition } from '../types/protocols-types.js'; +import type { Signer } from '../types/signer.js'; +import type { PermissionConditions, PermissionGrantModel, PermissionRequestModel, PermissionRevocationModel, PermissionScope, RecordsPermissionScope } from '../types/permissions-grant-descriptor.js'; + +import { Encoder } from '../utils/encoder.js'; +import { RecordsWrite } from '../../src/interfaces/records-write.js'; +import { normalizeProtocolUrl, normalizeSchemaUrl } from '../utils/url.js'; + +/** + * Options for creating a permission request. + */ +export type PermissionRequestCreateOptions = { + /** + * The signer of the request. + */ + signer?: Signer; + + dateRequested?: string; + + // remaining properties are contained within the data payload of the record + + description?: string; + delegated: boolean; + scope: PermissionScope; + conditions?: PermissionConditions; +}; + +/** + * Options for creating a permission grant. + */ +export type PermissionGrantCreateOptions = { + /** + * The signer of the grant. + */ + signer?: Signer; + grantedTo: string; + dateGranted?: string; + + // remaining properties are contained within the data payload of the record + + /** + * Expire time in UTC ISO-8601 format with microsecond precision. + */ + dateExpires: string; + requestId?: string; + description?: string; + delegated?: boolean; + scope: PermissionScope; + conditions?: PermissionConditions; +}; + +/** + * Options for creating a permission revocation. + */ +export type PermissionRevocationCreateOptions = { + /** + * The signer of the grant. + */ + signer?: Signer; + grantId: string; + dateRevoked?: string; + + // remaining properties are contained within the data payload of the record + + description?: string; +}; + +/** + * This is a first-class DWN protocol for managing permission grants of a given DWN. + */ +export class PermissionsProtocol { + /** + * The URI of the DWN Permissions protocol. + */ + public static readonly uri = 'https://tbd.website/dwn/permissions'; + + /** + * The protocol path of the `request` record. + */ + public static readonly requestPath = 'request'; + + /** + * The protocol path of the `grant` record. + */ + public static readonly grantPath = 'grant'; + + /** + * The protocol path of the `revocation` record. + */ + public static readonly revocationPath = 'grant/revocation'; + + /** + * The definition of the Permissions protocol. + */ + public static readonly definition: ProtocolDefinition = { + published : true, + protocol : PermissionsProtocol.uri, + types : { + request: { + dataFormats: ['application/json'] + }, + grant: { + dataFormats: ['application/json'] + }, + revocation: { + dataFormats: ['application/json'] + } + }, + structure: { + request: { + $size: { + max: 10000 + }, + $actions: [ + { + who : 'anyone', + can : ['create'] + } + ] + }, + grant: { + $size: { + max: 10000 + }, + $actions: [ + { + who : 'recipient', + of : 'grant', + can : ['read', 'query'] + } + ], + revocation: { + $size: { + max: 10000 + }, + $actions: [ + { + who : 'anyone', + can : ['read'] + } + ] + } + } + } + }; + + public static parseRequest(base64UrlEncodedRequest: string): PermissionRequestModel { + return Encoder.base64UrlToObject(base64UrlEncodedRequest); + } + + /** + * Convenience method to create a permission request. + */ + public static async createRequest(options: PermissionRequestCreateOptions): Promise<{ + recordsWrite: RecordsWrite, + permissionRequestModel: PermissionRequestModel, + permissionRequestBytes: Uint8Array + }> { + const scope = PermissionsProtocol.normalizePermissionScope(options.scope); + + const permissionRequestModel: PermissionRequestModel = { + description : options.description, + delegated : options.delegated, + scope, + conditions : options.conditions, + }; + + const permissionRequestBytes = Encoder.objectToBytes(permissionRequestModel); + const recordsWrite = await RecordsWrite.create({ + signer : options.signer, + messageTimestamp : options.dateRequested, + protocol : PermissionsProtocol.uri, + protocolPath : PermissionsProtocol.requestPath, + dataFormat : 'application/json', + data : permissionRequestBytes, + }); + + return { + recordsWrite, + permissionRequestModel, + permissionRequestBytes + }; + } + + /** + * Convenience method to create a permission grant. + */ + public static async createGrant(options: PermissionGrantCreateOptions): Promise<{ + recordsWrite: RecordsWrite, + permissionGrantModel: PermissionGrantModel, + permissionGrantBytes: Uint8Array + }> { + const scope = PermissionsProtocol.normalizePermissionScope(options.scope); + + const permissionGrantModel: PermissionGrantModel = { + dateExpires : options.dateExpires, + requestId : options.requestId, + description : options.description, + delegated : options.delegated, + scope, + conditions : options.conditions, + }; + + const permissionGrantBytes = Encoder.objectToBytes(permissionGrantModel); + const recordsWrite = await RecordsWrite.create({ + signer : options.signer, + messageTimestamp : options.dateGranted, + recipient : options.grantedTo, + protocol : PermissionsProtocol.uri, + protocolPath : PermissionsProtocol.grantPath, + dataFormat : 'application/json', + data : permissionGrantBytes, + }); + + return { + recordsWrite, + permissionGrantModel, + permissionGrantBytes + }; + } + + /** + * Convenience method to create a permission revocation. + */ + public static async createRevocation(options: PermissionRevocationCreateOptions): Promise<{ + recordsWrite: RecordsWrite, + permissionRevocationModel: PermissionRevocationModel, + permissionRevocationBytes: Uint8Array + }> { + const permissionRevocationModel: PermissionRevocationModel = { + description: options.description, + }; + + const permissionRevocationBytes = Encoder.objectToBytes(permissionRevocationModel); + const recordsWrite = await RecordsWrite.create({ + signer : options.signer, + parentContextId : options.grantId, + protocol : PermissionsProtocol.uri, + protocolPath : PermissionsProtocol.revocationPath, + dataFormat : 'application/json', + data : permissionRevocationBytes, + }); + + return { + recordsWrite, + permissionRevocationModel, + permissionRevocationBytes + }; + } + + /** + * Normalizes the given permission scope if needed. + * @returns The normalized permission scope. + */ + private static normalizePermissionScope(permissionScope: PermissionScope): PermissionScope { + const scope = { ...permissionScope }; + + if (PermissionsProtocol.isRecordPermissionScope(scope)) { + // normalize protocol and schema URLs if they are present + if (scope.protocol !== undefined) { + scope.protocol = normalizeProtocolUrl(scope.protocol); + } + if (scope.schema !== undefined) { + scope.schema = normalizeSchemaUrl(scope.schema); + } + } + + return scope; + } + + /** + * Type guard to determine if the scope is a record permission scope. + */ + private static isRecordPermissionScope(scope: PermissionScope): scope is RecordsPermissionScope { + return scope.interface === 'Records'; + } +}; \ No newline at end of file diff --git a/src/types/permissions-grant-descriptor.ts b/src/types/permissions-grant-descriptor.ts index 0b552d30f..9c15bb54c 100644 --- a/src/types/permissions-grant-descriptor.ts +++ b/src/types/permissions-grant-descriptor.ts @@ -44,13 +44,82 @@ export type PermissionsGrantDescriptor = { conditions?: PermissionConditions }; +/** + * The data model of a permission request. + */ +export type PermissionRequestModel = { + /** + * If the grant is a delegated grant or not. If `true`, the `grantedTo` will be able to act as the `grantedBy` within the scope of this grant. + */ + delegated: boolean; + + /** + * Optional string that communicates what the grant would be used for. + */ + description?: string; + + /** + * The scope of the allowed access. + */ + scope: PermissionScope; + + conditions?: PermissionConditions +}; + +/** + * The data model of a permission grant. + */ +export type PermissionGrantModel = { + /** + * Optional string that communicates what the grant would be used for + */ + description?: string; + + /** + * Optional CID of a permission request. This is optional because grants may be given without being officially requested + * */ + requestId?: string; + + /** + * Timestamp at which this grant will no longer be active. + */ + dateExpires: string; + + /** + * Whether this grant is delegated or not. If `true`, the `grantedTo` will be able to act as the `grantedTo` within the scope of this grant. + */ + delegated?: boolean; + + /** + * The scope of the allowed access. + */ + scope: PermissionScope; + + conditions?: PermissionConditions +}; + +/** + * The data model of a permission revocation. + */ +export type PermissionRevocationModel = { + /** + * Optional string that communicates the details of the revocation. + */ + description?: string; +}; + +/** + * The data model for a permission scope. + */ export type PermissionScope = { interface: DwnInterfaceName; method: DwnMethodName; } | RecordsPermissionScope; -// Method-specific scopes +/** + * The data model for a permission scope that is specific to the Records interface. + */ export type RecordsPermissionScope = { interface: DwnInterfaceName.Records; method: DwnMethodName.Read | DwnMethodName.Write | DwnMethodName.Query | DwnMethodName.Subscribe | DwnMethodName.Delete; diff --git a/src/types/protocols-types.ts b/src/types/protocols-types.ts index 6c06a596e..60bb8ff4d 100644 --- a/src/types/protocols-types.ts +++ b/src/types/protocols-types.ts @@ -127,6 +127,7 @@ export type ProtocolRuleSet = { min?: number, max?: number } + // JSON Schema verifies that properties other than properties prefixed with $ will actually have type ProtocolRuleSet [key: string]: any; }; diff --git a/tests/features/author-delegated-grant.spec.ts b/tests/features/author-delegated-grant.spec.ts index eaa89bb72..339fff9f3 100644 --- a/tests/features/author-delegated-grant.spec.ts +++ b/tests/features/author-delegated-grant.spec.ts @@ -29,7 +29,7 @@ import { DwnInterfaceName, DwnMethodName, Encoder, Message, PermissionsGrant, Pe chai.use(chaiAsPromised); export function testAuthorDelegatedGrant(): void { - describe('author delegated grant tests', async () => { + describe('author delegated grant', async () => { let didResolver: DidResolver; let messageStore: MessageStore; let dataStore: DataStore; diff --git a/tests/features/owner-delegated-grant.spec.ts b/tests/features/owner-delegated-grant.spec.ts index e1fa574c0..830c1709b 100644 --- a/tests/features/owner-delegated-grant.spec.ts +++ b/tests/features/owner-delegated-grant.spec.ts @@ -25,7 +25,7 @@ import { DwnInterfaceName, DwnMethodName, Encoder, Message, PermissionsGrant, Pe chai.use(chaiAsPromised); export function testOwnerDelegatedGrant(): void { - describe('owner delegated grant tests', async () => { + describe('owner delegated grant', async () => { let didResolver: DidResolver; let messageStore: MessageStore; let dataStore: DataStore; diff --git a/tests/features/owner-signature.spec.ts b/tests/features/owner-signature.spec.ts index 4c641ec7f..8f0344ffd 100644 --- a/tests/features/owner-signature.spec.ts +++ b/tests/features/owner-signature.spec.ts @@ -22,7 +22,7 @@ import { DidKey, UniversalResolver } from '@web5/dids'; chai.use(chaiAsPromised); export function testOwnerSignature(): void { - describe('owner signature tests', async () => { + describe('owner signature', async () => { let didResolver: DidResolver; let messageStore: MessageStore; let dataStore: DataStore; diff --git a/tests/features/permissions.spec.ts b/tests/features/permissions.spec.ts new file mode 100644 index 000000000..823f68965 --- /dev/null +++ b/tests/features/permissions.spec.ts @@ -0,0 +1,208 @@ +import type { DidResolver } from '@web5/dids'; +import type { EventStream } from '../../src/types/subscriptions.js'; +import type { PermissionScope } from '../../src/index.js'; +import type { DataStore, EventLog, MessageStore } from '../../src/index.js'; + +import chaiAsPromised from 'chai-as-promised'; +import sinon from 'sinon'; +import chai, { expect } from 'chai'; + +import { DataStream } from '../../src/utils/data-stream.js'; +import { Dwn } from '../../src/dwn.js'; +import { Jws } from '../../src/utils/jws.js'; +import { PermissionsProtocol } from '../../src/protocols/permissions.js'; +import { RecordsRead } from '../../src/interfaces/records-read.js'; +import { TestDataGenerator } from '../utils/test-data-generator.js'; +import { TestEventStream } from '../test-event-stream.js'; +import { TestStores } from '../test-stores.js'; +import { DidKey, UniversalResolver } from '@web5/dids'; +import { DwnErrorCode, DwnInterfaceName, DwnMethodName, RecordsQuery, Time } from '../../src/index.js'; + +chai.use(chaiAsPromised); + +export function testPermissions(): void { + describe('permissions', async () => { + let didResolver: DidResolver; + let messageStore: MessageStore; + let dataStore: DataStore; + let eventLog: EventLog; + let eventStream: EventStream; + let dwn: Dwn; + + // important to follow the `before` and `after` pattern to initialize and clean the stores in tests + // so that different test suites can reuse the same backend store for testing + before(async () => { + didResolver = new UniversalResolver({ didResolvers: [DidKey] }); + + const stores = TestStores.get(); + messageStore = stores.messageStore; + dataStore = stores.dataStore; + eventLog = stores.eventLog; + eventStream = TestEventStream.get(); + + dwn = await Dwn.create({ didResolver, messageStore, dataStore, eventLog, eventStream }); + }); + + beforeEach(async () => { + sinon.restore(); // wipe all previous stubs/spies/mocks/fakes + + // clean up before each test rather than after so that a test does not depend on other tests to do the clean up + await messageStore.clear(); + await dataStore.clear(); + await eventLog.clear(); + }); + + after(async () => { + await dwn.close(); + }); + + it('should support permission management through use of Request, Grants, and Revocations', async () => { + // scenario: + // 1. Verify anyone (Bob) can send a permission request to Alice + // 2. Alice queries her DWN for new permission requests + // 3. Verify a non-owner cannot create a grant for Bob in Alice's DWN + // 4. Alice creates a permission grant for Bob in her DWN + // 5. Verify that Bob can query the permission grant from Alice's DWN (even though Alice can also send it directly to Bob) + // 6. Verify that any third-party can fetch revocation of the grant and find it is still active (not revoked) + // 7. Verify that non-owner cannot revoke the grant + // 8. Alice revokes the permission grant for Bob + // 9. Verify that any third-party can fetch the revocation status of the permission grant + + const alice = await TestDataGenerator.generateDidKeyPersona(); + const bob = await TestDataGenerator.generateDidKeyPersona(); + + // 1. Verify anyone (Bob) can send a permission request to Alice + const permissionScope: PermissionScope = { + interface : DwnInterfaceName.Records, + method : DwnMethodName.Write, + protocol : `any-protocol`, + schema : `any-schema` + }; + const requestToAlice = await PermissionsProtocol.createRequest({ + signer : Jws.createSigner(bob), + description : `Requesting to write to Alice's DWN`, + delegated : false, + scope : permissionScope + }); + + const requestWriteReply = await dwn.processMessage( + alice.did, + requestToAlice.recordsWrite.message, + { dataStream: DataStream.fromBytes(requestToAlice.permissionRequestBytes) } + ); + expect(requestWriteReply.status.code).to.equal(202); + + // 2. Alice queries her DWN for new permission requests + const requestQuery = await RecordsQuery.create({ + signer : Jws.createSigner(alice), + filter : { + protocolPath : PermissionsProtocol.requestPath, + protocol : PermissionsProtocol.uri, + dateUpdated : { from: Time.createOffsetTimestamp({ seconds: -1 * 60 * 60 * 24 }) }// last 24 hours + } + }); + + const requestQueryReply = await dwn.processMessage(alice.did, requestQuery.message); + const requestFromBob = requestQueryReply.entries?.[0]!; + expect(requestQueryReply.status.code).to.equal(200); + expect(requestQueryReply.entries?.length).to.equal(1); + expect(requestFromBob.recordId).to.equal(requestToAlice.recordsWrite.message.recordId); + + // 3. Verify a non-owner cannot create a grant for Bob in Alice's DWN + const decodedRequest = PermissionsProtocol.parseRequest(requestFromBob.encodedData!); + const unauthorizedGrantWrite = await PermissionsProtocol.createGrant({ + signer : Jws.createSigner(bob), + dateExpires : Time.createOffsetTimestamp({ seconds: 100 }), + description : 'Allow Bob to write', + grantedTo : bob.did, + scope : decodedRequest.scope + }); + + const unauthorizedGrantWriteReply = await dwn.processMessage( + alice.did, + unauthorizedGrantWrite.recordsWrite.message, + { dataStream: DataStream.fromBytes(unauthorizedGrantWrite.permissionGrantBytes) } + ); + expect(unauthorizedGrantWriteReply.status.code).to.equal(401); + expect(unauthorizedGrantWriteReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationActionNotAllowed); + + // 4. Alice creates a permission grant for Bob in her DWN + const grantWrite = await PermissionsProtocol.createGrant({ + signer : Jws.createSigner(alice), + dateExpires : Time.createOffsetTimestamp({ seconds: 100 }), + description : 'Allow Bob to write', + grantedTo : bob.did, + scope : decodedRequest.scope + }); + + const grantWriteReply = await dwn.processMessage( + alice.did, + grantWrite.recordsWrite.message, + { dataStream: DataStream.fromBytes(grantWrite.permissionGrantBytes) } + ); + expect(grantWriteReply.status.code).to.equal(202); + + // 5. Verify that Bob can query the permission grant from Alice's DWN (even though Alice can also send it directly to Bob) + const grantQuery = await RecordsQuery.create({ + signer : Jws.createSigner(bob), + filter : { + protocolPath : PermissionsProtocol.grantPath, + protocol : PermissionsProtocol.uri, + dateUpdated : { from: Time.createOffsetTimestamp({ seconds: -1 * 60 * 60 * 24 }) }// last 24 hours + } + }); + + const grantQueryReply = await dwn.processMessage(alice.did, grantQuery.message); + const grantFromBob = grantQueryReply.entries?.[0]!; + expect(grantQueryReply.status.code).to.equal(200); + expect(grantQueryReply.entries?.length).to.equal(1); + expect(grantFromBob.recordId).to.equal(grantWrite.recordsWrite.message.recordId); + + // 6. Verify that any third-party can fetch revocation of the grant and find it is still active (not revoked) + const revocationRead = await RecordsRead.create({ + signer : Jws.createSigner(bob), + filter : { + contextId : grantWrite.recordsWrite.message.contextId, + protocolPath : PermissionsProtocol.revocationPath + } + }); + + const revocationReadReply = await dwn.processMessage(alice.did, revocationRead.message); + expect(revocationReadReply.status.code).to.equal(404); + + // 7. Verify that non-owner cannot revoke the grant + const unauthorizedRevokeWrite = await PermissionsProtocol.createRevocation({ + signer : Jws.createSigner(bob), + grantId : grantWrite.recordsWrite.message.recordId, + dateRevoked : Time.getCurrentTimestamp() + }); + + const unauthorizedRevokeWriteReply = await dwn.processMessage( + alice.did, + unauthorizedRevokeWrite.recordsWrite.message, + { dataStream: DataStream.fromBytes(unauthorizedRevokeWrite.permissionRevocationBytes) } + ); + expect(unauthorizedRevokeWriteReply.status.code).to.equal(401); + expect(unauthorizedGrantWriteReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationActionNotAllowed); + + // 8. Alice revokes the permission grant for Bob + const revokeWrite = await PermissionsProtocol.createRevocation({ + signer : Jws.createSigner(alice), + grantId : grantWrite.recordsWrite.message.recordId, + dateRevoked : Time.getCurrentTimestamp() + }); + + const revokeWriteReply = await dwn.processMessage( + alice.did, + revokeWrite.recordsWrite.message, + { dataStream: DataStream.fromBytes(revokeWrite.permissionRevocationBytes) } + ); + expect(revokeWriteReply.status.code).to.equal(202); + + // 9. Verify that any third-party can fetch the revocation status of the permission grant + const revocationReadReply2 = await dwn.processMessage(alice.did, revocationRead.message); + expect(revocationReadReply2.status.code).to.equal(200); + expect(revocationReadReply2.record?.recordId).to.equal(revokeWrite.recordsWrite.message.recordId); + }); + }); +} diff --git a/tests/test-suite.ts b/tests/test-suite.ts index a884ef838..5ce047caf 100644 --- a/tests/test-suite.ts +++ b/tests/test-suite.ts @@ -13,6 +13,7 @@ import { testMessageStore } from './store/message-store.spec.js'; import { testNestedRoleScenarios } from './scenarios/nested-roles.spec.js'; import { testOwnerDelegatedGrant } from './features/owner-delegated-grant.spec.js'; import { testOwnerSignature } from './features/owner-signature.spec.js'; +import { testPermissions } from './features/permissions.spec.js'; import { testPermissionsGrantHandler } from './handlers/permissions-grant.spec.js'; import { testPermissionsRequestHandler } from './handlers/permissions-request.spec.js'; import { testProtocolCreateAction } from './features/protocol-create-action.spec.js'; @@ -66,6 +67,7 @@ export class TestSuite { testAuthorDelegatedGrant(); testOwnerDelegatedGrant(); testOwnerSignature(); + testPermissions(); testProtocolCreateAction(); testProtocolDeleteAction(); testProtocolUpdateAction();