From 9f0a1280eb0b0b85794e6c2509aa3ed751511662 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Wed, 11 Sep 2024 13:13:59 -0400 Subject: [PATCH] Allow for Reading public Records/Protocols when in a delegated state (#899) * allow for reading/querying/subscribing to public records without explicit permissions when in a delegated state * vulnerability patch using pnpm audit --fix --- .changeset/slow-pets-vanish.md | 5 + package.json | 9 +- packages/api/src/dwn-api.ts | 118 +++++---- packages/api/src/record.ts | 39 +-- packages/api/tests/dwn-api.spec.ts | 372 +++++++++++++++++++++++++++++ packages/api/tests/record.spec.ts | 82 ++++++- packages/api/tests/web5.spec.ts | 23 +- pnpm-lock.yaml | 78 +++--- 8 files changed, 623 insertions(+), 103 deletions(-) create mode 100644 .changeset/slow-pets-vanish.md diff --git a/.changeset/slow-pets-vanish.md b/.changeset/slow-pets-vanish.md new file mode 100644 index 000000000..c8252277e --- /dev/null +++ b/.changeset/slow-pets-vanish.md @@ -0,0 +1,5 @@ +--- +"@web5/api": patch +--- + +Allow reading public records in delegate state even if no explicit grant exists. diff --git a/package.json b/package.json index 95818471f..d5a5292b5 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,14 @@ "elliptic@>=2.0.0 <=6.5.6": ">=6.5.7", "elliptic@>=5.2.1 <=6.5.6": ">=6.5.7", "micromatch@<4.0.8": ">=4.0.8", - "webpack@<5.94.0": ">=5.94.0" + "webpack@<5.94.0": ">=5.94.0", + "webpack@>=5.0.0-alpha.0 <5.94.0": ">=5.94.0", + "path-to-regexp@>=0.2.0 <8.0.0": ">=8.0.0", + "path-to-regexp@<0.1.10": ">=0.1.10", + "body-parser@<1.20.3": ">=1.20.3", + "send@<0.19.0": ">=0.19.0", + "serve-static@<1.16.0": ">=1.16.0", + "express@<4.20.0": ">=4.20.0" } } } diff --git a/packages/api/src/dwn-api.ts b/packages/api/src/dwn-api.ts index ab695e9a8..b7405958a 100644 --- a/packages/api/src/dwn-api.ts +++ b/packages/api/src/dwn-api.ts @@ -595,20 +595,31 @@ export class DwnApi { }; if (this.delegateDid) { - const { message: delegatedGrant } = await this.permissionsApi.getPermissionForRequest({ - connectedDid : this.connectedDid, - delegateDid : this.delegateDid, - protocol : request.protocol, - delegate : true, - cached : true, - messageType : agentRequest.messageType - }); - - agentRequest.messageParams = { - ...agentRequest.messageParams, - delegatedGrant - }; - agentRequest.granteeDid = this.delegateDid; + // if we don't find a delegated grant, we will attempt to query signing as the delegated DID + // This is to allow the API caller to query public records without needing to impersonate the delegate. + // + // NOTE: When a read-only DwnApi is implemented, callers should use that instead when they don't have an explicit permission. + // This should fail if a permission is not found. + // TODO: https://github.com/TBD54566975/web5-js/issues/898 + try { + const { message: delegatedGrant } = await this.permissionsApi.getPermissionForRequest({ + connectedDid : this.connectedDid, + delegateDid : this.delegateDid, + protocol : request.protocol, + delegate : true, + cached : true, + messageType : agentRequest.messageType + }); + + agentRequest.messageParams = { + ...agentRequest.messageParams, + delegatedGrant + }; + agentRequest.granteeDid = this.delegateDid; + } catch(error:any) { + // set the author of the request to the delegate did + agentRequest.author = this.delegateDid; + } } @@ -674,20 +685,32 @@ export class DwnApi { target : request.from || this.connectedDid }; if (this.delegateDid) { - const { message: delegatedGrant } = await this.permissionsApi.getPermissionForRequest({ - connectedDid : this.connectedDid, - delegateDid : this.delegateDid, - protocol : request.protocol, - delegate : true, - cached : true, - messageType : agentRequest.messageType - }); - - agentRequest.messageParams = { - ...agentRequest.messageParams, - delegatedGrant - }; - agentRequest.granteeDid = this.delegateDid; + // if we don't find a delegated grant, we will attempt to read signing as the delegated DID + // This is to allow the API caller to read public records without needing to impersonate the delegate. + // + // NOTE: When a read-only DwnApi is implemented, callers should use that instead when they don't have an explicit permission. + // This should fail if a permission is not found. + // TODO: https://github.com/TBD54566975/web5-js/issues/898 + + try { + const { message: delegatedGrant } = await this.permissionsApi.getPermissionForRequest({ + connectedDid : this.connectedDid, + delegateDid : this.delegateDid, + protocol : request.protocol, + delegate : true, + cached : true, + messageType : agentRequest.messageType + }); + + agentRequest.messageParams = { + ...agentRequest.messageParams, + delegatedGrant + }; + agentRequest.granteeDid = this.delegateDid; + } catch(_error:any) { + // set the author of the request to the delegate did + agentRequest.author = this.delegateDid; + } } let agentResponse: DwnResponse; @@ -766,20 +789,31 @@ export class DwnApi { }; if (this.delegateDid) { - const { message: delegatedGrant } = await this.permissionsApi.getPermissionForRequest({ - connectedDid : this.connectedDid, - delegateDid : this.delegateDid, - protocol : request.protocol, - delegate : true, - cached : true, - messageType : agentRequest.messageType - }); - - agentRequest.messageParams = { - ...agentRequest.messageParams, - delegatedGrant - }; - agentRequest.granteeDid = this.delegateDid; + // if we don't find a delegated grant, we will attempt to subscribe signing as the delegated DID + // This is to allow the API caller to subscribe to public records without needing to impersonate the delegate. + // + // NOTE: When a read-only DwnApi is implemented, callers should use that instead when they don't have an explicit permission. + // This should fail if a permission is not found. + // TODO: https://github.com/TBD54566975/web5-js/issues/898 + try { + const { message: delegatedGrant } = await this.permissionsApi.getPermissionForRequest({ + connectedDid : this.connectedDid, + delegateDid : this.delegateDid, + protocol : request.protocol, + delegate : true, + cached : true, + messageType : agentRequest.messageType + }); + + agentRequest.messageParams = { + ...agentRequest.messageParams, + delegatedGrant + }; + agentRequest.granteeDid = this.delegateDid; + } catch(_error:any) { + // set the author of the request to the delegate did + agentRequest.author = this.delegateDid; + } }; let agentResponse: DwnResponse; diff --git a/packages/api/src/record.ts b/packages/api/src/record.ts index 30ec77d9b..bb0c51055 100644 --- a/packages/api/src/record.ts +++ b/packages/api/src/record.ts @@ -951,7 +951,7 @@ export class Record implements RecordModel { let requestOptions: ProcessDwnRequest; // Now that we've processed a potential initial write, we can process the current record state. // If the record has been deleted, we need to send a delete request. Otherwise, we send a write request. - if(this.deleted) { + if (this.deleted) { requestOptions = { messageType : DwnInterface.RecordsDelete, rawMessage : this.rawMessage, @@ -1029,21 +1029,32 @@ export class Record implements RecordModel { }; if (this._delegateDid) { - const { message: delegatedGrant } = await this._permissionsApi.getPermissionForRequest({ - connectedDid : this._connectedDid, - delegateDid : this._delegateDid, - protocol : this.protocol, - delegate : true, - cached : true, - messageType : readRequest.messageType - }); + // When reading the data as a delegate, if we don't find a grant we will attempt to read it with the delegate DID as the author. + // This allows users to read publicly available data without needing explicit grants. + // + // NOTE: When a read-only Record class is implemented, callers would have that returned instead when they don't have an explicit permission. + // This should fail if a permission is not found, although it should not happen in practice. + // TODO: https://github.com/TBD54566975/web5-js/issues/898 + try { + const { message: delegatedGrant } = await this._permissionsApi.getPermissionForRequest({ + connectedDid : this._connectedDid, + delegateDid : this._delegateDid, + protocol : this.protocol, + delegate : true, + cached : true, + messageType : readRequest.messageType + }); - readRequest.messageParams = { - ...readRequest.messageParams, - delegatedGrant - }; + readRequest.messageParams = { + ...readRequest.messageParams, + delegatedGrant + }; - readRequest.granteeDid = this._delegateDid; + readRequest.granteeDid = this._delegateDid; + } catch(error) { + // If there is an error fetching the grant, we will attempt to read the data as the delegate. + readRequest.author = this._delegateDid; + } } const agentResponsePromise = isRemote ? diff --git a/packages/api/tests/dwn-api.spec.ts b/packages/api/tests/dwn-api.spec.ts index defad0c1c..16cdc0613 100644 --- a/packages/api/tests/dwn-api.spec.ts +++ b/packages/api/tests/dwn-api.spec.ts @@ -190,6 +190,37 @@ describe('DwnApi', () => { expect(record.rawMessage.authorization.authorDelegatedGrant).to.not.be.undefined; }); + it('should read records with a delegated grant', async () => { + const { status: writeStatus, record } = await dwnAlice.records.create({ + data : 'Hello, world!', + message : { + protocol : notesProtocol.protocol, + protocolPath : 'note', + schema : notesProtocol.types.note.schema, + dataFormat : 'text/plain', + } + }); + + expect(writeStatus.code).to.equal(202); + expect(record).to.not.be.undefined; + const { status: sendStatus } = await record.send(); + expect(sendStatus.code).to.equal(202); + + const { status: readStatus, record: readRecord } = await delegateDwn.records.read({ + from : aliceDid.uri, + protocol : notesProtocol.protocol, + message : { + filter: { + recordId: record.id + } + } + }); + + expect(readStatus.code).to.equal(200); + expect(readRecord).to.exist; + expect(readRecord.id).to.equal; + }); + it('should query records with a delegated grant', async () => { const { status: writeStatus, record } = await delegateDwn.records.create({ data : 'Hello, world!', @@ -304,6 +335,347 @@ describe('DwnApi', () => { expect(deletedRecord.deleted).to.be.true; }); }); + + it('should read records as the delegate DID if no grant is found', async () => { + // alice installs some other protocol + const { status: aliceConfigStatus, protocol: aliceOtherProtocol } = await dwnAlice.protocols.configure({ message: { definition: { + ...notesProtocol, + protocol: `http://other-protocol.xyz/protocol/${TestDataGenerator.randomString(15)}` + }} }); + expect(aliceConfigStatus.code).to.equal(202); + const { status: aliceOtherProtocolSend } = await aliceOtherProtocol.send(aliceDid.uri); + expect(aliceOtherProtocolSend.code).to.equal(202); + + // alice writes a note record to the permissioned protocol + const { status: writeStatus1, record: allowedRecord } = await dwnAlice.records.create({ + data : 'Hello, world!', + message : { + protocol : notesProtocol.protocol, + protocolPath : 'note', + schema : notesProtocol.types.note.schema, + dataFormat : 'text/plain', + } + }); + expect(writeStatus1.code).to.equal(202); + expect(allowedRecord).to.not.be.undefined; + const { status: allowedRecordSendStatus } = await allowedRecord.send(); + expect(allowedRecordSendStatus.code).to.equal(202); + + // alice writes a public and private note to the other protocol + const { status: writeStatus2, record: publicRecord } = await dwnAlice.records.create({ + data : 'Hello, world!', + message : { + published : true, + protocol : aliceOtherProtocol.definition.protocol, + protocolPath : 'note', + schema : aliceOtherProtocol.definition.types.note.schema, + dataFormat : 'text/plain', + } + }); + expect(writeStatus2.code).to.equal(202); + expect(publicRecord).to.not.be.undefined; + const { status: publicRecordSendStatus } = await publicRecord.send(); + expect(publicRecordSendStatus.code).to.equal(202); + + const { status: writeStatus3, record: privateRecord } = await dwnAlice.records.create({ + data : 'Hello, world!', + message : { + protocol : aliceOtherProtocol.definition.protocol, + protocolPath : 'note', + schema : aliceOtherProtocol.definition.types.note.schema, + dataFormat : 'text/plain', + } + }); + expect(writeStatus3.code).to.equal(202); + expect(privateRecord).to.not.be.undefined; + const { status: privateRecordSendStatus } = await privateRecord.send(); + expect(privateRecordSendStatus.code).to.equal(202); + + + // sanity: delegateDwn reads from the allowed record from alice's DWN + const { status: readStatus1, record: allowedRecordReturned } = await delegateDwn.records.read({ + from : aliceDid.uri, + protocol : notesProtocol.protocol, + message : { + filter: { + recordId: allowedRecord.id + } + } + }); + expect(readStatus1.code).to.equal(200); + expect(allowedRecordReturned).to.exist; + expect(allowedRecordReturned.id).to.equal(allowedRecord.id); + + // delegateDwn reads from the other protocol, which no permissions exist + // only the public record is successfully returned + const { status: readStatus2, record: publicRecordReturned } = await delegateDwn.records.read({ + from : aliceDid.uri, + protocol : aliceOtherProtocol.definition.protocol, + message : { + filter: { + recordId: publicRecord.id + } + } + }); + expect(readStatus2.code).to.equal(200); + expect(publicRecordReturned).to.exist; + expect(publicRecordReturned.id).to.equal(publicRecord.id); + + // attempt to read the private record, which should fail + const { status: readStatus3, record: privateRecordReturned } = await delegateDwn.records.read({ + from : aliceDid.uri, + protocol : aliceOtherProtocol.definition.protocol, + message : { + filter: { + recordId: privateRecord.id + } + } + }); + expect(readStatus3.code).to.equal(401); + expect(privateRecordReturned).to.be.undefined; + + // sanity: query as alice to get both records + const { status: readStatus4, record: privateRecordReturnedAlice } = await dwnAlice.records.read({ + from : aliceDid.uri, + protocol : aliceOtherProtocol.definition.protocol, + message : { + filter: { + recordId: privateRecord.id + } + } + }); + expect(readStatus4.code).to.equal(200); + expect(privateRecordReturnedAlice).to.exist; + expect(privateRecordReturnedAlice.id).to.equal(privateRecord.id); + }); + + it('should query records as the delegate DID if no grant is found', async () => { + // alice installs some other protocol + const { status: aliceConfigStatus, protocol: aliceOtherProtocol } = await dwnAlice.protocols.configure({ message: { definition: { + ...notesProtocol, + protocol: `http://other-protocol.xyz/protocol/${TestDataGenerator.randomString(15)}` + }} }); + expect(aliceConfigStatus.code).to.equal(202); + const { status: aliceOtherProtocolSend } = await aliceOtherProtocol.send(aliceDid.uri); + expect(aliceOtherProtocolSend.code).to.equal(202); + + // alice writes a note record to the permissioned protocol + const { status: writeStatus1, record: allowedRecord } = await dwnAlice.records.create({ + data : 'Hello, world!', + message : { + protocol : notesProtocol.protocol, + protocolPath : 'note', + schema : notesProtocol.types.note.schema, + dataFormat : 'text/plain', + } + }); + expect(writeStatus1.code).to.equal(202); + expect(allowedRecord).to.not.be.undefined; + const { status: allowedRecordSendStatus } = await allowedRecord.send(); + expect(allowedRecordSendStatus.code).to.equal(202); + + // alice writes a public and private note to the other protocol + const { status: writeStatus2, record: publicRecord } = await dwnAlice.records.create({ + data : 'Hello, world!', + message : { + published : true, + protocol : aliceOtherProtocol.definition.protocol, + protocolPath : 'note', + schema : aliceOtherProtocol.definition.types.note.schema, + dataFormat : 'text/plain', + } + }); + expect(writeStatus2.code).to.equal(202); + expect(publicRecord).to.not.be.undefined; + const { status: publicRecordSendStatus } = await publicRecord.send(); + expect(publicRecordSendStatus.code).to.equal(202); + + const { status: writeStatus3, record: privateRecord } = await dwnAlice.records.create({ + data : 'Hello, world!', + message : { + protocol : aliceOtherProtocol.definition.protocol, + protocolPath : 'note', + schema : aliceOtherProtocol.definition.types.note.schema, + dataFormat : 'text/plain', + } + }); + expect(writeStatus3.code).to.equal(202); + expect(privateRecord).to.not.be.undefined; + const { status: privateRecordSendStatus } = await privateRecord.send(); + expect(privateRecordSendStatus.code).to.equal(202); + + + // sanity: delegateDwn queries for the allowed record from alice's DWN + const { status: queryStatus1, records: allowedRecords } = await delegateDwn.records.query({ + from : aliceDid.uri, + protocol : notesProtocol.protocol, + message : { + filter: { + protocol: notesProtocol.protocol + } + } + }); + expect(queryStatus1.code).to.equal(200); + expect(allowedRecords).to.exist; + expect(allowedRecords).to.have.lengthOf(1); + + // delegateDwn queries for the other protocol, which no permissions exist + // only the public record is returned + const { status: queryStatus2, records: publicRecords } = await delegateDwn.records.query({ + from : aliceDid.uri, + protocol : aliceOtherProtocol.definition.protocol, + message : { + filter: { + protocol: aliceOtherProtocol.definition.protocol + } + } + }); + expect(queryStatus2.code).to.equal(200); + expect(publicRecords).to.exist; + expect(publicRecords).to.have.lengthOf(1); + expect(publicRecords![0].id).to.equal(publicRecord.id); + + // sanity: query as alice to get both records + const { status: queryStatus3, records: allRecords } = await dwnAlice.records.query({ + from : aliceDid.uri, + protocol : aliceOtherProtocol.definition.protocol, + message : { + filter: { + protocol: aliceOtherProtocol.definition.protocol + } + } + }); + expect(queryStatus3.code).to.equal(200); + expect(allRecords).to.exist; + expect(allRecords).to.have.lengthOf(2); + expect(allRecords.map(r => r.id)).to.have.members([publicRecord.id, privateRecord.id]); + }); + + it('should subscribe to records as the delegate DID if no grant is found', async () => { + // alice installs some other protocol + const { status: aliceConfigStatus, protocol: aliceOtherProtocol } = await dwnAlice.protocols.configure({ message: { definition: { + ...notesProtocol, + protocol: `http://other-protocol.xyz/protocol/${TestDataGenerator.randomString(15)}` + }} }); + expect(aliceConfigStatus.code).to.equal(202); + const { status: aliceOtherProtocolSend } = await aliceOtherProtocol.send(aliceDid.uri); + expect(aliceOtherProtocolSend.code).to.equal(202); + + // delegatedDwn subscribes to both protocols + const permissionedNotesRecords: Map = new Map(); + const permissionedNotesSubscriptionHandler = async (record: Record) => { + permissionedNotesRecords.set(record.id, record); + }; + const permissionedNotesSubscribeResult = await delegateDwn.records.subscribe({ + from : aliceDid.uri, + protocol : notesProtocol.protocol, + message : { + filter: { + protocol: notesProtocol.protocol + } + }, + subscriptionHandler: permissionedNotesSubscriptionHandler + }); + expect(permissionedNotesSubscribeResult.status.code).to.equal(200); + + const otherProtocolRecords: Map = new Map(); + const otherProtocolSubscriptionHandler = async (record: Record) => { + otherProtocolRecords.set(record.id, record); + }; + const otherProtocolSubscribeResult = await delegateDwn.records.subscribe({ + from : aliceDid.uri, + protocol : aliceOtherProtocol.definition.protocol, + message : { + filter: { + protocol: aliceOtherProtocol.definition.protocol + } + }, + subscriptionHandler: otherProtocolSubscriptionHandler + }); + expect(otherProtocolSubscribeResult.status.code).to.equal(200); + + // alice subscribes to the other protocol as a sanity + const aliceOtherProtocolRecords: Map = new Map(); + const aliceOtherProtocolSubscriptionHandler = async (record: Record) => { + aliceOtherProtocolRecords.set(record.id, record); + }; + const aliceOtherProtocolSubscribeResult = await dwnAlice.records.subscribe({ + from : aliceDid.uri, + protocol : aliceOtherProtocol.definition.protocol, + message : { + filter: { + protocol: aliceOtherProtocol.definition.protocol + } + }, + subscriptionHandler: aliceOtherProtocolSubscriptionHandler + }); + expect(aliceOtherProtocolSubscribeResult.status.code).to.equal(200); + + // NOTE: write the private record before the public so that it should be received first + // alice writes a public and private note to the other protocol + const { status: writeStatus2, record: publicRecord } = await dwnAlice.records.create({ + data : 'Hello, world!', + message : { + published : true, + protocol : aliceOtherProtocol.definition.protocol, + protocolPath : 'note', + schema : aliceOtherProtocol.definition.types.note.schema, + dataFormat : 'text/plain', + } + }); + expect(writeStatus2.code).to.equal(202); + expect(publicRecord).to.not.be.undefined; + const { status: publicRecordSendStatus } = await publicRecord.send(); + expect(publicRecordSendStatus.code).to.equal(202); + + // alice writes a note record to the permissioned protocol + const { status: writeStatus1, record: allowedRecord } = await dwnAlice.records.create({ + data : 'Hello, world!', + message : { + protocol : notesProtocol.protocol, + protocolPath : 'note', + schema : notesProtocol.types.note.schema, + dataFormat : 'text/plain', + } + }); + expect(writeStatus1.code).to.equal(202); + expect(allowedRecord).to.not.be.undefined; + const { status: allowedRecordSendStatus } = await allowedRecord.send(); + expect(allowedRecordSendStatus.code).to.equal(202); + + const { status: writeStatus3, record: privateRecord } = await dwnAlice.records.create({ + data : 'Hello, world!', + message : { + protocol : aliceOtherProtocol.definition.protocol, + protocolPath : 'note', + schema : aliceOtherProtocol.definition.types.note.schema, + dataFormat : 'text/plain', + } + }); + expect(writeStatus3.code).to.equal(202); + expect(privateRecord).to.not.be.undefined; + const { status: privateRecordSendStatus } = await privateRecord.send(); + expect(privateRecordSendStatus.code).to.equal(202); + + // wait for the records to be received + // alice receives both the public and private records on her subscription + await Poller.pollUntilSuccessOrTimeout(async () => { + expect(aliceOtherProtocolRecords.size).to.equal(2); + expect(aliceOtherProtocolRecords.get(publicRecord.id)).to.exist; + expect(aliceOtherProtocolRecords.get(privateRecord.id)).to.exist; + }); + + // delegated agent only receives the public record from the other protocol + await Poller.pollUntilSuccessOrTimeout(async () => { + // permissionedNotesRecords should have the allowedRecord + expect(permissionedNotesRecords.size).to.equal(1); + expect(permissionedNotesRecords.get(allowedRecord.id)).to.exist; + + // otherProtocolRecords should have only the publicRecord + expect(otherProtocolRecords.size).to.equal(1); + expect(otherProtocolRecords.get(publicRecord.id)).to.exist; + }); + }); }); describe('protocols.configure()', () => { diff --git a/packages/api/tests/record.spec.ts b/packages/api/tests/record.spec.ts index c57829f50..68f4388ee 100644 --- a/packages/api/tests/record.spec.ts +++ b/packages/api/tests/record.spec.ts @@ -6,7 +6,7 @@ import { expect } from 'chai'; import { NodeStream } from '@web5/common'; import { utils as didUtils } from '@web5/dids'; import { Web5UserAgent } from '@web5/user-agent'; -import { DwnConstant, DwnDateSort, DwnEncryptionAlgorithm, DwnInterface, DwnKeyDerivationScheme, dwnMessageConstructors, Oidc, PlatformAgentTestHarness, WalletConnect } from '@web5/agent'; +import { DwnConstant, DwnDateSort, DwnEncryptionAlgorithm, DwnInterface, DwnKeyDerivationScheme, dwnMessageConstructors, getRecordAuthor, Oidc, PlatformAgentTestHarness, WalletConnect } from '@web5/agent'; import { Record } from '../src/record.js'; import { DwnApi } from '../src/dwn-api.js'; import { dataToBlob } from '../src/utils.js'; @@ -491,6 +491,86 @@ describe('Record', () => { const dataBytes = await queriedRecord.data.bytes(); expect(dataBytes).to.deep.equal(largeDataBytes, 'bytes'); }); + + it('should read large data payloads as a stream with from a public record without an explicit grant', async () => { + // install some other protocol that the delegated did does not have a grant for + // alice installs some other protocol + const { status: aliceConfigStatus, protocol: aliceOtherProtocol } = await dwnAlice.protocols.configure({ message: { definition: { + ...notesProtocol, + protocol: `http://other-protocol.xyz/protocol/${TestDataGenerator.randomString(15)}` + }} }); + expect(aliceConfigStatus.code).to.equal(202); + const { status: aliceOtherProtocolSend } = await aliceOtherProtocol.send(aliceDid.uri); + expect(aliceOtherProtocolSend.code).to.equal(202); + + // alice writes a private and public note with a large data payload + const largeDataJson1 = TestDataGenerator.randomJson(DwnConstant.maxDataSizeAllowedToBeEncoded + 1000); + + const { status: aliceWritesStatus, record: aliceRecord } = await dwnAlice.records.write({ + data : largeDataJson1, + message : { + protocol : aliceOtherProtocol.definition.protocol, + protocolPath : 'note', + schema : aliceOtherProtocol.definition.types.note.schema, + dataFormat : 'application/json', + } + }); + expect(aliceWritesStatus.code).to.equal(202); + const { status: aliceSendStatus } = await aliceRecord!.send(); + expect(aliceSendStatus.code).to.equal(202); + + const largeDataJson2 = TestDataGenerator.randomJson(DwnConstant.maxDataSizeAllowedToBeEncoded + 1000); + const publicRecordDataBytes = new TextEncoder().encode(JSON.stringify(largeDataJson2)); + + const { status: aliceWritesStatus2, record: alicePublicRecord } = await dwnAlice.records.write({ + data : largeDataJson2, + message : { + published : true, + protocol : aliceOtherProtocol.definition.protocol, + protocolPath : 'note', + schema : aliceOtherProtocol.definition.types.note.schema, + dataFormat : 'application/json', + } + }); + expect(aliceWritesStatus2.code).to.equal(202); + const { status: aliceSendStatus2 } = await alicePublicRecord!.send(); + expect(aliceSendStatus2.code).to.equal(202); + + // the delegate attempts to read the public note + const { records: publicRecords, status: publicStatus } = await delegateDwn.records.query({ + from : aliceDid.uri, + protocol : aliceOtherProtocol.definition.protocol, + message : { + filter: { + protocol : aliceOtherProtocol.definition.protocol, + protocolPath : 'note', + } + } + }); + expect(publicStatus.code).to.equal(200); + expect(publicRecords.length).to.equal(1); + const publicRecord = publicRecords[0]; + expect(publicRecord.author).to.equal(aliceDid.uri); + const publicDataBytes = await publicRecord.data.bytes(); + expect(publicDataBytes).to.deep.equal(publicRecordDataBytes); + + // sanity, this won't happen in real-world, but testing the results if a read is attempted on an unaauthed record + const privateRecordOptions = { + author : getRecordAuthor(aliceRecord!.rawMessage), + connectedDid : aliceDid.uri, + remoteOrigin : aliceDid.uri, + delegateDid : delegateDid.uri, + ...aliceRecord!.rawMessage, + }; + + const record = new Record(delegateHarness.agent, privateRecordOptions); + try { + await record.data.bytes(); + expect.fail('Expected unauthorized data read to fail.'); + } catch(error:any) { + expect(error.message).to.include('Error encountered while attempting to read data:'); + } + }); }); it('imports a record that another user wrote', async () => { diff --git a/packages/api/tests/web5.spec.ts b/packages/api/tests/web5.spec.ts index da4a4d094..be0016963 100644 --- a/packages/api/tests/web5.spec.ts +++ b/packages/api/tests/web5.spec.ts @@ -496,19 +496,16 @@ describe('web5 api', () => { const readSigner = Jws.getSignerDid(readResult.record.authorization.signature.signatures[0]); expect(readSigner).to.equal(delegateDid); - // attempt to query or delete, should fail because we did not grant query permissions - try { - await web5.dwn.records.query({ - protocol : protocol.protocol, - message : { - filter: { protocol: protocol.protocol } - } - }); - - expect.fail('Should have thrown an error'); - } catch(error:any) { - expect(error.message).to.include('CachedPermissions: No permissions found for RecordsQuery'); - } + // Because no grants exist for query, it will not fail but instead author AND sign as the delegate DID. + // It will only return results if they are public, here it will return none. This is tested elsewhere. + const noPermissionQuery = await web5.dwn.records.query({ + protocol : protocol.protocol, + message : { + filter: { protocol: protocol.protocol } + } + }); + expect(noPermissionQuery.status.code).to.equal(200); + expect(noPermissionQuery.records).to.have.lengthOf(0); try { await web5.dwn.records.delete({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 83e2d7019..860e902e7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,13 @@ overrides: elliptic@>=5.2.1 <=6.5.6: '>=6.5.7' micromatch@<4.0.8: '>=4.0.8' webpack@<5.94.0: '>=5.94.0' + webpack@>=5.0.0-alpha.0 <5.94.0: '>=5.94.0' + path-to-regexp@>=0.2.0 <8.0.0: '>=8.0.0' + path-to-regexp@<0.1.10: '>=0.1.10' + body-parser@<1.20.3: '>=1.20.3' + send@<0.19.0: '>=0.19.0' + serve-static@<1.16.0: '>=1.16.0' + express@<4.20.0: '>=4.20.0' importers: @@ -2794,8 +2801,8 @@ packages: bn.js@5.2.1: resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==} - body-parser@1.20.2: - resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} bowser@2.11.0: @@ -3326,6 +3333,10 @@ packages: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} @@ -3521,8 +3532,8 @@ packages: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} - express@4.19.2: - resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==} + express@4.20.0: + resolution: {integrity: sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==} engines: {node: '>= 0.10.0'} extendable-error@0.1.7: @@ -4396,8 +4407,8 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} - merge-descriptors@1.0.1: - resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -4835,11 +4846,12 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-to-regexp@0.1.7: - resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + path-to-regexp@0.1.10: + resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==} - path-to-regexp@6.2.2: - resolution: {integrity: sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==} + path-to-regexp@8.1.0: + resolution: {integrity: sha512-Bqn3vc8CMHty6zuD+tG23s6v2kwxslHEhTj4eYaVKGIEB+YX/2wd0/rgXLFD9G9id9KCtbVy/3ZgmvZjpa0UdQ==} + engines: {node: '>=16'} path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} @@ -5222,8 +5234,8 @@ packages: engines: {node: '>=10'} hasBin: true - send@0.18.0: - resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} seq-queue@0.0.5: @@ -5235,8 +5247,8 @@ packages: serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - serve-static@1.15.0: - resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} + serve-static@1.16.0: + resolution: {integrity: sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==} engines: {node: '>= 0.8.0'} set-function-length@1.2.2: @@ -8349,10 +8361,10 @@ snapshots: '@tbd54566975/dwn-sql-store': 0.6.6 '@web5/crypto': 1.0.3 better-sqlite3: 8.7.0 - body-parser: 1.20.2 + body-parser: 1.20.3 bytes: 3.1.2 cors: 2.8.5 - express: 4.19.2 + express: 4.20.0 kysely: 0.26.3 loglevel: 1.9.1 loglevel-plugin-prefix: 0.8.4 @@ -8713,7 +8725,7 @@ snapshots: bn.js@5.2.1: {} - body-parser@1.20.2: + body-parser@1.20.3: dependencies: bytes: 3.1.2 content-type: 1.0.5 @@ -8723,7 +8735,7 @@ snapshots: http-errors: 2.0.0 iconv-lite: 0.4.24 on-finished: 2.4.1 - qs: 6.11.0 + qs: 6.13.0 raw-body: 2.5.2 type-is: 1.6.18 unpipe: 1.0.0 @@ -9319,6 +9331,8 @@ snapshots: encodeurl@1.0.2: {} + encodeurl@2.0.0: {} + end-of-stream@1.4.4: dependencies: once: 1.4.0 @@ -9745,34 +9759,34 @@ snapshots: expand-template@2.0.3: {} - express@4.19.2: + express@4.20.0: dependencies: accepts: 1.3.8 array-flatten: 1.1.1 - body-parser: 1.20.2 + body-parser: 1.20.3 content-disposition: 0.5.4 content-type: 1.0.5 cookie: 0.6.0 cookie-signature: 1.0.6 debug: 2.6.9 depd: 2.0.0 - encodeurl: 1.0.2 + encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 finalhandler: 1.2.0 fresh: 0.5.2 http-errors: 2.0.0 - merge-descriptors: 1.0.1 + merge-descriptors: 1.0.3 methods: 1.1.2 on-finished: 2.4.1 parseurl: 1.3.3 - path-to-regexp: 0.1.7 + path-to-regexp: 0.1.10 proxy-addr: 2.0.7 qs: 6.11.0 range-parser: 1.2.1 safe-buffer: 5.2.1 - send: 0.18.0 - serve-static: 1.15.0 + send: 0.19.0 + serve-static: 1.16.0 setprototypeof: 1.2.0 statuses: 2.0.1 type-is: 1.6.18 @@ -10733,7 +10747,7 @@ snapshots: media-typer@0.3.0: {} - merge-descriptors@1.0.1: {} + merge-descriptors@1.0.3: {} merge-stream@2.0.0: {} @@ -10974,7 +10988,7 @@ snapshots: '@sinonjs/fake-timers': 11.2.2 '@sinonjs/text-encoding': 0.7.2 just-extend: 6.2.0 - path-to-regexp: 6.2.2 + path-to-regexp: 8.1.0 node-abi@3.65.0: dependencies: @@ -11233,9 +11247,9 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - path-to-regexp@0.1.7: {} + path-to-regexp@0.1.10: {} - path-to-regexp@6.2.2: {} + path-to-regexp@8.1.0: {} path-type@4.0.0: {} @@ -11665,7 +11679,7 @@ snapshots: semver@7.6.3: {} - send@0.18.0: + send@0.19.0: dependencies: debug: 2.6.9 depd: 2.0.0 @@ -11693,12 +11707,12 @@ snapshots: dependencies: randombytes: 2.1.0 - serve-static@1.15.0: + serve-static@1.16.0: dependencies: encodeurl: 1.0.2 escape-html: 1.0.3 parseurl: 1.3.3 - send: 0.18.0 + send: 0.19.0 transitivePeerDependencies: - supports-color