Skip to content

Commit

Permalink
Authorize direct recipient of protocol records (#603)
Browse files Browse the repository at this point in the history
* Authorize direct recipient of protocol records

---------

Co-authored-by: Henry Tsai <henrytsai@outlook.com>
Co-authored-by: LiranCohen <c.liran.c@gmail.com>
  • Loading branch information
3 people committed Nov 14, 2023
1 parent 3f0a27a commit 5740a26
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 4 deletions.
1 change: 1 addition & 0 deletions src/core/dwn-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export enum DwnErrorCode {
ProtocolsConfigureInvalidRole = 'ProtocolsConfigureInvalidRole',
ProtocolsConfigureInvalidActionMissingOf = 'ProtocolsConfigureInvalidActionMissingOf',
ProtocolsConfigureInvalidActionOfNotAllowed = 'ProtocolsConfigureInvalidActionOfNotAllowed',
ProtocolsConfigureInvalidRecipientOfAction = 'ProtocolsConfigureInvalidRecipientOfAction',
ProtocolsConfigureQueryNotAllowed = 'ProtocolsConfigureQueryNotAllowed',
ProtocolsConfigureUnauthorized = 'ProtocolsConfigureUnauthorized',
ProtocolsQueryUnauthorized = 'ProtocolsQueryUnauthorized',
Expand Down
12 changes: 12 additions & 0 deletions src/core/protocol-authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
23 changes: 20 additions & 3 deletions src/interfaces/protocols-configure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -133,11 +134,27 @@ export class ProtocolsConfigure extends Message<ProtocolsConfigureMessage> {
);
}

// 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'`
);
}
}
Expand Down
48 changes: 48 additions & 0 deletions tests/handlers/records-delete.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
48 changes: 48 additions & 0 deletions tests/handlers/records-write.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
30 changes: 29 additions & 1 deletion tests/interfaces/protocols-configure.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
10 changes: 10 additions & 0 deletions tests/vectors/protocol-definitions/recipient-can.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
},
"structure": {
"post": {
"$actions": [
{
"who": "recipient",
"can": "update"
},
{
"who": "recipient",
"can": "delete"
}
],
"tag": {
"$actions": [
{
Expand Down

0 comments on commit 5740a26

Please sign in to comment.