Skip to content

Commit 77a7e38

Browse files
FrederikBoldingshanejonasadonesky1
authored
Add wallet_revokePermissions RPC method (#1889)
## Explanation Adds a `wallet_revokePermissions` RPC method that can be used to revoke permissions that a subject has been granted in the PermissionController. This initial version does not support revoking caveats and will revoke the entire requested permission key. ## References <!-- Are there any issues that this pull request is tied to? Are there other links that reviewers should consult to understand these changes better? For example: * Fixes #12345 * Related to #67890 --> ## Changelog <!-- If you're making any consumer-facing changes, list those changes here as if you were updating a changelog, using the template below as a guide. (CATEGORY is one of BREAKING, ADDED, CHANGED, DEPRECATED, REMOVED, or FIXED. For security-related issues, follow the Security Advisory process.) Please take care to name the exact pieces of the API you've added or changed (e.g. types, interfaces, functions, or methods). If there are any breaking changes, make sure to offer a solution for consumers to follow once they upgrade to the changes. Finally, if you're only making changes to development scripts or tests, you may replace the template below with "None". --> ### `@metamask/permission-controller` - **Added**: Added `wallet_revokePermissions` RPC method ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate --------- Co-authored-by: Shane <[email protected]> Co-authored-by: Alex Donesky <[email protected]>
1 parent fb98de9 commit 77a7e38

File tree

4 files changed

+255
-1
lines changed

4 files changed

+255
-1
lines changed

packages/permission-controller/src/rpc-methods/index.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ import type { GetPermissionsHooks } from './getPermissions';
22
import { getPermissionsHandler } from './getPermissions';
33
import type { RequestPermissionsHooks } from './requestPermissions';
44
import { requestPermissionsHandler } from './requestPermissions';
5+
import type { RevokePermissionsHooks } from './revokePermissions';
6+
import { revokePermissionsHandler } from './revokePermissions';
57

68
export type PermittedRpcMethodHooks = RequestPermissionsHooks &
7-
GetPermissionsHooks;
9+
GetPermissionsHooks &
10+
RevokePermissionsHooks;
811

912
export const handlers = [
1013
requestPermissionsHandler,
1114
getPermissionsHandler,
15+
revokePermissionsHandler,
1216
] as const;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { JsonRpcEngine } from '@metamask/json-rpc-engine';
2+
import { rpcErrors } from '@metamask/rpc-errors';
3+
4+
import { revokePermissionsHandler } from './revokePermissions';
5+
6+
describe('revokePermissionsHandler', () => {
7+
it('has the expected shape', () => {
8+
expect(revokePermissionsHandler).toStrictEqual({
9+
methodNames: ['wallet_revokePermissions'],
10+
implementation: expect.any(Function),
11+
hookNames: {
12+
revokePermissionsForOrigin: true,
13+
},
14+
});
15+
});
16+
});
17+
18+
describe('revokePermissions RPC method', () => {
19+
it('revokes permissions using revokePermissionsForOrigin', async () => {
20+
const { implementation } = revokePermissionsHandler;
21+
const mockRevokePermissionsForOrigin = jest.fn();
22+
23+
const engine = new JsonRpcEngine();
24+
engine.push((req, res, next, end) =>
25+
implementation(req as any, res as any, next, end, {
26+
revokePermissionsForOrigin: mockRevokePermissionsForOrigin,
27+
}),
28+
);
29+
30+
const response: any = await engine.handle({
31+
jsonrpc: '2.0',
32+
id: 1,
33+
method: 'wallet_revokePermissions',
34+
params: [
35+
{
36+
snap_dialog: {},
37+
},
38+
],
39+
});
40+
41+
expect(response.result).toBeNull();
42+
expect(mockRevokePermissionsForOrigin).toHaveBeenCalledTimes(1);
43+
expect(mockRevokePermissionsForOrigin).toHaveBeenCalledWith([
44+
'snap_dialog',
45+
]);
46+
});
47+
48+
it('returns an error if the request params is a plain object', async () => {
49+
const { implementation } = revokePermissionsHandler;
50+
const mockRevokePermissionsForOrigin = jest.fn();
51+
52+
const engine = new JsonRpcEngine();
53+
engine.push((req, res, next, end) =>
54+
implementation(req as any, res as any, next, end, {
55+
revokePermissionsForOrigin: mockRevokePermissionsForOrigin,
56+
}),
57+
);
58+
59+
const req = {
60+
jsonrpc: '2.0',
61+
id: 1,
62+
method: 'wallet_revokePermissions',
63+
params: {},
64+
};
65+
66+
const expectedError = rpcErrors
67+
.invalidParams({
68+
data: { request: { ...req } },
69+
})
70+
.serialize();
71+
delete expectedError.stack;
72+
73+
const response: any = await engine.handle(req as any);
74+
delete response.error.stack;
75+
expect(response.error).toStrictEqual(expectedError);
76+
expect(mockRevokePermissionsForOrigin).not.toHaveBeenCalled();
77+
});
78+
79+
it('returns an error if the permissionKeys is a plain object', async () => {
80+
const { implementation } = revokePermissionsHandler;
81+
const mockRevokePermissionsForOrigin = jest.fn();
82+
83+
const engine = new JsonRpcEngine();
84+
engine.push((req, res, next, end) =>
85+
implementation(req as any, res as any, next, end, {
86+
revokePermissionsForOrigin: mockRevokePermissionsForOrigin,
87+
}),
88+
);
89+
90+
const req = {
91+
jsonrpc: '2.0',
92+
id: 1,
93+
method: 'wallet_revokePermissions',
94+
params: [{}],
95+
};
96+
97+
const expectedError = rpcErrors
98+
.invalidParams({
99+
data: { request: { ...req } },
100+
})
101+
.serialize();
102+
delete expectedError.stack;
103+
104+
const response: any = await engine.handle(req as any);
105+
delete response.error.stack;
106+
expect(response.error).toStrictEqual(expectedError);
107+
expect(mockRevokePermissionsForOrigin).not.toHaveBeenCalled();
108+
});
109+
110+
it('returns an error if the params are not set', async () => {
111+
const { implementation } = revokePermissionsHandler;
112+
const mockRevokePermissionsForOrigin = jest.fn();
113+
114+
const engine = new JsonRpcEngine();
115+
engine.push((req, res, next, end) =>
116+
implementation(req as any, res as any, next, end, {
117+
revokePermissionsForOrigin: mockRevokePermissionsForOrigin,
118+
}),
119+
);
120+
121+
const req = {
122+
jsonrpc: '2.0',
123+
id: 1,
124+
method: 'wallet_revokePermissions',
125+
};
126+
127+
const expectedError = rpcErrors
128+
.invalidParams({
129+
data: { request: { ...req } },
130+
})
131+
.serialize();
132+
delete expectedError.stack;
133+
134+
const response: any = await engine.handle(req as any);
135+
delete response.error.stack;
136+
expect(response.error).toStrictEqual(expectedError);
137+
expect(mockRevokePermissionsForOrigin).not.toHaveBeenCalled();
138+
});
139+
140+
it('returns an error if the request params is an empty array', async () => {
141+
const { implementation } = revokePermissionsHandler;
142+
const mockRevokePermissionsForOrigin = jest.fn();
143+
144+
const engine = new JsonRpcEngine();
145+
engine.push((req, res, next, end) =>
146+
implementation(req as any, res as any, next, end, {
147+
revokePermissionsForOrigin: mockRevokePermissionsForOrigin,
148+
}),
149+
);
150+
151+
const req = {
152+
jsonrpc: '2.0',
153+
id: 1,
154+
method: 'wallet_revokePermissions',
155+
params: [],
156+
};
157+
158+
const expectedError = rpcErrors
159+
.invalidParams({
160+
data: { request: { ...req } },
161+
})
162+
.serialize();
163+
delete expectedError.stack;
164+
165+
const response: any = await engine.handle(req as any);
166+
delete response.error.stack;
167+
expect(response.error).toStrictEqual(expectedError);
168+
expect(mockRevokePermissionsForOrigin).not.toHaveBeenCalled();
169+
});
170+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine';
2+
import {
3+
isNonEmptyArray,
4+
type Json,
5+
type JsonRpcRequest,
6+
type NonEmptyArray,
7+
type PendingJsonRpcResponse,
8+
} from '@metamask/utils';
9+
10+
import { invalidParams } from '../errors';
11+
import type { PermissionConstraint } from '../Permission';
12+
import type { PermittedHandlerExport } from '../utils';
13+
import { MethodNames } from '../utils';
14+
15+
export const revokePermissionsHandler: PermittedHandlerExport<
16+
RevokePermissionsHooks,
17+
RevokePermissionArgs,
18+
null
19+
> = {
20+
methodNames: [MethodNames.revokePermissions],
21+
implementation: revokePermissionsImplementation,
22+
hookNames: {
23+
revokePermissionsForOrigin: true,
24+
},
25+
};
26+
27+
type RevokePermissionArgs = Record<
28+
PermissionConstraint['parentCapability'],
29+
Json
30+
>;
31+
32+
type RevokePermissions = (
33+
permissions: NonEmptyArray<PermissionConstraint['parentCapability']>,
34+
) => void;
35+
36+
export type RevokePermissionsHooks = {
37+
revokePermissionsForOrigin: RevokePermissions;
38+
};
39+
40+
/**
41+
* Revoke Permissions implementation to be used in JsonRpcEngine middleware.
42+
*
43+
* @param req - The JsonRpcEngine request
44+
* @param res - The JsonRpcEngine result object
45+
* @param _next - JsonRpcEngine next() callback - unused
46+
* @param end - JsonRpcEngine end() callback
47+
* @param options - Method hooks passed to the method implementation
48+
* @param options.revokePermissionsForOrigin - A hook that revokes given permission keys for an origin
49+
* @returns A promise that resolves to nothing
50+
*/
51+
async function revokePermissionsImplementation(
52+
req: JsonRpcRequest<RevokePermissionArgs>,
53+
res: PendingJsonRpcResponse<null>,
54+
_next: unknown,
55+
end: JsonRpcEngineEndCallback,
56+
{ revokePermissionsForOrigin }: RevokePermissionsHooks,
57+
): Promise<void> {
58+
const { params } = req;
59+
60+
const param = params?.[0];
61+
62+
if (!param) {
63+
return end(invalidParams({ data: { request: req } }));
64+
}
65+
66+
// For now, this API revokes the entire permission key
67+
// even if caveats are specified.
68+
const permissionKeys = Object.keys(param);
69+
70+
if (!isNonEmptyArray(permissionKeys)) {
71+
return end(invalidParams({ data: { request: req } }));
72+
}
73+
74+
revokePermissionsForOrigin(permissionKeys);
75+
76+
res.result = null;
77+
78+
return end();
79+
}

packages/permission-controller/src/utils.ts

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type {
2121
export enum MethodNames {
2222
requestPermissions = 'wallet_requestPermissions',
2323
getPermissions = 'wallet_getPermissions',
24+
revokePermissions = 'wallet_revokePermissions',
2425
}
2526

2627
/**

0 commit comments

Comments
 (0)