From 080359cbecb9015a942103841fad7a85fbdcdf98 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Tue, 10 Sep 2024 17:30:26 -0400 Subject: [PATCH] Protocol Interface Permissions (#803) - add delegateGrant ability to `ProtocolConfigure` - Allow `ProtocolPermissionScope` to be scoped down to a specific protocol - move getAuthor helper from `Record` util class to `Message` core class. When DWAs request permissions, they will now be issued a `ProtocolsQuery` permission scoped to the protocol they are being authorized, as well as optionally a `ProtocolsConfigure` for that protocol. Satisfies: https://github.com/TBD54566975/dwn-sdk-js/issues/801 https://github.com/TBD54566975/dwn-sdk-js/issues/802 --- .../protocols-configure.json | 2 +- .../permissions/permissions-definitions.json | 3 + json-schemas/permissions/scopes.json | 22 ++++ src/core/abstract-message.ts | 7 + src/core/auth.ts | 21 +-- src/core/dwn-error.ts | 4 +- src/core/message.ts | 19 +++ src/core/protocols-grant-authorization.ts | 88 +++++++++++++ src/handlers/protocols-configure.ts | 29 ++++- src/handlers/protocols-query.ts | 4 +- src/interfaces/protocols-configure.ts | 24 ++++ src/interfaces/protocols-query.ts | 9 +- src/interfaces/records-write.ts | 4 +- src/types/permission-types.ts | 1 + src/utils/records.ts | 15 --- tests/core/message.spec.ts | 80 +++++++++++- tests/features/author-delegated-grant.spec.ts | 123 +++++++++++++++++- tests/features/resumable-tasks.spec.ts | 7 + tests/handlers/protocols-configure.spec.ts | 120 ++++++++++++++++- tests/handlers/protocols-query.spec.ts | 114 ++++++++++++++++ tests/interfaces/messages-subscribe.spec.ts | 4 - tests/utils/records.spec.ts | 58 +-------- tests/utils/test-data-generator.ts | 4 +- 23 files changed, 651 insertions(+), 111 deletions(-) create mode 100644 src/core/protocols-grant-authorization.ts diff --git a/json-schemas/interface-methods/protocols-configure.json b/json-schemas/interface-methods/protocols-configure.json index 166ea8915..003638bf7 100644 --- a/json-schemas/interface-methods/protocols-configure.json +++ b/json-schemas/interface-methods/protocols-configure.json @@ -9,7 +9,7 @@ ], "properties": { "authorization": { - "$ref": "https://identity.foundation/dwn/json-schemas/authorization.json" + "$ref": "https://identity.foundation/dwn/json-schemas/authorization-delegated-grant.json" }, "descriptor": { "type": "object", diff --git a/json-schemas/permissions/permissions-definitions.json b/json-schemas/permissions/permissions-definitions.json index e561ceb04..d3512e9b6 100644 --- a/json-schemas/permissions/permissions-definitions.json +++ b/json-schemas/permissions/permissions-definitions.json @@ -14,6 +14,9 @@ { "$ref": "https://identity.foundation/dwn/json-schemas/permissions/scopes.json#/$defs/messages-subscribe-scope" }, + { + "$ref": "https://identity.foundation/dwn/json-schemas/permissions/scopes.json#/$defs/protocols-configure-scope" + }, { "$ref": "https://identity.foundation/dwn/json-schemas/permissions/scopes.json#/$defs/protocols-query-scope" }, diff --git a/json-schemas/permissions/scopes.json b/json-schemas/permissions/scopes.json index 13ac0d529..f91312084 100644 --- a/json-schemas/permissions/scopes.json +++ b/json-schemas/permissions/scopes.json @@ -60,6 +60,25 @@ } } }, + "protocols-configure-scope": { + "type": "object", + "additionalProperties": false, + "required": [ + "interface", + "method" + ], + "properties": { + "interface": { + "const": "Protocols" + }, + "method": { + "const": "Configure" + }, + "protocol": { + "type": "string" + } + } + }, "protocols-query-scope": { "type": "object", "additionalProperties": false, @@ -73,6 +92,9 @@ }, "method": { "const": "Query" + }, + "protocol": { + "type": "string" } } }, diff --git a/src/core/abstract-message.ts b/src/core/abstract-message.ts index 4f038a82b..3019ccad4 100644 --- a/src/core/abstract-message.ts +++ b/src/core/abstract-message.ts @@ -28,6 +28,13 @@ export abstract class AbstractMessage implements Messa return this._signaturePayload; } + /** + * If this message is signed by an author-delegate. + */ + public get isSignedByAuthorDelegate(): boolean { + return Message.isSignedByAuthorDelegate(this._message); + } + protected constructor(message: M) { this._message = message; diff --git a/src/core/auth.ts b/src/core/auth.ts index 852e1bb68..fcf9034a2 100644 --- a/src/core/auth.ts +++ b/src/core/auth.ts @@ -1,6 +1,5 @@ +import type { AuthorizationModel } from '../types/message-types.js'; import type { DidResolver } from '@web5/dids'; -import type { MessageInterface } from '../types/message-interface.js'; -import type { AuthorizationModel, GenericMessage } from '../types/message-types.js'; import { GeneralJwsVerifier } from '../jose/jws/general/verifier.js'; import { RecordsWrite } from '../interfaces/records-write.js'; @@ -34,20 +33,4 @@ export async function authenticate(authorizationModel: AuthorizationModel | unde const ownerDelegatedGrant = await RecordsWrite.parse(authorizationModel.ownerDelegatedGrant); await GeneralJwsVerifier.verifySignatures(ownerDelegatedGrant.message.authorization.signature, didResolver); } -} - -/** - * Authorizes owner authored message. - * @throws {DwnError} if fails authorization. - */ -export async function authorizeOwner(tenant: string, incomingMessage: MessageInterface): Promise { - // if author is the same as the target tenant, we can directly grant access - if (incomingMessage.author === tenant) { - return; - } else { - throw new DwnError( - DwnErrorCode.AuthorizationAuthorNotOwner, - `Message authored by ${incomingMessage.author}, not authored by expected owner ${tenant}.` - ); - } -} +} \ No newline at end of file diff --git a/src/core/dwn-error.ts b/src/core/dwn-error.ts index a8d5b5283..c7ca8130d 100644 --- a/src/core/dwn-error.ts +++ b/src/core/dwn-error.ts @@ -16,7 +16,6 @@ export enum DwnErrorCode { AuthenticateJwsMissing = 'AuthenticateJwsMissing', AuthenticateDescriptorCidMismatch = 'AuthenticateDescriptorCidMismatch', AuthenticationMoreThanOneSignatureNotSupported = 'AuthenticationMoreThanOneSignatureNotSupported', - AuthorizationAuthorNotOwner = 'AuthorizationAuthorNotOwner', AuthorizationNotGrantedToAuthor = 'AuthorizationNotGrantedToAuthor', ComputeCidCodecNotSupported = 'ComputeCidCodecNotSupported', ComputeCidMultihashNotSupported = 'ComputeCidMultihashNotSupported', @@ -79,6 +78,7 @@ export enum DwnErrorCode { ProtocolAuthorizationProtocolNotFound = 'ProtocolAuthorizationProtocolNotFound', ProtocolAuthorizationRoleMissingRecipient = 'ProtocolAuthorizationRoleMissingRecipient', ProtocolAuthorizationTagsInvalidSchema = 'ProtocolAuthorizationTagsInvalidSchema', + ProtocolsConfigureAuthorizationFailed = 'ProtocolsConfigureAuthorizationFailed', ProtocolsConfigureDuplicateActorInRuleSet = 'ProtocolsConfigureDuplicateActorInRuleSet', ProtocolsConfigureDuplicateRoleInRuleSet = 'ProtocolsConfigureDuplicateRoleInRuleSet', ProtocolsConfigureInvalidSize = 'ProtocolsConfigureInvalidSize', @@ -91,6 +91,8 @@ export enum DwnErrorCode { ProtocolsConfigureInvalidTagSchema = 'ProtocolsConfigureInvalidTagSchema', ProtocolsConfigureRecordNestingDepthExceeded = 'ProtocolsConfigureRecordNestingDepthExceeded', ProtocolsConfigureRoleDoesNotExistAtGivenPath = 'ProtocolsConfigureRoleDoesNotExistAtGivenPath', + ProtocolsGrantAuthorizationQueryProtocolScopeMismatch = 'ProtocolsGrantAuthorizationQueryProtocolScopeMismatch', + ProtocolsGrantAuthorizationScopeProtocolMismatch = 'ProtocolsGrantAuthorizationScopeProtocolMismatch', ProtocolsQueryUnauthorized = 'ProtocolsQueryUnauthorized', RecordsAuthorDelegatedGrantAndIdExistenceMismatch = 'RecordsAuthorDelegatedGrantAndIdExistenceMismatch', RecordsAuthorDelegatedGrantCidMismatch = 'RecordsAuthorDelegatedGrantCidMismatch', diff --git a/src/core/message.ts b/src/core/message.ts index 9b8bda31a..4e92b07e1 100644 --- a/src/core/message.ts +++ b/src/core/message.ts @@ -16,6 +16,25 @@ import { DwnError, DwnErrorCode } from './dwn-error.js'; * A class containing utility methods for working with DWN messages. */ export class Message { + + /** + * Gets the DID of the author of the given message. + */ + public static getAuthor(message: GenericMessage): string | undefined { + if (message.authorization === undefined) { + return undefined; + } + + let author; + if (message.authorization.authorDelegatedGrant !== undefined) { + author = Message.getSigner(message.authorization.authorDelegatedGrant); + } else { + author = Message.getSigner(message); + } + + return author; + } + /** * Validates the given message against the corresponding JSON schema. * @throws {Error} if fails validation. diff --git a/src/core/protocols-grant-authorization.ts b/src/core/protocols-grant-authorization.ts new file mode 100644 index 000000000..a0ba0fcc7 --- /dev/null +++ b/src/core/protocols-grant-authorization.ts @@ -0,0 +1,88 @@ +import type { MessageStore } from '../types/message-store.js'; +import type { PermissionGrant } from '../protocols/permission-grant.js'; +import type { ProtocolPermissionScope } from '../types/permission-types.js'; +import type { ProtocolsConfigureMessage, ProtocolsQueryMessage } from '../types/protocols-types.js'; + +import { GrantAuthorization } from './grant-authorization.js'; +import { DwnError, DwnErrorCode } from './dwn-error.js'; + +export class ProtocolsGrantAuthorization { + /** + * Authorizes the given ProtocolsConfigure in the scope of the DID given. + */ + public static async authorizeConfigure(input: { + protocolsConfigureMessage: ProtocolsConfigureMessage, + expectedGrantor: string, + expectedGrantee: string, + permissionGrant: PermissionGrant, + messageStore: MessageStore, + }): Promise { + const { + protocolsConfigureMessage, expectedGrantor, expectedGrantee, permissionGrant, messageStore + } = input; + + await GrantAuthorization.performBaseValidation({ + incomingMessage: protocolsConfigureMessage, + expectedGrantor, + expectedGrantee, + permissionGrant, + messageStore + }); + + ProtocolsGrantAuthorization.verifyScope(protocolsConfigureMessage, permissionGrant.scope as ProtocolPermissionScope); + } + + /** + * Authorizes the scope of a permission grant for a ProtocolsQuery message. + * @param messageStore Used to check if the grant has been revoked. + */ + public static async authorizeQuery(input: { + expectedGrantor: string, + expectedGrantee: string, + incomingMessage: ProtocolsQueryMessage; + permissionGrant: PermissionGrant; + messageStore: MessageStore; + }): Promise { + const { expectedGrantee, expectedGrantor, incomingMessage, permissionGrant, messageStore } = input; + + await GrantAuthorization.performBaseValidation({ + incomingMessage: incomingMessage, + expectedGrantor, + expectedGrantee, + permissionGrant, + messageStore + }); + + // If the grant specifies a protocol, the query must specify the same protocol. + const permissionScope = permissionGrant.scope as ProtocolPermissionScope; + const protocolInGrant = permissionScope.protocol; + const protocolInMessage = incomingMessage.descriptor.filter?.protocol; + if (protocolInGrant !== undefined && protocolInMessage !== protocolInGrant) { + throw new DwnError( + DwnErrorCode.ProtocolsGrantAuthorizationQueryProtocolScopeMismatch, + `Grant protocol scope ${protocolInGrant} does not match protocol in message ${protocolInMessage}` + ); + } + } + + /** + * Verifies a ProtocolsConfigure against the scope of the given grant. + */ + private static verifyScope( + protocolsConfigureMessage: ProtocolsConfigureMessage, + grantScope: ProtocolPermissionScope + ): void { + + // if the grant scope does not specify a protocol, then it is am unrestricted grant + if (grantScope.protocol === undefined) { + return; + } + + if (grantScope.protocol !== protocolsConfigureMessage.descriptor.definition.protocol) { + throw new DwnError( + DwnErrorCode.ProtocolsGrantAuthorizationScopeProtocolMismatch, + `Grant scope specifies different protocol than what appears in the configure message.` + ); + } + } +} \ No newline at end of file diff --git a/src/handlers/protocols-configure.ts b/src/handlers/protocols-configure.ts index 95d4701c5..8259fc72c 100644 --- a/src/handlers/protocols-configure.ts +++ b/src/handlers/protocols-configure.ts @@ -6,10 +6,13 @@ import type { MessageStore } from '../types//message-store.js'; import type { MethodHandler } from '../types/method-handler.js'; import type { ProtocolsConfigureMessage } from '../types/protocols-types.js'; +import { authenticate } from '../core/auth.js'; import { Message } from '../core/message.js'; import { messageReplyFromError } from '../core/message-reply.js'; +import { PermissionsProtocol } from '../protocols/permissions.js'; import { ProtocolsConfigure } from '../interfaces/protocols-configure.js'; -import { authenticate, authorizeOwner } from '../core/auth.js'; +import { ProtocolsGrantAuthorization } from '../core/protocols-grant-authorization.js'; +import { DwnError, DwnErrorCode } from '../core/dwn-error.js'; import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js'; export class ProtocolsConfigureHandler implements MethodHandler { @@ -35,7 +38,7 @@ export class ProtocolsConfigureHandler implements MethodHandler { // authentication & authorization try { await authenticate(message.authorization, this.didResolver); - await authorizeOwner(tenant, protocolsConfigure); + await ProtocolsConfigureHandler.authorizeProtocolsConfigure(tenant, protocolsConfigure, this.messageStore); } catch (e) { return messageReplyFromError(e, 401); } @@ -109,4 +112,26 @@ export class ProtocolsConfigureHandler implements MethodHandler { return indexes; } + + private static async authorizeProtocolsConfigure(tenant: string, protocolConfigure: ProtocolsConfigure, messageStore: MessageStore): Promise { + + if (protocolConfigure.isSignedByAuthorDelegate) { + await protocolConfigure.authorizeAuthorDelegate(messageStore); + } + + if (protocolConfigure.author === tenant) { + return; + } else if (protocolConfigure.author !== undefined && protocolConfigure.signaturePayload!.permissionGrantId !== undefined) { + const permissionGrant = await PermissionsProtocol.fetchGrant(tenant, messageStore, protocolConfigure.signaturePayload!.permissionGrantId); + await ProtocolsGrantAuthorization.authorizeConfigure({ + protocolsConfigureMessage : protocolConfigure.message, + expectedGrantor : tenant, + expectedGrantee : protocolConfigure.author, + permissionGrant, + messageStore + }); + } else { + throw new DwnError(DwnErrorCode.ProtocolsConfigureAuthorizationFailed, 'message failed authorization'); + } + } } \ No newline at end of file diff --git a/src/handlers/protocols-query.ts b/src/handlers/protocols-query.ts index 3e5d58a0b..e45b1bf53 100644 --- a/src/handlers/protocols-query.ts +++ b/src/handlers/protocols-query.ts @@ -36,7 +36,9 @@ export class ProtocolsQueryHandler implements MethodHandler { // return public ProtocolsConfigures if query fails with a certain authentication or authorization code if (error.code === DwnErrorCode.AuthenticateJwsMissing || // unauthenticated - error.code === DwnErrorCode.ProtocolsQueryUnauthorized) { + error.code === DwnErrorCode.ProtocolsQueryUnauthorized || + error.code === DwnErrorCode.ProtocolsGrantAuthorizationQueryProtocolScopeMismatch + ) { const entries: ProtocolsConfigureMessage[] = await this.fetchPublishedProtocolsConfigure(tenant, protocolsQuery); return { diff --git a/src/interfaces/protocols-configure.ts b/src/interfaces/protocols-configure.ts index 5276e496d..0ecba08b1 100644 --- a/src/interfaces/protocols-configure.ts +++ b/src/interfaces/protocols-configure.ts @@ -1,9 +1,13 @@ +import type { DataEncodedRecordsWriteMessage } from '../types/records-types.js'; +import type { MessageStore } from '../types/message-store.js'; import type { Signer } from '../types/signer.js'; import type { ProtocolDefinition, ProtocolRuleSet, ProtocolsConfigureDescriptor, ProtocolsConfigureMessage } from '../types/protocols-types.js'; import { AbstractMessage } from '../core/abstract-message.js'; import Ajv from 'ajv/dist/2020.js'; import { Message } from '../core/message.js'; +import { PermissionGrant } from '../protocols/permission-grant.js'; +import { ProtocolsGrantAuthorization } from '../core/protocols-grant-authorization.js'; import { Time } from '../utils/time.js'; import { DwnError, DwnErrorCode } from '../core/dwn-error.js'; import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js'; @@ -14,6 +18,10 @@ export type ProtocolsConfigureOptions = { messageTimestamp?: string; definition: ProtocolDefinition; signer: Signer; + /** + * The delegated grant invoked to sign on behalf of the logical author, which is the grantor of the delegated grant. + */ + delegatedGrant?: DataEncodedRecordsWriteMessage; permissionGrantId?: string; }; @@ -38,6 +46,7 @@ export class ProtocolsConfigure extends AbstractMessage { + const delegatedGrant = await PermissionGrant.parse(this.message.authorization.authorDelegatedGrant!); + await ProtocolsGrantAuthorization.authorizeConfigure({ + protocolsConfigureMessage : this.message, + expectedGrantor : this.author!, + expectedGrantee : this.signer!, + permissionGrant : delegatedGrant, + messageStore + }); + } + /** * Performs validation on the given protocol definition that are not easy to do using a JSON schema. */ diff --git a/src/interfaces/protocols-query.ts b/src/interfaces/protocols-query.ts index 3d51ba5a7..ae6d3aae3 100644 --- a/src/interfaces/protocols-query.ts +++ b/src/interfaces/protocols-query.ts @@ -4,16 +4,15 @@ import type { Signer } from '../types/signer.js'; import type { ProtocolsQueryDescriptor, ProtocolsQueryFilter, ProtocolsQueryMessage } from '../types/protocols-types.js'; import { AbstractMessage } from '../core/abstract-message.js'; -import { GrantAuthorization } from '../core/grant-authorization.js'; import { Message } from '../core/message.js'; import { PermissionsProtocol } from '../protocols/permissions.js'; +import { ProtocolsGrantAuthorization } from '../core/protocols-grant-authorization.js'; import { removeUndefinedProperties } from '../utils/object.js'; import { Time } from '../utils/time.js'; +import { DwnError, DwnErrorCode } from '../core/dwn-error.js'; import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js'; import { normalizeProtocolUrl, validateProtocolUrlNormalized } from '../utils/url.js'; -import { DwnError, DwnErrorCode } from '../core/dwn-error.js'; - export type ProtocolsQueryOptions = { messageTimestamp?: string; filter?: ProtocolsQueryFilter, @@ -80,10 +79,10 @@ export class ProtocolsQuery extends AbstractMessage { return; } else if (this.author !== undefined && this.signaturePayload!.permissionGrantId) { const permissionGrant = await PermissionsProtocol.fetchGrant(tenant, messageStore, this.signaturePayload!.permissionGrantId); - await GrantAuthorization.performBaseValidation({ - incomingMessage : this.message, + await ProtocolsGrantAuthorization.authorizeQuery({ expectedGrantor : tenant, expectedGrantee : this.author, + incomingMessage : this.message, permissionGrant, messageStore }); diff --git a/src/interfaces/records-write.ts b/src/interfaces/records-write.ts index 584daaf98..49af333d8 100644 --- a/src/interfaces/records-write.ts +++ b/src/interfaces/records-write.ts @@ -251,7 +251,7 @@ export class RecordsWrite implements MessageInterface { this._message = message; if (message.authorization !== undefined) { - this._author = Records.getAuthor(message as RecordsWriteMessage); + this._author = Message.getAuthor(message as RecordsWriteMessage); this._signaturePayload = Jws.decodePlainObjectPayload(message.authorization.signature); @@ -831,7 +831,7 @@ export class RecordsWrite implements MessageInterface { } const recordsWriteMessage = message as RecordsWriteMessage; - const author = Records.getAuthor(recordsWriteMessage); + const author = Message.getAuthor(recordsWriteMessage); const entryId = await RecordsWrite.getEntryId(author, recordsWriteMessage.descriptor); return (entryId === recordsWriteMessage.recordId); } diff --git a/src/types/permission-types.ts b/src/types/permission-types.ts index 723ff72bb..bd17556a3 100644 --- a/src/types/permission-types.ts +++ b/src/types/permission-types.ts @@ -73,6 +73,7 @@ export type PermissionScope = ProtocolPermissionScope | MessagesPermissionScope export type ProtocolPermissionScope = { interface: DwnInterfaceName.Protocols; method: DwnMethodName.Configure | DwnMethodName.Query; + protocol?: string; }; export type MessagesPermissionScope = { diff --git a/src/utils/records.ts b/src/utils/records.ts index e6f33d643..f4e20111c 100644 --- a/src/utils/records.ts +++ b/src/utils/records.ts @@ -34,21 +34,6 @@ export class Records { return isRecordsWrite; } - /** - * Gets the DID of the author of the given message. - */ - public static getAuthor(message: RecordsWriteMessage | RecordsDeleteMessage): string | undefined { - let author; - - if (message.authorization.authorDelegatedGrant !== undefined) { - author = Message.getSigner(message.authorization.authorDelegatedGrant); - } else { - author = Message.getSigner(message); - } - - return author; - } - /** * Decrypts the encrypted data in a message reply using the given ancestor private key. * @param ancestorPrivateKey Any ancestor private key in the key derivation path. diff --git a/tests/core/message.spec.ts b/tests/core/message.spec.ts index f27053821..342e8ae4e 100644 --- a/tests/core/message.spec.ts +++ b/tests/core/message.spec.ts @@ -1,12 +1,90 @@ +import type { PermissionScope } from '../../src/types/permission-types.js'; import type { RecordsQueryReplyEntry } from '../../src/types/records-types.js'; import { expect } from 'chai'; +import { Jws } from '../../src/utils/jws.js'; import { Message } from '../../src/core/message.js'; -import { RecordsRead } from '../../src/index.js'; +import { PermissionsProtocol } from '../../src/protocols/permissions.js'; +import { RecordsRead } from '../../src/interfaces/records-read.js'; +import { RecordsWrite } from '../../src/interfaces/records-write.js'; import { TestDataGenerator } from '../utils/test-data-generator.js'; import { Time } from '../../src/utils/time.js'; +import { DwnInterfaceName, DwnMethodName } from '../../src/enums/dwn-interface-method.js'; describe('Message', () => { + describe('getAuthor()', () => { + it('should return the author of a message', async () => { + const bob = await TestDataGenerator.generatePersona(); + + // create a record message + const { message: recordsWriteMessage } = await TestDataGenerator.generateRecordsWrite({ author: bob }); + const recordsWriteAuthor = Message.getAuthor(recordsWriteMessage); + expect(recordsWriteAuthor).to.equal(bob.did); + + // create a delete message + const { message: recordsDeleteMessage } = await TestDataGenerator.generateRecordsDelete({ author: bob }); + const recordsDeleteAuthor = Message.getAuthor(recordsDeleteMessage); + expect(recordsDeleteAuthor).to.equal(bob.did); + + // create a protocol configure message + const { message: protocolsConfigureMessage } = await TestDataGenerator.generateProtocolsConfigure({ author: bob }); + const protocolsConfigureAuthor = Message.getAuthor(protocolsConfigureMessage); + expect(protocolsConfigureAuthor).to.equal(bob.did); + }); + + it('should get the author of a delegated message', async () => { + const alice = await TestDataGenerator.generatePersona(); + const deviceX = await TestDataGenerator.generatePersona(); + + // create a delegation scope from alice to deviceX for writing records in a protocol + const scope:PermissionScope = { + interface : DwnInterfaceName.Records, + method : DwnMethodName.Write, + protocol : 'https://example.com/protocol/test', + }; + + // create the delegated grant message + const bobGrant = await PermissionsProtocol.createGrant({ + delegated : true, + dateExpires : Time.createOffsetTimestamp({ seconds: 100 }), + grantedTo : deviceX.did, + scope : scope, + signer : Jws.createSigner(alice) + }); + + // create a record message using the grant + const writeData = TestDataGenerator.randomBytes(32); + + const { message } = await RecordsWrite.create({ + signer : Jws.createSigner(deviceX), + delegatedGrant : bobGrant.dataEncodedMessage, + protocol : 'https://example.com/protocol/test', + protocolPath : 'test/path', + dataFormat : 'application/json', + data : writeData, + }); + + // expect message author to be alice + const author = Message.getAuthor(message); + expect(author).to.equal(alice.did); + + // expect message signer to be deviceX + const signer = Message.getSigner(message); + expect(signer).to.equal(deviceX.did); + }); + + it('returns undefined for an unsigned message', async () => { + const { message } = await RecordsRead.create({ + filter: { + recordId: await TestDataGenerator.randomCborSha256Cid() + } + }); + + const author = Message.getAuthor(message); + expect(author).to.be.undefined; + }); + }); + describe('getSigner()', () => { it('should return `undefined` if message is not signed', async () => { const recordsRead = await RecordsRead.create({ diff --git a/tests/features/author-delegated-grant.spec.ts b/tests/features/author-delegated-grant.spec.ts index 05d4d0ce9..79469b183 100644 --- a/tests/features/author-delegated-grant.spec.ts +++ b/tests/features/author-delegated-grant.spec.ts @@ -25,7 +25,7 @@ import { TestStores } from '../test-stores.js'; import { Time } from '../../src/utils/time.js'; import { DidKey, UniversalResolver } from '@web5/dids'; -import { DwnInterfaceName, DwnMethodName, Encoder, PermissionsProtocol, RecordsDelete, RecordsQuery, RecordsRead, RecordsSubscribe } from '../../src/index.js'; +import { DwnInterfaceName, DwnMethodName, Encoder, Message, PermissionsProtocol, RecordsDelete, RecordsQuery, RecordsRead, RecordsSubscribe } from '../../src/index.js'; chai.use(chaiAsPromised); @@ -68,6 +68,127 @@ export function testAuthorDelegatedGrant(): void { await dwn.close(); }); + describe('ProtocolsConfigure', () => { + it('should allow author-delegated grant to configure protocols', async () => { + const alice = await TestDataGenerator.generateDidKeyPersona(); + const bob = await TestDataGenerator.generateDidKeyPersona(); + + // Alice grants Bob ability to configure any protocol, bob will use it to configure the email protocol + const scope: PermissionScope = { + interface : DwnInterfaceName.Protocols, + method : DwnMethodName.Configure, + }; + + const grantToBob = await PermissionsProtocol.createGrant({ + delegated : true, // this is a delegated grant + dateExpires : Time.createOffsetTimestamp({ seconds: 100 }), + description : 'Allow Bob to configure the email protocol', + grantedTo : bob.did, + scope, + signer : Jws.createSigner(alice) + }); + + // Bob attempts to configure a protocol + const protocolConfigure = await TestDataGenerator.generateProtocolsConfigure({ + delegatedGrant : grantToBob.dataEncodedMessage, + author : bob, + protocolDefinition : emailProtocolDefinition, + }); + + // Bob should be abel to configure a protocol on behalf of alice + const protocolConfigureReply = await dwn.processMessage(alice.did, protocolConfigure.message); + expect(protocolConfigureReply.status.code).to.equal(202); + + // verify the protocol configure message was processed + const protocolsQuery = await TestDataGenerator.generateProtocolsQuery({ + author : alice, + filter : { protocol: emailProtocolDefinition.protocol } + }); + + const { status, entries } = await dwn.processMessage(alice.did, protocolsQuery.message); + expect(status.code).to.equal(200); + expect(entries?.length).to.equal(1); + + const fetchedProtocolConfigure = entries![0]; + expect(fetchedProtocolConfigure.descriptor.definition).to.deep.equal(emailProtocolDefinition); + + // author should be alice + const author = Message.getAuthor(fetchedProtocolConfigure); + expect(author).to.equal(alice.did); + + const signer = Message.getSigner(fetchedProtocolConfigure); + expect(signer).to.equal(bob.did); + }); + + it('should allow author-delegated grant to configure a specific protocol', async () => { + const alice = await TestDataGenerator.generateDidKeyPersona(); + const bob = await TestDataGenerator.generateDidKeyPersona(); + + // Alice grants Bob to configure the email protocol + const scope: PermissionScope = { + interface : DwnInterfaceName.Protocols, + method : DwnMethodName.Configure, + protocol : emailProtocolDefinition.protocol, + }; + + const grantToBob = await PermissionsProtocol.createGrant({ + delegated : true, // this is a delegated grant + dateExpires : Time.createOffsetTimestamp({ seconds: 100 }), + description : 'Allow Bob to configure the email protocol', + grantedTo : bob.did, + scope, + signer : Jws.createSigner(alice) + }); + + // Bob attempts to configure a protocol + const protocolConfigure = await TestDataGenerator.generateProtocolsConfigure({ + delegatedGrant : grantToBob.dataEncodedMessage, + author : bob, + protocolDefinition : emailProtocolDefinition, + }); + + // Bob should be abel to configure a protocol on behalf of alice + const protocolConfigureReply = await dwn.processMessage(alice.did, protocolConfigure.message); + expect(protocolConfigureReply.status.code).to.equal(202); + + // verify the protocol configure message was processed + const protocolsQuery = await TestDataGenerator.generateProtocolsQuery({ + author : alice, + filter : { protocol: emailProtocolDefinition.protocol } + }); + + const { status, entries } = await dwn.processMessage(alice.did, protocolsQuery.message); + expect(status.code).to.equal(200); + expect(entries?.length).to.equal(1); + + const fetchedProtocolConfigure = entries![0]; + expect(fetchedProtocolConfigure.descriptor.definition).to.deep.equal(emailProtocolDefinition); + + // author should be alice + const author = Message.getAuthor(fetchedProtocolConfigure); + expect(author).to.equal(alice.did); + + const signer = Message.getSigner(fetchedProtocolConfigure); + expect(signer).to.equal(bob.did); + + // verify that bob cannot configure a different protocol + const otherProtocolDefinition = { + ...emailProtocolDefinition, + protocol: 'https://example.com/protocol/otherProtocol' + }; + + const otherProtocolConfigure = await TestDataGenerator.generateProtocolsConfigure({ + delegatedGrant : grantToBob.dataEncodedMessage, + author : bob, + protocolDefinition : otherProtocolDefinition, + }); + + const otherProtocolConfigureReply = await dwn.processMessage(alice.did, otherProtocolConfigure.message); + expect(otherProtocolConfigureReply.status.code).to.equal(401); + expect(otherProtocolConfigureReply.status.detail).to.contain(DwnErrorCode.ProtocolsGrantAuthorizationScopeProtocolMismatch); + }); + }); + describe('RecordsWrite.parse()', async () => { it('should throw if a message invokes a author-delegated grant (ID) but the author-delegated grant is not given', async () => { const alice = await TestDataGenerator.generatePersona(); diff --git a/tests/features/resumable-tasks.spec.ts b/tests/features/resumable-tasks.spec.ts index c90d090fc..ea74e35fc 100644 --- a/tests/features/resumable-tasks.spec.ts +++ b/tests/features/resumable-tasks.spec.ts @@ -35,10 +35,15 @@ export function testResumableTasks(): void { let eventLog: EventLog; let eventStream: EventStream; let dwn: Dwn; + let consoleError: (message?: any, ...optionalParams: any[]) => void;; // 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 () => { + // suppress console.error output during tests + consoleError = console.error; + console.error = ():void => { }; + didResolver = new UniversalResolver({ didResolvers: [DidKey] }); const stores = TestStores.get(); @@ -69,6 +74,8 @@ export function testResumableTasks(): void { }); after(async () => { + // restore console.error + console.error = consoleError; await dwn.close(); }); diff --git a/tests/handlers/protocols-configure.spec.ts b/tests/handlers/protocols-configure.spec.ts index bc9fcd93c..283eddceb 100644 --- a/tests/handlers/protocols-configure.spec.ts +++ b/tests/handlers/protocols-configure.spec.ts @@ -27,8 +27,8 @@ import { TestStores } from '../test-stores.js'; import { TestStubGenerator } from '../utils/test-stub-generator.js'; import { Time } from '../../src/utils/time.js'; +import { DataStream, Dwn, DwnErrorCode, DwnInterfaceName, DwnMethodName, Encoder, Jws, PermissionGrant, PermissionsProtocol } from '../../src/index.js'; import { DidKey, UniversalResolver } from '@web5/dids'; -import { Dwn, DwnErrorCode, DwnInterfaceName, DwnMethodName, Encoder, Jws } from '../../src/index.js'; chai.use(chaiAsPromised); @@ -293,7 +293,7 @@ export function testProtocolsConfigureHandler(): void { }); const protocolsConfigureReply = await dwn.processMessage(alice.did, protocolsConfig.message); expect(protocolsConfigureReply.status.code).to.equal(401); - expect(protocolsConfigureReply.status.detail).to.contain(DwnErrorCode.AuthorizationAuthorNotOwner); + expect(protocolsConfigureReply.status.detail).to.contain(DwnErrorCode.ProtocolsConfigureAuthorizationFailed); }); it('should reject ProtocolsConfigure with action rule containing duplicated actor (`who` or `who` + `of` combination) within a rule set', async () => { @@ -437,6 +437,122 @@ export function testProtocolsConfigureHandler(): void { expect(protocolsConfigureReply.status.detail).to.contain(DwnErrorCode.ProtocolsConfigureDuplicateRoleInRuleSet); }); + describe('Grant authorization', () => { + it('allows an external party to ProtocolsConfigure only if they have a valid grant', async () => { + // scenario: + // 1. Alice grants Bob the access to ProtocolsConfigure on her DWN + // 2. Verify Bob can perform a ProtocolsConfigure + // 3. Verify that Mallory cannot to use Bob's permission grant to gain access to Alice's DWN + // 4. Alice revokes Bob's grant + // 5. Verify Bob cannot perform ProtocolsConfigure with the revoked grant + const alice = await TestDataGenerator.generateDidKeyPersona(); + const bob = await TestDataGenerator.generateDidKeyPersona(); + const mallory = await TestDataGenerator.generateDidKeyPersona(); + + // 1. Alice grants Bob the access to ProtocolsConfigure on her DWN + const permissionGrant = await PermissionsProtocol.createGrant({ + signer : Jws.createSigner(alice), + grantedTo : bob.did, + dateExpires : Time.createOffsetTimestamp({ seconds: 60 * 60 * 24 }), + scope : { interface: DwnInterfaceName.Protocols, method: DwnMethodName.Configure } + }); + const dataStream = DataStream.fromBytes(permissionGrant.permissionGrantBytes); + + const grantRecordsWriteReply = await dwn.processMessage(alice.did, permissionGrant.recordsWrite.message, { dataStream }); + expect(grantRecordsWriteReply.status.code).to.equal(202); + + // 2. Verify Bob can perform a ProtocolsConfigure + const permissionGrantId = permissionGrant.recordsWrite.message.recordId; + const protocolsConfigure = await TestDataGenerator.generateProtocolsConfigure({ + permissionGrantId, + author : bob, + protocolDefinition : minimalProtocolDefinition + }); + const protocolsConfigureReply = await dwn.processMessage(alice.did, protocolsConfigure.message); + expect(protocolsConfigureReply.status.code).to.equal(202); + + // 3. Verify that Mallory cannot to use Bob's permission grant to gain access to Alice's DWN + const malloryProtocolsQuery = await TestDataGenerator.generateProtocolsConfigure({ + permissionGrantId, + author : mallory, + protocolDefinition : minimalProtocolDefinition + }); + const malloryProtocolsQueryReply = await dwn.processMessage(alice.did, malloryProtocolsQuery.message); + expect(malloryProtocolsQueryReply.status.code).to.equal(401); + expect(malloryProtocolsQueryReply.status.detail).to.contain(DwnErrorCode.GrantAuthorizationNotGrantedToAuthor); + + // 4. Alice revokes Bob's grant + const revokeWrite = await PermissionsProtocol.createRevocation({ + signer : Jws.createSigner(alice), + grant : await PermissionGrant.parse(permissionGrant.dataEncodedMessage), + dateRevoked : Time.getCurrentTimestamp() + }); + + const revokeWriteReply = await dwn.processMessage( + alice.did, + revokeWrite.recordsWrite.message, + { dataStream: DataStream.fromBytes(revokeWrite.permissionRevocationBytes) } + ); + expect(revokeWriteReply.status.code).to.equal(202); + + // 5. Verify Bob cannot perform ProtocolsQuery with the revoked grant + const unauthorizedProtocolsConfigure = await TestDataGenerator.generateProtocolsConfigure({ + permissionGrantId, + author : bob, + protocolDefinition : { + ...minimalProtocolDefinition, + protocol: 'https://example.com/protocol/another-protocol' + } + }); + const unauthorizedProtocolsConfigureReply = await dwn.processMessage(alice.did, unauthorizedProtocolsConfigure.message); + expect(unauthorizedProtocolsConfigureReply.status.code).to.equal(401); + expect(unauthorizedProtocolsConfigureReply.status.detail).to.contain(DwnErrorCode.GrantAuthorizationGrantRevoked); + }); + + it('should allow to scope a ProtocolsConfigure to a specific protocol', async () => { + const alice = await TestDataGenerator.generateDidKeyPersona(); + const bob = await TestDataGenerator.generateDidKeyPersona(); + + // Alice grants Bob the access to ProtocolsConfigure on her DWN for a specific protocol + const permissionGrant = await PermissionsProtocol.createGrant({ + signer : Jws.createSigner(alice), + grantedTo : bob.did, + dateExpires : Time.createOffsetTimestamp({ seconds: 60 * 60 * 24 }), + scope : { interface: DwnInterfaceName.Protocols, method: DwnMethodName.Configure, protocol: 'https://example.com/protocol/allowed' } + }); + + const dataStream = DataStream.fromBytes(permissionGrant.permissionGrantBytes); + const grantRecordsWriteReply = await dwn.processMessage(alice.did, permissionGrant.recordsWrite.message, { dataStream }); + expect(grantRecordsWriteReply.status.code).to.equal(202); + + // Bob tries to ProtocolsConfigure to Alice's DWN for the allowed protocol + const protocolConfigureAllowed = await TestDataGenerator.generateProtocolsConfigure({ + author : bob, + protocolDefinition : { + ...minimalProtocolDefinition, + protocol: 'https://example.com/protocol/allowed' + }, + permissionGrantId: permissionGrant.recordsWrite.message.recordId + }); + + const protocolConfigureAllowedReply = await dwn.processMessage(alice.did, protocolConfigureAllowed.message); + expect(protocolConfigureAllowedReply.status.code).to.equal(202); + + // Bob tries to ProtocolsConfigure to Alice's DWN for a different protocol + const protocolConfigureNotAllowed = await TestDataGenerator.generateProtocolsConfigure({ + author : bob, + protocolDefinition : { + ...minimalProtocolDefinition, + protocol: 'https://example.com/protocol/not-allowed' + }, + permissionGrantId: permissionGrant.recordsWrite.message.recordId + }); + + const protocolConfigureNotAllowedReply = await dwn.processMessage(alice.did, protocolConfigureNotAllowed.message); + expect(protocolConfigureNotAllowedReply.status.code).to.equal(401); + }); + }); + describe('event log', () => { it('should add event for ProtocolsConfigure', async () => { const alice = await TestDataGenerator.generateDidKeyPersona(); diff --git a/tests/handlers/protocols-query.spec.ts b/tests/handlers/protocols-query.spec.ts index eaf9f472e..7a4c46261 100644 --- a/tests/handlers/protocols-query.spec.ts +++ b/tests/handlers/protocols-query.spec.ts @@ -239,6 +239,23 @@ export function testProtocolsQueryHandler(): void { const bob = await TestDataGenerator.generateDidKeyPersona(); const mallory = await TestDataGenerator.generateDidKeyPersona(); + // Alice creates a public and private protocol to test query results + const { message: publicProtocolMessage } = await TestDataGenerator.generateProtocolsConfigure({ + author : alice, + published : true, + }); + + const { status: publicProtocolStatus } = await dwn.processMessage(alice.did, publicProtocolMessage); + expect(publicProtocolStatus.code).to.equal(202); + + const { message: privateProtocolMessage } = await TestDataGenerator.generateProtocolsConfigure({ + author : alice, + published : false, + }); + + const { status: privateProtocolStatus } = await dwn.processMessage(alice.did, privateProtocolMessage); + expect(privateProtocolStatus.code).to.equal(202); + // 1. Alice grants Bob the access to ProtocolsQuery on her DWN const permissionGrant = await PermissionsProtocol.createGrant({ signer : Jws.createSigner(alice), @@ -259,6 +276,7 @@ export function testProtocolsQueryHandler(): void { }); const protocolsQueryReply = await dwn.processMessage(alice.did, protocolsQuery.message); expect(protocolsQueryReply.status.code).to.equal(200); + expect(protocolsQueryReply.entries?.length).to.equal(2); // 3. Verify that Mallory cannot to use Bob's permission grant to gain access to Alice's DWN const malloryProtocolsQuery = await TestDataGenerator.generateProtocolsQuery({ @@ -293,6 +311,102 @@ export function testProtocolsQueryHandler(): void { expect(unauthorizedProtocolsQueryReply.status.detail).to.contain(DwnErrorCode.GrantAuthorizationGrantRevoked); }); + it('should allow to scope a ProtocolsQuery to a specific protocol', async () => { + const alice = await TestDataGenerator.generateDidKeyPersona(); + const bob = await TestDataGenerator.generateDidKeyPersona(); + + // create 2 unpublished protocols, and one published protocol + const { message: allowedProtocolMessage } = await TestDataGenerator.generateProtocolsConfigure({ + author : alice, + published : false, + }); + const allowedProtocol = allowedProtocolMessage.descriptor.definition.protocol; + const { status: allowedStatus } = await dwn.processMessage(alice.did, allowedProtocolMessage); + expect(allowedStatus.code).to.equal(202); + + + const { message: notAllowedProtocolMessage } = await TestDataGenerator.generateProtocolsConfigure({ + author : alice, + published : false, + }); + const notAllowedProtocol = notAllowedProtocolMessage.descriptor.definition.protocol; + const { status: notAllowedStatus } = await dwn.processMessage(alice.did, notAllowedProtocolMessage); + expect(notAllowedStatus.code).to.equal(202); + + const { message: publishedProtocolMessage } = await TestDataGenerator.generateProtocolsConfigure({ + author : alice, + published : true, + }); + const publishedProtocol = publishedProtocolMessage.descriptor.definition.protocol; + const { status: publishedStatus } = await dwn.processMessage(alice.did, publishedProtocolMessage); + expect(publishedStatus.code).to.equal(202); + + + // Alice grants Bob the access to ProtocolsQuery on her DWN for a specific protocol + const permissionGrant = await PermissionsProtocol.createGrant({ + signer : Jws.createSigner(alice), + grantedTo : bob.did, + dateExpires : Time.createOffsetTimestamp({ seconds: 60 * 60 * 24 }), + scope : { interface: DwnInterfaceName.Protocols, method: DwnMethodName.Query, protocol: allowedProtocol } + }); + + const dataStream = DataStream.fromBytes(permissionGrant.permissionGrantBytes); + const grantRecordsWriteReply = await dwn.processMessage(alice.did, permissionGrant.recordsWrite.message, { dataStream }); + expect(grantRecordsWriteReply.status.code).to.equal(202); + + // Bob tries to ProtocolsQuery to Alice's DWN for the allowed protocol + const protocolsQueryAllowed = await TestDataGenerator.generateProtocolsQuery({ + author : bob, + filter : { + protocol: allowedProtocol + }, + permissionGrantId: permissionGrant.recordsWrite.message.recordId + }); + + const protocolQueryAllowedReply = await dwn.processMessage(alice.did, protocolsQueryAllowed.message); + expect(protocolQueryAllowedReply.status.code).to.equal(200); + expect(protocolQueryAllowedReply.entries?.length).to.equal(1); + expect(protocolQueryAllowedReply.entries![0].descriptor.definition.protocol).to.deep.equal(allowedProtocol); + + // Bob tries to ProtocolsQuery to Alice's DWN for a different protocol + const protocolQueryNotAllowed = await TestDataGenerator.generateProtocolsQuery({ + author : bob, + filter : { + protocol: notAllowedProtocol, + }, + permissionGrantId: permissionGrant.recordsWrite.message.recordId + }); + + const protocolQueryNotAllowedReply = await dwn.processMessage(alice.did, protocolQueryNotAllowed.message); + expect(protocolQueryNotAllowedReply.status.code).to.equal(200); + expect(protocolQueryNotAllowedReply.entries?.length).to.equal(0); + + // Bob tries to ProtocolsQuery to Alice's DWN for a published protocol with the same grant + const protocolQueryPublished = await TestDataGenerator.generateProtocolsQuery({ + author : bob, + filter : { + protocol: publishedProtocol, + }, + permissionGrantId: permissionGrant.recordsWrite.message.recordId + }); + + const protocolQueryPublishedReply = await dwn.processMessage(alice.did, protocolQueryPublished.message); + expect(protocolQueryPublishedReply.status.code).to.equal(200); + expect(protocolQueryPublishedReply.entries?.length).to.equal(1); + expect(protocolQueryPublishedReply.entries![0].descriptor.definition.protocol).to.deep.equal(publishedProtocol); + + // Bob tries to ProtocolsQuery to Alice's DWN with no filters, using the same grant + const protocolQueryNoFilters = await ProtocolsQuery.create({ + signer : Jws.createSigner(bob), + permissionGrantId : permissionGrant.recordsWrite.message.recordId, + }); + + const protocolQueryNoFiltersReply = await dwn.processMessage(alice.did, protocolQueryNoFilters.message); + expect(protocolQueryNoFiltersReply.status.code).to.equal(200); + expect(protocolQueryNoFiltersReply.entries?.length).to.equal(1); + expect(protocolQueryNoFiltersReply.entries![0].descriptor.definition.protocol).to.deep.equal(publishedProtocol); + }); + it('rejects with 401 when an external party attempts to ProtocolsQuery if they present an expired grant', async () => { // scenario: Alice grants Bob access to ProtocolsQuery, but when Bob invokes the grant it has expired const alice = await TestDataGenerator.generateDidKeyPersona(); diff --git a/tests/interfaces/messages-subscribe.spec.ts b/tests/interfaces/messages-subscribe.spec.ts index 096946ad3..8755c1504 100644 --- a/tests/interfaces/messages-subscribe.spec.ts +++ b/tests/interfaces/messages-subscribe.spec.ts @@ -1,4 +1,3 @@ -import { authorizeOwner } from '../../src/core/auth.js'; import { MessagesSubscribe } from '../../src/interfaces/messages-subscribe.js'; import { DwnInterfaceName, DwnMethodName, Jws, TestDataGenerator, Time } from '../../src/index.js'; @@ -19,9 +18,6 @@ describe('MessagesSubscribe', () => { expect(message.descriptor.method).to.eql(DwnMethodName.Subscribe); expect(message.authorization).to.exist; expect(message.descriptor.messageTimestamp).to.equal(timestamp); - - // MessagesSubscribe authorizes against owner - await authorizeOwner(alice.did, messagesSubscribe); }); }); }); diff --git a/tests/utils/records.spec.ts b/tests/utils/records.spec.ts index 970e322cc..b7fef5833 100644 --- a/tests/utils/records.spec.ts +++ b/tests/utils/records.spec.ts @@ -1,11 +1,10 @@ -import type { DerivedPrivateJwk, PermissionScope, RecordsWriteDescriptor } from '../../src/index.js'; +import type { DerivedPrivateJwk, RecordsWriteDescriptor } from '../../src/index.js'; import { expect } from 'chai'; import { DwnErrorCode } from '../../src/core/dwn-error.js'; import { ed25519 } from '../../src/jose/algorithms/signing/ed25519.js'; -import { RecordsWrite } from '../../src/interfaces/records-write.js'; -import { DwnInterfaceName, DwnMethodName, Jws, KeyDerivationScheme, PermissionsProtocol, Records, TestDataGenerator, Time } from '../../src/index.js'; +import { DwnInterfaceName, DwnMethodName, KeyDerivationScheme, Records } from '../../src/index.js'; describe('Records', () => { describe('deriveLeafPrivateKey()', () => { @@ -19,59 +18,6 @@ describe('Records', () => { }); }); - describe('getAuthor()', () => { - it('should return the author of RecordsWrite, RecordsDelete types', async () => { - const bob = await TestDataGenerator.generatePersona(); - - // create a record message - const { message: recordsWriteMessage } = await TestDataGenerator.generateRecordsWrite({ author: bob }); - const recordsWriteAuthor = Records.getAuthor(recordsWriteMessage); - expect(recordsWriteAuthor).to.equal(bob.did); - - // create a delete message - const { message: recordsDeleteMessage } = await TestDataGenerator.generateRecordsDelete({ author: bob }); - const recordsDeleteAuthor = Records.getAuthor(recordsDeleteMessage); - expect(recordsDeleteAuthor).to.equal(bob.did); - }); - - it('should get the author of a delegated message', async () => { - const alice = await TestDataGenerator.generatePersona(); - const deviceX = await TestDataGenerator.generatePersona(); - - // create a delegation scope from alice to deviceX for writing records in a protocol - const scope:PermissionScope = { - interface : DwnInterfaceName.Records, - method : DwnMethodName.Write, - protocol : 'https://example.com/protocol/test', - }; - - // create the delegated grant message - const bobGrant = await PermissionsProtocol.createGrant({ - delegated : true, - dateExpires : Time.createOffsetTimestamp({ seconds: 100 }), - grantedTo : deviceX.did, - scope : scope, - signer : Jws.createSigner(alice) - }); - - // create a record message using the grant - const writeData = TestDataGenerator.randomBytes(32); - - const { message } = await RecordsWrite.create({ - signer : Jws.createSigner(deviceX), - delegatedGrant : bobGrant.dataEncodedMessage, - protocol : 'https://example.com/protocol/test', - protocolPath : 'test/path', - dataFormat : 'application/json', - data : writeData, - }); - - // expect message author to be alice - const author = Records.getAuthor(message); - expect(author).to.equal(alice.did); - }); - }); - describe('constructKeyDerivationPathUsingProtocolPathScheme()', () => { it('should throw if given a flat-space descriptor', async () => { const descriptor: RecordsWriteDescriptor = { diff --git a/tests/utils/test-data-generator.ts b/tests/utils/test-data-generator.ts index e777eef07..cd7c8b6cf 100644 --- a/tests/utils/test-data-generator.ts +++ b/tests/utils/test-data-generator.ts @@ -72,6 +72,7 @@ export type GenerateProtocolsConfigureInput = { messageTimestamp?: string; protocolDefinition?: ProtocolDefinition; permissionGrantId?: string; + delegatedGrant?: DataEncodedRecordsWriteMessage; }; export type GenerateProtocolsConfigureOutput = { @@ -321,7 +322,8 @@ export class TestDataGenerator { messageTimestamp : input?.messageTimestamp, definition, signer, - permissionGrantId : input?.permissionGrantId + permissionGrantId : input?.permissionGrantId, + delegatedGrant : input?.delegatedGrant }; const protocolsConfigure = await ProtocolsConfigure.create(options);