Skip to content

Commit

Permalink
use grants in records requests for DwnApi
Browse files Browse the repository at this point in the history
  • Loading branch information
LiranCohen committed Aug 5, 2024
1 parent e2e1885 commit 8a1f7bd
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 17 deletions.
174 changes: 162 additions & 12 deletions packages/api/src/dwn-api.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import type {
import {
Web5Agent,
DwnMessage,
DwnResponse,
DwnMessageParams,
DwnResponseStatus,
ProcessDwnRequest,
DwnPaginationCursor,
DwnDataEncodedRecordsWriteMessage
} from '@web5/agent';

import { isEmptyObject } from '@web5/common';
import { DwnInterface, getRecordAuthor } from '@web5/agent';
import { isEmptyObject, TtlCache } from '@web5/common';
import { DwnInterface, getRecordAuthor, DwnPermissionsUtil } from '@web5/agent';

import { Record } from './record.js';
import { dataToBlob } from './utils.js';
import { Protocol } from './protocol.js';
import { DataEncodedRecordsWriteMessage } from '@tbd54566975/dwn-sdk-js';

/**
* Represents the request payload for configuring a protocol on a Decentralized Web Node (DWN).
Expand Down Expand Up @@ -101,6 +103,9 @@ export type RecordsDeleteRequest = {
/** Optional DID specifying the remote target DWN tenant the record will be deleted from. */
from?: string;

/** Records must be scoped to a specific protocol */
protocol?: string;

/** The parameters for the delete operation. */
message: Omit<DwnMessageParams[DwnInterface.RecordsDelete], 'signer'>;
}
Expand All @@ -116,6 +121,9 @@ export type RecordsQueryRequest = {
/** Optional DID specifying the remote target DWN tenant to query from and return results. */
from?: string;

/** Records must be scoped to a specific protocol */
protocol?: string;

/** The parameters for the query operation, detailing the criteria for selecting records. */
message: Omit<DwnMessageParams[DwnInterface.RecordsQuery], 'signer'>;
}
Expand Down Expand Up @@ -143,6 +151,9 @@ export type RecordsReadRequest = {
/** Optional DID specifying the remote target DWN tenant the record will be read from. */
from?: string;

/** Records must be scoped to a specific protocol */
protocol?: string;

/** The parameters for the read operation, detailing the criteria for selecting the record. */
message: Omit<DwnMessageParams[DwnInterface.RecordsRead], 'signer'>;
}
Expand Down Expand Up @@ -215,11 +226,88 @@ export class DwnApi {
/** The DID of the DWN tenant under which operations are being performed. */
private connectedDid: string;

constructor(options: { agent: Web5Agent, connectedDid: string }) {
/** (optional) The DID of the signer when signing with permissions */
private impersonatorDid?: string;

private cachedPermissions: TtlCache<string, DataEncodedRecordsWriteMessage> = new TtlCache({ ttl: 60 * 1000 });

constructor(options: { agent: Web5Agent, connectedDid: string, impersonatorDid?: string }) {
this.agent = options.agent;
this.connectedDid = options.connectedDid;
this.impersonatorDid = options.impersonatorDid;
}

private async findDelegatedPermissionGrant<T extends DwnInterface>({ messageParams }:{
messageParams: {
messageType: T,
protocol: string,
}
}): Promise<DataEncodedRecordsWriteMessage> {
const { messageType, protocol } = messageParams;
const cacheKey = [ messageType, protocol ].join('~');
const cachedPermission = this.cachedPermissions.get(cacheKey);
if (cachedPermission) {
return cachedPermission;
}

const permissions = await this.fetchGrants({
grantor : this.connectedDid,
grantee : this.impersonatorDid
});

// get the delegate grants that match the messageParams and are associated with the connectedDid as the grantor
const delegateGrant = await DwnPermissionsUtil.matchGrantFromArray(
this.connectedDid,
this.impersonatorDid,
messageParams,
permissions,
true
);

if (!delegateGrant) {
throw new Error(`AgentDwnApi: No permissions found for ${cacheKey}`);
}

this.cachedPermissions.set(cacheKey, delegateGrant.message);
return delegateGrant.message;
}

/**
* Performs a RecordsQuery for permission grants that match the given parameters.
*/
private async fetchGrants({ author, target, grantee, grantor }: {
/** author of the query message, defaults to grantee */
author?: string,
/** target of the query message, defaults to author */
target?: string,
grantor: string,
grantee: string
}): Promise<DwnDataEncodedRecordsWriteMessage[]> {
// if no author is provided, use the grantee's DID
author ??= grantee;
// if no target is explicitly provided, use the author
target ??= author;

const { reply: grantsReply } = await this.agent.processDwnRequest({
author,
target,
messageType : DwnInterface.RecordsQuery,
messageParams : {
filter: {
author : grantor, // the author of the grant would be the grantor and the logical author of the message
recipient : grantee, // the recipient of the grant would be the grantee
...DwnPermissionsUtil.permissionsProtocolParams('grant')
}
}
});

if (grantsReply.status.code !== 200) {
throw new Error(`AgentDwnApi: Failed to fetch grants: ${grantsReply.status.detail}`);
}

return grantsReply.entries! as DwnDataEncodedRecordsWriteMessage[];
};

/**
* API to interact with DWN protocols (e.g., `dwn.protocols.configure()`).
*/
Expand Down Expand Up @@ -345,6 +433,20 @@ export class DwnApi {
target : request.from || this.connectedDid
};

if (this.impersonatorDid) {
// if an app is scoped down to a specific protocolPath or contextId, it must include those filters in the read request
const delegatedGrant = await this.findDelegatedPermissionGrant({
messageParams: {
messageType : DwnInterface.RecordsQuery,
protocol : request.protocol,
}
});

// set the required delegated grant and grantee DID for the read operation
agentRequest.messageParams.delegatedGrant = delegatedGrant;
agentRequest.granteeDid = this.impersonatorDid;
}

let agentResponse: DwnResponse<DwnInterface.RecordsDelete>;

if (request.from) {
Expand Down Expand Up @@ -378,6 +480,21 @@ export class DwnApi {
target : request.from || this.connectedDid
};

if (this.impersonatorDid) {
// if an app is scoped down to a specific protocolPath or contextId, it must include those filters in the read request
const delegatedGrant = await this.findDelegatedPermissionGrant({
messageParams: {
messageType : DwnInterface.RecordsQuery,
protocol : agentRequest.messageParams.filter.protocol,
}
});

// set the required delegated grant and grantee DID for the read operation
agentRequest.messageParams.delegatedGrant = delegatedGrant;
agentRequest.granteeDid = this.impersonatorDid;
}


let agentResponse: DwnResponse<DwnInterface.RecordsQuery>;

if (request.from) {
Expand Down Expand Up @@ -420,7 +537,7 @@ export class DwnApi {
},

/**
* Read a single record based on the given filter
* Read a single record based on the given filter.
*/
read: async (request: RecordsReadRequest): Promise<RecordsReadResponse> => {
const agentRequest: ProcessDwnRequest<DwnInterface.RecordsRead> = {
Expand All @@ -439,6 +556,20 @@ export class DwnApi {
target : request.from || this.connectedDid
};

if (this.impersonatorDid) {
// if an app is scoped down to a specific protocolPath or contextId, it must include those filters in the read request
const delegatedGrant = await this.findDelegatedPermissionGrant({
messageParams: {
messageType : DwnInterface.RecordsRead,
protocol : agentRequest.messageParams.filter.protocol
}
});

// set the required delegated grant and grantee DID for the read operation
agentRequest.messageParams.delegatedGrant = delegatedGrant;
agentRequest.granteeDid = this.impersonatorDid;
}

let agentResponse: DwnResponse<DwnInterface.RecordsRead>;

if (request.from) {
Expand Down Expand Up @@ -491,14 +622,33 @@ export class DwnApi {
write: async (request: RecordsWriteRequest): Promise<RecordsWriteResponse> => {
const { dataBlob, dataFormat } = dataToBlob(request.data, request.message?.dataFormat);

const agentResponse = await this.agent.processDwnRequest({
author : this.connectedDid,
dataStream : dataBlob,
messageParams : { ...request.message, dataFormat },
messageType : DwnInterface.RecordsWrite,
const dwnRequestParams: ProcessDwnRequest<DwnInterface.RecordsWrite> = {
store : request.store,
target : this.connectedDid
});
messageType : DwnInterface.RecordsWrite,
messageParams : {
...request.message,
dataFormat
},
author : this.connectedDid,
target : this.connectedDid,
dataStream : dataBlob
};

// if impersonation is enabled, fetch the delegated grant to use with the write operation
if (this.impersonatorDid) {
const delegatedGrant = await this.findDelegatedPermissionGrant({
messageParams: {
messageType : DwnInterface.RecordsWrite,
protocol : dwnRequestParams.messageParams.protocol,
}
});

// set the required delegated grant and grantee DID for the write operation
dwnRequestParams.messageParams.delegatedGrant = delegatedGrant;
dwnRequestParams.granteeDid = this.impersonatorDid;
};

const agentResponse = await this.agent.processDwnRequest(dwnRequestParams);

const { message: responseMessage, reply: { status } } = agentResponse;

Expand Down
6 changes: 1 addition & 5 deletions packages/api/tests/dwn-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1240,11 +1240,7 @@ describe('DwnApi', () => {
expect(writeResult.record).to.exist;

// Delete the record
await dwnAlice.records.delete({
message: {
recordId: writeResult.record!.id
}
});
await writeResult.record!.delete();

const result = await dwnAlice.records.read({
message: {
Expand Down

0 comments on commit 8a1f7bd

Please sign in to comment.