Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Protocol Query with or without grant, Configure with grant. #894

Merged
merged 11 commits into from
Sep 12, 2024
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 { 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 @@
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;
}

Check warning on line 616 in packages/agent/src/oidc.ts

View check run for this annotation

Codecov / codecov/patch

packages/agent/src/oidc.ts#L610-L616

Added lines #L610 - L616 were not covered by tests
Comment on lines +610 to +616
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add tests to cover these lines before merge?


/**
* 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 @@
// 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 @@
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
Loading