diff --git a/Q_AND_A.md b/Q_AND_A.md index a026e2e4b..611a03f4d 100644 --- a/Q_AND_A.md +++ b/Q_AND_A.md @@ -72,4 +72,10 @@ This design choice is primarily driven by performance considerations. If we were to make `protocolPath` optional, and it is not specified, we would need to search records across protocol paths. Since protocol rules (protocol rule set) are defined at the per protocol path level, this means we would need to parse the protocol rules for every protocol path in the protocol definition to determine which protocol path the invoked role has access to. Then, we would need to make a database query for each qualified protocol path, which could be quite costly. This is not to say that we should never consider it, but this is the current design choice. +- What is the difference between `write` and `update` actions? + + (Last update: 2023/11/09) + + - `write` - allows a DID to create and update the record they have created + - `update` - allows a DID to update a record, regardless of the initial author diff --git a/json-schemas/interface-methods/records-read.json b/json-schemas/interface-methods/records-read.json index 3969d8bc1..ec8700cd7 100644 --- a/json-schemas/interface-methods/records-read.json +++ b/json-schemas/interface-methods/records-read.json @@ -8,7 +8,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/src/core/abstract-message.ts b/src/core/abstract-message.ts new file mode 100644 index 000000000..0f8be2991 --- /dev/null +++ b/src/core/abstract-message.ts @@ -0,0 +1,48 @@ +import type { MessageInterface } from '../types/message-interface.js'; +import type { GenericMessage, GenericSignaturePayload } from '../types/message-types.js'; + +import { Jws } from '../utils/jws.js'; +import { Message } from './message.js'; + +/** + * An abstract implementation of the `MessageInterface` interface. + */ +export abstract class AbstractMessage implements MessageInterface { + private _message: M; + public get message(): M { + return this._message as M; + } + + private _author: string | undefined; + public get author(): string | undefined { + return this._author; + } + + private _signaturePayload: GenericSignaturePayload | undefined; + public get signaturePayload(): GenericSignaturePayload | undefined { + return this._signaturePayload; + } + + protected constructor(message: M) { + this._message = message; + + if (message.authorization !== undefined) { + // if the message authorization contains author delegated grant, the author would be the grantor of the grant + // else the author would be the signer of the message + if (message.authorization.authorDelegatedGrant !== undefined) { + this._author = Message.getSigner(message.authorization.authorDelegatedGrant); + } else { + this._author = Message.getSigner(message as GenericMessage); + } + + this._signaturePayload = Jws.decodePlainObjectPayload(message.authorization.signature); + } + } + + /** + * Called by `JSON.stringify(...)` automatically. + */ + toJSON(): GenericMessage { + return this.message; + } +} \ No newline at end of file diff --git a/src/core/dwn-error.ts b/src/core/dwn-error.ts index 0658fe002..a937161e3 100644 --- a/src/core/dwn-error.ts +++ b/src/core/dwn-error.ts @@ -52,6 +52,7 @@ export enum DwnErrorCode { PrivateKeySignerUnableToDeduceKeyId = 'PrivateKeySignerUnableToDeduceKeyId', PrivateKeySignerUnsupportedCurve = 'PrivateKeySignerUnsupportedCurve', ProtocolAuthorizationActionNotAllowed = 'ProtocolAuthorizationActionNotAllowed', + ProtocolAuthorizationActionRulesNotFound = 'ProtocolAuthorizationActionRulesNotFound', ProtocolAuthorizationDuplicateContextRoleRecipient = 'ProtocolAuthorizationDuplicateContextRoleRecipient', ProtocolAuthorizationDuplicateGlobalRoleRecipient = 'ProtocolAuthorizationDuplicateGlobalRoleRecipient', ProtocolAuthorizationIncorrectDataFormat = 'ProtocolAuthorizationIncorrectDataFormat', diff --git a/src/core/grant-authorization.ts b/src/core/grant-authorization.ts index 78e0437b8..79366141c 100644 --- a/src/core/grant-authorization.ts +++ b/src/core/grant-authorization.ts @@ -1,4 +1,5 @@ import type { GenericMessage } from '../types/message-types.js'; +import type { MessageInterface } from '../types/message-interface.js'; import type { MessageStore } from '../types/message-store.js'; import type { PermissionsGrantMessage } from '../types/permissions-types.js'; @@ -15,7 +16,7 @@ export class GrantAuthorization { */ public static async authorizeGenericMessage( tenant: string, - incomingMessage: Message, + incomingMessage: MessageInterface, author: string, permissionsGrantId: string, messageStore: MessageStore, diff --git a/src/core/message.ts b/src/core/message.ts index 2e8cef668..c7597f3e0 100644 --- a/src/core/message.ts +++ b/src/core/message.ts @@ -12,27 +12,10 @@ import { removeUndefinedProperties } from '../utils/object.js'; import { validateJsonSchema } from '../schema-validator.js'; import { DwnError, DwnErrorCode } from './dwn-error.js'; -export abstract class Message { - readonly message: M; - readonly signaturePayload: GenericSignaturePayload | undefined; - readonly author: string | undefined; - - constructor(message: M) { - this.message = message; - - if (message.authorization !== undefined) { - this.signaturePayload = Jws.decodePlainObjectPayload(message.authorization.signature); - this.author = Message.getSigner(message as GenericMessage); - } - } - - /** - * Called by `JSON.stringify(...)` automatically. - */ - toJSON(): GenericMessage { - return this.message; - } - +/** + * A class containing utility methods for working with DWN messages. + */ +export class Message { /** * Validates the given message against the corresponding JSON schema. * @throws {Error} if fails validation. diff --git a/src/core/protocol-authorization.ts b/src/core/protocol-authorization.ts index f3b36d68d..4b4a3fe47 100644 --- a/src/core/protocol-authorization.ts +++ b/src/core/protocol-authorization.ts @@ -475,6 +475,9 @@ 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 an "update" RecordsWrite by the original record author, + * the RecordsWrite can either be authorized by a `write` or `update` allow 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 an "update" by the same author is attempted. */ private static async getActionsSeekingARuleMatch( tenant: string, @@ -529,7 +532,7 @@ export class ProtocolAuthorization { // We have already checked that the message is not from tenant, owner, or permissionsGrant if (actionRules === undefined) { throw new DwnError( - DwnErrorCode.ProtocolAuthorizationActionNotAllowed, + DwnErrorCode.ProtocolAuthorizationActionRulesNotFound, `no action rule defined for ${incomingMessageMethod}, ${author} is unauthorized` ); } diff --git a/src/interfaces/events-get.ts b/src/interfaces/events-get.ts index 51d32b283..8f5342ef8 100644 --- a/src/interfaces/events-get.ts +++ b/src/interfaces/events-get.ts @@ -1,6 +1,7 @@ import type { Signer } from '../types/signer.js'; import type { EventsGetDescriptor, EventsGetMessage } from '../types/event-types.js'; +import { AbstractMessage } from '../core/abstract-message.js'; import { Message } from '../core/message.js'; import { Time } from '../utils/time.js'; import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js'; @@ -11,7 +12,7 @@ export type EventsGetOptions = { messageTimestamp?: string; }; -export class EventsGet extends Message { +export class EventsGet extends AbstractMessage { public static async parse(message: EventsGetMessage): Promise { Message.validateJsonSchema(message); diff --git a/src/interfaces/messages-get.ts b/src/interfaces/messages-get.ts index d5f937228..144935b51 100644 --- a/src/interfaces/messages-get.ts +++ b/src/interfaces/messages-get.ts @@ -1,6 +1,7 @@ import type { Signer } from '../types/signer.js'; import type { MessagesGetDescriptor, MessagesGetMessage } from '../types/messages-types.js'; +import { AbstractMessage } from '../core/abstract-message.js'; import { Cid } from '../utils/cid.js'; import { Message } from '../core/message.js'; import { Time } from '../utils/time.js'; @@ -13,7 +14,7 @@ export type MessagesGetOptions = { messageTimestamp?: string; }; -export class MessagesGet extends Message { +export class MessagesGet extends AbstractMessage { public static async parse(message: MessagesGetMessage): Promise { Message.validateJsonSchema(message); this.validateMessageCids(message.descriptor.messageCids); diff --git a/src/interfaces/permissions-grant.ts b/src/interfaces/permissions-grant.ts index 02a65fe1e..606c71cd4 100644 --- a/src/interfaces/permissions-grant.ts +++ b/src/interfaces/permissions-grant.ts @@ -4,6 +4,7 @@ import type { PermissionsRequest } from './permissions-request.js'; import type { Signer } from '../types/signer.js'; import type { PermissionConditions, PermissionScope, PermissionsGrantDescriptor, RecordsPermissionScope } from '../types/permissions-grant-descriptor.js'; +import { AbstractMessage } from '../core/abstract-message.js'; import { Message } from '../core/message.js'; import { removeUndefinedProperties } from '../utils/object.js'; import { Time } from '../utils/time.js'; @@ -35,7 +36,7 @@ export type CreateFromPermissionsRequestOverrides = { conditions?: PermissionConditions; }; -export class PermissionsGrant extends Message { +export class PermissionsGrant extends AbstractMessage { public static async parse(message: PermissionsGrantMessage): Promise { await Message.validateMessageSignatureIntegrity(message.authorization.signature, message.descriptor); diff --git a/src/interfaces/permissions-request.ts b/src/interfaces/permissions-request.ts index 2a2f8cff7..f7322a4ce 100644 --- a/src/interfaces/permissions-request.ts +++ b/src/interfaces/permissions-request.ts @@ -2,6 +2,7 @@ import type { Signer } from '../types/signer.js'; import type { PermissionConditions, PermissionScope } from '../types/permissions-grant-descriptor.js'; import type { PermissionsRequestDescriptor, PermissionsRequestMessage } from '../types/permissions-types.js'; +import { AbstractMessage } from '../core/abstract-message.js'; import { Message } from '../core/message.js'; import { removeUndefinedProperties } from '../utils/object.js'; import { Time } from '../utils/time.js'; @@ -18,7 +19,7 @@ export type PermissionsRequestOptions = { signer: Signer; }; -export class PermissionsRequest extends Message { +export class PermissionsRequest extends AbstractMessage { public static async parse(message: PermissionsRequestMessage): Promise { await Message.validateMessageSignatureIntegrity(message.authorization.signature, message.descriptor); diff --git a/src/interfaces/permissions-revoke.ts b/src/interfaces/permissions-revoke.ts index 4a9a386b4..0b5af0f88 100644 --- a/src/interfaces/permissions-revoke.ts +++ b/src/interfaces/permissions-revoke.ts @@ -1,6 +1,7 @@ import type { Signer } from '../types/signer.js'; import type { PermissionsGrantMessage, PermissionsRevokeDescriptor, PermissionsRevokeMessage } from '../types/permissions-types.js'; +import { AbstractMessage } from '../core/abstract-message.js'; import { Message } from '../core/message.js'; import { Time } from '../utils/time.js'; import { DwnError, DwnErrorCode } from '../core/dwn-error.js'; @@ -12,7 +13,7 @@ export type PermissionsRevokeOptions = { signer: Signer; }; -export class PermissionsRevoke extends Message { +export class PermissionsRevoke extends AbstractMessage { public static async parse(message: PermissionsRevokeMessage): Promise { await Message.validateMessageSignatureIntegrity(message.authorization.signature, message.descriptor); Time.validateTimestamp(message.descriptor.messageTimestamp); diff --git a/src/interfaces/protocols-configure.ts b/src/interfaces/protocols-configure.ts index 511a5933a..f06ab0794 100644 --- a/src/interfaces/protocols-configure.ts +++ b/src/interfaces/protocols-configure.ts @@ -1,6 +1,7 @@ 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 { Message } from '../core/message.js'; import { Time } from '../utils/time.js'; import { DwnError, DwnErrorCode } from '../core/dwn-error.js'; @@ -14,10 +15,7 @@ export type ProtocolsConfigureOptions = { permissionsGrantId?: string; }; -export class ProtocolsConfigure extends Message { - // JSON Schema guarantees presence of `authorization` which contains author DID - readonly author!: string; - +export class ProtocolsConfigure extends AbstractMessage { public static async parse(message: ProtocolsConfigureMessage): Promise { Message.validateJsonSchema(message); ProtocolsConfigure.validateProtocolDefinition(message.descriptor.definition); diff --git a/src/interfaces/protocols-query.ts b/src/interfaces/protocols-query.ts index 2abf05d3e..ff337667d 100644 --- a/src/interfaces/protocols-query.ts +++ b/src/interfaces/protocols-query.ts @@ -3,6 +3,7 @@ import type { MessageStore } from '../types/message-store.js'; 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 { removeUndefinedProperties } from '../utils/object.js'; @@ -19,7 +20,7 @@ export type ProtocolsQueryOptions = { permissionsGrantId?: string; }; -export class ProtocolsQuery extends Message { +export class ProtocolsQuery extends AbstractMessage { public static async parse(message: ProtocolsQueryMessage): Promise { if (message.authorization !== undefined) { diff --git a/src/interfaces/records-delete.ts b/src/interfaces/records-delete.ts index 05b632222..7e9416d6d 100644 --- a/src/interfaces/records-delete.ts +++ b/src/interfaces/records-delete.ts @@ -1,8 +1,8 @@ import type { Signer } from '../types/signer.js'; import type { RecordsDeleteDescriptor, RecordsDeleteMessage } from '../types/records-types.js'; +import { AbstractMessage } from '../core/abstract-message.js'; import { Message } from '../core/message.js'; - import { Time } from '../utils/time.js'; import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js'; @@ -13,7 +13,7 @@ export type RecordsDeleteOptions = { signer: Signer; }; -export class RecordsDelete extends Message { +export class RecordsDelete extends AbstractMessage { public static async parse(message: RecordsDeleteMessage): Promise { await Message.validateMessageSignatureIntegrity(message.authorization.signature, message.descriptor); diff --git a/src/interfaces/records-query.ts b/src/interfaces/records-query.ts index 4920b1094..2ea600bb9 100644 --- a/src/interfaces/records-query.ts +++ b/src/interfaces/records-query.ts @@ -1,9 +1,9 @@ import type { DelegatedGrantMessage } from '../types/delegated-grant-message.js'; +import type { Pagination } from '../types/message-types.js'; import type { Signer } from '../types/signer.js'; import type { DateSort, RecordsFilter, RecordsQueryDescriptor, RecordsQueryMessage } from '../types/records-types.js'; -import type { GenericMessage, GenericSignaturePayload, Pagination } from '../types/message-types.js'; -import { Jws } from '../utils/jws.js'; +import { AbstractMessage } from '../core/abstract-message.js'; import { Message } from '../core/message.js'; import { Records } from '../utils/records.js'; import { removeUndefinedProperties } from '../utils/object.js'; @@ -29,49 +29,7 @@ export type RecordsQueryOptions = { /** * A class representing a RecordsQuery DWN message. */ -export class RecordsQuery { - private _message: RecordsQueryMessage; - /** - * Valid JSON message representing this RecordsQuery. - */ - public get message(): RecordsQueryMessage { - return this._message as RecordsQueryMessage; - } - - private _author: string | undefined; - /** - * DID of the logical author of this message. - * NOTE: we say "logical" author because a message can be signed by a delegate of the actual author, - * in which case the author DID would not be the same as the signer/delegate DID, - * but be the DID of the grantor (`grantedBy`) of the delegated grant presented. - */ - public get author(): string | undefined { - return this._author; - } - - private _signaturePayload: GenericSignaturePayload | undefined; - /** - * Decoded payload of the signature of this message. - */ - public get signaturePayload(): GenericSignaturePayload | undefined { - return this._signaturePayload; - } - - private constructor(message: RecordsQueryMessage) { - this._message = message; - - if (message.authorization !== undefined) { - // if the message authorization contains author delegated grant, the author would be the grantor of the grant - // else the author would be the signer of the message - if (message.authorization.authorDelegatedGrant !== undefined) { - this._author = Message.getSigner(message.authorization.authorDelegatedGrant); - } else { - this._author = Message.getSigner(message as GenericMessage); - } - - this._signaturePayload = Jws.decodePlainObjectPayload(message.authorization.signature); - } - } +export class RecordsQuery extends AbstractMessage { public static async parse(message: RecordsQueryMessage): Promise { let signaturePayload; @@ -95,6 +53,7 @@ export class RecordsQuery { if (message.descriptor.filter.schema !== undefined) { validateSchemaUrlNormalized(message.descriptor.filter.schema); } + Time.validateTimestamp(message.descriptor.messageTimestamp); return new RecordsQuery(message); diff --git a/src/interfaces/records-read.ts b/src/interfaces/records-read.ts index 97d663bbd..60de58c87 100644 --- a/src/interfaces/records-read.ts +++ b/src/interfaces/records-read.ts @@ -1,6 +1,8 @@ +import type { DelegatedGrantMessage } from '../types/delegated-grant-message.js'; import type { Signer } from '../types/signer.js'; import type { RecordsFilter , RecordsReadDescriptor, RecordsReadMessage } from '../types/records-types.js'; +import { AbstractMessage } from '../core/abstract-message.js'; import { Message } from '../core/message.js'; import { Records } from '../utils/records.js'; import { removeUndefinedProperties } from '../utils/object.js'; @@ -17,14 +19,23 @@ export type RecordsReadOptions = { * The protocol path to a $globalRole record whose recipient is the author of this RecordsRead */ protocolRole?: string; + + /** + * The delegated grant to sign on behalf of the logical author, which is the grantor (`grantedBy`) of the delegated grant. + */ + delegatedGrant?: DelegatedGrantMessage; }; -export class RecordsRead extends Message { +export class RecordsRead extends AbstractMessage { public static async parse(message: RecordsReadMessage): Promise { + let signaturePayload; if (message.authorization !== undefined) { - await Message.validateMessageSignatureIntegrity(message.authorization.signature, message.descriptor); + signaturePayload = await Message.validateMessageSignatureIntegrity(message.authorization.signature, message.descriptor); } + + Records.validateDelegatedGrantReferentialIntegrity(message, signaturePayload); + Time.validateTimestamp(message.descriptor.messageTimestamp); const recordsRead = new RecordsRead(message); @@ -58,7 +69,8 @@ export class RecordsRead extends Message { descriptor, signer, permissionsGrantId, - protocolRole + protocolRole, + delegatedGrant: options.delegatedGrant }); } const message: RecordsReadMessage = { descriptor, authorization }; diff --git a/src/interfaces/records-write.ts b/src/interfaces/records-write.ts index a95ee184a..bc7a28164 100644 --- a/src/interfaces/records-write.ts +++ b/src/interfaces/records-write.ts @@ -1,5 +1,6 @@ import type { DelegatedGrantMessage } from '../types/delegated-grant-message.js'; import type { GeneralJws } from '../types/jws-types.js'; +import type { MessageInterface } from '../types/message-interface.js'; import type { MessageStore } from '../types/message-store.js'; import type { PublicJwk } from '../types/jose-types.js'; import type { Signer } from '../types/signer.js'; @@ -139,8 +140,9 @@ export type CreateFromOptions = { /** * A class representing a RecordsWrite DWN message. + * NOTE: Unable to extend `AbstractMessage` directly because the incompatible `_message` type, which is not just a generic `` type. */ -export class RecordsWrite { +export class RecordsWrite implements MessageInterface { private _message: InternalRecordsWriteMessage; /** * Valid JSON message representing this RecordsWrite. @@ -158,28 +160,16 @@ export class RecordsWrite { } private _author: string | undefined; - /** - * DID of the logical author of this message. - * NOTE: we say "logical" author because a message can be signed by a delegate of the actual author, - * in which case the author DID would not be the same as the signer/delegate DID, - * but be the DID of the grantor (`grantedBy`) of the delegated grant presented. - */ public get author(): string | undefined { return this._author; } private _signaturePayload: RecordsWriteSignaturePayload | undefined; - /** - * Decoded payload of the signature of this message. - */ public get signaturePayload(): RecordsWriteSignaturePayload | undefined { return this._signaturePayload; } private _owner: string | undefined; - /** - * DID of owner of this message. - */ public get owner(): string | undefined { return this._owner; } diff --git a/src/types/authorization-model.ts b/src/types/authorization-model.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/types/message-interface.ts b/src/types/message-interface.ts new file mode 100644 index 000000000..6f2820ff4 --- /dev/null +++ b/src/types/message-interface.ts @@ -0,0 +1,24 @@ +import type { GenericMessage, GenericSignaturePayload } from './message-types.js'; + +/** + * An generic interface that represents a DWN message and convenience methods for working with it. + */ +export interface MessageInterface { + /** + * Valid JSON message representing this DWN message. + */ + get message(): M; + + /** + * DID of the logical author of this message. + * NOTE: we say "logical" author because a message can be signed by a delegate of the actual author, + * in which case the author DID would not be the same as the signer/delegate DID, + * but be the DID of the grantor (`grantedBy`) of the delegated grant presented. + */ + get author(): string | undefined; + + /** + * Decoded payload of the signature of this message. + */ + get signaturePayload(): GenericSignaturePayload | undefined; +} diff --git a/src/utils/records.ts b/src/utils/records.ts index 2c94b5521..62160a1c5 100644 --- a/src/utils/records.ts +++ b/src/utils/records.ts @@ -1,7 +1,7 @@ import type { DerivedPrivateJwk } from './hd-key.js'; import type { Readable } from 'readable-stream'; import type { Filter, GenericSignaturePayload, RangeFilter } from '../types/message-types.js'; -import type { RangeCriterion, RecordsFilter, RecordsQueryMessage, RecordsWriteDescriptor, RecordsWriteMessage } from '../types/records-types.js'; +import type { RangeCriterion, RecordsFilter, RecordsQueryMessage, RecordsReadMessage, RecordsWriteDescriptor, RecordsWriteMessage } from '../types/records-types.js'; import { Encoder } from './encoder.js'; import { Encryption } from './encryption.js'; @@ -296,7 +296,7 @@ export class Records { * Usage of this property is purely for performance optimization so we don't have to decode the signature payload again. */ public static validateDelegatedGrantReferentialIntegrity( - message: RecordsQueryMessage | RecordsWriteMessage, + message: RecordsReadMessage | RecordsQueryMessage | RecordsWriteMessage, signaturePayload: GenericSignaturePayload | undefined ): void { // `deletedGrantId` in the payload of the message signature and `authorDelegatedGrant` in `authorization` must both exist or be both undefined diff --git a/tests/handlers/records-read.spec.ts b/tests/handlers/records-read.spec.ts index 9e57e8aae..268a6a568 100644 --- a/tests/handlers/records-read.spec.ts +++ b/tests/handlers/records-read.spec.ts @@ -899,7 +899,7 @@ export function testRecordsReadHandler(): void { }); const recordsReadWithoutGrantReply = await dwn.processMessage(alice.did, recordsReadWithoutGrant.message); expect(recordsReadWithoutGrantReply.status.code).to.equal(401); - expect(recordsReadWithoutGrantReply.status.detail).to.contain('no action rule defined for Read'); + expect(recordsReadWithoutGrantReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationActionRulesNotFound); // Bob is able to read the record when he uses the PermissionsGrant const recordsReadWithGrant = await RecordsRead.create({ @@ -963,7 +963,7 @@ export function testRecordsReadHandler(): void { }); const recordsReadWithoutGrantReply = await dwn.processMessage(alice.did, recordsReadWithoutGrant.message); expect(recordsReadWithoutGrantReply.status.code).to.equal(401); - expect(recordsReadWithoutGrantReply.status.detail).to.contain('no action rule defined for Read'); + expect(recordsReadWithoutGrantReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationActionRulesNotFound); // Bob is able to read the record when he uses the PermissionsGrant const recordsReadWithGrant = await RecordsRead.create({ diff --git a/tests/handlers/records-write.spec.ts b/tests/handlers/records-write.spec.ts index e08773395..d88849459 100644 --- a/tests/handlers/records-write.spec.ts +++ b/tests/handlers/records-write.spec.ts @@ -2669,7 +2669,7 @@ export function testRecordsWriteHandler(): void { reply = await dwn.processMessage(alice.did, bobWriteMessageData.message, bobWriteMessageData.dataStream); expect(reply.status.code).to.equal(401); - expect(reply.status.detail).to.contain(`no action rule defined for Write`); + expect(reply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationActionRulesNotFound); }); it('should look up recipient path with ancestor depth of 2+ (excluding self) in action rule correctly', async () => { diff --git a/tests/scenarios/delegated-grant.spec.ts b/tests/scenarios/delegated-grant.spec.ts index 307e73350..d106c6ece 100644 --- a/tests/scenarios/delegated-grant.spec.ts +++ b/tests/scenarios/delegated-grant.spec.ts @@ -19,7 +19,7 @@ import { TestDataGenerator } from '../utils/test-data-generator.js'; import { TestStores } from '../test-stores.js'; import { Time } from '../../src/utils/time.js'; -import { DwnInterfaceName, DwnMethodName, PermissionsGrant, RecordsQuery } from '../../src/index.js'; +import { DwnInterfaceName, DwnMethodName, PermissionsGrant, RecordsQuery, RecordsRead } from '../../src/index.js'; chai.use(chaiAsPromised); @@ -31,7 +31,6 @@ export function testDelegatedGrantScenarios(): void { let eventLog: EventLog; 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 () => { @@ -162,10 +161,7 @@ export function testDelegatedGrantScenarios(): void { expect(fetchedDeviceYWrite.author).to.equal(alice.did); }); - xit('should allow entity invoking a valid delegated grant to read', async () => { - }); - - it('should allow entity invoking a valid delegated grant to query', async () => { + it('should allow entity invoking a valid delegated grant to read or query', async () => { // scenario: // 1. Alice creates a delegated grant for device X, // 2. Bob starts a chat thread with Alice on his DWN @@ -235,7 +231,7 @@ export function testDelegatedGrantScenarios(): void { signer : Jws.createSigner(alice) }); - // sanity verify Bob himself is able to read the chat thread from Bob's DWN + // sanity verify Bob himself is able to query for the chat thread from Bob's DWN const recordsQueryByBob = await TestDataGenerator.generateRecordsQuery({ author : bob, filter : { protocol } @@ -244,7 +240,7 @@ export function testDelegatedGrantScenarios(): void { expect(bobRecordsQueryReply.status.code).to.equal(200); expect(bobRecordsQueryReply.entries?.length).to.equal(3); - // sanity verify Alice herself is able to read the chat message from Bob's DWN + // sanity verify Alice herself is able to query for the chat message from Bob's DWN const recordsQueryByAlice = await RecordsQuery.create({ signer : Jws.createSigner(alice), protocolRole : 'thread/participant', @@ -258,7 +254,7 @@ export function testDelegatedGrantScenarios(): void { expect(aliceRecordsQueryReply.status.code).to.equal(200); expect(aliceRecordsQueryReply.entries?.length).to.equal(1); - // verify device X is able to read the chat message from Bob's DWN + // verify device X is able to query for the chat message from Bob's DWN const recordsQueryByDeviceX = await RecordsQuery.create({ signer : Jws.createSigner(deviceX), delegatedGrant : grantToDeviceX.asDelegatedGrant(), @@ -272,6 +268,19 @@ export function testDelegatedGrantScenarios(): void { const deviceXRecordsQueryReply = await dwn.processMessage(bob.did, recordsQueryByDeviceX.message); expect(deviceXRecordsQueryReply.status.code).to.equal(200); expect(deviceXRecordsQueryReply.entries?.length).to.equal(1); + + // verify device X is able to read the chat message from Bob's DWN + const recordsReadByDeviceX = await RecordsRead.create({ + signer : Jws.createSigner(deviceX), + delegatedGrant : grantToDeviceX.asDelegatedGrant(), + protocolRole : 'thread/participant', + filter : { + recordId: chatRecord.message.recordId + } + }); + const deviceXRecordsReadReply = await dwn.processMessage(bob.did, recordsReadByDeviceX.message); + expect(deviceXRecordsReadReply.status.code).to.equal(200); + expect(deviceXRecordsReadReply.record?.recordId).to.equal(chatRecord.message.recordId); }); xit('should allow entity invoking a valid delegated grant to delete', async () => { @@ -388,10 +397,7 @@ export function testDelegatedGrantScenarios(): void { expect(danielWriteReply.status.detail).to.contain(DwnErrorCode.RecordsValidateIntegrityGrantedToAndSignerMismatch); }); - xit('should fail if invoking a delegated grant that is issued to a different entity to read', async () => { - }); - - it('should fail if invoking a delegated grant that is issued to a different entity to query', async () => { + it('should fail if invoking a delegated grant that is issued to a different entity to read or query', async () => { // scenario: // 1. Alice creates a delegated grant for device X, // 2. Bob starts a chat thread with Alice on his DWN @@ -477,7 +483,7 @@ export function testDelegatedGrantScenarios(): void { expect(deviceXRecordsQueryReply.status.code).to.equal(200); expect(deviceXRecordsQueryReply.entries?.length).to.equal(1); - // Verify that Daniel cannot write a chat message as Alice by invoking the delegated grant granted to Bob + // Verify that Carol cannot query as Alice by invoking the delegated grant granted to Device X const recordsQueryByCarol = await RecordsQuery.create({ signer : Jws.createSigner(carol), delegatedGrant : grantToDeviceX.asDelegatedGrant(), @@ -491,6 +497,19 @@ export function testDelegatedGrantScenarios(): void { const recordsQueryByCarolReply = await dwn.processMessage(bob.did, recordsQueryByCarol.message); expect(recordsQueryByCarolReply.status.code).to.equal(400); expect(recordsQueryByCarolReply.status.detail).to.contain(DwnErrorCode.RecordsValidateIntegrityGrantedToAndSignerMismatch); + + // Verify that Carol cannot read as Alice by invoking the delegated grant granted to Device X + const recordsReadByCarol = await RecordsRead.create({ + signer : Jws.createSigner(carol), + delegatedGrant : grantToDeviceX.asDelegatedGrant(), + protocolRole : 'thread/participant', + filter : { + recordId: chatRecord.message.recordId + } + }); + const recordsReadByCarolReply = await dwn.processMessage(bob.did, recordsReadByCarol.message); + expect(recordsReadByCarolReply.status.code).to.equal(400); + expect(recordsQueryByCarolReply.status.detail).to.contain(DwnErrorCode.RecordsValidateIntegrityGrantedToAndSignerMismatch); }); xit('should fail if invoking a delegated grant that is issued to a different entity to delete', async () => {