diff --git a/src/core/dwn-error.ts b/src/core/dwn-error.ts index 9a8f5bd52..308d4b039 100644 --- a/src/core/dwn-error.ts +++ b/src/core/dwn-error.ts @@ -50,6 +50,7 @@ export enum DwnErrorCode { ParseCidMultihashNotSupported = 'ParseCidMultihashNotSupported', PermissionsProtocolCreateGrantRecordsScopeMissingProtocol = 'PermissionsProtocolCreateGrantRecordsScopeMissingProtocol', PermissionsProtocolCreateRequestRecordsScopeMissingProtocol = 'PermissionsProtocolCreateRequestRecordsScopeMissingProtocol', + PermissionsProtocolGetScopeInvalidProtocol = 'PermissionsProtocolGetScopeInvalidProtocol', PermissionsProtocolValidateSchemaUnexpectedRecord = 'PermissionsProtocolValidateSchemaUnexpectedRecord', PermissionsProtocolValidateScopeContextIdProhibitedProperties = 'PermissionsProtocolValidateScopeContextIdProhibitedProperties', PermissionsProtocolValidateScopeProtocolMismatch = 'PermissionsProtocolValidateScopeProtocolMismatch', diff --git a/src/core/messages-grant-authorization.ts b/src/core/messages-grant-authorization.ts index 1d5d2a6ac..81b0a0859 100644 --- a/src/core/messages-grant-authorization.ts +++ b/src/core/messages-grant-authorization.ts @@ -1,12 +1,15 @@ import type { GenericMessage } from '../types/message-types.js'; import type { MessagesGetMessage } from '../types/messages-types.js'; -import type { MessagesPermissionScope } from '../types/permission-types.js'; import type { MessageStore } from '../types/message-store.js'; -import type { PermissionGrant } from '../protocols/permission-grant.js'; -import type { RecordsDeleteMessage, RecordsWriteMessage } from '../types/records-types.js'; +import type { ProtocolsConfigureMessage } from '../types/protocols-types.js'; +import type { DataEncodedRecordsWriteMessage, RecordsDeleteMessage, RecordsWriteMessage } from '../types/records-types.js'; +import type { MessagesPermissionScope, PermissionScope } from '../types/permission-types.js'; import { GrantAuthorization } from './grant-authorization.js'; import { Message } from './message.js'; +import { PermissionGrant } from '../protocols/permission-grant.js'; +import { PermissionRequest } from '../protocols/permission-request.js'; +import { PermissionsProtocol } from '../protocols/permissions.js'; import { Records } from '../utils/records.js'; import { DwnError, DwnErrorCode } from './dwn-error.js'; import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js'; @@ -64,6 +67,38 @@ export class MessagesGrantAuthorization { // the record protocol matches the incoming scope protocol return; } + + // otherwise we check if the protocol is the internal PermissionsProtocol for further validation + if (recordsWriteMessage.descriptor.protocol === PermissionsProtocol.uri) { + if (recordsWriteMessage.descriptor.protocolPath === PermissionsProtocol.revocationPath) { + // anyone can read a revocation message + return; + } + + // get the permission scope from the permission message + // if the permission message is a revocation, the scope is fetched from the grant that is being revoked + let permissionScope!: PermissionScope; + if (recordsWriteMessage.descriptor.protocolPath === PermissionsProtocol.grantPath) { + const grant = await PermissionGrant.parse(recordsWriteMessage as DataEncodedRecordsWriteMessage); + permissionScope = grant.scope; + } else if (recordsWriteMessage.descriptor.protocolPath === PermissionsProtocol.requestPath) { + const request = await PermissionRequest.parse(recordsWriteMessage as DataEncodedRecordsWriteMessage); + permissionScope = request.scope; + } + + if (PermissionsProtocol.hasProtocolScope(permissionScope) && permissionScope.protocol === incomingScope.protocol) { + // the permissions record scoped protocol matches the incoming scope protocol + return; + } + } + } else if (messageToGet.descriptor.interface === DwnInterfaceName.Protocols) { + // if the message is a protocol message, it must be a `ProtocolConfigure` message + const protocolsConfigureMessage = messageToGet as ProtocolsConfigureMessage; + const configureProtocol = protocolsConfigureMessage.descriptor.definition.protocol; + if (configureProtocol === incomingScope.protocol) { + // the configured protocol matches the incoming scope protocol + return; + } } throw new DwnError(DwnErrorCode.MessagesGetVerifyScopeFailed, 'record message failed scope authorization'); diff --git a/src/handlers/messages-get.ts b/src/handlers/messages-get.ts index a6bb117fa..1643696d2 100644 --- a/src/handlers/messages-get.ts +++ b/src/handlers/messages-get.ts @@ -47,9 +47,7 @@ export class MessagesGetHandler implements MethodHandler { return messageReplyFromError(error, 401); } - // Include associated data as `encodedData` IF: - // * its a RecordsWrite - // * `encodedData` exists which means the data size is equal or smaller than the size threshold + // If the message is a RecordsWrite, we include the data in the response if it is available const entry: MessagesGetReplyEntry = { message: messageResult, messageCid: message.descriptor.messageCid }; if (Records.isRecordsWrite(messageResult)) { const recordsWrite = entry.message as RecordsQueryReplyEntry; @@ -60,12 +58,13 @@ export class MessagesGetHandler implements MethodHandler { entry.message.data = DataStream.fromBytes(dataBytes); delete recordsWrite.encodedData; } else { - // check the data store for the associated data + // otherwise check the data store for the associated data const result = await this.dataStore.get(tenant, recordsWrite.recordId, recordsWrite.descriptor.dataCid); if (result?.dataStream !== undefined) { entry.message.data = result.dataStream; } else { // if there is no data, return with the data property undefined + // when records are deleted, their data is removed from the data store but the message remains in the message store delete entry.message.data; } } diff --git a/src/protocols/permissions.ts b/src/protocols/permissions.ts index 34fc6d9b0..3b050aa9b 100644 --- a/src/protocols/permissions.ts +++ b/src/protocols/permissions.ts @@ -287,7 +287,8 @@ export class PermissionsProtocol { public static async createRevocation(options: PermissionRevocationCreateOptions): Promise<{ recordsWrite: RecordsWrite, permissionRevocationData: PermissionRevocationData, - permissionRevocationBytes: Uint8Array + permissionRevocationBytes: Uint8Array, + dataEncodedMessage: DataEncodedRecordsWriteMessage, }> { const permissionRevocationData: PermissionRevocationData = { description: options.description, @@ -316,10 +317,16 @@ export class PermissionsProtocol { tags : permissionTags, }); + const dataEncodedMessage: DataEncodedRecordsWriteMessage = { + ...recordsWrite.message, + encodedData: Encoder.bytesToBase64Url(permissionRevocationBytes) + }; + return { recordsWrite, permissionRevocationData, - permissionRevocationBytes + permissionRevocationBytes, + dataEncodedMessage }; } diff --git a/tests/handlers/messages-get.spec.ts b/tests/handlers/messages-get.spec.ts index 6c1caa7cb..4a15ae799 100644 --- a/tests/handlers/messages-get.spec.ts +++ b/tests/handlers/messages-get.spec.ts @@ -15,7 +15,7 @@ import minimalProtocolDefinition from '../vectors/protocol-definitions/minimal.j import { TestDataGenerator } from '../utils/test-data-generator.js'; import { TestEventStream } from '../test-event-stream.js'; import { TestStores } from '../test-stores.js'; -import { DataStream, Dwn, DwnConstant, DwnErrorCode, DwnInterfaceName, DwnMethodName, Jws, PermissionsProtocol, Time } from '../../src/index.js'; +import { DataStream, Dwn, DwnConstant, DwnErrorCode, DwnInterfaceName, DwnMethodName, Jws, PermissionGrant, PermissionsProtocol, Time } from '../../src/index.js'; import { DidKey, UniversalResolver } from '@web5/dids'; import sinon from 'sinon'; @@ -60,7 +60,7 @@ export function testMessagesGetHandler(): void { await dwn.close(); }); - it('returns 401 if authentication fails', async () => { + it('returns a 401 if authentication fails', async () => { const alice = await TestDataGenerator.generateDidKeyPersona(); sinon.stub(GeneralJwsVerifier, 'verifySignatures').throws(new Error('Invalid signature')); @@ -129,7 +129,7 @@ export function testMessagesGetHandler(): void { describe('without a grant', () =>{ describe('records interface messages', () => { - it('returns 401 if the tenant is not the author', async () => { + it('returns a 401 if the tenant is not the author', async () => { const alice = await TestDataGenerator.generateDidKeyPersona(); const bob = await TestDataGenerator.generateDidKeyPersona(); @@ -254,52 +254,113 @@ export function testMessagesGetHandler(): void { }); }); - describe('protocol interface messages', () => { - it('returns 401 if the tenant is not the author', async () => { - // scenario: Alice creates a non-published protocol, installs it, and writes a record. Bob tries to get the protocol message. - // Bob is unable to get the protocol message because it is not published. + describe('Protocol interface messages', () => { + it('returns a 401 if the tenant is not the author', async () => { + // scenario: Alice configures both a published and non-published protocol and writes it to her DWN. + // Bob is unable to get either of the ProtocolConfigure messages because he is not the author. const alice = await TestDataGenerator.generateDidKeyPersona(); const bob = await TestDataGenerator.generateDidKeyPersona(); - const protocolDefinition = { ...minimalProtocolDefinition, published: false }; - const { message: protocolsConfigure } = await TestDataGenerator.generateProtocolsConfigure({ - author: alice, - protocolDefinition + // unpublished protocol configuration + const unpublishedProtocolDefinition = { + ...minimalProtocolDefinition, + protocol : 'http://example.com/protocol/unpublished', + published : false + }; + const { message: unpublishedProtocolsConfigure } = await TestDataGenerator.generateProtocolsConfigure({ + author : alice, + protocolDefinition : unpublishedProtocolDefinition }); - const protocolsConfigureReply = await dwn.processMessage(alice.did, protocolsConfigure); - expect(protocolsConfigureReply.status.code).to.equal(202); + const unpublishedProtocolsConfigureReply = await dwn.processMessage(alice.did, unpublishedProtocolsConfigure); + expect(unpublishedProtocolsConfigureReply.status.code).to.equal(202); + + // published protocol configuration + const publishedProtocolDefinition = { + ...minimalProtocolDefinition, + protocol : 'http://example.com/protocol/published', + published : true + }; + const { message: publishedProtocolsConfigure } = await TestDataGenerator.generateProtocolsConfigure({ + author : alice, + protocolDefinition : publishedProtocolDefinition + }); + const publishedProtocolsConfigureReply = await dwn.processMessage(alice.did, publishedProtocolsConfigure); + expect(publishedProtocolsConfigureReply.status.code).to.equal(202); - const protocolMessageCid = await Message.getCid(protocolsConfigure); + // get the message CIDs + const unpublishedProtocolMessageCid = await Message.getCid(unpublishedProtocolsConfigure); + const publishedProtocolMessageCid = await Message.getCid(publishedProtocolsConfigure); - // bob attempts to get the message - const { message: getProtocolConfigure } = await TestDataGenerator.generateMessagesGet({ + // bob attempts to get the unpublished protocol configuration + const { message: getUnpublishedProtocolConfigure } = await TestDataGenerator.generateMessagesGet({ author : bob, - messageCid : protocolMessageCid, + messageCid : unpublishedProtocolMessageCid, }); - const getProtocolConfigureReply = await dwn.processMessage(alice.did, getProtocolConfigure); - expect(getProtocolConfigureReply.status.code).to.equal(401); - expect(getProtocolConfigureReply.status.detail).to.include(DwnErrorCode.MessagesGetAuthorizationFailed); - expect(getProtocolConfigureReply.entry).to.be.undefined; + const getUnpublishedProtocolConfigureReply = await dwn.processMessage(alice.did, getUnpublishedProtocolConfigure); + expect(getUnpublishedProtocolConfigureReply.status.code).to.equal(401); + expect(getUnpublishedProtocolConfigureReply.status.detail).to.include(DwnErrorCode.MessagesGetAuthorizationFailed); + expect(getUnpublishedProtocolConfigureReply.entry).to.be.undefined; + + // bob attempts to get the published protocol configuration + const { message: getPublishedProtocolConfigure } = await TestDataGenerator.generateMessagesGet({ + author : bob, + messageCid : publishedProtocolMessageCid, + }); + const getPublishedProtocolConfigureReply = await dwn.processMessage(alice.did, getPublishedProtocolConfigure); + expect(getPublishedProtocolConfigureReply.status.code).to.equal(401); + expect(getPublishedProtocolConfigureReply.status.detail).to.include(DwnErrorCode.MessagesGetAuthorizationFailed); + expect(getPublishedProtocolConfigureReply.entry).to.be.undefined; + + // control: alice is able to get both the published and unpublished protocol configurations + const { message: getUnpublishedProtocolConfigureAlice } = await TestDataGenerator.generateMessagesGet({ + author : alice, + messageCid : unpublishedProtocolMessageCid, + }); + const getUnpublishedProtocolConfigureAliceReply = await dwn.processMessage(alice.did, getUnpublishedProtocolConfigureAlice); + expect(getUnpublishedProtocolConfigureAliceReply.status.code).to.equal(200); + expect(getUnpublishedProtocolConfigureAliceReply.entry).to.exist; + expect(getUnpublishedProtocolConfigureAliceReply.entry!.messageCid).to.equal(unpublishedProtocolMessageCid); + expect(getUnpublishedProtocolConfigureAliceReply.entry!.message).to.deep.equal(unpublishedProtocolsConfigure); + + const { message: getPublishedProtocolConfigureAlice } = await TestDataGenerator.generateMessagesGet({ + author : alice, + messageCid : publishedProtocolMessageCid, + }); + const getPublishedProtocolConfigureAliceReply = await dwn.processMessage(alice.did, getPublishedProtocolConfigureAlice); + expect(getPublishedProtocolConfigureAliceReply.status.code).to.equal(200); + expect(getPublishedProtocolConfigureAliceReply.entry).to.exist; + expect(getPublishedProtocolConfigureAliceReply.entry!.messageCid).to.equal(publishedProtocolMessageCid); + expect(getPublishedProtocolConfigureAliceReply.entry!.message).to.deep.equal(publishedProtocolsConfigure); }); }); }); describe('with a grant', () => { - it('rejects with 401 an external party attempts to MessagesGet if grant has different DWN interface scope', async () => { + it('returns a 401 if grant has different DWN interface scope', async () => { // scenario: Alice grants Bob access to RecordsWrite, then Bob tries to invoke the grant with MessagesGet const alice = await TestDataGenerator.generateDidKeyPersona(); const bob = await TestDataGenerator.generateDidKeyPersona(); + // alice installs a protocol + const protocolsConfig = await TestDataGenerator.generateProtocolsConfigure({ + author : alice, + protocolDefinition : minimalProtocolDefinition + }); + const protocolsConfigureReply = await dwn.processMessage(alice.did, protocolsConfig.message); + expect(protocolsConfigureReply.status.code).to.equal(202); + // Alice writes a record which Bob will later try to read const { recordsWrite, dataStream } = await TestDataGenerator.generateRecordsWrite({ - author: alice, + author : alice, + protocol : minimalProtocolDefinition.protocol, + protocolPath : 'foo', }); const recordsWriteReply = await dwn.processMessage(alice.did, recordsWrite.message, { dataStream }); expect(recordsWriteReply.status.code).to.equal(202); - // Alice gives Bob a permission grant with scope MessagesGet + // Alice gives Bob a permission grant scoped to a RecordsWrite and the protocol const permissionGrant = await PermissionsProtocol.createGrant({ signer : Jws.createSigner(alice), grantedTo : bob.did, @@ -318,7 +379,7 @@ export function testMessagesGetHandler(): void { ); expect(permissionGrantWriteReply.status.code).to.equal(202); - // Bob tries to MessagesGet + // Bob tries to MessagesGet using the RecordsWrite grant const messagesGet = await TestDataGenerator.generateMessagesGet({ author : bob, messageCid : await Message.getCid(recordsWrite.message), @@ -370,28 +431,74 @@ export function testMessagesGetHandler(): void { expect(readReply.entry!.messageCid).to.equal(messageCid); }); - describe('protocol scoped records', () => { - it('allows reads of protocol messages that are RecordsDelete', async () => { - // Scenario: Alice writes a protocol record. - // Alice deletes the record. - // Alice gives Bob a grant to read messages in the protocol. - // Bob invokes that grant to read the delete message. + describe('protocol scoped messages', () => { + it('allows reads of protocol messages with a protocol restricted grant scope', async () => { + // This test will verify that a grant scoped to a specific protocol will allow a user to read messages associated with that protocol. + // These messages include the ProtocolConfiguration itself, even if not published, + // any RecordsWrite or RecordsDelete messages associated with the protocol, + // and any PermissionProtocol RecordsWrite messages associated with the protocol. + + // scenario: Alice configures a protocol that is unpublished and writes it to her DWN. + // Alice then gives Bob a grant to get messages from that protocol. + // Carol requests a grant to RecordsWrite to the protocol, and Alice grants it. + // Alice and Carol write records associated with the protocol. + // Alice also deletes a record associated with the protocol. + // Alice revokes the grant to Carol. + // Bob invokes his grant to read the various messages. + // As a control, Alice writes a record not associated with the protocol and Bob tries to unsuccessfully read it. const alice = await TestDataGenerator.generateDidKeyPersona(); const bob = await TestDataGenerator.generateDidKeyPersona(); + const carol = await TestDataGenerator.generateDidKeyPersona(); - const protocolDefinition = minimalProtocolDefinition; + const protocolDefinition = { ...minimalProtocolDefinition, published: false }; - // Alice installs the protocol + // Alice installs the unpublished protocol const protocolsConfig = await TestDataGenerator.generateProtocolsConfigure({ author: alice, - protocolDefinition, + protocolDefinition }); - const protocolsConfigureReply = await dwn.processMessage(alice.did, protocolsConfig.message); expect(protocolsConfigureReply.status.code).to.equal(202); + const protocolConfigureMessageCid = await Message.getCid(protocolsConfig.message); + + // Carol requests a grant to write records to the protocol + const permissionRequestCarol = await PermissionsProtocol.createRequest({ + signer : Jws.createSigner(alice), + delegated : false, + scope : { + interface : DwnInterfaceName.Records, + method : DwnMethodName.Write, + protocol : protocolDefinition.protocol, + } + }); + const requestDataStreamCarol = DataStream.fromBytes(permissionRequestCarol.permissionRequestBytes); + const permissionRequestWriteReplyCarol = await dwn.processMessage( + alice.did, + permissionRequestCarol.recordsWrite.message, + { dataStream: requestDataStreamCarol } + ); + expect(permissionRequestWriteReplyCarol.status.code).to.equal(202); + + // Alice gives Carol a grant to write records to the protocol + const permissionGrantCarol = await PermissionsProtocol.createGrant({ + signer : Jws.createSigner(alice), + grantedTo : carol.did, + dateExpires : Time.createOffsetTimestamp({ seconds: 60 * 60 * 24 }), // 24 hours + delegated : permissionRequestCarol.permissionRequestData.delegated, + scope : permissionRequestCarol.permissionRequestData.scope, + }); - // Alice writes a record which will be deleted + const grantDataStreamCarol = DataStream.fromBytes(permissionGrantCarol.permissionGrantBytes); + const permissionGrantWriteReplyCarol = await dwn.processMessage( + alice.did, + permissionGrantCarol.recordsWrite.message, + { dataStream: grantDataStreamCarol } + ); + expect(permissionGrantWriteReplyCarol.status.code).to.equal(202); + const carolGrantMessageCiD = await Message.getCid(permissionGrantCarol.recordsWrite.message); + + // Alice writes a record associated with the protocol const { recordsWrite, dataStream } = await TestDataGenerator.generateRecordsWrite({ author : alice, protocol : protocolDefinition.protocol, @@ -399,8 +506,9 @@ export function testMessagesGetHandler(): void { }); const recordsWriteReply = await dwn.processMessage(alice.did, recordsWrite.message, { dataStream }); expect(recordsWriteReply.status.code).to.equal(202); + const aliceRecordMessageCid = await Message.getCid(recordsWrite.message); - // Alice deletes the record + // Alice deletes a record associated with the protocol const recordsDelete = await TestDataGenerator.generateRecordsDelete({ author : alice, recordId : recordsWrite.message.recordId, @@ -408,64 +516,29 @@ export function testMessagesGetHandler(): void { const recordsDeleteReply = await dwn.processMessage(alice.did, recordsDelete.message); expect(recordsDeleteReply.status.code).to.equal(202); - // Alice grants Bob access to read messages in the protocol - const permissionGrant = await PermissionsProtocol.createGrant({ - signer : Jws.createSigner(alice), - grantedTo : bob.did, - dateExpires : Time.createOffsetTimestamp({ seconds: 60 * 60 * 24 }), // 24 hours - scope : { - interface : DwnInterfaceName.Messages, - method : DwnMethodName.Get, - protocol : protocolDefinition.protocol, - } + // Carol writes a record associated with the protocol + const { recordsWrite: recordsWriteCarol, dataStream: dataStreamCarol } = await TestDataGenerator.generateRecordsWrite({ + author : carol, + protocol : protocolDefinition.protocol, + protocolPath : 'foo', + permissionGrantId : permissionGrantCarol.recordsWrite.message.recordId, }); - const grantDataStream = DataStream.fromBytes(permissionGrant.permissionGrantBytes); - const permissionGrantWriteReply = await dwn.processMessage( + const recordsWriteReplyCarol = await dwn.processMessage(alice.did, recordsWriteCarol.message, { dataStream: dataStreamCarol }); + expect(recordsWriteReplyCarol.status.code).to.equal(202); + const carolRecordMessageCid = await Message.getCid(recordsWriteCarol.message); + + // Alice revokes Carol's grant + const permissionRevocationCarol = await PermissionsProtocol.createRevocation({ + signer : Jws.createSigner(alice), + grant : await PermissionGrant.parse(permissionGrantCarol.dataEncodedMessage), + }); + const permissionRevocationCarolDataStream = DataStream.fromBytes(permissionRevocationCarol.permissionRevocationBytes); + const permissionRevocationCarolReply = await dwn.processMessage( alice.did, - permissionGrant.recordsWrite.message, - { dataStream: grantDataStream } + permissionRevocationCarol.recordsWrite.message, + { dataStream: permissionRevocationCarolDataStream } ); - expect(permissionGrantWriteReply.status.code).to.equal(202); - - // Bob is able to read the delete message - const deleteMessageCid = await Message.getCid(recordsDelete.message); - const messagesGet = await TestDataGenerator.generateMessagesGet({ - author : bob, - messageCid : deleteMessageCid, - permissionGrantId : permissionGrant.recordsWrite.message.recordId, - }); - const messagesGetReply = await dwn.processMessage(alice.did, messagesGet.message); - expect(messagesGetReply.status.code).to.equal(200); - expect(messagesGetReply.entry).to.exist; - expect(messagesGetReply.entry!.messageCid).to.equal(deleteMessageCid); - }); - - it('allows reads of protocol messages with a an unrestricted grant scope', async () => { - // scenario: Alice writes a protocol record. Alice gives Bob a grant to read any messages - // Bob invokes that grant to read the protocol messages. - - const alice = await TestDataGenerator.generateDidKeyPersona(); - const bob = await TestDataGenerator.generateDidKeyPersona(); - - const protocolDefinition = minimalProtocolDefinition; - - // Alice installs the protocol - const protocolsConfig = await TestDataGenerator.generateProtocolsConfigure({ - author: alice, - protocolDefinition - }); - const protocolsConfigureReply = await dwn.processMessage(alice.did, protocolsConfig.message); - expect(protocolsConfigureReply.status.code).to.equal(202); - - // Alice writes a record which Bob will later try to read - const { recordsWrite, dataStream } = await TestDataGenerator.generateRecordsWrite({ - author : alice, - protocol : protocolDefinition.protocol, - protocolPath : 'foo', - }); - const recordsWriteReply = await dwn.processMessage(alice.did, recordsWrite.message, { dataStream }); - expect(recordsWriteReply.status.code).to.equal(202); - const recordMessageCid = await Message.getCid(recordsWrite.message); + expect(permissionRevocationCarolReply.status.code).to.equal(202); // Alice gives Bob a permission grant with scope MessagesGet const permissionGrant = await PermissionsProtocol.createGrant({ @@ -475,6 +548,7 @@ export function testMessagesGetHandler(): void { scope : { interface : DwnInterfaceName.Messages, method : DwnMethodName.Get, + protocol : protocolDefinition.protocol, } }); const grantDataStream = DataStream.fromBytes(permissionGrant.permissionGrantBytes); @@ -488,95 +562,124 @@ export function testMessagesGetHandler(): void { // Bob is unable to get the message without using the permission grant const messagesGetWithoutGrant = await TestDataGenerator.generateMessagesGet({ author : bob, - messageCid : recordMessageCid, + messageCid : aliceRecordMessageCid, }); const messagesGetWithoutGrantReply = await dwn.processMessage(alice.did, messagesGetWithoutGrant.message); expect(messagesGetWithoutGrantReply.status.code).to.equal(401); expect(messagesGetWithoutGrantReply.status.detail).to.contain(DwnErrorCode.MessagesGetAuthorizationFailed); - // Bob is able to get the message when he uses the permission grant + // Bob is able to get all the associated messages when using the permission grant + // Expected Messages: + // - Protocol Configuration + // - Alice's RecordsWrite + // - Alice's RecordsDelete + // - Carol's Permission Request + // - Alice's Permission Grant to Carol + // - Carol's RecordsWrite + // - Alice's Revocation of Carol's Grant + + // Protocol configuration + const messagesGetProtocolConfigure = await TestDataGenerator.generateMessagesGet({ + author : bob, + messageCid : protocolConfigureMessageCid, + permissionGrantId : permissionGrant.recordsWrite.message.recordId, + }); + const messagesGetProtocolConfigureReply = await dwn.processMessage(alice.did, messagesGetProtocolConfigure.message); + expect(messagesGetProtocolConfigureReply.status.code).to.equal(200); + expect(messagesGetProtocolConfigureReply.entry).to.exist; + expect(messagesGetProtocolConfigureReply.entry!.message).to.deep.equal(protocolsConfig.message); + + // alice RecordsWrite const messagesGetWithGrant = await TestDataGenerator.generateMessagesGet({ author : bob, - messageCid : recordMessageCid, + messageCid : aliceRecordMessageCid, permissionGrantId : permissionGrant.recordsWrite.message.recordId, }); const messagesGetWithGrantReply = await dwn.processMessage(alice.did, messagesGetWithGrant.message); expect(messagesGetWithGrantReply.status.code).to.equal(200); + expect(messagesGetWithGrantReply.entry).to.exist; + // delete the data field from the message for comparison of the message + delete messagesGetWithGrantReply.entry!.message.data; + expect(messagesGetWithGrantReply.entry!.message).to.deep.equal(recordsWrite.message); - // Bob is able to get the message of the grant associated with the protocol - const grantMessageCid = await Message.getCid(permissionGrant.recordsWrite.message); - const grantMessageRead = await TestDataGenerator.generateMessagesGet({ + // alice RecordsDelete + const messagesGetDelete = await TestDataGenerator.generateMessagesGet({ author : bob, - messageCid : grantMessageCid, + messageCid : await Message.getCid(recordsDelete.message), permissionGrantId : permissionGrant.recordsWrite.message.recordId, }); - const grantMessageReadReply = await dwn.processMessage(alice.did, grantMessageRead.message); - expect(grantMessageReadReply.status.code).to.equal(200); - }); - - it('allows reads of protocol messages with a protocol restricted grant scope', async () => { - // scenario: Alice writes a protocol record. Alice gives Bob a grant to read messages in the protocol - // Bob invokes that grant to read the protocol messages. - - const alice = await TestDataGenerator.generateDidKeyPersona(); - const bob = await TestDataGenerator.generateDidKeyPersona(); + const messagesGetDeleteReply = await dwn.processMessage(alice.did, messagesGetDelete.message); + expect(messagesGetDeleteReply.status.code).to.equal(200); + expect(messagesGetDeleteReply.entry).to.exist; + expect(messagesGetDeleteReply.entry!.message).to.deep.equal(recordsDelete.message); - const protocolDefinition = minimalProtocolDefinition; - - // Alice installs the protocol - const protocolsConfig = await TestDataGenerator.generateProtocolsConfigure({ - author: alice, - protocolDefinition + // carol's Permission Request + const messagesGetCarolRequest = await TestDataGenerator.generateMessagesGet({ + author : bob, + messageCid : await Message.getCid(permissionRequestCarol.recordsWrite.message), + permissionGrantId : permissionGrant.recordsWrite.message.recordId, }); - const protocolsConfigureReply = await dwn.processMessage(alice.did, protocolsConfig.message); - expect(protocolsConfigureReply.status.code).to.equal(202); - - // Alice writes a record which Bob will later try to read - const { recordsWrite, dataStream } = await TestDataGenerator.generateRecordsWrite({ - author : alice, - protocol : protocolDefinition.protocol, - protocolPath : 'foo', + const messagesGetCarolRequestReply = await dwn.processMessage(alice.did, messagesGetCarolRequest.message); + expect(messagesGetCarolRequestReply.status.code).to.equal(200); + expect(messagesGetCarolRequestReply.entry).to.exist; + // delete the data field from the message for comparison of the message + delete messagesGetCarolRequestReply.entry!.message.data; + expect(messagesGetCarolRequestReply.entry!.message).to.deep.equal(permissionRequestCarol.recordsWrite.message); + + // carol's Permission Grant + const messagesGetCarolGrant = await TestDataGenerator.generateMessagesGet({ + author : bob, + messageCid : carolGrantMessageCiD, + permissionGrantId : permissionGrant.recordsWrite.message.recordId, }); - const recordsWriteReply = await dwn.processMessage(alice.did, recordsWrite.message, { dataStream }); - expect(recordsWriteReply.status.code).to.equal(202); - const recordMessageCid = await Message.getCid(recordsWrite.message); - - // Alice gives Bob a permission grant with scope MessagesGet - const permissionGrant = await PermissionsProtocol.createGrant({ - signer : Jws.createSigner(alice), - grantedTo : bob.did, - dateExpires : Time.createOffsetTimestamp({ seconds: 60 * 60 * 24 }), // 24 hours - scope : { - interface : DwnInterfaceName.Messages, - method : DwnMethodName.Get, - protocol : protocolDefinition.protocol, - } + const messagesGetCarolGrantReply = await dwn.processMessage(alice.did, messagesGetCarolGrant.message); + expect(messagesGetCarolGrantReply.status.code).to.equal(200); + expect(messagesGetCarolGrantReply.entry).to.exist; + // delete the data field from the message for comparison of the message + delete messagesGetCarolGrantReply.entry!.message.data; + expect(messagesGetCarolGrantReply.entry!.message).to.deep.equal(permissionGrantCarol.recordsWrite.message); + + // carol's RecordsWrite + const messagesGetCarolRecord = await TestDataGenerator.generateMessagesGet({ + author : bob, + messageCid : carolRecordMessageCid, + permissionGrantId : permissionGrant.recordsWrite.message.recordId, }); - const grantDataStream = DataStream.fromBytes(permissionGrant.permissionGrantBytes); - const permissionGrantWriteReply = await dwn.processMessage( - alice.did, - permissionGrant.recordsWrite.message, - { dataStream: grantDataStream } - ); - expect(permissionGrantWriteReply.status.code).to.equal(202); - - // Bob is unable to get the message without using the permission grant - const messagesGetWithoutGrant = await TestDataGenerator.generateMessagesGet({ - author : bob, - messageCid : recordMessageCid, + const messagesGetCarolRecordReply = await dwn.processMessage(alice.did, messagesGetCarolRecord.message); + expect(messagesGetCarolRecordReply.status.code).to.equal(200); + expect(messagesGetCarolRecordReply.entry).to.exist; + // delete the data field from the message for comparison of the message + delete messagesGetCarolRecordReply.entry!.message.data; + expect(messagesGetCarolRecordReply.entry!.message).to.deep.equal(recordsWriteCarol.message); + + // carol's Grant Revocation + const messagesGetCarolGrantRevocation = await TestDataGenerator.generateMessagesGet({ + author : bob, + messageCid : await Message.getCid(permissionRevocationCarol.recordsWrite.message), + permissionGrantId : permissionGrant.recordsWrite.message.recordId, }); - const messagesGetWithoutGrantReply = await dwn.processMessage(alice.did, messagesGetWithoutGrant.message); - expect(messagesGetWithoutGrantReply.status.code).to.equal(401); - expect(messagesGetWithoutGrantReply.status.detail).to.contain(DwnErrorCode.MessagesGetAuthorizationFailed); + const messagesGetCarolGrantRevocationReply = await dwn.processMessage(alice.did, messagesGetCarolGrantRevocation.message); + expect(messagesGetCarolGrantRevocationReply.status.code).to.equal(200); + expect(messagesGetCarolGrantRevocationReply.entry).to.exist; + // delete the data field from the message for comparison of the message + delete messagesGetCarolGrantRevocationReply.entry!.message.data; + expect(messagesGetCarolGrantRevocationReply.entry!.message).to.deep.equal(permissionRevocationCarol.recordsWrite.message); + + // CONTROL: Alice writes a record not associated with the protocol + const { recordsWrite: recordsWriteControl, dataStream: dataStreamControl } = await TestDataGenerator.generateRecordsWrite({ + author: alice, + }); + const recordsWriteControlReply = await dwn.processMessage(alice.did, recordsWriteControl.message, { dataStream: dataStreamControl }); + expect(recordsWriteControlReply.status.code).to.equal(202); - // Bob is able to get the message when he uses the permission grant - const messagesGetWithGrant = await TestDataGenerator.generateMessagesGet({ + // Bob is unable to get the control message + const messagesGetControl = await TestDataGenerator.generateMessagesGet({ author : bob, - messageCid : recordMessageCid, + messageCid : await Message.getCid(recordsWriteControl.message), permissionGrantId : permissionGrant.recordsWrite.message.recordId, }); - const messagesGetWithGrantReply = await dwn.processMessage(alice.did, messagesGetWithGrant.message); - expect(messagesGetWithGrantReply.status.code).to.equal(200); + const messagesGetControlReply = await dwn.processMessage(alice.did, messagesGetControl.message); + expect(messagesGetControlReply.status.code).to.equal(401); }); it('rejects message get of protocol messages with mismatching protocol grant scopes', async () => {