diff --git a/packages/api/src/dwn-api.ts b/packages/api/src/dwn-api.ts index 9ba19d57c..37988ca70 100644 --- a/packages/api/src/dwn-api.ts +++ b/packages/api/src/dwn-api.ts @@ -788,7 +788,6 @@ export class DwnApi { * payload must be read again (e.g., if the data stream is consumed). */ remoteOrigin : request.from, - protocolRole : request.message.protocolRole, delegateDid : this.delegateDid, data : entry.data, initialWrite : entry.initialWrite, @@ -831,7 +830,6 @@ export class DwnApi { connectedDid : this.connectedDid, delegateDid : this.delegateDid, permissionsApi : this.permissionsApi, - protocolRole : request.message.protocolRole, request }) }; diff --git a/packages/api/src/record.ts b/packages/api/src/record.ts index 306f53656..f0a67e376 100644 --- a/packages/api/src/record.ts +++ b/packages/api/src/record.ts @@ -183,6 +183,9 @@ export type RecordDeleteParams = { /** The timestamp indicating when the record was deleted. */ dateModified?: DwnMessageDescriptor[DwnInterface.RecordsDelete]['messageTimestamp']; + + /** The protocol role under which this record will be deleted. */ + protocolRole?: RecordOptions['protocolRole']; }; /** @@ -353,12 +356,14 @@ export class Record implements RecordModel { descriptor : this._descriptor, attestation : this._attestation, authorization : this._authorization, + protocolRole : this._protocolRole, encryption : this._encryption, })); } else { message = JSON.parse(JSON.stringify({ descriptor : this._descriptor, authorization : this._authorization, + protocolRole : this._protocolRole, })); } diff --git a/packages/api/tests/dwn-api.spec.ts b/packages/api/tests/dwn-api.spec.ts index 7f332c000..07e73b692 100644 --- a/packages/api/tests/dwn-api.spec.ts +++ b/packages/api/tests/dwn-api.spec.ts @@ -9,7 +9,7 @@ import { DwnApi } from '../src/dwn-api.js'; import { testDwnUrl } from './utils/test-config.js'; import emailProtocolDefinition from './fixtures/protocol-definitions/email.json' assert { type: 'json' }; import photosProtocolDefinition from './fixtures/protocol-definitions/photos.json' assert { type: 'json' }; -import { DwnInterfaceName, DwnMethodName, Jws, PermissionsProtocol, Poller, Time } from '@tbd54566975/dwn-sdk-js'; +import { DwnConstant, DwnInterfaceName, DwnMethodName, Jws, PermissionsProtocol, Poller, Time } from '@tbd54566975/dwn-sdk-js'; import { PermissionGrant } from '../src/permission-grant.js'; import { Record } from '../src/record.js'; import { TestDataGenerator } from './utils/test-data-generator.js'; @@ -1055,6 +1055,129 @@ describe('DwnApi', () => { expect(await result.record?.data.json()).to.deep.equal(dataJson); }); + it.only('ensure that a protocolRole used to query is also used to read the data of the result', async () => { + // Scenario: Bob installs a photo protocol and creates an album. They grant a role to Alice as a participant of the album. + // Bob then adds 3 photos to the album. Alice uses the role to add a photo to the album. + // NOTE: We are purposfully making these photos larger than the max allowed size to the creation of a RecordsRead to read the large data. + // Alice then queries the album and reads the photos using the protocolRole. + // Alice then is able to read the data of each of the records. + + // Configure the photos protocol on Alice and Bob's local and remote DWNs. + const { status: bobProtocolStatus, protocol: bobProtocol } = await dwnBob.protocols.configure({ + message: { + definition: photosProtocolDefinition + } + }); + expect(bobProtocolStatus.code).to.equal(202); + const { status: bobRemoteProtocolStatus } = await bobProtocol.send(bobDid.uri); + expect(bobRemoteProtocolStatus.code).to.equal(202); + + // Bob creates an album + const { status: albumCreateStatus, record: albumRecord } = await dwnBob.records.create({ + data: 'My Album', + message: { + protocol : photosProtocolDefinition.protocol, + protocolPath : 'album', + schema : photosProtocolDefinition.types.album.schema, + dataFormat : 'text/plain' + } + }); + expect(albumCreateStatus.code).to.equal(202); + const { status: albumSendStatus } = await albumRecord.send(); + expect(albumSendStatus.code).to.equal(202); + + // Bob makes Alice a `participant` and sends the record to her and his own remote node. + const { status: participantCreateStatus, record: participantRecord} = await dwnBob.records.create({ + data : 'test', + message : { + parentContextId : albumRecord.contextId, + recipient : aliceDid.uri, + protocol : photosProtocolDefinition.protocol, + protocolPath : 'album/participant', + schema : photosProtocolDefinition.types.participant.schema, + dataFormat : 'text/plain' + } + }); + expect(participantCreateStatus.code).to.equal(202); + const { status: bobParticipantSendStatus } = await participantRecord.send(bobDid.uri); + expect(bobParticipantSendStatus.code).to.equal(202); + + // bob adds 3 photos to the album + for (let i = 0; i < 3; i++) { + const { status: photoCreateStatus, record: photoRecord } = await dwnBob.records.create({ + data: TestDataGenerator.randomString(DwnConstant.maxDataSizeAllowedToBeEncoded + 1), + message: { + parentContextId : albumRecord.contextId, + protocol : photosProtocolDefinition.protocol, + protocolPath : 'album/photo', + schema : photosProtocolDefinition.types.photo.schema, + dataFormat : 'text/plain', + } + }); + expect(photoCreateStatus.code).to.equal(202); + const { status: photoSendStatus } = await photoRecord.send(); + expect(photoSendStatus.code).to.equal(202); + } + + // alice uses the role to add a photo to the album + const { status: photoCreateStatusAlice, record: photoRecordAlice } = await dwnAlice.records.create({ + store: false, + data: TestDataGenerator.randomString(DwnConstant.maxDataSizeAllowedToBeEncoded + 1), + message: { + parentContextId : albumRecord.contextId, + protocol : photosProtocolDefinition.protocol, + protocolPath : 'album/photo', + protocolRole : 'album/participant', + schema : photosProtocolDefinition.types.photo.schema, + dataFormat : 'text/plain' + } + }); + expect(photoCreateStatusAlice.code).to.equal(202); + const { status: albumSendStatusAlice } = await photoRecordAlice.send(bobDid.uri); + expect(albumSendStatusAlice.code).to.equal(202); + + //SANITY: Alice attempts to fetch the photos without the role, she should only see her own photo + const { status: alicePhotosReadResultWithoutRole, records: alicePhotosRecordsWithoutRole } = await dwnAlice.records.query({ + from : bobDid.uri, + message : { + filter: { + protocol: photosProtocolDefinition.protocol, + protocolPath: 'album/photo', + contextId: albumRecord.contextId + } + } + }); + expect(alicePhotosReadResultWithoutRole.code).to.equal(200); + expect(alicePhotosRecordsWithoutRole).to.exist; + expect(alicePhotosRecordsWithoutRole).to.have.lengthOf(1); + + // Attempt to read the data of the photo, which should succeed + const readResultWithoutRole = await alicePhotosRecordsWithoutRole[0].data.text(); + expect(readResultWithoutRole.length).to.equal(DwnConstant.maxDataSizeAllowedToBeEncoded + 1); + + // Alice fetches all of the photos from the album + const alicePhotosReadResult = await dwnAlice.records.query({ + from : bobDid.uri, + message : { + protocolRole: 'album/participant', + filter: { + protocol: photosProtocolDefinition.protocol, + protocolPath: 'album/photo', + contextId: albumRecord.contextId + } + } + }); + expect(alicePhotosReadResult.status.code).to.equal(200); + expect(alicePhotosReadResult.records).to.exist; + expect(alicePhotosReadResult.records).to.have.lengthOf(4); + + // attempt to read data from the photos + for (const record of alicePhotosReadResult.records) { + const readResult = await record.data.text(); + expect(readResult.length).to.equal(DwnConstant.maxDataSizeAllowedToBeEncoded + 1); + } + }); + it('creates a role record for another user that they can use to create role-based records', async () => { /** * WHAT IS BEING TESTED? diff --git a/packages/api/tests/fixtures/protocol-definitions/photos.json b/packages/api/tests/fixtures/protocol-definitions/photos.json index 4a5c6c4ca..dfa3083dd 100644 --- a/packages/api/tests/fixtures/protocol-definitions/photos.json +++ b/packages/api/tests/fixtures/protocol-definitions/photos.json @@ -32,7 +32,7 @@ { "role": "friend", "can": [ - "create", "update" + "create", "update", "read", "query", "subscribe" ] } ], @@ -54,7 +54,7 @@ { "role": "album/participant", "can": [ - "create", "update" + "create", "update", "read", "query", "subscribe" ] } ] @@ -64,7 +64,7 @@ { "role": "album/participant", "can": [ - "create", "update" + "create", "update", "read", "query", "subscribe" ] }, {