diff --git a/README.md b/README.md index 506cd68ba..48eac21a6 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # Decentralized Web Node (DWN) SDK Code Coverage -![Statements](https://img.shields.io/badge/statements-97.77%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-95.03%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-94.28%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-97.77%25-brightgreen.svg?style=flat) +![Statements](https://img.shields.io/badge/statements-97.77%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-95.04%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-94.28%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-97.77%25-brightgreen.svg?style=flat) - [Introduction](#introduction) - [Installation](#installation) diff --git a/json-schemas/authorization-payloads/base-authorization-payload.json b/json-schemas/authorization-payloads/base-authorization-payload.json index 367645803..8589d285c 100644 --- a/json-schemas/authorization-payloads/base-authorization-payload.json +++ b/json-schemas/authorization-payloads/base-authorization-payload.json @@ -12,6 +12,10 @@ }, "permissionsGrantId": { "type": "string" + }, + "protocolRole": { + "$comment": "Used in the Records interface to authorize role-authorized actions for protocol records", + "type": "string" } } } \ No newline at end of file diff --git a/json-schemas/interface-methods/records-read.json b/json-schemas/interface-methods/records-read.json index f682eba9a..aacf0e87c 100644 --- a/json-schemas/interface-methods/records-read.json +++ b/json-schemas/interface-methods/records-read.json @@ -37,9 +37,6 @@ }, "filter": { "$ref": "https://identity.foundation/dwn/json-schemas/records-filter.json" - }, - "protocolRole": { - "type": "string" } } } diff --git a/src/core/message.ts b/src/core/message.ts index c070fa0ab..6eb075dcb 100644 --- a/src/core/message.ts +++ b/src/core/message.ts @@ -139,11 +139,11 @@ export abstract class Message { public static async signAsAuthorization( descriptor: Descriptor, signatureInput: Signer, - permissionsGrantId?: string, + additionalPayloadProperties?: { permissionsGrantId?: string, protocolRole?: string } ): Promise { const descriptorCid = await Cid.computeCid(descriptor); - const authPayload: BaseAuthorizationPayload = { descriptorCid, permissionsGrantId }; + const authPayload: BaseAuthorizationPayload = { descriptorCid, ...additionalPayloadProperties }; removeUndefinedProperties(authPayload); const authPayloadStr = JSON.stringify(authPayload); const authPayloadBytes = new TextEncoder().encode(authPayloadStr); diff --git a/src/core/protocol-authorization.ts b/src/core/protocol-authorization.ts index 2a48ebf9d..a548253cc 100644 --- a/src/core/protocol-authorization.ts +++ b/src/core/protocol-authorization.ts @@ -276,20 +276,13 @@ export class ProtocolAuthorization { return; } - const protocolRole = (incomingMessage as RecordsRead).message.descriptor.protocolRole; + const protocolRole = (incomingMessage as RecordsRead).authorizationPayload?.protocolRole; // Only verify role if there is a role being invoked if (protocolRole === undefined) { return; } - if (incomingMessage.author === undefined) { - throw new DwnError( - DwnErrorCode.ProtocolAuthorizationRoleInvokedByAuthorless, - 'Authorless messages may not invoke a protocolRole' - ); - } - const roleRuleSet = ProtocolAuthorization.getRuleSetAtProtocolPath(protocolRole, protocolDefinition); if (roleRuleSet?.$globalRole === undefined) { throw new DwnError( @@ -356,7 +349,7 @@ export class ProtocolAuthorization { // Get role being invoked. Currently only Reads support role-based authorization let invokedRole: string | undefined; if (incomingMessage.message.descriptor.method === DwnMethodName.Read) { - invokedRole = incomingMessage.message.descriptor.protocolRole; + invokedRole = (incomingMessage as RecordsRead).authorizationPayload?.protocolRole; } for (const actionRule of actionRules) { diff --git a/src/interfaces/protocols-configure.ts b/src/interfaces/protocols-configure.ts index 9e17703dc..fa6e195bd 100644 --- a/src/interfaces/protocols-configure.ts +++ b/src/interfaces/protocols-configure.ts @@ -34,7 +34,11 @@ export class ProtocolsConfigure extends Message { definition : ProtocolsConfigure.normalizeDefinition(options.definition) }; - const authorization = await Message.signAsAuthorization(descriptor, options.authorizationSigner, options.permissionsGrantId); + const authorization = await Message.signAsAuthorization( + descriptor, + options.authorizationSigner, + { permissionsGrantId: options.permissionsGrantId } + ); const message = { descriptor, authorization }; Message.validateJsonSchema(message); diff --git a/src/interfaces/protocols-query.ts b/src/interfaces/protocols-query.ts index 3c8127139..fc7029aec 100644 --- a/src/interfaces/protocols-query.ts +++ b/src/interfaces/protocols-query.ts @@ -48,7 +48,11 @@ export class ProtocolsQuery extends Message { // only generate the `authorization` property if signature input is given let authorization: GeneralJws | undefined; if (options.authorizationSigner !== undefined) { - authorization = await Message.signAsAuthorization(descriptor, options.authorizationSigner, options.permissionsGrantId); + authorization = await Message.signAsAuthorization( + descriptor, + options.authorizationSigner, + { permissionsGrantId: options.permissionsGrantId } + ); } const message = { descriptor, authorization }; diff --git a/src/interfaces/records-read.ts b/src/interfaces/records-read.ts index 144afebf1..26bfbcc20 100644 --- a/src/interfaces/records-read.ts +++ b/src/interfaces/records-read.ts @@ -44,7 +44,7 @@ export class RecordsRead extends Message { * @throws {DwnError} when a combination of required RecordsReadOptions are missing */ public static async create(options: RecordsReadOptions): Promise { - const { filter, authorizationSigner, permissionsGrantId } = options; + const { filter, authorizationSigner, permissionsGrantId, protocolRole } = options; const currentTime = getCurrentTimeInHighPrecision(); const descriptor: RecordsReadDescriptor = { @@ -52,7 +52,6 @@ export class RecordsRead extends Message { method : DwnMethodName.Read, filter : Records.normalizeFilter(filter), messageTimestamp : options.date ?? currentTime, - protocolRole : options.protocolRole, }; removeUndefinedProperties(descriptor); @@ -60,7 +59,7 @@ export class RecordsRead extends Message { // only generate the `authorization` property if signature input is given let authorization = undefined; if (authorizationSigner !== undefined) { - authorization = await Message.signAsAuthorization(descriptor, authorizationSigner, permissionsGrantId); + authorization = await Message.signAsAuthorization(descriptor, authorizationSigner, { permissionsGrantId, protocolRole }); } const message: RecordsReadMessage = { descriptor, authorization }; diff --git a/src/interfaces/records-write.ts b/src/interfaces/records-write.ts index 285a7bce0..18450c0c7 100644 --- a/src/interfaces/records-write.ts +++ b/src/interfaces/records-write.ts @@ -665,6 +665,7 @@ export class RecordsWrite { if (encryptionCid !== undefined) { authorizationPayload.encryptionCid = encryptionCid; } // assign `encryptionCid` only if it is defined if (permissionsGrantId !== undefined) { authorizationPayload.permissionsGrantId = permissionsGrantId; } + const authorizationPayloadBytes = Encoder.objectToBytes(authorizationPayload); const builder = await GeneralJwsBuilder.create(authorizationPayloadBytes, [signer]); diff --git a/src/types/message-types.ts b/src/types/message-types.ts index 37feb95f9..5341e35af 100644 --- a/src/types/message-types.ts +++ b/src/types/message-types.ts @@ -14,6 +14,10 @@ export type GenericMessage = { export type BaseAuthorizationPayload = { descriptorCid: string; permissionsGrantId?: string; + /** + * Used in the Records interface to authorize role-authorized actions for protocol records. + */ + protocolRole?: string; }; /** diff --git a/src/types/records-types.ts b/src/types/records-types.ts index 95a8b2474..e64b14c04 100644 --- a/src/types/records-types.ts +++ b/src/types/records-types.ts @@ -161,7 +161,6 @@ export type RecordsReadDescriptor = { method: DwnMethodName.Read; filter: RecordsFilter; messageTimestamp: string; - protocolRole?: string; }; export type RecordsDeleteMessage = GenericMessage & { diff --git a/tests/handlers/records-read.spec.ts b/tests/handlers/records-read.spec.ts index 5cda223d0..60dc3d90b 100644 --- a/tests/handlers/records-read.spec.ts +++ b/tests/handlers/records-read.spec.ts @@ -524,45 +524,6 @@ export function testRecordsReadHandler(): void { expect(chatReadReply.status.code).to.equal(200); }); - it('rejects role-authorized reads if the message has no author', async () => { - // scenario: Alice writes a chat message writes a chat message. An anonymous message tries and fails - // to invoke a role to read it. - - const alice = await DidKeyResolver.generate(); - - const protocolDefinition = friendRoleProtocolDefinition; - - const protocolsConfig = await TestDataGenerator.generateProtocolsConfigure({ - author: alice, - protocolDefinition - }); - const protocolWriteReply = await dwn.processMessage(alice.did, protocolsConfig.message); - expect(protocolWriteReply.status.code).to.equal(202); - - // Alice writes a 'chat' record - const chatRecord = await TestDataGenerator.generateRecordsWrite({ - author : alice, - recipient : alice.did, - protocol : protocolDefinition.protocol, - protocolPath : 'chat', - data : new TextEncoder().encode('Bob can read this cuz he is my friend'), - }); - const chatReply = await dwn.processMessage(alice.did, chatRecord.message, chatRecord.dataStream); - expect(chatReply.status.code).to.equal(202); - - // An anonymous message reads Alice's chat record - const readChatRecord = await RecordsRead.create({ - // authorizationSigner intentionally omitted - filter: { - recordId: chatRecord.message.recordId, - }, - protocolRole: 'friend' - }); - const chatReadReply = await dwn.processMessage(alice.did, readChatRecord.message); - expect(chatReadReply.status.code).to.equal(401); - expect(chatReadReply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationRoleInvokedByAuthorless); - }); - it('rejects role-authorized reads if the protocolRole is not a valid protocol path to a role record', async () => { // scenario: Alice writes a chat message writes a chat message. Bob tries to invoke the 'chat' role, // but 'chat' is not a role.