Skip to content

Commit

Permalink
Added create action to protocol rules (#699)
Browse files Browse the repository at this point in the history
  • Loading branch information
thehenrytsai authored Mar 6, 2024
1 parent 8861ead commit fa409f9
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 11 deletions.
2 changes: 2 additions & 0 deletions json-schemas/interface-methods/protocol-rule-set.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"enum": [
"co-delete",
"co-update",
"create",
"read",
"write"
]
Expand All @@ -66,6 +67,7 @@
"enum": [
"co-delete",
"co-update",
"create",
"query",
"subscribe",
"read",
Expand Down
22 changes: 12 additions & 10 deletions src/core/protocol-authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,9 +506,11 @@ export class ProtocolAuthorization {
}

/**
* Returns a list of ProtocolAction(s) based on the incoming message, one of which must be allowed for the message to be authorized.
* NOTE: the reason why there could be multiple actions is because in case of a "non-initial" RecordsWrite by the original record author,
* the RecordsWrite can either be authorized by a `write` or `co-update` allow rule.
* Returns all the ProtocolActions that would authorized the incoming message
* (but we still need to later verify if there is a rule defined that matches one of the actions).
* NOTE: the reason why there could be multiple actions is because:
* - In case of an initial RecordsWrite, the RecordsWrite can be authorized by an allow `create` or `write` rule.
* - In case of a non-initial RecordsWrite by the original record author, the RecordsWrite can be authorized by a `write` or `co-update` rule.
*
* It is important to recognize that the `write` access that allowed the original record author to create the record maybe revoked
* (e.g. by role revocation) by the time a "non-initial" write by the same author is attempted.
Expand All @@ -535,8 +537,7 @@ export class ProtocolAuthorization {
case DwnMethodName.Write:
const incomingRecordsWrite = incomingMessage as RecordsWrite;
if (await incomingRecordsWrite.isInitialWrite()) {
// only 'write' allows initial RecordsWrites
return [ProtocolAction.Write];
return [ProtocolAction.Write, ProtocolAction.Create];
} else if (await incomingRecordsWrite.isAuthoredByInitialRecordAuthor(tenant, messageStore)) {
// Both 'co-update' and 'write' authorize the incoming message
return [ProtocolAction.Write, ProtocolAction.CoUpdate];
Expand All @@ -562,7 +563,7 @@ export class ProtocolAuthorization {
messageStore: MessageStore,
): Promise<void> {
const incomingMessageMethod = incomingMessage.message.descriptor.method;
const inboundMessageActions = await ProtocolAuthorization.getActionsSeekingARuleMatch(tenant, incomingMessage, messageStore);
const actionsSeekingARuleMatch = await ProtocolAuthorization.getActionsSeekingARuleMatch(tenant, incomingMessage, messageStore);
const author = incomingMessage.author;
const actionRules = inboundMessageRuleSet.$actions;

Expand All @@ -577,7 +578,7 @@ export class ProtocolAuthorization {
const invokedRole = incomingMessage.signaturePayload?.protocolRole;

for (const actionRule of actionRules) {
if (!inboundMessageActions.includes(actionRule.can as ProtocolAction)) {
if (!actionsSeekingARuleMatch.includes(actionRule.can as ProtocolAction)) {
continue;
}

Expand Down Expand Up @@ -608,6 +609,7 @@ export class ProtocolAuthorization {
continue;
}

// else we need to check the actor (e.g. author/recipient) allowed by current action rule in the corresponding ancestor message
const ancestorRuleSuccess: boolean = await ProtocolAuthorization.checkActor(author, actionRule, ancestorMessageChain);
if (ancestorRuleSuccess) {
return;
Expand Down Expand Up @@ -737,10 +739,10 @@ export class ProtocolAuthorization {
recordsWriteMessage.descriptor.protocolPath === actionRule.of!
);

// If this is reached, there is likely an issue with the protocol definition.
// The protocolPath to the actionRule should start with actionRule.of
// consider moving this check to ProtocolsConfigure message ingestion
if (ancestorRecordsWrite === undefined) {
// If this is reached, there is likely an issue with the protocol definition.
// The protocolPath to the actionRule should start with actionRule.of
// consider moving this check to ProtocolsConfigure message ingestion
return false;
}

Expand Down
1 change: 1 addition & 0 deletions src/types/protocols-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export enum ProtocolActor {
export enum ProtocolAction {
CoDelete = 'co-delete',
CoUpdate = 'co-update',
Create = 'create',
Query = 'query',
Read = 'read',
Subscribe = 'subscribe',
Expand Down
239 changes: 238 additions & 1 deletion tests/handlers/records-write.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { GeneralJwsBuilder } from '../../src/jose/jws/general/builder.js';
import { Jws } from '../../src/utils/jws.js';
import { Message } from '../../src/core/message.js';
import { PermissionsConditionPublication } from '../../src/types/permissions-grant-descriptor.js';
import { ProtocolAction } from '../../src/types/protocols-types.js';
import { RecordsRead } from '../../src/interfaces/records-read.js';
import { RecordsWrite } from '../../src/interfaces/records-write.js';
import { RecordsWriteHandler } from '../../src/handlers/records-write.js';
Expand All @@ -45,7 +46,7 @@ import { TestStores } from '../test-stores.js';
import { TestStubGenerator } from '../utils/test-stub-generator.js';
import { Time } from '../../src/utils/time.js';

import { DwnConstant, DwnInterfaceName, DwnMethodName, KeyDerivationScheme, RecordsDelete, RecordsQuery } from '../../src/index.js';
import { DwnConstant, DwnInterfaceName, DwnMethodName, KeyDerivationScheme, ProtocolsConfigure, RecordsDelete, RecordsQuery } from '../../src/index.js';
import { Encryption, EncryptionAlgorithm } from '../../src/utils/encryption.js';

chai.use(chaiAsPromised);
Expand Down Expand Up @@ -1177,6 +1178,242 @@ export function testRecordsWriteHandler(): void {
});

describe('protocol based writes', () => {
it('should process "create" rule correctly', async () => {
// scenario:
// verify requester cannot create without a matching "create" rule
// verify role authorized create rule
// verify authorized author of ancestor create rule
// verify authorized recipient of ancestor create rule
// verify anyone can create rule
// verify create rule does not grant subsequent write (update)

const alice = await TestDataGenerator.generateDidKeyPersona();
const bob = await TestDataGenerator.generateDidKeyPersona();
const carol = await TestDataGenerator.generateDidKeyPersona();
const daniel = await TestDataGenerator.generateDidKeyPersona();

// Alice installs a protocol with "can create" rules
const protocolDefinition: ProtocolDefinition = {
protocol : 'foo-bar-baz',
published : true,
types : {
admin : {},
foo : {},
bar : {},
baz : {}
},
structure: {
admin: {
$role: true
},
foo: {
$actions: [
{
role : 'admin',
can : ProtocolAction.Create
},
],
bar: {
$actions: [
{
who : 'author',
of : 'foo',
can : ProtocolAction.Create
},
{
who : 'recipient',
of : 'foo',
can : ProtocolAction.Create
}
],
baz: {
$actions: [
{
who : 'anyone',
can : ProtocolAction.Create
}
],
}
}
}
}
};
const protocolsConfig = await ProtocolsConfigure.create({
definition : protocolDefinition,
signer : Jws.createSigner(alice)
});

const protocolsConfigureReply = await dwn.processMessage(alice.did, protocolsConfig.message);
expect(protocolsConfigureReply.status.code).to.equal(202);

// Verify Bob cannot just create a record without a matching create rule
const bobFooBytes = TestDataGenerator.randomBytes(100);
const bobUnauthorizedWrite = await RecordsWrite.create(
{
signer : Jws.createSigner(bob),
protocol : protocolDefinition.protocol,
protocolPath : 'foo',
schema : 'any-schema',
dataFormat : 'any-format',
data : bobFooBytes
}
);

const bobUnauthorizedCreateReply
= await dwn.processMessage(alice.did, bobUnauthorizedWrite.message, { dataStream: DataStream.fromBytes(bobFooBytes) });
expect(bobUnauthorizedCreateReply.status.code).to.equal(401);
expect(bobUnauthorizedCreateReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationActionNotAllowed);

// Alice gives Bob the "admin" role to be able to write `foo` records.
const adminBobRecordsWrite = await TestDataGenerator.generateRecordsWrite({
author : alice,
recipient : bob.did,
protocol : protocolDefinition.protocol,
protocolPath : 'admin'
});
const adminBobRecordsWriteReply
= await dwn.processMessage(alice.did, adminBobRecordsWrite.message, { dataStream: adminBobRecordsWrite.dataStream });
expect(adminBobRecordsWriteReply.status.code).to.equal(202);

// Verify that Bob can create `foo` by invoking the admin role.
const bobRoleAuthorizedFoo = await RecordsWrite.create(
{
signer : Jws.createSigner(bob),
recipient : carol.did,
protocolRole : 'admin',
protocol : protocolDefinition.protocol,
protocolPath : 'foo',
schema : 'any-schema',
dataFormat : 'any-format',
data : bobFooBytes
}
);
const bobRoleAuthorizedCreateReply
= await dwn.processMessage(alice.did, bobRoleAuthorizedFoo.message, { dataStream: DataStream.fromBytes(bobFooBytes) });
expect(bobRoleAuthorizedCreateReply.status.code).to.equal(202);

// Verify that Bob cannot update `foo`
const bobUnauthorizedFooUpdate = await RecordsWrite.createFrom(
{
recordsWriteMessage : bobRoleAuthorizedFoo.message,
dataFormat : `any-new-format`,
signer : Jws.createSigner(bob)
}
);
const bobUnauthorizedFooUpdateReply
= await dwn.processMessage(alice.did, bobUnauthorizedFooUpdate.message);
expect(bobUnauthorizedFooUpdateReply.status.code).to.equal(401);
expect(bobUnauthorizedFooUpdateReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationActionNotAllowed);

// Verify that Bob can create `bar` as the author of the ancestor `foo`
const bobBarBytes = TestDataGenerator.randomBytes(100);
const bobAuthorAuthorizedBar = await RecordsWrite.create(
{
signer : Jws.createSigner(bob),
protocol : protocolDefinition.protocol,
protocolPath : 'foo/bar',
parentContextId : bobRoleAuthorizedFoo.message.contextId,
schema : 'any-schema',
dataFormat : 'any-format',
data : bobBarBytes
}
);
const bobBarCreateReply
= await dwn.processMessage(alice.did, bobAuthorAuthorizedBar.message, { dataStream: DataStream.fromBytes(bobBarBytes) });
expect(bobBarCreateReply.status.code).to.equal(202);

// Verify that Bob cannot update `bar`
const bobUnauthorizedBarUpdate = await RecordsWrite.createFrom(
{
recordsWriteMessage : bobAuthorAuthorizedBar.message,
dataFormat : `any-new-format`,
signer : Jws.createSigner(bob)
}
);
const bobUnauthorizedBarUpdateReply
= await dwn.processMessage(alice.did, bobUnauthorizedBarUpdate.message);
expect(bobUnauthorizedBarUpdateReply.status.code).to.equal(401);
expect(bobUnauthorizedBarUpdateReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationActionNotAllowed);

// Verify that Carol can create `bar` as the recipient of the ancestor `foo`
const carolBarBytes = TestDataGenerator.randomBytes(100);
const carolRecipientAuthorizedBar = await RecordsWrite.create(
{
signer : Jws.createSigner(carol),
protocol : protocolDefinition.protocol,
protocolPath : 'foo/bar',
parentContextId : bobRoleAuthorizedFoo.message.contextId,
schema : 'any-schema',
dataFormat : 'any-format',
data : carolBarBytes
}
);
const carolBarCreateReply
= await dwn.processMessage(alice.did, carolRecipientAuthorizedBar.message, { dataStream: DataStream.fromBytes(carolBarBytes) });
expect(carolBarCreateReply.status.code).to.equal(202);

// Verify that Carol cannot update `bar`
const carolUnauthorizedBarUpdate = await RecordsWrite.createFrom(
{
recordsWriteMessage : carolRecipientAuthorizedBar.message,
dataFormat : `any-new-format`,
signer : Jws.createSigner(carol)
}
);
const carolUnauthorizedBarUpdateReply
= await dwn.processMessage(alice.did, carolUnauthorizedBarUpdate.message);
expect(carolUnauthorizedBarUpdateReply.status.code).to.equal(401);
expect(carolUnauthorizedBarUpdateReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationActionNotAllowed);

// Verify that Daniel cannot create `bar` as no create rule applies to him
const danielBarBytes = TestDataGenerator.randomBytes(100);
const danielUnauthorizedBar = await RecordsWrite.create(
{
signer : Jws.createSigner(daniel),
protocol : protocolDefinition.protocol,
protocolPath : 'foo/bar',
parentContextId : bobRoleAuthorizedFoo.message.contextId,
schema : 'any-schema',
dataFormat : 'any-format',
data : danielBarBytes
}
);
const danielUnauthorizedBarCreateReply
= await dwn.processMessage(alice.did, danielUnauthorizedBar.message, { dataStream: DataStream.fromBytes(danielBarBytes) });
expect(danielUnauthorizedBarCreateReply.status.code).to.equal(401);
expect(danielUnauthorizedBarCreateReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationActionNotAllowed);

// Verify anyone can create `baz`
const danielBazBytes = TestDataGenerator.randomBytes(100);
const danielAnyoneAuthorizedBar = await RecordsWrite.create(
{
signer : Jws.createSigner(daniel),
protocol : protocolDefinition.protocol,
protocolPath : 'foo/bar/baz',
parentContextId : carolRecipientAuthorizedBar.message.contextId,
schema : 'any-schema',
dataFormat : 'any-format',
data : danielBazBytes
}
);
const danielBazCreateReply
= await dwn.processMessage(alice.did, danielAnyoneAuthorizedBar.message, { dataStream: DataStream.fromBytes(danielBazBytes) });
expect(danielBazCreateReply.status.code).to.equal(202);

// Verify that Daniel cannot update `baz`
const danielUnauthorizedBazUpdate = await RecordsWrite.createFrom(
{
recordsWriteMessage : bobAuthorAuthorizedBar.message,
dataFormat : `any-new-format`,
signer : Jws.createSigner(daniel)
}
);
const danielUnauthorizedBazUpdateReply
= await dwn.processMessage(alice.did, danielUnauthorizedBazUpdate.message);
expect(danielUnauthorizedBazUpdateReply.status.code).to.equal(401);
expect(danielUnauthorizedBazUpdateReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationActionNotAllowed);
});

it('should allow write with allow-anyone rule', async () => {
// scenario: Bob writes into Alice's DWN given Alice's "email" protocol allow-anyone rule

Expand Down

0 comments on commit fa409f9

Please sign in to comment.