From 70623844a789d4b6c8e35712410a7d90f45227b3 Mon Sep 17 00:00:00 2001 From: Guillaume Roux Date: Thu, 31 Oct 2024 12:36:23 +0100 Subject: [PATCH 1/5] Move `snap_manageAccounts` to a gated permitted method --- packages/snaps-rpc-methods/jest.config.js | 8 +- .../snaps-rpc-methods/src/permissions.test.ts | 9 - .../src/permitted/handlers.ts | 2 + .../snaps-rpc-methods/src/permitted/index.ts | 4 +- .../src/permitted/manageAccounts.test.ts | 208 ++++++++++++++++++ .../src/permitted/manageAccounts.ts | 128 +++++++++++ .../snaps-rpc-methods/src/restricted/index.ts | 4 - .../src/restricted/manageAccounts.test.ts | 158 ------------- .../src/restricted/manageAccounts.ts | 119 ---------- .../src/types/methods/manage-accounts.ts | 12 +- .../snaps-utils/src/manifest/validation.ts | 1 - 11 files changed, 349 insertions(+), 304 deletions(-) create mode 100644 packages/snaps-rpc-methods/src/permitted/manageAccounts.test.ts create mode 100644 packages/snaps-rpc-methods/src/permitted/manageAccounts.ts delete mode 100644 packages/snaps-rpc-methods/src/restricted/manageAccounts.test.ts delete mode 100644 packages/snaps-rpc-methods/src/restricted/manageAccounts.ts diff --git a/packages/snaps-rpc-methods/jest.config.js b/packages/snaps-rpc-methods/jest.config.js index 266c39bf23..991d0efef6 100644 --- a/packages/snaps-rpc-methods/jest.config.js +++ b/packages/snaps-rpc-methods/jest.config.js @@ -10,10 +10,10 @@ module.exports = deepmerge(baseConfig, { ], coverageThreshold: { global: { - branches: 92.85, - functions: 97.23, - lines: 97.8, - statements: 97.31, + branches: 92.88, + functions: 97.22, + lines: 97.81, + statements: 97.32, }, }, }); diff --git a/packages/snaps-rpc-methods/src/permissions.test.ts b/packages/snaps-rpc-methods/src/permissions.test.ts index 50f911b100..5f1c57d907 100644 --- a/packages/snaps-rpc-methods/src/permissions.test.ts +++ b/packages/snaps-rpc-methods/src/permissions.test.ts @@ -211,15 +211,6 @@ describe('buildSnapRestrictedMethodSpecifications', () => { ], "targetName": "snap_getPreferences", }, - "snap_manageAccounts": { - "allowedCaveats": null, - "methodImplementation": [Function], - "permissionType": "RestrictedMethod", - "subjectTypes": [ - "snap", - ], - "targetName": "snap_manageAccounts", - }, "snap_manageState": { "allowedCaveats": null, "methodImplementation": [Function], diff --git a/packages/snaps-rpc-methods/src/permitted/handlers.ts b/packages/snaps-rpc-methods/src/permitted/handlers.ts index 5bfacdacd4..0ad2776c62 100644 --- a/packages/snaps-rpc-methods/src/permitted/handlers.ts +++ b/packages/snaps-rpc-methods/src/permitted/handlers.ts @@ -8,6 +8,7 @@ import { getInterfaceStateHandler } from './getInterfaceState'; import { getSnapsHandler } from './getSnaps'; import { invokeKeyringHandler } from './invokeKeyring'; import { invokeSnapSugarHandler } from './invokeSnapSugar'; +import { manageAccountsHandler } from './manageAccounts'; import { requestSnapsHandler } from './requestSnaps'; import { resolveInterfaceHandler } from './resolveInterface'; import { updateInterfaceHandler } from './updateInterface'; @@ -27,6 +28,7 @@ export const methodHandlers = { snap_resolveInterface: resolveInterfaceHandler, snap_getCurrencyRate: getCurrencyRateHandler, snap_experimentalProviderRequest: providerRequestHandler, + snap_manageAccounts: manageAccountsHandler, }; /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/packages/snaps-rpc-methods/src/permitted/index.ts b/packages/snaps-rpc-methods/src/permitted/index.ts index 5aa676fce3..1b0293b9c4 100644 --- a/packages/snaps-rpc-methods/src/permitted/index.ts +++ b/packages/snaps-rpc-methods/src/permitted/index.ts @@ -5,6 +5,7 @@ import type { GetClientStatusHooks } from './getClientStatus'; import type { GetCurrencyRateMethodHooks } from './getCurrencyRate'; import type { GetInterfaceStateMethodHooks } from './getInterfaceState'; import type { GetSnapsHooks } from './getSnaps'; +import type { ManageAccountsMethodHooks } from './manageAccounts'; import type { RequestSnapsHooks } from './requestSnaps'; import type { ResolveInterfaceMethodHooks } from './resolveInterface'; import type { UpdateInterfaceMethodHooks } from './updateInterface'; @@ -18,7 +19,8 @@ export type PermittedRpcMethodHooks = GetAllSnapsHooks & GetInterfaceStateMethodHooks & ResolveInterfaceMethodHooks & GetCurrencyRateMethodHooks & - ProviderRequestMethodHooks; + ProviderRequestMethodHooks & + ManageAccountsMethodHooks; export * from './handlers'; export * from './middleware'; diff --git a/packages/snaps-rpc-methods/src/permitted/manageAccounts.test.ts b/packages/snaps-rpc-methods/src/permitted/manageAccounts.test.ts new file mode 100644 index 0000000000..e4774c4362 --- /dev/null +++ b/packages/snaps-rpc-methods/src/permitted/manageAccounts.test.ts @@ -0,0 +1,208 @@ +import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { rpcErrors } from '@metamask/rpc-errors'; +import type { ManageAccountsResult } from '@metamask/snaps-sdk'; +import type { + JsonRpcFailure, + JsonRpcRequest, + PendingJsonRpcResponse, +} from '@metamask/utils'; + +import type { ManageAccountsParameters } from './manageAccounts'; +import { manageAccountsHandler } from './manageAccounts'; + +describe('snap_manageAccounts', () => { + describe('manageAccountsHandler', () => { + it('has the expected shape', () => { + expect(manageAccountsHandler).toMatchObject({ + methodNames: ['snap_manageAccounts'], + implementation: expect.any(Function), + hookNames: { + hasPermission: true, + handleKeyringSnapMessage: true, + }, + }); + }); + }); + + describe('implementation', () => { + it('returns the result from the `handleKeyringSnapMessage` hook', async () => { + const { implementation } = manageAccountsHandler; + + const hasPermission = jest.fn().mockReturnValue(true); + const handleKeyringSnapMessage = jest.fn().mockReturnValue('foo'); + + const hooks = { + hasPermission, + handleKeyringSnapMessage, + }; + + const engine = new JsonRpcEngine(); + + engine.push((request, response, next, end) => { + const result = implementation( + request as JsonRpcRequest, + response as PendingJsonRpcResponse, + next, + end, + hooks, + ); + + result?.catch(end); + }); + + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'snap_manageAccounts', + params: { + method: 'foo', + params: { bar: 'baz' }, + }, + }); + + expect(handleKeyringSnapMessage).toHaveBeenCalledWith({ + method: 'foo', + params: { bar: 'baz' }, + }); + + expect(response).toStrictEqual({ jsonrpc: '2.0', id: 1, result: 'foo' }); + }); + + it('throws an error if the snap does not have permission', async () => { + const { implementation } = manageAccountsHandler; + + const hasPermission = jest.fn().mockReturnValue(false); + const handleKeyringSnapMessage = jest.fn().mockReturnValue('foo'); + + const hooks = { + hasPermission, + handleKeyringSnapMessage, + }; + + const engine = new JsonRpcEngine(); + + engine.push((request, response, next, end) => { + const result = implementation( + request as JsonRpcRequest, + response as PendingJsonRpcResponse, + next, + end, + hooks, + ); + + result?.catch(end); + }); + + const response = (await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'snap_manageAccounts', + params: { + method: 'foo', + params: { bar: 'baz' }, + }, + })) as JsonRpcFailure; + + expect(response.error).toStrictEqual({ + ...rpcErrors.methodNotFound().serialize(), + stack: expect.any(String), + }); + }); + + it('throws an error if the `handleKeyringSnapMessage` hook throws', async () => { + const { implementation } = manageAccountsHandler; + + const hasPermission = jest.fn().mockReturnValue(true); + const handleKeyringSnapMessage = jest + .fn() + .mockRejectedValue(new Error('foo')); + + const hooks = { + hasPermission, + handleKeyringSnapMessage, + }; + + const engine = new JsonRpcEngine(); + + engine.push((request, response, next, end) => { + const result = implementation( + request as JsonRpcRequest, + response as PendingJsonRpcResponse, + next, + end, + hooks, + ); + + result?.catch(end); + }); + + const response = (await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'snap_manageAccounts', + params: { + method: 'foo', + params: { bar: 'baz' }, + }, + })) as JsonRpcFailure; + + expect(response.error).toStrictEqual({ + code: -32603, + message: 'foo', + data: { + cause: { + message: 'foo', + stack: expect.any(String), + }, + }, + }); + }); + + it('throws on invalid params', async () => { + const { implementation } = manageAccountsHandler; + + const hasPermission = jest.fn().mockReturnValue(true); + const handleKeyringSnapMessage = jest.fn().mockReturnValue('foo'); + + const hooks = { + hasPermission, + handleKeyringSnapMessage, + }; + + const engine = new JsonRpcEngine(); + + engine.push((request, response, next, end) => { + const result = implementation( + request as JsonRpcRequest, + response as PendingJsonRpcResponse, + next, + end, + hooks, + ); + + result?.catch(end); + }); + + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'snap_manageAccounts', + params: { + method: 'foo', + params: 42, + }, + }); + + expect(response).toStrictEqual({ + jsonrpc: '2.0', + id: 1, + error: { + code: -32602, + message: + 'Invalid params: At path: params -- Expected the value to satisfy a union of `array | record`, but received: 42.', + stack: expect.any(String), + }, + }); + }); + }); +}); diff --git a/packages/snaps-rpc-methods/src/permitted/manageAccounts.ts b/packages/snaps-rpc-methods/src/permitted/manageAccounts.ts new file mode 100644 index 0000000000..ea2be888a3 --- /dev/null +++ b/packages/snaps-rpc-methods/src/permitted/manageAccounts.ts @@ -0,0 +1,128 @@ +import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { PermittedHandlerExport } from '@metamask/permission-controller'; +import { rpcErrors } from '@metamask/rpc-errors'; +import type { + ManageAccountsParams, + ManageAccountsResult, +} from '@metamask/snaps-sdk'; +import { type InferMatching } from '@metamask/snaps-utils'; +import { + array, + create, + object, + optional, + record, + string, + StructError, + union, +} from '@metamask/superstruct'; +import type { + Json, + JsonRpcRequest, + PendingJsonRpcResponse, +} from '@metamask/utils'; +import { JsonStruct } from '@metamask/utils'; + +import { SnapEndowments } from '../endowments'; +import type { MethodHooksObject } from '../utils'; + +const hookNames: MethodHooksObject = { + hasPermission: true, + handleKeyringSnapMessage: true, +}; + +export type ManageAccountsMethodHooks = { + /** + * Checks if the current snap has a permission. + * + * @param permissionName - The name of the permission. + * @returns Whether the snap has the permission. + */ + hasPermission: (permissionName: string) => boolean; + /** + * Handles the keyring snap message. + * + * @returns The snap keyring message result. + */ + handleKeyringSnapMessage: ( + message: ManageAccountsParameters, + ) => Promise; +}; + +export const manageAccountsHandler: PermittedHandlerExport< + ManageAccountsMethodHooks, + ManageAccountsParams, + ManageAccountsResult +> = { + methodNames: ['snap_manageAccounts'], + implementation: getManageAccountsImplementation, + hookNames, +}; + +const ManageAccountsParametersStruct = object({ + method: string(), + params: optional(union([array(JsonStruct), record(string(), JsonStruct)])), +}); + +export type ManageAccountsParameters = InferMatching< + typeof ManageAccountsParametersStruct, + ManageAccountsParams +>; + +/** + * The `snap_manageAccounts` method implementation. + * + * @param req - The JSON-RPC request object. + * @param res - The JSON-RPC response object. + * @param _next - The `json-rpc-engine` "next" callback. Not used by this + * function. + * @param end - The `json-rpc-engine` "end" callback. + * @param hooks - The RPC method hooks. + * @param hooks.hasPermission - The function to check if the snap has a permission. + * @param hooks.handleKeyringSnapMessage - The function to handle the keyring snap message. + * @returns Nothing. + */ +async function getManageAccountsImplementation( + req: JsonRpcRequest, + res: PendingJsonRpcResponse, + _next: unknown, + end: JsonRpcEngineEndCallback, + { hasPermission, handleKeyringSnapMessage }: ManageAccountsMethodHooks, +): Promise { + if (!hasPermission(SnapEndowments.Keyring)) { + return end(rpcErrors.methodNotFound()); + } + + const { params } = req; + + try { + const validatedParams = getValidatedParams(params); + + res.result = await handleKeyringSnapMessage(validatedParams); + } catch (error) { + return end(error); + } + + return end(); +} + +/** + * Validate the manageAccounts method `params` and returns them cast to the correct + * type. Throws if validation fails. + * + * @param params - The unvalidated params object from the method request. + * @returns The validated manageAccounts method parameter object. + */ +function getValidatedParams(params: unknown): ManageAccountsParameters { + try { + return create(params, ManageAccountsParametersStruct); + } catch (error) { + if (error instanceof StructError) { + throw rpcErrors.invalidParams({ + message: `Invalid params: ${error.message}.`, + }); + } + /* istanbul ignore next */ + throw rpcErrors.internal(); + } +} diff --git a/packages/snaps-rpc-methods/src/restricted/index.ts b/packages/snaps-rpc-methods/src/restricted/index.ts index 93722da6d8..bc2c2c2f99 100644 --- a/packages/snaps-rpc-methods/src/restricted/index.ts +++ b/packages/snaps-rpc-methods/src/restricted/index.ts @@ -14,8 +14,6 @@ import type { GetPreferencesMethodHooks } from './getPreferences'; import { getPreferencesBuilder } from './getPreferences'; import type { InvokeSnapMethodHooks } from './invokeSnap'; import { invokeSnapBuilder } from './invokeSnap'; -import type { ManageAccountsMethodHooks } from './manageAccounts'; -import { manageAccountsBuilder } from './manageAccounts'; import type { ManageStateMethodHooks } from './manageState'; import { manageStateBuilder } from './manageState'; import type { NotifyMethodHooks } from './notify'; @@ -32,7 +30,6 @@ export type RestrictedMethodHooks = DialogMethodHooks & InvokeSnapMethodHooks & ManageStateMethodHooks & NotifyMethodHooks & - ManageAccountsMethodHooks & GetLocaleMethodHooks & GetPreferencesMethodHooks; @@ -45,7 +42,6 @@ export const restrictedMethodPermissionBuilders = { [invokeSnapBuilder.targetName]: invokeSnapBuilder, [manageStateBuilder.targetName]: manageStateBuilder, [notifyBuilder.targetName]: notifyBuilder, - [manageAccountsBuilder.targetName]: manageAccountsBuilder, [getLocaleBuilder.targetName]: getLocaleBuilder, [getPreferencesBuilder.targetName]: getPreferencesBuilder, } as const; diff --git a/packages/snaps-rpc-methods/src/restricted/manageAccounts.test.ts b/packages/snaps-rpc-methods/src/restricted/manageAccounts.test.ts deleted file mode 100644 index 44503f44a3..0000000000 --- a/packages/snaps-rpc-methods/src/restricted/manageAccounts.test.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { SubjectType, PermissionType } from '@metamask/permission-controller'; -import { MOCK_SNAP_ID } from '@metamask/snaps-utils/test-utils'; - -import { - methodName, - manageAccountsBuilder, - manageAccountsImplementation, - specificationBuilder, -} from './manageAccounts'; - -// To Do: -// Move the class SnapKeyring to it's own module -// add mock the method in this test instead of the entire class -class SnapKeyringMock { - static type = 'Snap Keyring'; - - accounts: string[] = []; - - handleKeyringSnapMessage = async ( - _origin: string, - _params: any, - ): Promise => { - return true; - }; -} - -describe('specification', () => { - it('builds specification', () => { - const methodHooks = { - getSnapKeyring: jest.fn(), - }; - - expect( - specificationBuilder({ - allowedCaveats: null, - methodHooks, - }), - ).toStrictEqual({ - allowedCaveats: null, - methodImplementation: expect.anything(), - permissionType: PermissionType.RestrictedMethod, - targetName: methodName, - subjectTypes: [SubjectType.Snap], - }); - }); -}); - -describe('builder', () => { - it('has the expected shape', () => { - expect(manageAccountsBuilder).toMatchObject({ - targetName: methodName, - specificationBuilder: expect.any(Function), - methodHooks: { - getSnapKeyring: true, - }, - }); - }); - - it('builder outputs expected specification', () => { - expect( - manageAccountsBuilder.specificationBuilder({ - methodHooks: { - getSnapKeyring: jest.fn(), - }, - }), - ).toMatchObject({ - permissionType: PermissionType.RestrictedMethod, - targetName: methodName, - allowedCaveats: null, - methodImplementation: expect.any(Function), - }); - }); -}); - -describe('manageAccountsImplementation', () => { - const MOCK_CAIP_10_ACCOUNT = - 'eip155:1:0xab16a96D359eC26a11e2C2b3d8f8B8942d5Bfcdb'; - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should throw params are not set', async () => { - const mockKeyring = new SnapKeyringMock(); - const getSnapKeyring = jest.fn().mockResolvedValue(mockKeyring); - - const manageAccounts = manageAccountsImplementation({ - getSnapKeyring, - }); - - await expect( - manageAccounts({ - method: 'snap_manageAccounts', - context: { - origin: MOCK_SNAP_ID, - }, - // @ts-expect-error Error expected. - params: {}, - }), - ).rejects.toThrow( - 'Expected the value to satisfy a union of `object | object`, but received: [object Object]', - ); - }); - - it('should throw params accountId is not set', async () => { - const mockKeyring = new SnapKeyringMock(); - const getSnapKeyring = jest.fn().mockResolvedValue(mockKeyring); - - const manageAccounts = manageAccountsImplementation({ - getSnapKeyring, - }); - - await expect( - manageAccounts({ - method: 'snap_manageAccounts', - context: { - origin: MOCK_SNAP_ID, - }, - // @ts-expect-error Error expected. - params: { method: 123, params: {} }, - }), - ).rejects.toThrow( - 'Expected the value to satisfy a union of `object | object`, but received: [object Object]', - ); - }); - - it('should route request to snap keyring', async () => { - const mockKeyring = new SnapKeyringMock(); - const getSnapKeyring = jest.fn().mockResolvedValue(mockKeyring); - - const createAccountSpy = jest - .spyOn(mockKeyring, 'handleKeyringSnapMessage') - .mockResolvedValue(true); - - const manageAccounts = manageAccountsImplementation({ - getSnapKeyring, - }); - - const requestResponse = await manageAccounts({ - method: 'snap_manageAccounts', - context: { - origin: MOCK_SNAP_ID, - }, - params: { - method: 'deleteAccount', - params: { accountId: MOCK_CAIP_10_ACCOUNT }, - }, - }); - - expect(createAccountSpy).toHaveBeenCalledTimes(1); - expect(createAccountSpy).toHaveBeenCalledWith(MOCK_SNAP_ID, { - method: 'deleteAccount', - params: { accountId: MOCK_CAIP_10_ACCOUNT }, - }); - expect(requestResponse).toBe(true); - createAccountSpy.mockClear(); - }); -}); diff --git a/packages/snaps-rpc-methods/src/restricted/manageAccounts.ts b/packages/snaps-rpc-methods/src/restricted/manageAccounts.ts deleted file mode 100644 index 220fa25ba2..0000000000 --- a/packages/snaps-rpc-methods/src/restricted/manageAccounts.ts +++ /dev/null @@ -1,119 +0,0 @@ -import type { - RestrictedMethodOptions, - ValidPermissionSpecification, - PermissionSpecificationBuilder, -} from '@metamask/permission-controller'; -import { SubjectType, PermissionType } from '@metamask/permission-controller'; -import type { - ManageAccountsParams, - ManageAccountsResult, -} from '@metamask/snaps-sdk'; -import type { InferMatching } from '@metamask/snaps-utils'; -import { - assert, - string, - object, - union, - array, - record, -} from '@metamask/superstruct'; -import type { Json, NonEmptyArray } from '@metamask/utils'; -import { JsonStruct } from '@metamask/utils'; - -const SnapMessageStruct = union([ - object({ - method: string(), - }), - object({ - method: string(), - params: union([array(JsonStruct), record(string(), JsonStruct)]), - }), -]); - -type Message = InferMatching; - -export const methodName = 'snap_manageAccounts'; - -export type ManageAccountsMethodHooks = { - /** - * Gets the snap keyring implementation. - */ - getSnapKeyring: (snapOrigin: string) => Promise<{ - handleKeyringSnapMessage: ( - snapId: string, - message: Message, - ) => Promise; - }>; -}; - -type ManageAccountsSpecificationBuilderOptions = { - allowedCaveats?: Readonly> | null; - methodHooks: ManageAccountsMethodHooks; -}; - -type ManageAccountsSpecification = ValidPermissionSpecification<{ - permissionType: PermissionType.RestrictedMethod; - targetName: typeof methodName; - methodImplementation: ReturnType; - allowedCaveats: Readonly> | null; -}>; - -/** - * The specification builder for the `snap_manageAccounts` permission. - * `snap_manageAccounts` lets the Snap manage a set of accounts via a custom keyring. - * - * @param options - The specification builder options. - * @param options.allowedCaveats - The optional allowed caveats for the permission. - * @param options.methodHooks - The RPC method hooks needed by the method implementation. - * @returns The specification for the `snap_manageAccounts` permission. - */ -export const specificationBuilder: PermissionSpecificationBuilder< - PermissionType.RestrictedMethod, - ManageAccountsSpecificationBuilderOptions, - ManageAccountsSpecification -> = ({ - allowedCaveats = null, - methodHooks, -}: ManageAccountsSpecificationBuilderOptions) => { - return { - permissionType: PermissionType.RestrictedMethod, - targetName: methodName, - allowedCaveats, - methodImplementation: manageAccountsImplementation(methodHooks), - subjectTypes: [SubjectType.Snap], - }; -}; - -/** - * Builds the method implementation for `snap_manageAccounts`. - * - * @param hooks - The RPC method hooks. - * @param hooks.getSnapKeyring - A function to get the snap keyring. - * @returns The method implementation which either returns `null` for a - * successful state update/deletion or returns the decrypted state. - * @throws If the params are invalid. - */ -export function manageAccountsImplementation({ - getSnapKeyring, -}: ManageAccountsMethodHooks) { - return async function manageAccounts( - options: RestrictedMethodOptions, - ): Promise { - const { - context: { origin }, - params, - } = options; - - assert(params, SnapMessageStruct); - const keyring = await getSnapKeyring(origin); - return await keyring.handleKeyringSnapMessage(origin, params); - }; -} - -export const manageAccountsBuilder = Object.freeze({ - targetName: methodName, - specificationBuilder, - methodHooks: { - getSnapKeyring: true, - }, -} as const); diff --git a/packages/snaps-sdk/src/types/methods/manage-accounts.ts b/packages/snaps-sdk/src/types/methods/manage-accounts.ts index 192fe683df..948f9ece4a 100644 --- a/packages/snaps-sdk/src/types/methods/manage-accounts.ts +++ b/packages/snaps-sdk/src/types/methods/manage-accounts.ts @@ -6,14 +6,10 @@ import type { Json } from '@metamask/utils'; * @property method - The method to call on the Snap. * @property params - The optional parameters to pass to the Snap method. */ -export type ManageAccountsParams = - | { - method: string; - } - | { - method: string; - params: Json[] | Record; - }; +export type ManageAccountsParams = { + method: string; + params?: Json[] | Record; +}; /** * The result returned by the `snap_manageAccounts` method, which is the result diff --git a/packages/snaps-utils/src/manifest/validation.ts b/packages/snaps-utils/src/manifest/validation.ts index dd65132a10..2e312a3a9f 100644 --- a/packages/snaps-utils/src/manifest/validation.ts +++ b/packages/snaps-utils/src/manifest/validation.ts @@ -232,7 +232,6 @@ export const PermissionsStruct: Describe = type({ 'endowment:webassembly': optional(EmptyObjectStruct), snap_dialog: optional(EmptyObjectStruct), snap_manageState: optional(EmptyObjectStruct), - snap_manageAccounts: optional(EmptyObjectStruct), snap_notify: optional(EmptyObjectStruct), snap_getBip32Entropy: optional(SnapGetBip32EntropyPermissionsStruct), snap_getBip32PublicKey: optional(SnapGetBip32EntropyPermissionsStruct), From 59f242fad88472659e8ea0ed7e66a5b3e573b877 Mon Sep 17 00:00:00 2001 From: Guillaume Roux Date: Thu, 31 Oct 2024 14:19:18 +0100 Subject: [PATCH 2/5] Fix simulation test --- .../snaps-simulation/src/methods/specifications.test.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/snaps-simulation/src/methods/specifications.test.ts b/packages/snaps-simulation/src/methods/specifications.test.ts index 0e3f7ea897..e700da4fbf 100644 --- a/packages/snaps-simulation/src/methods/specifications.test.ts +++ b/packages/snaps-simulation/src/methods/specifications.test.ts @@ -242,15 +242,6 @@ describe('getPermissionSpecifications', () => { ], "targetName": "snap_getPreferences", }, - "snap_manageAccounts": { - "allowedCaveats": null, - "methodImplementation": [Function], - "permissionType": "RestrictedMethod", - "subjectTypes": [ - "snap", - ], - "targetName": "snap_manageAccounts", - }, "snap_manageState": { "allowedCaveats": null, "methodImplementation": [Function], From a7e496e62a44d6756d4c58a56c46b0ce14addbea Mon Sep 17 00:00:00 2001 From: Guillaume Roux Date: Mon, 4 Nov 2024 12:52:01 +0100 Subject: [PATCH 3/5] Filter removed permissions --- packages/snaps-rpc-methods/src/permissions.ts | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/packages/snaps-rpc-methods/src/permissions.ts b/packages/snaps-rpc-methods/src/permissions.ts index 6c0e6d43dc..1b97bc78c2 100644 --- a/packages/snaps-rpc-methods/src/permissions.ts +++ b/packages/snaps-rpc-methods/src/permissions.ts @@ -15,6 +15,21 @@ import { } from './restricted'; import { selectHooks } from './utils'; +const REMOVED_PERMISSIONS = Object.freeze(['snap_manageAccounts']); + +/** + * Filters out permissions that have been removed from the Snap API. + * + * @param initialPermission - The initial permission to filter. + * @returns Whether the permission has been removed. + */ +export const filterRemovedPermissions = ( + initialPermission: [string, unknown], +) => { + const [value] = initialPermission; + return REMOVED_PERMISSIONS.some((permission) => permission === value); +}; + /** * Map initial permissions as defined in a Snap manifest to something that can * be processed by the PermissionsController. Each caveat mapping function @@ -30,22 +45,32 @@ export function processSnapPermissions( initialPermissions: SnapPermissions, ): Record> { return Object.fromEntries( - Object.entries(initialPermissions).map(([initialPermission, value]) => { - if (hasProperty(caveatMappers, initialPermission)) { - return [initialPermission, caveatMappers[initialPermission](value)]; - } else if (hasProperty(endowmentCaveatMappers, initialPermission)) { + Object.entries(initialPermissions) + .filter(filterRemovedPermissions) + .map(([initialPermission, value]) => { + if ( + REMOVED_PERMISSIONS.some( + (permission) => permission === initialPermission, + ) + ) { + return []; + } + + if (hasProperty(caveatMappers, initialPermission)) { + return [initialPermission, caveatMappers[initialPermission](value)]; + } else if (hasProperty(endowmentCaveatMappers, initialPermission)) { + return [ + initialPermission, + endowmentCaveatMappers[initialPermission](value), + ]; + } + + // If we have no mapping, this may be a non-snap permission, return as-is return [ initialPermission, - endowmentCaveatMappers[initialPermission](value), + value as Pick, ]; - } - - // If we have no mapping, this may be a non-snap permission, return as-is - return [ - initialPermission, - value as Pick, - ]; - }), + }), ); } From 4f124a12633247c198f25fa4c704f1267950d67d Mon Sep 17 00:00:00 2001 From: Guillaume Roux Date: Wed, 6 Nov 2024 11:56:53 +0100 Subject: [PATCH 4/5] Add test --- .../src/snaps/SnapController.test.tsx | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/snaps-controllers/src/snaps/SnapController.test.tsx b/packages/snaps-controllers/src/snaps/SnapController.test.tsx index 575b495317..a21b00125d 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.test.tsx +++ b/packages/snaps-controllers/src/snaps/SnapController.test.tsx @@ -18,7 +18,7 @@ import { handlerEndowments, SnapEndowments, } from '@metamask/snaps-rpc-methods'; -import type { SnapId } from '@metamask/snaps-sdk'; +import type { Snap, SnapId } from '@metamask/snaps-sdk'; import { AuxiliaryFileEncoding, text } from '@metamask/snaps-sdk'; import { Text } from '@metamask/snaps-sdk/jsx'; import type { SnapPermissions, RpcOrigins } from '@metamask/snaps-utils'; @@ -1010,6 +1010,42 @@ describe('SnapController', () => { snapController.destroy(); }); + it('filters out removed permissions', async () => { + const messenger = getSnapControllerMessenger(); + const initialPermissions: SnapPermissions = { + [handlerEndowments.onRpcRequest as string]: { snaps: false, dapps: true }, + // eslint-disable-next-line @typescript-eslint/naming-convention + snap_manageAccounts: {}, + }; + + const { manifest } = await getMockSnapFilesWithUpdatedChecksum({ + manifest: getSnapManifest({ + initialPermissions, + }), + }); + + const snapController = getSnapController( + getSnapControllerOptions({ + messenger, + detectSnapLocation: loopbackDetect({ + manifest: manifest.result, + }), + }), + ); + + const snap = await snapController.installSnaps(MOCK_ORIGIN, { + [MOCK_SNAP_ID]: {}, + }); + + const permissions = (snap[MOCK_SNAP_ID] as Snap).initialPermissions; + + expect(permissions).toStrictEqual({ + [handlerEndowments.onRpcRequest as string]: { snaps: false, dapps: true }, + }); + + snapController.destroy(); + }); + it('throws an error if the installation is disabled during installSnaps', async () => { const controller = getSnapController( getSnapControllerOptions({ From 5808ffeaf363645b5d49cf67e12801453cde126c Mon Sep 17 00:00:00 2001 From: Guillaume Roux Date: Wed, 6 Nov 2024 12:15:57 +0100 Subject: [PATCH 5/5] forgotten file --- packages/snaps-rpc-methods/src/permissions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/snaps-rpc-methods/src/permissions.ts b/packages/snaps-rpc-methods/src/permissions.ts index 1b97bc78c2..c43641da6b 100644 --- a/packages/snaps-rpc-methods/src/permissions.ts +++ b/packages/snaps-rpc-methods/src/permissions.ts @@ -27,7 +27,7 @@ export const filterRemovedPermissions = ( initialPermission: [string, unknown], ) => { const [value] = initialPermission; - return REMOVED_PERMISSIONS.some((permission) => permission === value); + return !REMOVED_PERMISSIONS.some((permission) => permission === value); }; /**