diff --git a/packages/agent/src/cached-permissions.ts b/packages/agent/src/cached-permissions.ts deleted file mode 100644 index 81c11e29e..000000000 --- a/packages/agent/src/cached-permissions.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { TtlCache } from '@web5/common'; -import { AgentPermissionsApi } from './permissions-api.js'; -import { Web5Agent } from './types/agent.js'; -import { PermissionGrantEntry } from './types/permissions.js'; -import { DwnInterface } from './types/dwn.js'; - -export class CachedPermissions { - - /** the default value for whether a fetch is cached or not */ - private cachedDefault: boolean; - - /** Holds the instance of {@link AgentPermissionsApi} that helps when dealing with permissions protocol records */ - private permissionsApi: AgentPermissionsApi; - - /** cache for fetching a permission {@link PermissionGrant}, keyed by a specific MessageType and protocol */ - private cachedPermissions: TtlCache = new TtlCache({ ttl: 60 * 1000 }); - - constructor({ agent, cachedDefault }:{ agent: Web5Agent, cachedDefault?: boolean }) { - this.permissionsApi = new AgentPermissionsApi({ agent }); - this.cachedDefault = cachedDefault ?? false; - } - - public async getPermission({ connectedDid, delegateDid, delegate, messageType, protocol, cached = this.cachedDefault }: { - connectedDid: string; - delegateDid: string; - messageType: T; - protocol?: string; - cached?: boolean; - delegate?: boolean; - }): Promise { - // Currently we only support finding grants based on protocols - // A different approach may be necessary when we introduce `protocolPath` and `contextId` specific impersonation - const cacheKey = [ connectedDid, delegateDid, messageType, protocol ].join('~'); - const cachedGrant = cached ? this.cachedPermissions.get(cacheKey) : undefined; - if (cachedGrant) { - return cachedGrant; - } - - const permissionGrants = await this.permissionsApi.fetchGrants({ - author : delegateDid, - target : delegateDid, - grantor : connectedDid, - grantee : delegateDid, - }); - - // get the delegate grants that match the messageParams and are associated with the connectedDid as the grantor - const grant = await AgentPermissionsApi.matchGrantFromArray( - connectedDid, - delegateDid, - { messageType, protocol }, - permissionGrants, - delegate - ); - - if (!grant) { - throw new Error(`CachedPermissions: No permissions found for ${messageType}: ${protocol}`); - } - - this.cachedPermissions.set(cacheKey, grant); - return grant; - } - - public async clear(): Promise { - this.cachedPermissions.clear(); - } -} \ No newline at end of file diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index 7f7457575..83971304d 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -9,7 +9,6 @@ export type * from './types/vc.js'; export * from './agent-did-resolver-cache.js'; export * from './bearer-identity.js'; -export * from './cached-permissions.js'; export * from './crypto-api.js'; export * from './did-api.js'; export * from './dwn-api.js'; diff --git a/packages/agent/src/permissions-api.ts b/packages/agent/src/permissions-api.ts index 1d2d85cc8..3a4986f14 100644 --- a/packages/agent/src/permissions-api.ts +++ b/packages/agent/src/permissions-api.ts @@ -1,12 +1,15 @@ import { PermissionGrant, PermissionGrantData, PermissionRequestData, PermissionRevocationData, PermissionsProtocol } from '@tbd54566975/dwn-sdk-js'; import { Web5Agent } from './types/agent.js'; import { DwnDataEncodedRecordsWriteMessage, DwnInterface, DwnMessageParams, DwnMessagesPermissionScope, DwnPermissionGrant, DwnPermissionRequest, DwnPermissionScope, DwnProtocolPermissionScope, DwnRecordsPermissionScope, ProcessDwnRequest } from './types/dwn.js'; -import { Convert } from '@web5/common'; -import { CreateGrantParams, CreateRequestParams, CreateRevocationParams, FetchPermissionRequestParams, FetchPermissionsParams, IsGrantRevokedParams, PermissionGrantEntry, PermissionRequestEntry, PermissionRevocationEntry, PermissionsApi } from './types/permissions.js'; +import { Convert, TtlCache } from '@web5/common'; +import { CreateGrantParams, CreateRequestParams, CreateRevocationParams, FetchPermissionRequestParams, FetchPermissionsParams, GetPermissionParams, IsGrantRevokedParams, PermissionGrantEntry, PermissionRequestEntry, PermissionRevocationEntry, PermissionsApi } from './types/permissions.js'; import { isRecordsType } from './dwn-api.js'; export class AgentPermissionsApi implements PermissionsApi { + /** cache for fetching a permission {@link PermissionGrant}, keyed by a specific MessageType and protocol */ + private _cachedPermissions: TtlCache = new TtlCache({ ttl: 60 * 1000 }); + private _agent?: Web5Agent; get agent(): Web5Agent { @@ -24,6 +27,46 @@ export class AgentPermissionsApi implements PermissionsApi { this._agent = agent; } + async getPermission({ + connectedDid, + delegateDid, + delegate, + messageType, + protocol, + cached = false + }: GetPermissionParams): Promise { + // Currently we only support finding grants based on protocols + // A different approach may be necessary when we introduce `protocolPath` and `contextId` specific impersonation + const cacheKey = [ connectedDid, delegateDid, messageType, protocol ].join('~'); + const cachedGrant = this._cachedPermissions?.get(cacheKey); + if (cachedGrant) { + return cachedGrant; + } + + const permissionGrants = await this.fetchGrants({ + author : delegateDid, + target : delegateDid, + grantor : connectedDid, + grantee : delegateDid, + }); + + // get the delegate grants that match the messageParams and are associated with the connectedDid as the grantor + const grant = await AgentPermissionsApi.matchGrantFromArray( + connectedDid, + delegateDid, + { messageType, protocol }, + permissionGrants, + delegate + ); + + if (!grant) { + throw new Error(`CachedPermissions: No permissions found for ${messageType}: ${protocol}`); + } + + this._cachedPermissions?.set(cacheKey, grant); + return grant; + } + async fetchGrants({ author, target, @@ -269,6 +312,10 @@ export class AgentPermissionsApi implements PermissionsApi { return { message: dataEncodedMessage }; } + async clear():Promise { + this._cachedPermissions?.clear(); + } + /** * Matches the appropriate grant from an array of grants based on the provided parameters. * diff --git a/packages/agent/src/sync-engine-level.ts b/packages/agent/src/sync-engine-level.ts index 72336b3b3..238001912 100644 --- a/packages/agent/src/sync-engine-level.ts +++ b/packages/agent/src/sync-engine-level.ts @@ -22,7 +22,8 @@ import type { Web5Agent, Web5PlatformAgent } from './types/agent.js'; import { DwnInterface } from './types/dwn.js'; import { getDwnServiceEndpointUrls, isRecordsWrite } from './utils.js'; -import { CachedPermissions } from './cached-permissions.js'; +import { PermissionsApi } from './types/permissions.js'; +import { AgentPermissionsApi } from './permissions-api.js'; export type SyncEngineLevelParams = { agent?: Web5PlatformAgent; @@ -64,7 +65,7 @@ export class SyncEngineLevel implements SyncEngine { /** * An instance of the `AgentPermissionsApi` that is used to interact with permissions grants used during sync */ - private _cachedPermissionsApi: CachedPermissions; + private _permissionsApi: PermissionsApi;; private _db: AbstractLevel; private _syncIntervalId?: ReturnType; @@ -72,7 +73,7 @@ export class SyncEngineLevel implements SyncEngine { constructor({ agent, dataPath, db }: SyncEngineLevelParams) { this._agent = agent; - this._cachedPermissionsApi = new CachedPermissions({ agent: agent as Web5Agent, cachedDefault: true }); + this._permissionsApi = new AgentPermissionsApi({ agent: agent as Web5Agent }); this._db = (db) ? db : new Level(dataPath ?? 'DATA/AGENT/SYNC_STORE'); this._ulidFactory = monotonicFactory(); } @@ -93,11 +94,11 @@ export class SyncEngineLevel implements SyncEngine { set agent(agent: Web5PlatformAgent) { this._agent = agent; - this._cachedPermissionsApi = new CachedPermissions({ agent: agent as Web5Agent, cachedDefault: true }); + this._permissionsApi = new AgentPermissionsApi({ agent: agent as Web5Agent }); } public async clear(): Promise { - await this._cachedPermissionsApi.clear(); + await this._permissionsApi.clear(); await this._db.clear(); } @@ -133,7 +134,7 @@ export class SyncEngineLevel implements SyncEngine { let granteeDid: string | undefined; if (delegateDid) { try { - const messagesReadGrant = await this._cachedPermissionsApi.getPermission({ + const messagesReadGrant = await this._permissionsApi.getPermission({ connectedDid : did, messageType : DwnInterface.MessagesRead, delegateDid, @@ -402,7 +403,7 @@ export class SyncEngineLevel implements SyncEngine { if (delegateDid) { // fetch the grants for the delegate DID try { - const messagesQueryGrant = await this._cachedPermissionsApi.getPermission({ + const messagesQueryGrant = await this._permissionsApi.getPermission({ connectedDid : did, messageType : DwnInterface.MessagesQuery, delegateDid, @@ -469,7 +470,7 @@ export class SyncEngineLevel implements SyncEngine { let permissionGrantId: string | undefined; if (delegateDid) { try { - const messagesReadGrant = await this._cachedPermissionsApi.getPermission({ + const messagesReadGrant = await this._permissionsApi.getPermission({ connectedDid : author, messageType : DwnInterface.MessagesRead, delegateDid, diff --git a/packages/agent/src/test-harness.ts b/packages/agent/src/test-harness.ts index ec2e89d4d..a98b7c9f2 100644 --- a/packages/agent/src/test-harness.ts +++ b/packages/agent/src/test-harness.ts @@ -94,6 +94,7 @@ export class PlatformAgentTestHarness { await this.dwnResumableTaskStore.clear(); await this.syncStore.clear(); await this.vaultStore.clear(); + await this.agent.permissions.clear(); this.dwnStores.clear(); // Reset the indexes and caches for the Agent's DWN data stores. diff --git a/packages/agent/src/types/permissions.ts b/packages/agent/src/types/permissions.ts index 99758179b..3fc1900d3 100644 --- a/packages/agent/src/types/permissions.ts +++ b/packages/agent/src/types/permissions.ts @@ -1,4 +1,4 @@ -import { DwnDataEncodedRecordsWriteMessage, DwnPermissionGrant, DwnPermissionRequest, DwnPermissionScope } from './dwn.js'; +import { DwnDataEncodedRecordsWriteMessage, DwnInterface, DwnPermissionGrant, DwnPermissionRequest, DwnPermissionScope } from './dwn.js'; export type FetchPermissionsParams = { author: string; @@ -63,7 +63,21 @@ export type CreateRevocationParams = { description?: string; } +export type GetPermissionParams = { + connectedDid: string; + delegateDid: string; + messageType: DwnInterface; + protocol?: string; + cached?: boolean; + delegate?: boolean; +} + export interface PermissionsApi { + /** + * Get the permission grant for a given author, target, and protocol. To be used when authoring delegated requests. + */ + getPermission: (params: GetPermissionParams) => Promise; + /** * Fetch all grants for a given author and target, optionally filtered by a specific grantee, grantor, or protocol. */ @@ -93,4 +107,9 @@ export interface PermissionsApi { * Create a new permission revocation, optionally storing it in the DWN. */ createRevocation(params: CreateRevocationParams): Promise; + + /** + * Clears the cache of matched permissions. + */ + clear: () => Promise; } diff --git a/packages/agent/tests/cached-permissions.spec.ts b/packages/agent/tests/cached-permissions.spec.ts deleted file mode 100644 index 134e3b56c..000000000 --- a/packages/agent/tests/cached-permissions.spec.ts +++ /dev/null @@ -1,237 +0,0 @@ -import sinon from 'sinon'; -import { expect } from 'chai'; -import { AgentPermissionsApi } from '../src/permissions-api.js'; -import { PlatformAgentTestHarness } from '../src/test-harness.js'; -import { TestAgent } from './utils/test-agent.js'; -import { BearerDid } from '@web5/dids'; - -import { DwnInterfaceName, DwnMethodName, Time } from '@tbd54566975/dwn-sdk-js'; -import { CachedPermissions, DwnInterface } from '../src/index.js'; -import { Convert } from '@web5/common'; - -describe('CachedPermissions', () => { - let permissions: AgentPermissionsApi; - let testHarness: PlatformAgentTestHarness; - let aliceDid: BearerDid; - let bobDid: BearerDid; - - before(async () => { - testHarness = await PlatformAgentTestHarness.setup({ - agentClass : TestAgent, - agentStores : 'dwn' - }); - }); - - after(async () => { - sinon.restore(); - await testHarness.clearStorage(); - await testHarness.closeStorage(); - }); - - beforeEach(async () => { - sinon.restore(); - await testHarness.clearStorage(); - await testHarness.createAgentDid(); - - // Create an "alice" Identity to author the DWN messages. - const alice = await testHarness.agent.identity.create({ didMethod: 'jwk', metadata: { name: 'Alice' } }); - await testHarness.agent.identity.manage({ portableIdentity: await alice.export() }); - aliceDid = alice.did; - - const bob = await testHarness.agent.identity.create({ didMethod: 'jwk', metadata: { name: 'Bob' } }); - await testHarness.agent.identity.manage({ portableIdentity: await bob.export() }); - bobDid = bob.did; - - permissions = new AgentPermissionsApi({ agent: testHarness.agent }); - }); - - describe('cachedDefault', () => { - it('caches permissions by default if defaultCache is set to true', async () => { - // create a permission grant to fetch - const messagesQueryGrant = await permissions.createGrant({ - store : true, - author : aliceDid.uri, - grantedTo : bobDid.uri, - dateExpires : Time.createOffsetTimestamp({ seconds: 60 }), - scope : { - interface : DwnInterfaceName.Messages, - method : DwnMethodName.Query, - } - }); - - // store the grant as owner from bob so that it can be fetched - const { encodedData, ...messagesQueryGrantMessage } = messagesQueryGrant.message; - const grantReply = await testHarness.agent.processDwnRequest({ - target : bobDid.uri, - author : bobDid.uri, - signAsOwner : true, - messageType : DwnInterface.RecordsWrite, - rawMessage : messagesQueryGrantMessage, - dataStream : new Blob([ Convert.base64Url(encodedData).toUint8Array() ]) - }); - expect(grantReply.reply.status.code).to.equal(202); - - const permissionGrantsApiSpy = sinon.spy(AgentPermissionsApi.prototype, 'fetchGrants'); - - // with defaultCache set to true - const cachedPermissions = new CachedPermissions({ agent: testHarness.agent, cachedDefault: true }); - - // fetch the grant - let fetchedMessagesQueryGrant = await cachedPermissions.getPermission({ - connectedDid : aliceDid.uri, - delegateDid : bobDid.uri, - messageType : DwnInterface.MessagesQuery, - }); - expect(fetchedMessagesQueryGrant).to.not.be.undefined; - expect(fetchedMessagesQueryGrant.message.recordId).to.equal(messagesQueryGrant.message.recordId); - - // confirm that the permission was fetched from the API - expect(permissionGrantsApiSpy.calledOnce).to.be.true; - permissionGrantsApiSpy.resetHistory(); - - // fetch the grant again to confirm that it was cached - fetchedMessagesQueryGrant = await cachedPermissions.getPermission({ - connectedDid : aliceDid.uri, - delegateDid : bobDid.uri, - messageType : DwnInterface.MessagesQuery, - }); - expect(fetchedMessagesQueryGrant).to.not.be.undefined; - expect(fetchedMessagesQueryGrant.message.recordId).to.equal(messagesQueryGrant.message.recordId); - - // confirm that the permission was not fetched again from the API - expect(permissionGrantsApiSpy.called).to.be.false; - - // confirm that the permissions is fetched from teh api if cache is set to false on a single call - fetchedMessagesQueryGrant = await cachedPermissions.getPermission({ - connectedDid : aliceDid.uri, - delegateDid : bobDid.uri, - messageType : DwnInterface.MessagesQuery, - cached : false, - }); - expect(fetchedMessagesQueryGrant).to.not.be.undefined; - expect(fetchedMessagesQueryGrant.message.recordId).to.equal(messagesQueryGrant.message.recordId); - - // confirm that the permission was fetched from the API - expect(permissionGrantsApiSpy.calledOnce).to.be.true; - }); - - it('does not cache permission by default defaultCache is set to false', async () => { - // create a permission grant to fetch - const messagesQueryGrant = await permissions.createGrant({ - store : true, - author : aliceDid.uri, - grantedTo : bobDid.uri, - dateExpires : Time.createOffsetTimestamp({ seconds: 60 }), - scope : { - interface : DwnInterfaceName.Messages, - method : DwnMethodName.Query, - } - }); - - // store the grant as owner from bob so that it can be fetched - const { encodedData, ...messagesQueryGrantMessage } = messagesQueryGrant.message; - const grantReply = await testHarness.agent.processDwnRequest({ - target : bobDid.uri, - author : bobDid.uri, - signAsOwner : true, - messageType : DwnInterface.RecordsWrite, - rawMessage : messagesQueryGrantMessage, - dataStream : new Blob([ Convert.base64Url(encodedData).toUint8Array() ]) - }); - expect(grantReply.reply.status.code).to.equal(202); - - const permissionGrantsApiSpy = sinon.spy(AgentPermissionsApi.prototype, 'fetchGrants'); - - // with defaultCache set to false by default - const cachedPermissions = new CachedPermissions({ agent: testHarness.agent }); - - // fetch the grant - let fetchedMessagesQueryGrant = await cachedPermissions.getPermission({ - connectedDid : aliceDid.uri, - delegateDid : bobDid.uri, - messageType : DwnInterface.MessagesQuery, - }); - expect(fetchedMessagesQueryGrant).to.not.be.undefined; - expect(fetchedMessagesQueryGrant.message.recordId).to.equal(messagesQueryGrant.message.recordId); - - // confirm that the permission was fetched from the API - expect(permissionGrantsApiSpy.calledOnce).to.be.true; - permissionGrantsApiSpy.resetHistory(); - - // fetch the grant again to confirm that it was cached - fetchedMessagesQueryGrant = await cachedPermissions.getPermission({ - connectedDid : aliceDid.uri, - delegateDid : bobDid.uri, - messageType : DwnInterface.MessagesQuery, - }); - expect(fetchedMessagesQueryGrant).to.not.be.undefined; - expect(fetchedMessagesQueryGrant.message.recordId).to.equal(messagesQueryGrant.message.recordId); - - // confirm that the permission was fetched a second time from the API - expect(permissionGrantsApiSpy.called).to.be.true; - permissionGrantsApiSpy.resetHistory(); - - // confirm that the permissions is not fetched from the api if cache is set to true on a single call - fetchedMessagesQueryGrant = await cachedPermissions.getPermission({ - connectedDid : aliceDid.uri, - delegateDid : bobDid.uri, - messageType : DwnInterface.MessagesQuery, - cached : true, - }); - expect(fetchedMessagesQueryGrant).to.not.be.undefined; - expect(fetchedMessagesQueryGrant.message.recordId).to.equal(messagesQueryGrant.message.recordId); - - // confirm that the permission was fetched from the API - expect(permissionGrantsApiSpy.calledOnce).to.be.false; - }); - }); - - describe('getPermission', () => { - it('throws an error if no permissions are found', async () => { - const cachedPermissions = new CachedPermissions({ agent: testHarness.agent }); - - try { - await cachedPermissions.getPermission({ - connectedDid : aliceDid.uri, - delegateDid : bobDid.uri, - messageType : DwnInterface.MessagesQuery, - }); - expect.fail('Expected an error to be thrown'); - } catch(error: any) { - expect(error.message).to.equal('CachedPermissions: No permissions found for MessagesQuery: undefined'); - } - - // create a permission grant to fetch - const messagesQueryGrant = await permissions.createGrant({ - store : true, - author : aliceDid.uri, - grantedTo : bobDid.uri, - dateExpires : Time.createOffsetTimestamp({ seconds: 60 }), - scope : { - interface : DwnInterfaceName.Messages, - method : DwnMethodName.Query, - } - }); - - // store the grant as owner from bob so that it can be fetched - const { encodedData, ...messagesQueryGrantMessage } = messagesQueryGrant.message; - const grantReply = await testHarness.agent.processDwnRequest({ - target : bobDid.uri, - author : bobDid.uri, - signAsOwner : true, - messageType : DwnInterface.RecordsWrite, - rawMessage : messagesQueryGrantMessage, - dataStream : new Blob([ Convert.base64Url(encodedData).toUint8Array() ]) - }); - expect(grantReply.reply.status.code).to.equal(202); - - // fetch the grant - const fetchedMessagesQueryGrant = await cachedPermissions.getPermission({ - connectedDid : aliceDid.uri, - delegateDid : bobDid.uri, - messageType : DwnInterface.MessagesQuery, - }); - expect(fetchedMessagesQueryGrant.message.recordId).to.equal(messagesQueryGrant.message.recordId); - }); - }); -}); \ No newline at end of file diff --git a/packages/agent/tests/permissions-api.spec.ts b/packages/agent/tests/permissions-api.spec.ts index bb925d8a3..06f8defd9 100644 --- a/packages/agent/tests/permissions-api.spec.ts +++ b/packages/agent/tests/permissions-api.spec.ts @@ -7,11 +7,13 @@ import { BearerDid } from '@web5/dids'; import { DwnInterfaceName, DwnMethodName, Time } from '@tbd54566975/dwn-sdk-js'; import { DwnInterface, DwnPermissionGrant, DwnPermissionScope, Web5PlatformAgent } from '../src/index.js'; +import { Convert } from '@web5/common'; describe('AgentPermissionsApi', () => { let testHarness: PlatformAgentTestHarness; let aliceDid: BearerDid; + let bobDid: BearerDid; before(async () => { testHarness = await PlatformAgentTestHarness.setup({ @@ -35,6 +37,10 @@ describe('AgentPermissionsApi', () => { const alice = await testHarness.agent.identity.create({ didMethod: 'jwk', metadata: { name: 'Alice' } }); await testHarness.agent.identity.manage({ portableIdentity: await alice.export() }); aliceDid = alice.did; + + const bob = await testHarness.agent.identity.create({ didMethod: 'jwk', metadata: { name: 'Bob' } }); + await testHarness.agent.identity.manage({ portableIdentity: await bob.export() }); + bobDid = bob.did; }); describe('get agent', () => { @@ -54,6 +60,53 @@ describe('AgentPermissionsApi', () => { }); }); + describe('getPermission', () => { + it('throws an error if no permissions are found', async () => { + try { + await testHarness.agent.permissions.getPermission({ + connectedDid : aliceDid.uri, + delegateDid : bobDid.uri, + messageType : DwnInterface.MessagesQuery, + }); + expect.fail('Expected an error to be thrown'); + } catch(error: any) { + expect(error.message).to.equal('CachedPermissions: No permissions found for MessagesQuery: undefined'); + } + + // create a permission grant to fetch + const messagesQueryGrant = await testHarness.agent.permissions.createGrant({ + store : true, + author : aliceDid.uri, + grantedTo : bobDid.uri, + dateExpires : Time.createOffsetTimestamp({ seconds: 60 }), + scope : { + interface : DwnInterfaceName.Messages, + method : DwnMethodName.Query, + } + }); + + // store the grant as owner from bob so that it can be fetched + const { encodedData, ...messagesQueryGrantMessage } = messagesQueryGrant.message; + const grantReply = await testHarness.agent.processDwnRequest({ + target : bobDid.uri, + author : bobDid.uri, + signAsOwner : true, + messageType : DwnInterface.RecordsWrite, + rawMessage : messagesQueryGrantMessage, + dataStream : new Blob([ Convert.base64Url(encodedData).toUint8Array() ]) + }); + expect(grantReply.reply.status.code).to.equal(202); + + // fetch the grant + const fetchedMessagesQueryGrant = await testHarness.agent.permissions.getPermission({ + connectedDid : aliceDid.uri, + delegateDid : bobDid.uri, + messageType : DwnInterface.MessagesQuery, + }); + expect(fetchedMessagesQueryGrant.message.recordId).to.equal(messagesQueryGrant.message.recordId); + }); + }); + describe('fetchGrants', () => { it('from remote', async () => { // spy on the processDwnRequest method diff --git a/packages/api/src/dwn-api.ts b/packages/api/src/dwn-api.ts index 5b0205056..231f3ec05 100644 --- a/packages/api/src/dwn-api.ts +++ b/packages/api/src/dwn-api.ts @@ -19,7 +19,6 @@ import { DwnMessageParams, DwnMessageSubscription, DwnResponseStatus, - CachedPermissions, ProcessDwnRequest, DwnPaginationCursor, AgentPermissionsApi, @@ -305,15 +304,11 @@ export class DwnApi { /** Holds the instance of {@link AgentPermissionsApi} that helps when dealing with permissions protocol records */ private permissionsApi: AgentPermissionsApi; - /** cache for fetching a permission {@link PermissionGrant}, keyed by a specific MessageType and protocol */ - private cachedPermissionsApi: CachedPermissions; - constructor(options: { agent: Web5Agent, connectedDid: string, delegateDid?: string }) { this.agent = options.agent; this.connectedDid = options.connectedDid; this.delegateDid = options.delegateDid; this.permissionsApi = new AgentPermissionsApi({ agent: this.agent }); - this.cachedPermissionsApi = new CachedPermissions({ agent: this.agent, cachedDefault: true }); } /** @@ -488,30 +483,6 @@ export class DwnApi { */ get records() { - /** - * Adds the appropriate delegate grant to the messageParams for the given messageType. - * - * @param messageType the type of message to add the delegate grant to - * @param messageParams the message parameters to add the delegate grant to - * @param protocol the protocol to scope the delegate grant to - * - * @returns a copy of the messageParams with the delegate grant added - */ - const addDelegateGrantToMessageParams = ({ messageParams, protocol, messageType }:{ - messageType: T, - messageParams: DwnMessageParams[T], - protocol: string - }) => { - return Record.addDelegateGrantToMessageParams({ - connectedDid : this.connectedDid, - delegateDid : this.delegateDid, - messageParams : messageParams, - cachedPermissions : this.cachedPermissionsApi, - messageType, - protocol, - }); - }; - return { /** * Alias for the `write` method @@ -575,11 +546,19 @@ export class DwnApi { }; if (this.delegateDid) { - agentRequest.messageParams = await addDelegateGrantToMessageParams({ - messageType : DwnInterface.RecordsDelete, - messageParams : agentRequest.messageParams, - protocol : request.protocol + const { message: delegatedGrant } = await this.permissionsApi.getPermission({ + 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; } @@ -616,11 +595,19 @@ export class DwnApi { }; if (this.delegateDid) { - agentRequest.messageParams = await addDelegateGrantToMessageParams({ - messageType : DwnInterface.RecordsQuery, - messageParams : agentRequest.messageParams, - protocol : request.protocol + const { message: delegatedGrant } = await this.permissionsApi.getPermission({ + 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; } @@ -643,25 +630,24 @@ export class DwnApi { * Extract the `author` DID from the record entry since records may be signed by the * tenant owner or any other entity. */ - author : getRecordAuthor(entry), + author : getRecordAuthor(entry), /** * Set the `connectedDid` to currently connected DID so that subsequent calls to * {@link Record} instance methods, such as `record.update()` are executed on the * local DWN even if the record was returned by a query of a remote DWN. */ - connectedDid : this.connectedDid, + connectedDid : this.connectedDid, /** * If the record was returned by a query of a remote DWN, set the `remoteOrigin` to * the DID of the DWN that returned the record. The `remoteOrigin` property will be used * to determine which DWN to send subsequent read requests to in the event the data * payload exceeds the threshold for being returned with queries. */ - remoteOrigin : request.from, - cachedPermissions : this.cachedPermissionsApi, - delegateDid : this.delegateDid, + remoteOrigin : request.from, + delegateDid : this.delegateDid, ...entry as DwnMessage[DwnInterface.RecordsWrite] }; - const record = new Record(this.agent, recordOptions); + const record = new Record(this.agent, recordOptions, this.permissionsApi); return record; }); @@ -688,12 +674,19 @@ export class DwnApi { target : request.from || this.connectedDid }; if (this.delegateDid) { - agentRequest.messageParams = await addDelegateGrantToMessageParams({ - messageType : DwnInterface.RecordsRead, - messageParams : agentRequest.messageParams, - protocol : request.protocol + const { message: delegatedGrant } = await this.permissionsApi.getPermission({ + 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; } @@ -714,26 +707,25 @@ export class DwnApi { * Extract the `author` DID from the record since records may be signed by the * tenant owner or any other entity. */ - author : getRecordAuthor(responseRecord), + author : getRecordAuthor(responseRecord), /** * Set the `connectedDid` to currently connected DID so that subsequent calls to * {@link Record} instance methods, such as `record.update()` are executed on the * local DWN even if the record was read from a remote DWN. */ - connectedDid : this.connectedDid, + connectedDid : this.connectedDid, /** * If the record was returned by reading from a remote DWN, set the `remoteOrigin` to * the DID of the DWN that returned the record. The `remoteOrigin` property will be used * to determine which DWN to send subsequent read requests to in the event the data * payload must be read again (e.g., if the data stream is consumed). */ - remoteOrigin : request.from, - cachedPermissions : this.cachedPermissionsApi, - delegateDid : this.delegateDid, + remoteOrigin : request.from, + delegateDid : this.delegateDid, ...responseRecord, }; - record = new Record(this.agent, recordOptions); + record = new Record(this.agent, recordOptions, this.permissionsApi); } return { record, status }; @@ -765,21 +757,28 @@ export class DwnApi { * The handler to process the subscription events. */ subscriptionHandler: SubscriptionUtil.recordSubscriptionHandler({ - agent : this.agent, - connectedDid : this.connectedDid, - delegateDid : this.delegateDid, - cachedPermissions : this.cachedPermissionsApi, + agent : this.agent, + connectedDid : this.connectedDid, + delegateDid : this.delegateDid, + permissionsApi : this.permissionsApi, request }) }; if (this.delegateDid) { - agentRequest.messageParams = await addDelegateGrantToMessageParams({ - messageType : DwnInterface.RecordsSubscribe, - messageParams : agentRequest.messageParams, - protocol : request.protocol + const { message: delegatedGrant } = await this.permissionsApi.getPermission({ + 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; }; @@ -823,11 +822,19 @@ export class DwnApi { // if impersonation is enabled, fetch the delegated grant to use with the write operation if (this.delegateDid) { - dwnRequestParams.messageParams = await addDelegateGrantToMessageParams({ - messageType : DwnInterface.RecordsWrite, - messageParams : dwnRequestParams.messageParams, - protocol : request.message?.protocol + const { message: delegatedGrant } = await this.permissionsApi.getPermission({ + connectedDid : this.connectedDid, + delegateDid : this.delegateDid, + protocol : request.message.protocol, + delegate : true, + cached : true, + messageType : dwnRequestParams.messageType }); + + dwnRequestParams.messageParams = { + ...dwnRequestParams.messageParams, + delegatedGrant + }; dwnRequestParams.granteeDid = this.delegateDid; }; @@ -842,20 +849,19 @@ export class DwnApi { * Assume the author is the connected DID since the record was just written to the * local DWN. */ - author : this.connectedDid, + author : this.connectedDid, /** * Set the `connectedDid` to currently connected DID so that subsequent calls to * {@link Record} instance methods, such as `record.update()` are executed on the * local DWN. */ - connectedDid : this.connectedDid, - encodedData : dataBlob, - cachedPermissions : this.cachedPermissionsApi, - delegateDid : this.delegateDid, + connectedDid : this.connectedDid, + encodedData : dataBlob, + delegateDid : this.delegateDid, ...responseMessage, }; - record = new Record(this.agent, recordOptions); + record = new Record(this.agent, recordOptions, this.permissionsApi); } return { record, status }; diff --git a/packages/api/src/record.ts b/packages/api/src/record.ts index 26721d7b7..3159a9dab 100644 --- a/packages/api/src/record.ts +++ b/packages/api/src/record.ts @@ -19,8 +19,8 @@ import { DwnPaginationCursor, isDwnMessage, SendDwnRequest, - CachedPermissions, - DwnRecordsInterfaces, + PermissionsApi, + AgentPermissionsApi, } from '@web5/agent'; import { Convert, isEmptyObject, NodeStream, removeUndefinedProperties, Stream } from '@web5/common'; @@ -97,9 +97,6 @@ export type RecordOptions = DwnMessage[DwnInterface.RecordsWrite | DwnInterface. /** The optional DID that will sign the records on behalf of the connectedDid */ delegateDid?: string; - /** cached permission API for fast grant lookup */ - cachedPermissions?: CachedPermissions; - /** The data of the record, either as a Base64 URL encoded string or a Blob. */ encodedData?: string | Blob; @@ -216,7 +213,7 @@ export class Record implements RecordModel { /** The optional DID that is delegated to act on behalf of the connectedDid */ private _delegateDid?: string; /** cache for fetching a permission {@link PermissionGrant}, keyed by a specific MessageType and protocol */ - private _cachedPermissions?: CachedPermissions; + private _permissionsApi: PermissionsApi; /** Encoded data of the record, if available. */ private _encodedData?: Blob; /** Stream of the record's data. */ @@ -267,8 +264,8 @@ export class Record implements RecordModel { /** Record's ID */ get id() { return this._recordId; } - /** Record's context ID */ - get contextId() { return this._contextId; } + /** Record's context ID. If the record is deleted, the context Id comes from the initial write */ + get contextId() { return this.deleted ? this._initialWrite.contextId : this._contextId; } /** Record's creation date */ get dateCreated() { return this._immutableProperties.dateCreated; } @@ -360,7 +357,7 @@ export class Record implements RecordModel { return message; } - constructor(agent: Web5Agent, options: RecordOptions) { + constructor(agent: Web5Agent, options: RecordOptions, permissionsApi?: PermissionsApi) { this._agent = agent; @@ -368,11 +365,11 @@ export class Record implements RecordModel { // that they don't have to decode the signer's DID from the JWS. this._author = options.author; - // Store the currently `connectedDid` so that subsequent message signing is done with the - // connected DID's keys and DWN requests target the connected DID's DWN. + // Store the `connectedDid`, and optionally the `delegateDid` and `permissionsApi` in order to be able + // to perform operations on the record (update, delete, data) as a delegate of the connected DID. this._connectedDid = options.connectedDid; this._delegateDid = options.delegateDid; - this._cachedPermissions = options.cachedPermissions; + this._permissionsApi = permissionsApi ?? new AgentPermissionsApi({ agent }); // If the record was queried or read from a remote DWN, the `remoteOrigin` DID will be // defined. This value is used to send subsequent read requests to the same remote DWN in the @@ -383,7 +380,7 @@ export class Record implements RecordModel { // RecordsWriteMessage properties. this._attestation = options.attestation; this._authorization = options.authorization; - this._contextId = options.contextId ?? options.initialWrite?.contextId; + this._contextId = options.contextId; this._descriptor = options.descriptor; this._encryption = options.encryption; this._initialWrite = options.initialWrite; @@ -751,7 +748,15 @@ export class Record implements RecordModel { }; if (this._delegateDid) { - requestOptions.messageParams = await this.addDelegateGrantToMessageParams(DwnInterface.RecordsWrite, requestOptions); + const { message: delegatedGrant } = await this._permissionsApi.getPermission({ + connectedDid : this._connectedDid, + delegateDid : this._delegateDid, + protocol : this.protocol, + delegate : true, + cached : true, + messageType : requestOptions.messageType + }); + requestOptions.messageParams.delegatedGrant = delegatedGrant; requestOptions.granteeDid = this._delegateDid; } @@ -834,7 +839,20 @@ export class Record implements RecordModel { } if (this._delegateDid) { - deleteOptions.messageParams = await this.addDelegateGrantToMessageParams(DwnInterface.RecordsDelete, deleteOptions); + const { message: delegatedGrant } = await this._permissionsApi.getPermission({ + connectedDid : this._connectedDid, + delegateDid : this._delegateDid, + protocol : this.protocol, + delegate : true, + cached : true, + messageType : deleteOptions.messageType + }); + + deleteOptions.messageParams = { + ...deleteOptions.messageParams, + delegatedGrant + }; + deleteOptions.granteeDid = this._delegateDid; } @@ -855,6 +873,7 @@ export class Record implements RecordModel { this._encodedData = undefined; this._encryption = undefined; this._attestation = undefined; + this._contextId = undefined; return { status }; } @@ -874,7 +893,20 @@ export class Record implements RecordModel { }; if (this._delegateDid) { - initialWriteRequest.messageParams = await this.addDelegateGrantToMessageParams(DwnInterface.RecordsWrite, initialWriteRequest); + const { message: delegatedGrant } = await this._permissionsApi.getPermission({ + connectedDid : this._connectedDid, + delegateDid : this._delegateDid, + protocol : this.protocol, + delegate : true, + cached : true, + messageType : initialWriteRequest.messageType + }); + + initialWriteRequest.messageParams = { + ...initialWriteRequest.messageParams, + delegatedGrant + }; + initialWriteRequest.granteeDid = this._delegateDid; } @@ -933,7 +965,20 @@ export class Record implements RecordModel { } if (this._delegateDid) { - requestOptions.messageParams = await this.addDelegateGrantToMessageParams(requestOptions.messageType, requestOptions); + const { message: delegatedGrant } = await this._permissionsApi.getPermission({ + connectedDid : this._connectedDid, + delegateDid : this._delegateDid, + protocol : this.protocol, + delegate : true, + cached : true, + messageType : requestOptions.messageType + }); + + requestOptions.messageParams = { + ...requestOptions.messageParams, + delegatedGrant + }; + requestOptions.granteeDid = this._delegateDid; } @@ -974,7 +1019,20 @@ export class Record implements RecordModel { }; if (this._delegateDid) { - readRequest.messageParams = await this.addDelegateGrantToMessageParams(DwnInterface.RecordsRead, readRequest); + const { message: delegatedGrant } = await this._permissionsApi.getPermission({ + connectedDid : this._connectedDid, + delegateDid : this._delegateDid, + protocol : this.protocol, + delegate : true, + cached : true, + messageType : readRequest.messageType + }); + + readRequest.messageParams = { + ...readRequest.messageParams, + delegatedGrant + }; + readRequest.granteeDid = this._delegateDid; } @@ -1031,67 +1089,4 @@ export class Record implements RecordModel { private isRecordsDeleteDescriptor(descriptor: DwnMessageDescriptor[DwnInterface.RecordsWrite | DwnInterface.RecordsDelete]): descriptor is DwnMessageDescriptor[DwnInterface.RecordsDelete] { return descriptor.interface + descriptor.method === DwnInterface.RecordsDelete; } - - /** - * Adds the appropriate delegate grant to the messageParams for the given messageType. - * - * @param messageType The type of message to add the delegate grant to. - * @param request The request object containing the message parameters. - * - * @returns a copy of the message parameters with the delegate grant added. - */ - private addDelegateGrantToMessageParams(messageType: T, request: ProcessDwnRequest): Promise { - return Record.addDelegateGrantToMessageParams({ - messageType, - delegateDid : this._delegateDid, - connectedDid : this._connectedDid, - protocol : this.protocol, - cachedPermissions : this._cachedPermissions, - messageParams : request.messageParams - }); - } - - /** - * Adds the appropriate delegate grant to the messageParams for the given messageType. - * - * @param messageType The type of message to add the delegate grant to. - * @param messageParams The message parameters to add the delegate grant to. - * @param delegateDid The DID the grant is granted to (grantee). - * @param connectedDid The DID the grant is granted by (grantor). - * @param protocol The protocol the grant is for. - * @param cachedPermissions The cached permissions object. - * - * @returns a copy of the message parameters with the delegate grant added. - */ - static async addDelegateGrantToMessageParams({ - messageType, - delegateDid, - connectedDid, - protocol, - cachedPermissions, - messageParams = {} as DwnMessageParams[T] - }:{ - messageType: T, - delegateDid: string, - connectedDid: string, - protocol: string, - cachedPermissions: CachedPermissions; - messageParams?: DwnMessageParams[T]; - }): Promise { - - // if an app is scoped down to a specific protocolPath or contextId, it must include those filters in the read request - const { message: delegatedGrant } = await cachedPermissions.getPermission({ - connectedDid : connectedDid, - delegateDid : delegateDid, - protocol : protocol, - delegate : true, - cached : true, - messageType - }); - - return { - ...messageParams, - delegatedGrant, - }; - } } \ No newline at end of file diff --git a/packages/api/src/subscription-util.ts b/packages/api/src/subscription-util.ts index f79c13292..5316733d0 100644 --- a/packages/api/src/subscription-util.ts +++ b/packages/api/src/subscription-util.ts @@ -1,4 +1,4 @@ -import { CachedPermissions, DwnRecordSubscriptionHandler, getRecordAuthor, Web5Agent } from '@web5/agent'; +import { DwnRecordSubscriptionHandler, getRecordAuthor, PermissionsApi, Web5Agent } from '@web5/agent'; import { RecordsSubscribeRequest } from './dwn-api.js'; import { Record } from './record.js'; @@ -9,11 +9,11 @@ export class SubscriptionUtil { /** * Creates a record subscription handler that can be used to process incoming {Record} messages. */ - static recordSubscriptionHandler({ agent, connectedDid, request, delegateDid, cachedPermissions }:{ + static recordSubscriptionHandler({ agent, connectedDid, request, delegateDid, permissionsApi }:{ agent: Web5Agent; connectedDid: string; delegateDid?: string; - cachedPermissions: CachedPermissions; + permissionsApi?: PermissionsApi; request: RecordsSubscribeRequest; }): DwnRecordSubscriptionHandler { const { subscriptionHandler, from: remoteOrigin } = request; @@ -29,10 +29,11 @@ export class SubscriptionUtil { }; const record = new Record(agent, { - ...message, ...recordOptions, - cachedPermissions : cachedPermissions, - delegateDid : delegateDid, - }); + ...message, + ...recordOptions, + delegateDid: delegateDid, + }, permissionsApi); + subscriptionHandler(record); }; } diff --git a/packages/api/tests/dwn-api.spec.ts b/packages/api/tests/dwn-api.spec.ts index 409a1d63d..5f7791004 100644 --- a/packages/api/tests/dwn-api.spec.ts +++ b/packages/api/tests/dwn-api.spec.ts @@ -58,12 +58,9 @@ describe('DwnApi', () => { await testHarness.dwnEventLog.clear(); await testHarness.dwnMessageStore.clear(); await testHarness.dwnResumableTaskStore.clear(); + await testHarness.agent.permissions.clear(); testHarness.dwnStores.clear(); - // clear cached permissions between test runs - dwnAlice['cachedPermissionsApi'].clear(); - dwnBob['cachedPermissionsApi'].clear(); - dwnAlice['connectedDid'] = aliceDid.uri; dwnBob['connectedDid'] = bobDid.uri; diff --git a/packages/api/tests/record.spec.ts b/packages/api/tests/record.spec.ts index 36b07bac4..97580466e 100644 --- a/packages/api/tests/record.spec.ts +++ b/packages/api/tests/record.spec.ts @@ -3654,7 +3654,7 @@ describe('Record', () => { expect(processMessageSpy.callCount).to.equal(2); }); - it('stores as owner a deleted record to the local DWN from an external signer', async () => { + it('stores a deleted record as owner to the local DWN from an external signer', async () => { // subscribe to records so that we can receive a record in a deleted state const records = new Map(); const subscriptionHandler = (record: Record) => {