Skip to content

Commit

Permalink
Added update protocol action (#701)
Browse files Browse the repository at this point in the history
  • Loading branch information
thehenrytsai authored Mar 7, 2024
1 parent fa409f9 commit dc54bd8
Show file tree
Hide file tree
Showing 8 changed files with 896 additions and 247 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 @@ -47,6 +47,7 @@
"co-update",
"create",
"read",
"update",
"write"
]
}
Expand All @@ -71,6 +72,7 @@
"query",
"subscribe",
"read",
"update",
"write"
]
}
Expand Down
15 changes: 10 additions & 5 deletions src/core/protocol-authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,14 +536,19 @@ export class ProtocolAuthorization {

case DwnMethodName.Write:
const incomingRecordsWrite = incomingMessage as RecordsWrite;

if (await incomingRecordsWrite.isInitialWrite()) {
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];
} else {
// Actors other than the initial record author must be authorized to 'co-update' the message
return [ProtocolAction.CoUpdate];
// else not initial write

if (await incomingRecordsWrite.isAuthoredByInitialRecordAuthor(tenant, messageStore)) {
// 'write', 'update' or 'co-update' action authorizes the incoming message
return [ProtocolAction.Write, ProtocolAction.CoUpdate, ProtocolAction.Update];
} else {
// An update by someone who is not the record author can only be authorized by a 'co-update' rule.
return [ProtocolAction.CoUpdate];
}
}

// default:
Expand Down
9 changes: 5 additions & 4 deletions src/interfaces/records-write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ export class RecordsWrite implements MessageInterface<RecordsWriteMessage> {
*/
public static async createFrom(options: CreateFromOptions): Promise<RecordsWrite> {
const sourceMessage = options.recordsWriteMessage;
const sourceRecordsWrite = await RecordsWrite.parse(sourceMessage);
const currentTime = Time.getCurrentTimestamp();

// inherit published value from parent if neither published nor datePublished is specified
Expand All @@ -399,7 +400,7 @@ export class RecordsWrite implements MessageInterface<RecordsWriteMessage> {
}

const createOptions: RecordsWriteOptions = {
// immutable properties below, just derive from the message given
// immutable properties below, just copy from the source message
recipient : sourceMessage.descriptor.recipient,
recordId : sourceMessage.recordId,
dateCreated : sourceMessage.descriptor.dateCreated,
Expand All @@ -412,10 +413,10 @@ export class RecordsWrite implements MessageInterface<RecordsWriteMessage> {
published,
datePublished,
data : options.data,
dataCid : options.data ? undefined : sourceMessage.descriptor.dataCid, // if data not given, use base message dataCid
dataSize : options.data ? undefined : sourceMessage.descriptor.dataSize, // if data not given, use base message dataSize
dataCid : options.data ? undefined : sourceMessage.descriptor.dataCid, // if new `data` not given, use value from source message
dataSize : options.data ? undefined : sourceMessage.descriptor.dataSize, // if new `data` not given, use value from source message
dataFormat : options.dataFormat ?? sourceMessage.descriptor.dataFormat,
protocolRole : options.protocolRole,
protocolRole : options.protocolRole ?? sourceRecordsWrite.signaturePayload!.protocolRole, // if not given, use value from source message
delegatedGrant : options.delegatedGrant,
// finally still need signers
signer : options.signer,
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 @@ -43,6 +43,7 @@ export enum ProtocolAction {
Query = 'query',
Read = 'read',
Subscribe = 'subscribe',
Update = 'update',
Write = 'write'
}

Expand Down
297 changes: 297 additions & 0 deletions tests/features/protocol-create-action.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
import type { EventStream } from '../../src/types/subscriptions.js';
import type { ProtocolDefinition } from '../../src/types/protocols-types.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 { DidKey } from '@web5/dids';
import { DidResolver } from '@web5/dids';
import { Dwn } from '../../src/dwn.js';
import { Jws } from '../../src/utils/jws.js';
import { ProtocolAction } from '../../src/types/protocols-types.js';
import { RecordsWrite } from '../../src/interfaces/records-write.js';
import { TestDataGenerator } from '../utils/test-data-generator.js';
import { TestEventStream } from '../test-event-stream.js';
import { TestStores } from '../test-stores.js';

import { DwnErrorCode, ProtocolsConfigure } from '../../src/index.js';

chai.use(chaiAsPromised);

export function testProtocolCreateAction(): void {
describe('Protocol `create` action', () => {
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 DidResolver({ 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 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 create a record without a matching create rule
const bobFooBytes = TestDataGenerator.randomBytes(100);
const bobUnauthorizedWrite = await RecordsWrite.create(
{
signer : Jws.createSigner(bob),
recipient : carol.did,
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);
});
});
}
Loading

0 comments on commit dc54bd8

Please sign in to comment.