diff --git a/src/core/dwn-error.ts b/src/core/dwn-error.ts index 0658fe002..ed6cdb725 100644 --- a/src/core/dwn-error.ts +++ b/src/core/dwn-error.ts @@ -72,6 +72,7 @@ export enum DwnErrorCode { ProtocolsConfigureInvalidRole = 'ProtocolsConfigureInvalidRole', ProtocolsConfigureInvalidActionMissingOf = 'ProtocolsConfigureInvalidActionMissingOf', ProtocolsConfigureInvalidActionOfNotAllowed = 'ProtocolsConfigureInvalidActionOfNotAllowed', + ProtocolsConfigureInvalidRecipientOfAction = 'ProtocolsConfigureInvalidRecipientOfAction', ProtocolsConfigureQueryNotAllowed = 'ProtocolsConfigureQueryNotAllowed', ProtocolsConfigureUnauthorized = 'ProtocolsConfigureUnauthorized', ProtocolsQueryUnauthorized = 'ProtocolsQueryUnauthorized', diff --git a/src/core/protocol-authorization.ts b/src/core/protocol-authorization.ts index f3b36d68d..b94e2d142 100644 --- a/src/core/protocol-authorization.ts +++ b/src/core/protocol-authorization.ts @@ -549,6 +549,18 @@ export class ProtocolAuthorization { } else { continue; } + } else if (actionRule.who === ProtocolActor.Recipient && actionRule.of === undefined && author !== undefined) { + // Author must be recipient of the record being accessed + let recordsWriteMessage: RecordsWriteMessage; + if (incomingMessage.message.descriptor.method === DwnMethodName.Write) { + recordsWriteMessage = incomingMessage.message as RecordsWriteMessage; + } else { + // else the incoming message must be a RecordsDelete because only `update` and `delete` are allowed recipient actions + recordsWriteMessage = ancestorMessageChain[ancestorMessageChain.length - 1]; + } + if (recordsWriteMessage.descriptor.recipient === author) { + return; + } } else if (actionRule.who === ProtocolActor.Anyone) { return; } else if (author === undefined) { diff --git a/src/interfaces/protocols-configure.ts b/src/interfaces/protocols-configure.ts index 511a5933a..559be191e 100644 --- a/src/interfaces/protocols-configure.ts +++ b/src/interfaces/protocols-configure.ts @@ -2,6 +2,7 @@ import type { Signer } from '../types/signer.js'; import type { ProtocolDefinition, ProtocolRuleSet, ProtocolsConfigureDescriptor, ProtocolsConfigureMessage } from '../types/protocols-types.js'; import { Message } from '../core/message.js'; +import { ProtocolActor } from '../types/protocols-types.js'; import { Time } from '../utils/time.js'; import { DwnError, DwnErrorCode } from '../core/dwn-error.js'; import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js'; @@ -133,11 +134,27 @@ export class ProtocolsConfigure extends Message { ); } - // Validate that if `who` is not set to `anyone` then `of` is set - if (action.who !== undefined && ['author', 'recipient'].includes(action.who) && !action.of) { + // Validate that if `who === recipient` and `of === undefined`, then `can` is either `delete` or `update` + // We will not use direct recipient for `read`, `write`, or `query` because: + // - Recipients are always allowed to `read`. + // - `write` entails ability to create and update, whereas `update` only allows for updates. + // There is no 'recipient' until the record has been created, so it makes no sense to allow recipient to write. + // - At this time, `query` is only authorized using roles, so allowing direct recipients to query is outside the scope of this PR. + if (action.who === ProtocolActor.Recipient && + action.of === undefined && + !['update', 'delete'].includes(action.can) + ) { + throw new DwnError( + DwnErrorCode.ProtocolsConfigureInvalidRecipientOfAction, + 'Rules for `recipient` without `of` property must have `can` === `delete` or `update`' + ); + } + + // Validate that if `who` is set to `author` then `of` is set + if (action.who === ProtocolActor.Author && !action.of) { throw new DwnError( DwnErrorCode.ProtocolsConfigureInvalidActionMissingOf, - `'of' is required at protocol path (${protocolPath})` + `'of' is required when 'author' is specified as 'who'` ); } } diff --git a/tests/handlers/records-delete.spec.ts b/tests/handlers/records-delete.spec.ts index a754060c6..5a0fd19a7 100644 --- a/tests/handlers/records-delete.spec.ts +++ b/tests/handlers/records-delete.spec.ts @@ -388,6 +388,54 @@ export function testRecordsDeleteHandler(): void { const recordsDeleteReply = await dwn.processMessage(alice.did, recordsDelete.message); expect(recordsDeleteReply.status.code).to.eq(202); }); + + it('should allow delete with direct recipient rule', async () => { + // scenario: Alice creates a 'post' with Bob as recipient. Bob is able to delete + // the 'post' because he was recipient of it. Carol is not able to delete. + + const protocolDefinition = recipientCanProtocolDefinition as ProtocolDefinition; + const alice = await TestDataGenerator.generatePersona(); + const bob = await TestDataGenerator.generatePersona(); + const carol = await TestDataGenerator.generatePersona(); + + const protocolsConfig = await TestDataGenerator.generateProtocolsConfigure({ + author: alice, + protocolDefinition + }); + + // setting up a stub DID resolver + TestStubGenerator.stubDidResolver(didResolver, [alice, bob, carol]); + + const protocolsConfigureReply = await dwn.processMessage(alice.did, protocolsConfig.message); + expect(protocolsConfigureReply.status.code).to.equal(202); + + // Alice creates a 'post' with Bob as recipient + const recordsWrite = await TestDataGenerator.generateRecordsWrite({ + author : alice, + recipient : bob.did, + protocol : protocolDefinition.protocol, + protocolPath : 'post', + }); + const recordsWriteReply = await dwn.processMessage(alice.did, recordsWrite.message, recordsWrite.dataStream); + expect(recordsWriteReply.status.code).to.eq(202); + + // Carol is unable to delete the 'post' + const carolRecordsDelete = await TestDataGenerator.generateRecordsDelete({ + author : carol, + recordId : recordsWrite.message.recordId, + }); + const carolRecordsDeleteReply = await dwn.processMessage(alice.did, carolRecordsDelete.message); + expect(carolRecordsDeleteReply.status.code).to.eq(401); + expect(carolRecordsDeleteReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationActionNotAllowed); + + // Bob is able to delete the post + const bobRecordsDelete = await TestDataGenerator.generateRecordsDelete({ + author : bob, + recordId : recordsWrite.message.recordId, + }); + const bobRecordsDeleteReply = await dwn.processMessage(alice.did, bobRecordsDelete.message); + expect(bobRecordsDeleteReply.status.code).to.eq(202); + }); }); describe('author action rules', () => { diff --git a/tests/handlers/records-write.spec.ts b/tests/handlers/records-write.spec.ts index e08773395..3f53a85c0 100644 --- a/tests/handlers/records-write.spec.ts +++ b/tests/handlers/records-write.spec.ts @@ -1258,6 +1258,54 @@ export function testRecordsWriteHandler(): void { expect(bobTagRecordsReply.status.code).to.equal(401); expect(bobTagRecordsReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationActionNotAllowed); }); + + it('should allowed update with direct recipient rule', async () => { + // scenario: Alice creates a 'post' with Bob as recipient. Bob is able to update + // the 'post' because he was recipient of it. Carol is not able to update it. + + const protocolDefinition = recipientCanProtocol as ProtocolDefinition; + const alice = await TestDataGenerator.generatePersona(); + const bob = await TestDataGenerator.generatePersona(); + const carol = await TestDataGenerator.generatePersona(); + + const protocolsConfig = await TestDataGenerator.generateProtocolsConfigure({ + author: alice, + protocolDefinition + }); + + // setting up a stub DID resolver + TestStubGenerator.stubDidResolver(didResolver, [alice, bob, carol]); + + const protocolsConfigureReply = await dwn.processMessage(alice.did, protocolsConfig.message); + expect(protocolsConfigureReply.status.code).to.equal(202); + + // Alice creates a 'post' with Bob as recipient + const recordsWrite = await TestDataGenerator.generateRecordsWrite({ + author : alice, + recipient : bob.did, + protocol : protocolDefinition.protocol, + protocolPath : 'post', + }); + const recordsWriteReply = await dwn.processMessage(alice.did, recordsWrite.message, recordsWrite.dataStream); + expect(recordsWriteReply.status.code).to.eq(202); + + // Carol is unable to update the 'post' + const carolRecordsWrite = await TestDataGenerator.generateFromRecordsWrite({ + author : carol, + existingWrite : recordsWrite.recordsWrite + }); + const carolRecordsWriteReply = await dwn.processMessage(alice.did, carolRecordsWrite.message); + expect(carolRecordsWriteReply.status.code).to.eq(401); + expect(carolRecordsWriteReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationActionNotAllowed); + + // Bob is able to update the post + const bobRecordsWrite = await TestDataGenerator.generateFromRecordsWrite({ + author : bob, + existingWrite : recordsWrite.recordsWrite, + }); + const bobRecordsWriteReply = await dwn.processMessage(alice.did, bobRecordsWrite.message, bobRecordsWrite.dataStream); + expect(bobRecordsWriteReply.status.code).to.eq(202); + }); }); describe('author action rules', () => { diff --git a/tests/interfaces/protocols-configure.spec.ts b/tests/interfaces/protocols-configure.spec.ts index d89503ffa..9eb47e173 100644 --- a/tests/interfaces/protocols-configure.spec.ts +++ b/tests/interfaces/protocols-configure.spec.ts @@ -285,7 +285,35 @@ describe('ProtocolsConfigure', () => { .to.be.rejectedWith(DwnErrorCode.ProtocolsConfigureInvalidActionOfNotAllowed); }); - it('rejects protocol definitions with actions that don\'t contain `of` and `who` is `author` or `recipient`', async () => { + it('rejects protocol definitions with actions that have direct-recipient-can rules with actions other than delete or update', async () => { + const definition = { + published : true, + protocol : 'http://example.com', + types : { + message: {}, + }, + structure: { + message: { + $actions: [{ + who : 'recipient', + can : 'read' // not allowed, should be either delete or update + }] + } + } + }; + + const alice = await TestDataGenerator.generatePersona(); + + const createProtocolsConfigurePromise = ProtocolsConfigure.create({ + signer: Jws.createSigner(alice), + definition + }); + + await expect(createProtocolsConfigurePromise) + .to.be.rejectedWith(DwnErrorCode.ProtocolsConfigureInvalidRecipientOfAction); + }); + + it('rejects protocol definitions with actions that don\'t contain `of` and `who` is `author`', async () => { const definition = { published : true, protocol : 'http://example.com', diff --git a/tests/vectors/protocol-definitions/recipient-can.json b/tests/vectors/protocol-definitions/recipient-can.json index 98bf724c8..e766c4cb3 100644 --- a/tests/vectors/protocol-definitions/recipient-can.json +++ b/tests/vectors/protocol-definitions/recipient-can.json @@ -7,6 +7,16 @@ }, "structure": { "post": { + "$actions": [ + { + "who": "recipient", + "can": "update" + }, + { + "who": "recipient", + "can": "delete" + } + ], "tag": { "$actions": [ {