Skip to content

Commit

Permalink
add grant to Record class methods (#859)
Browse files Browse the repository at this point in the history
This PR fixes a bug where the `Record` class method which is retrieved with queries, reads, and writes did not respect the delegated permission state. The `Record` class now accepts an optional PermissionsApi so that it can share the TTL cache with a caller. If one is not provided a new one is instantiated from the agent that is passed.

Took advantage of removing the specific `CachedPermissions` class and wrapped it into the `PermissionsApi` interface.

Refactored some unnecessary abstractions when fetching the grant, added tests for missing cases and subscription cases.
  • Loading branch information
LiranCohen committed Sep 3, 2024
1 parent 3da24db commit 7fc1f1d
Show file tree
Hide file tree
Showing 21 changed files with 1,712 additions and 761 deletions.
8 changes: 8 additions & 0 deletions .changeset/green-plums-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@web5/agent": minor
"@web5/identity-agent": minor
"@web5/proxy-agent": minor
"@web5/user-agent": minor
---

Tefactor getting permissions for grants into a single Permission API interface
5 changes: 5 additions & 0 deletions .changeset/plenty-brooms-suffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@web5/api": patch
---

Consume single PermissionApi for dealing with permissions, fix bug for Record class not fetching delegate permissions for request.
66 changes: 0 additions & 66 deletions packages/agent/src/cached-permissions.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
51 changes: 49 additions & 2 deletions packages/agent/src/permissions-api.ts
Original file line number Diff line number Diff line change
@@ -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<string, PermissionGrantEntry> = new TtlCache({ ttl: 60 * 1000 });

private _agent?: Web5Agent;

get agent(): Web5Agent {
Expand All @@ -24,6 +27,46 @@ export class AgentPermissionsApi implements PermissionsApi {
this._agent = agent;
}

async getPermissionForRequest({
connectedDid,
delegateDid,
delegate,
messageType,
protocol,
cached = false
}: GetPermissionParams): Promise<PermissionGrantEntry> {
// 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.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,
Expand Down Expand Up @@ -269,6 +312,10 @@ export class AgentPermissionsApi implements PermissionsApi {
return { message: dataEncodedMessage };
}

async clear():Promise<void> {
this._cachedPermissions.clear();
}

/**
* Matches the appropriate grant from an array of grants based on the provided parameters.
*
Expand Down
20 changes: 12 additions & 8 deletions packages/agent/src/sync-engine-level.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -64,15 +65,15 @@ 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<string | Buffer | Uint8Array>;
private _syncIntervalId?: ReturnType<typeof setInterval>;
private _ulidFactory: ULIDFactory;

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<string, string>(dataPath ?? 'DATA/AGENT/SYNC_STORE');
this._ulidFactory = monotonicFactory();
}
Expand All @@ -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<void> {
await this._cachedPermissionsApi.clear();
await this._permissionsApi.clear();
await this._db.clear();
}

Expand Down Expand Up @@ -133,11 +134,12 @@ export class SyncEngineLevel implements SyncEngine {
let granteeDid: string | undefined;
if (delegateDid) {
try {
const messagesReadGrant = await this._cachedPermissionsApi.getPermission({
const messagesReadGrant = await this._permissionsApi.getPermissionForRequest({
connectedDid : did,
messageType : DwnInterface.MessagesRead,
delegateDid,
protocol,
cached : true
});

permissionGrantId = messagesReadGrant.grant.id;
Expand Down Expand Up @@ -402,11 +404,12 @@ 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.getPermissionForRequest({
connectedDid : did,
messageType : DwnInterface.MessagesQuery,
delegateDid,
protocol,
cached : true
});

permissionGrantId = messagesQueryGrant.grant.id;
Expand Down Expand Up @@ -469,11 +472,12 @@ export class SyncEngineLevel implements SyncEngine {
let permissionGrantId: string | undefined;
if (delegateDid) {
try {
const messagesReadGrant = await this._cachedPermissionsApi.getPermission({
const messagesReadGrant = await this._permissionsApi.getPermissionForRequest({
connectedDid : author,
messageType : DwnInterface.MessagesRead,
delegateDid,
protocol,
cached : true
});

permissionGrantId = messagesReadGrant.grant.id;
Expand Down
1 change: 1 addition & 0 deletions packages/agent/src/test-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
21 changes: 20 additions & 1 deletion packages/agent/src/types/permissions.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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.
*/
getPermissionForRequest: (params: GetPermissionParams) => Promise<PermissionGrantEntry>;

/**
* Fetch all grants for a given author and target, optionally filtered by a specific grantee, grantor, or protocol.
*/
Expand Down Expand Up @@ -93,4 +107,9 @@ export interface PermissionsApi {
* Create a new permission revocation, optionally storing it in the DWN.
*/
createRevocation(params: CreateRevocationParams): Promise<PermissionRevocationEntry>;

/**
* Clears the cache of matched permissions.
*/
clear: () => Promise<void>;
}
Loading

0 comments on commit 7fc1f1d

Please sign in to comment.