Skip to content

Commit

Permalink
Protocol Query with or without grant, Configure with grant. (#894)
Browse files Browse the repository at this point in the history
- protocol query with regular permission grant
  - if grant is not found, author query as delegate did to get any public protocols
- protocol configure with delegate grant
- `Permission` can now include `configure` which represents `ProtocolsConfigure` of a particular protocol
- `createPermissionRequestForProtocol` now includes a grant for `ProtocolsQuery` for the protocol.
  • Loading branch information
LiranCohen authored Sep 12, 2024
1 parent b5b36e3 commit e7cb25a
Show file tree
Hide file tree
Showing 15 changed files with 711 additions and 501 deletions.
5 changes: 5 additions & 0 deletions .changeset/eighty-bikes-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@web5/api": patch
---

Enable Protocol Query/Configure with delegate Grant
8 changes: 8 additions & 0 deletions .changeset/slimy-bulldogs-kiss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@web5/agent": patch
"@web5/identity-agent": patch
"@web5/proxy-agent": patch
"@web5/user-agent": patch
---

Enable ProtocolQuery/Configure with delegate grant
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"@changesets/cli": "^2.27.5",
"@npmcli/package-json": "5.0.0",
"@typescript-eslint/eslint-plugin": "7.9.0",
"@web5/dwn-server": "0.4.9",
"@web5/dwn-server": "0.4.10",
"audit-ci": "^7.0.1",
"eslint-plugin-mocha": "10.4.3",
"globals": "^13.24.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"dependencies": {
"@noble/ciphers": "0.5.3",
"@scure/bip39": "1.2.2",
"@tbd54566975/dwn-sdk-js": "0.4.6",
"@tbd54566975/dwn-sdk-js": "0.4.7",
"@web5/common": "1.0.0",
"@web5/crypto": "workspace:*",
"@web5/dids": "workspace:*",
Expand Down
20 changes: 18 additions & 2 deletions packages/agent/src/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export type ConnectPermissionRequest = {
/**
* Shorthand for the types of permissions that can be requested.
*/
export type Permission = 'write' | 'read' | 'delete' | 'query' | 'subscribe';
export type Permission = 'write' | 'read' | 'delete' | 'query' | 'subscribe' | 'configure';

/**
* The options for creating a permission request for a given protocol.
Expand All @@ -203,11 +203,20 @@ export type ProtocolPermissionOptions = {

/**
* Creates a set of Dwn Permission Scopes to request for a given protocol.
* If no permissions are provided, the default is to request all permissions (write, read, delete, query, subscribe).
*
* If no permissions are provided, the default is to request all relevant record permissions (write, read, delete, query, subscribe).
* 'configure' is not included by default, as this gives the application a lot of control over the protocol.
*/
function createPermissionRequestForProtocol({ definition, permissions }: ProtocolPermissionOptions): ConnectPermissionRequest {
const requests: DwnPermissionScope[] = [];

// Add the ability to query for the specific protocol
requests.push({
protocol : definition.protocol,
interface : DwnInterfaceName.Protocols,
method : DwnMethodName.Query,
});

// In order to enable sync, we must request permissions for `MessagesQuery`, `MessagesRead` and `MessagesSubscribe`
requests.push({
protocol : definition.protocol,
Expand Down Expand Up @@ -261,6 +270,13 @@ function createPermissionRequestForProtocol({ definition, permissions }: Protoco
method : DwnMethodName.Subscribe,
});
break;
case 'configure':
requests.push({
protocol : definition.protocol,
interface : DwnInterfaceName.Protocols,
method : DwnMethodName.Configure,
});
break;
}
}

Expand Down
21 changes: 17 additions & 4 deletions packages/agent/src/oidc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { DwnDataEncodedRecordsWriteMessage, DwnInterface, DwnPermissionScope, Dw
import { AgentPermissionsApi } from './permissions-api.js';
import type { Web5Agent } from './types/agent.js';
import { isRecordPermissionScope } from './dwn-api.js';
import { DwnInterfaceName, DwnMethodName } from '@tbd54566975/dwn-sdk-js';

/**
* Sent to an OIDC server to authorize a client. Allows clients
Expand Down Expand Up @@ -600,6 +601,20 @@ function encryptAuthResponse({
return compactJwe;
}

function shouldUseDelegatePermission(scope: DwnPermissionScope): boolean {
// Currently all record permissions are treated as delegated permissions
// In the future only methods that modify state will be delegated and the rest will be normal permissions
if (isRecordPermissionScope(scope)) {
return true;
} else if (scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Configure) {
// ProtocolConfigure messages are also delegated, as they modify state
return true;
}

// All other permissions are not treated as delegated
return false;
}

/**
* Creates the permission grants that assign to the selectedDid the level of
* permissions that the web app requested in the {@link Web5ConnectAuthRequest}
Expand All @@ -615,9 +630,8 @@ async function createPermissionGrants(
// TODO: cleanup all grants if one fails by deleting them from the DWN: https://github.com/TBD54566975/web5-js/issues/849
const permissionGrants = await Promise.all(
scopes.map((scope) => {

// check if the scope is a records permission scope, if so it is a delegated permission
const delegated = isRecordPermissionScope(scope);
// check if the scope is a records permission scope, or a protocol configure scope, if so it should use a delegated permission.
const delegated = shouldUseDelegatePermission(scope);
return permissionsApi.createGrant({
delegated,
store : true,
Expand All @@ -626,7 +640,6 @@ async function createPermissionGrants(
dateExpires : '2040-06-25T16:09:16.693356Z', // TODO: make dateExpires optional
author : selectedDid,
});

})
);

Expand Down
18 changes: 4 additions & 14 deletions packages/agent/src/permissions-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ export class AgentPermissionsApi implements PermissionsApi {
if (scopeMessageType === messageType) {
if (isRecordsType(messageType)) {
const recordScope = scope as DwnRecordsPermissionScope;
if (!this.matchesProtocol(recordScope, protocol)) {
if (recordScope.protocol !== protocol) {
return false;
}

Expand All @@ -386,11 +386,12 @@ export class AgentPermissionsApi implements PermissionsApi {
}
} else {
const messagesScope = scope as DwnMessagesPermissionScope | DwnProtocolPermissionScope;
if (this.protocolScopeUnrestricted(messagesScope)) {
// Checks for unrestricted protocol scope, if no protocol is defined in the scope it is unrestricted
if (messagesScope.protocol === undefined) {
return true;
}

if (!this.matchesProtocol(messagesScope, protocol)) {
if (messagesScope.protocol !== protocol) {
return false;
}

Expand All @@ -401,17 +402,6 @@ export class AgentPermissionsApi implements PermissionsApi {
return false;
}

private static matchesProtocol(scope: DwnPermissionScope & { protocol?: string }, protocol?: string): boolean {
return scope.protocol !== undefined && scope.protocol === protocol;
}

/**
* Checks if the scope is restricted to a specific protocol
*/
private static protocolScopeUnrestricted(scope: DwnPermissionScope & { protocol?: string }): boolean {
return scope.protocol === undefined;
}

private static isUnrestrictedProtocolScope(scope: DwnPermissionScope & { contextId?: string, protocolPath?: string }): boolean {
return scope.contextId === undefined && scope.protocolPath === undefined;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/agent/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export async function getDwnServiceEndpointUrls(didUri: string, dereferencer: Di
}

export function getRecordAuthor(record: RecordsWriteMessage | RecordsDeleteMessage): string | undefined {
return Records.getAuthor(record);
return Message.getAuthor(record);
}

export function isRecordsWrite(obj: unknown): obj is RecordsWrite {
Expand Down
15 changes: 9 additions & 6 deletions packages/agent/tests/connect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -827,10 +827,11 @@ describe('web5 connect', function () {
});

expect(permissionRequests.protocolDefinition).to.deep.equal(protocol);
expect(permissionRequests.permissionScopes.length).to.equal(3); // only includes the sync permissions
expect(permissionRequests.permissionScopes.length).to.equal(4); // only includes the sync permissions + protocol query permission
expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Messages && scope.method === DwnMethodName.Read)).to.not.be.undefined;
expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Messages && scope.method === DwnMethodName.Query)).to.not.be.undefined;
expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Messages && scope.method === DwnMethodName.Subscribe)).to.not.be.undefined;
expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Query)).to.not.be.undefined;
});

it('should add requested permissions to the request', async () => {
Expand All @@ -854,13 +855,13 @@ describe('web5 connect', function () {

expect(permissionRequests.protocolDefinition).to.deep.equal(protocol);

// the 3 sync permissions plus the 2 requested permissions
expect(permissionRequests.permissionScopes.length).to.equal(5);
// the 3 sync permissions plus the 2 requested permissions, and a protocol query permission
expect(permissionRequests.permissionScopes.length).to.equal(6);
expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Read)).to.not.be.undefined;
expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Write)).to.not.be.undefined;
});

it('supports requesting `read`, `write`, `delete`, `query` and `subscribe` permissions', async () => {
it('supports requesting `read`, `write`, `delete`, `query`, `subscribe` and `configure` permissions', async () => {
const protocol:DwnProtocolDefinition = {
published : true,
protocol : 'https://exmaple.org/protocols/social',
Expand All @@ -876,18 +877,20 @@ describe('web5 connect', function () {
};

const permissionRequests = WalletConnect.createPermissionRequestForProtocol({
definition: protocol, permissions: ['write', 'read', 'delete', 'query', 'subscribe']
definition: protocol, permissions: ['write', 'read', 'delete', 'query', 'subscribe', 'configure']
});

expect(permissionRequests.protocolDefinition).to.deep.equal(protocol);

// the 3 sync permissions plus the 5 requested permissions
expect(permissionRequests.permissionScopes.length).to.equal(8);
expect(permissionRequests.permissionScopes.length).to.equal(10);
expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Read)).to.not.be.undefined;
expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Write)).to.not.be.undefined;
expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Delete)).to.not.be.undefined;
expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Query)).to.not.be.undefined;
expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Subscribe)).to.not.be.undefined;
expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Query)).to.not.be.undefined;
expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Configure)).to.not.be.undefined;
});
});
});
2 changes: 1 addition & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
},
"devDependencies": {
"@playwright/test": "1.45.3",
"@tbd54566975/dwn-sdk-js": "0.4.6",
"@tbd54566975/dwn-sdk-js": "0.4.7",
"@types/chai": "4.3.6",
"@types/eslint": "8.56.10",
"@types/mocha": "10.0.1",
Expand Down
57 changes: 50 additions & 7 deletions packages/api/src/dwn-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import type {
CreateGrantParams,
CreateRequestParams,
DwnRecordsInterfaces,
FetchPermissionRequestParams,
FetchPermissionsParams
} from '@web5/agent';
Expand Down Expand Up @@ -428,12 +427,32 @@ export class DwnApi {
* Configure method, used to setup a new protocol (or update) with the passed definitions
*/
configure: async (request: ProtocolsConfigureRequest): Promise<ProtocolsConfigureResponse> => {
const agentResponse = await this.agent.processDwnRequest({

const agentRequest:ProcessDwnRequest<DwnInterface.ProtocolsConfigure> = {
author : this.connectedDid,
messageParams : request.message,
messageType : DwnInterface.ProtocolsConfigure,
target : this.connectedDid
});
};

if (this.delegateDid) {
const { message: delegatedGrant } = await this.permissionsApi.getPermissionForRequest({
connectedDid : this.connectedDid,
delegateDid : this.delegateDid,
protocol : request.message.definition.protocol,
delegate : true,
cached : true,
messageType : agentRequest.messageType
});

agentRequest.messageParams = {
...agentRequest.messageParams,
delegatedGrant
};
agentRequest.granteeDid = this.delegateDid;
}

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

const { message, messageCid, reply: { status }} = agentResponse;
const response: ProtocolsConfigureResponse = { status };
Expand All @@ -457,6 +476,30 @@ export class DwnApi {
target : request.from || this.connectedDid
};

if (this.delegateDid) {
// We attempt to get a grant within a try catch, if there is no grant we will still sign the query with the delegate DID's key
// If the protocol is public, the query should be successful. This allows the app to query for public protocols without having a grant.

try {
const { grant: { id: permissionGrantId } } = await this.permissionsApi.getPermissionForRequest({
connectedDid : this.connectedDid,
delegateDid : this.delegateDid,
protocol : request.message.filter.protocol,
cached : true,
messageType : agentRequest.messageType
});

agentRequest.messageParams = {
...agentRequest.messageParams,
permissionGrantId
};
agentRequest.granteeDid = this.delegateDid;
} catch(_error:any) {
// if a grant is not found, we should author the request as the delegated DID to get public protocols
agentRequest.author = this.delegateDid;
}
}

let agentResponse: DwnResponse<DwnInterface.ProtocolsQuery>;

if (request.from) {
Expand Down Expand Up @@ -616,8 +659,8 @@ export class DwnApi {
delegatedGrant
};
agentRequest.granteeDid = this.delegateDid;
} catch(error:any) {
// set the author of the request to the delegate did
} catch(_error:any) {
// if a grant is not found, we should author the request as the delegated DID to get public records
agentRequest.author = this.delegateDid;
}
}
Expand Down Expand Up @@ -708,7 +751,7 @@ export class DwnApi {
};
agentRequest.granteeDid = this.delegateDid;
} catch(_error:any) {
// set the author of the request to the delegate did
// if a grant is not found, we should author the request as the delegated DID to get public records
agentRequest.author = this.delegateDid;
}
}
Expand Down Expand Up @@ -811,7 +854,7 @@ export class DwnApi {
};
agentRequest.granteeDid = this.delegateDid;
} catch(_error:any) {
// set the author of the request to the delegate did
// if a grant is not found, we should author the request as the delegated DID to get public records
agentRequest.author = this.delegateDid;
}
};
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/web5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type {
} from '@web5/agent';

import { Web5UserAgent } from '@web5/user-agent';
import { DwnRegistrar, WalletConnect } from '@web5/agent';
import { DwnInterface, DwnRegistrar, WalletConnect } from '@web5/agent';

import { DidApi } from './did-api.js';
import { DwnApi } from './dwn-api.js';
Expand Down
Loading

0 comments on commit e7cb25a

Please sign in to comment.