Skip to content

Commit

Permalink
Ensure protocolRole is maintained between query/read and subscribe/…
Browse files Browse the repository at this point in the history
…read. (#954)

Before this PR that were some inconsistencies with using `protocolRole`.

There were instances where a user would query using a role but not be able to read the data of the given record because the role was not being applied. Same would happen during update/delete.

This PR allows the READ operation to inherit the `protocolRole` used for a `query` or `subscribe` if it exists.
Additionally it provides the user the ability to provide a different role when performing an `update` or `delete` operation.
  • Loading branch information
LiranCohen authored Oct 21, 2024
1 parent 3f39bf1 commit 5120f6f
Show file tree
Hide file tree
Showing 10 changed files with 621 additions and 15 deletions.
8 changes: 8 additions & 0 deletions .changeset/many-suns-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@web5/agent": patch
"@web5/identity-agent": patch
"@web5/proxy-agent": patch
"@web5/user-agent": patch
---

Add `getProtocolRole` util
5 changes: 5 additions & 0 deletions .changeset/slimy-mayflies-hide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@web5/api": patch
---

Ensure protocolRole is maintained between query/read and subscribe/read.
10 changes: 9 additions & 1 deletion packages/agent/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { DidUrlDereferencer } from '@web5/dids';
import { PaginationCursor, RecordsDeleteMessage, RecordsWriteMessage } from '@tbd54566975/dwn-sdk-js';
import { Jws, PaginationCursor, RecordsDeleteMessage, RecordsWriteMessage } from '@tbd54566975/dwn-sdk-js';

import { Readable } from '@web5/common';
import { utils as didUtils } from '@web5/dids';
Expand Down Expand Up @@ -42,6 +42,14 @@ export function getRecordAuthor(record: RecordsWriteMessage | RecordsDeleteMessa
return Message.getAuthor(record);
}

/**
* Get the `protocolRole` string from the signature payload of the given RecordsWriteMessage or RecordsDeleteMessage.
*/
export function getRecordProtocolRole(message: RecordsWriteMessage | RecordsDeleteMessage): string | undefined {
const signaturePayload = Jws.decodePlainObjectPayload(message.authorization.signature);
return signaturePayload?.protocolRole;
}

export function isRecordsWrite(obj: unknown): obj is RecordsWrite {
// Validate that the given value is an object.
if (!obj || typeof obj !== 'object' || obj === null) return false;
Expand Down
44 changes: 42 additions & 2 deletions packages/agent/tests/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { expect } from 'chai';
import sinon from 'sinon';

import { DateSort, Message, TestDataGenerator } from '@tbd54566975/dwn-sdk-js';
import { getPaginationCursor, getRecordAuthor, getRecordMessageCid } from '../src/utils.js';
import { DateSort, Jws, Message, TestDataGenerator } from '@tbd54566975/dwn-sdk-js';
import { getPaginationCursor, getRecordAuthor, getRecordMessageCid, getRecordProtocolRole } from '../src/utils.js';

describe('Utils', () => {
beforeEach(() => {
sinon.restore();
});

after(() => {
sinon.restore();
});

describe('getPaginationCursor', () => {
it('should return a PaginationCursor object', async () => {
// create a RecordWriteMessage object which is published
Expand Down Expand Up @@ -84,4 +93,35 @@ describe('Utils', () => {
expect(deleteAuthorFromFunction!).to.equal(recordsDeleteAuthor.did);
});
});

describe('getRecordProtocolRole', () => {
it('gets a protocol role from a RecordsWrite', async () => {
const recordsWrite = await TestDataGenerator.generateRecordsWrite({ protocolRole: 'some-role' });
const role = getRecordProtocolRole(recordsWrite.message);
expect(role).to.equal('some-role');
});

it('gets a protocol role from a RecordsDelete', async () => {
const recordsDelete = await TestDataGenerator.generateRecordsDelete({ protocolRole: 'some-role' });
const role = getRecordProtocolRole(recordsDelete.message);
expect(role).to.equal('some-role');
});

it('returns undefined if no role is defined', async () => {
const recordsWrite = await TestDataGenerator.generateRecordsWrite();
const writeRole = getRecordProtocolRole(recordsWrite.message);
expect(writeRole).to.be.undefined;

const recordsDelete = await TestDataGenerator.generateRecordsDelete();
const deleteRole = getRecordProtocolRole(recordsDelete.message);
expect(deleteRole).to.be.undefined;
});

it('returns undefined if decodedObject is undefined', async () => {
sinon.stub(Jws, 'decodePlainObjectPayload').returns(undefined);
const recordsWrite = await TestDataGenerator.generateRecordsWrite();
const writeRole = getRecordProtocolRole(recordsWrite.message);
expect(writeRole).to.be.undefined;
});
});
});
2 changes: 2 additions & 0 deletions packages/api/src/dwn-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,7 @@ export class DwnApi {
*/
remoteOrigin : request.from,
delegateDid : this.delegateDid,
protocolRole : agentRequest.messageParams.protocolRole,
...entry as DwnMessage[DwnInterface.RecordsWrite]
};
const record = new Record(this.agent, recordOptions, this.permissionsApi);
Expand Down Expand Up @@ -829,6 +830,7 @@ export class DwnApi {
connectedDid : this.connectedDid,
delegateDid : this.delegateDid,
permissionsApi : this.permissionsApi,
protocolRole : request.message.protocolRole,
request
})
};
Expand Down
20 changes: 14 additions & 6 deletions packages/api/src/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
SendDwnRequest,
PermissionsApi,
AgentPermissionsApi,
getRecordProtocolRole
} from '@web5/agent';

import { Convert, isEmptyObject, NodeStream, removeUndefinedProperties, Stream } from '@web5/common';
Expand Down Expand Up @@ -183,6 +184,9 @@ export type RecordDeleteParams = {

/** The timestamp indicating when the record was deleted. */
dateModified?: DwnMessageDescriptor[DwnInterface.RecordsDelete]['messageTimestamp'];

/** The protocol role under which this record will be deleted. */
protocolRole?: string;
};

/**
Expand Down Expand Up @@ -311,7 +315,6 @@ export class Record implements RecordModel {
/** Tags of the record */
get tags() { return this._recordsWriteDescriptor?.tags; }


// Getters for for properties that depend on the current state of the Record.
/** DID that is the logical author of the Record. */
get author(): string { return this._author; }
Expand Down Expand Up @@ -703,7 +706,7 @@ export class Record implements RecordModel {
*
* @beta
*/
async update({ dateModified, data, ...params }: RecordUpdateParams): Promise<DwnResponseStatus> {
async update({ dateModified, data, protocolRole, ...params }: RecordUpdateParams): Promise<DwnResponseStatus> {

if (this.deleted) {
throw new Error('Record: Cannot revive a deleted record.');
Expand All @@ -718,6 +721,7 @@ export class Record implements RecordModel {
...descriptor,
...params,
parentContextId,
protocolRole : protocolRole ?? this._protocolRole, // Use the current protocolRole if not provided.
messageTimestamp : dateModified, // Map Record class `dateModified` property to DWN SDK `messageTimestamp`
recordId : this._recordId
};
Expand Down Expand Up @@ -786,7 +790,7 @@ export class Record implements RecordModel {

// Only update the local Record instance mutable properties if the record was successfully (over)written.
this._authorization = responseMessage.authorization;
this._protocolRole = params.protocolRole;
this._protocolRole = updateMessage.protocolRole;
mutableDescriptorProperties.forEach(property => {
this._descriptor[property] = responseMessage.descriptor[property];
});
Expand Down Expand Up @@ -834,15 +838,19 @@ export class Record implements RecordModel {
store
};

if (this.deleted) {
// if we have a delete message we can just use it
// Check to see if the provided protocolRole within the deleteParams is different from the current protocolRole.
const differentRole = deleteParams?.protocolRole ? getRecordProtocolRole(this.rawMessage) !== deleteParams.protocolRole : false;
// If the record is already in a deleted state but the protocolRole is different, we need to construct a delete message with the new protocolRole
// otherwise we can just use the existing delete message.
if (this.deleted && !differentRole) {
deleteOptions.rawMessage = this.rawMessage as DwnMessage[DwnInterface.RecordsDelete];
} else {
// otherwise we construct a delete message given the `RecordDeleteParams`
deleteOptions.messageParams = {
prune : prune,
recordId : this._recordId,
messageTimestamp : dateModified,
protocolRole : deleteParams?.protocolRole ?? this._protocolRole // if no protocolRole is provided, use the current protocolRole
};
}

Expand Down Expand Up @@ -1023,7 +1031,7 @@ export class Record implements RecordModel {
private async readRecordData({ target, isRemote }: { target: string, isRemote: boolean }) {
const readRequest: ProcessDwnRequest<DwnInterface.RecordsRead> = {
author : this._connectedDid,
messageParams : { filter: { recordId: this.id } },
messageParams : { filter: { recordId: this.id }, protocolRole: this._protocolRole },
messageType : DwnInterface.RecordsRead,
target,
};
Expand Down
4 changes: 3 additions & 1 deletion packages/api/src/subscription-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ export class SubscriptionUtil {
/**
* Creates a record subscription handler that can be used to process incoming {Record} messages.
*/
static recordSubscriptionHandler({ agent, connectedDid, request, delegateDid, permissionsApi }:{
static recordSubscriptionHandler({ agent, connectedDid, request, delegateDid, protocolRole, permissionsApi }:{
agent: Web5Agent;
connectedDid: string;
delegateDid?: string;
protocolRole?: string;
permissionsApi?: PermissionsApi;
request: RecordsSubscribeRequest;
}): DwnRecordSubscriptionHandler {
Expand All @@ -31,6 +32,7 @@ export class SubscriptionUtil {
const record = new Record(agent, {
...message,
...recordOptions,
protocolRole,
delegateDid: delegateDid,
}, permissionsApi);

Expand Down
Loading

0 comments on commit 5120f6f

Please sign in to comment.