diff --git a/packages/snaps-controllers/jest.config.js b/packages/snaps-controllers/jest.config.js index 35b1764892..96340fb853 100644 --- a/packages/snaps-controllers/jest.config.js +++ b/packages/snaps-controllers/jest.config.js @@ -7,8 +7,8 @@ module.exports = deepmerge(baseConfig, { global: { branches: 88.74, functions: 97.6, - lines: 96.74, - statements: 96.74, + lines: 97.1, + statements: 97.1, }, }, projects: [ diff --git a/packages/snaps-controllers/src/snaps/SnapController.test.ts b/packages/snaps-controllers/src/snaps/SnapController.test.ts index 97dbfccff8..0c86ce1f37 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.test.ts +++ b/packages/snaps-controllers/src/snaps/SnapController.test.ts @@ -45,12 +45,16 @@ import { getSnapControllerWithEES, getSnapControllerWithEESOptions, MOCK_BLOCK_NUMBER, + MOCK_DAPP_SUBJECT_METADATA, + MOCK_DAPPS_RPC_ORIGINS_PERMISSION, MOCK_NAMESPACES, + MOCK_RPC_ORIGINS_PERMISSION, + MOCK_SNAP_SUBJECT_METADATA, PERSISTED_MOCK_KEYRING_SNAP, sleep, } from '../test-utils'; import { delay } from '../utils'; -import { SnapEndowments } from './endowments'; +import { handlerEndowments, SnapEndowments } from './endowments'; import { SnapControllerState, SNAP_APPROVAL_UPDATE } from './SnapController'; const { subtle } = new Crypto(); @@ -1319,6 +1323,123 @@ describe('SnapController', () => { await service.terminateAllSnaps(); }); + describe('handleRequest', () => { + it.each(Object.keys(handlerEndowments) as HandlerType[])( + 'throws if the snap does not have permission for the handler', + async (handler) => { + const rootMessenger = getControllerMessenger(); + const messenger = getSnapControllerMessenger(rootMessenger); + const snapController = getSnapController( + getSnapControllerOptions({ + messenger, + state: { + snaps: getPersistedSnapsState(), + }, + }), + ); + + rootMessenger.registerActionHandler( + 'PermissionController:hasPermission', + () => false, + ); + + const snap = snapController.getExpect(MOCK_SNAP_ID); + await expect( + snapController.handleRequest({ + snapId: snap.id, + origin: 'foo.com', + handler, + request: { jsonrpc: '2.0', method: 'test' }, + }), + ).rejects.toThrow( + `Snap "${snap.id}" is not permitted to use "${handlerEndowments[handler]}".`, + ); + + snapController.destroy(); + }, + ); + + it('throws if the snap does not have permission to handle JSON-RPC requests from dapps', async () => { + const rootMessenger = getControllerMessenger(); + const messenger = getSnapControllerMessenger(rootMessenger); + const snapController = getSnapController( + getSnapControllerOptions({ + messenger, + state: { + snaps: getPersistedSnapsState(), + }, + }), + ); + + rootMessenger.registerActionHandler( + 'PermissionController:getPermissions', + () => ({ + // Permission to receive JSON-RPC requests from other Snaps. + [SnapEndowments.Rpc]: MOCK_RPC_ORIGINS_PERMISSION, + }), + ); + + rootMessenger.registerActionHandler( + 'SubjectMetadataController:getSubjectMetadata', + () => MOCK_DAPP_SUBJECT_METADATA, + ); + + const snap = snapController.getExpect(MOCK_SNAP_ID); + await expect( + snapController.handleRequest({ + snapId: snap.id, + origin: MOCK_ORIGIN, + handler: HandlerType.OnRpcRequest, + request: { jsonrpc: '2.0', method: 'test' }, + }), + ).rejects.toThrow( + `Snap "${snap.id}" is not permitted to handle JSON-RPC requests from "${MOCK_ORIGIN}".`, + ); + + snapController.destroy(); + }); + + it('throws if the snap does not have permission to handle JSON-RPC requests from snaps', async () => { + const rootMessenger = getControllerMessenger(); + const messenger = getSnapControllerMessenger(rootMessenger); + const snapController = getSnapController( + getSnapControllerOptions({ + messenger, + state: { + snaps: getPersistedSnapsState(), + }, + }), + ); + + rootMessenger.registerActionHandler( + 'PermissionController:getPermissions', + () => ({ + // Permission to receive JSON-RPC requests from dapps. + [SnapEndowments.Rpc]: MOCK_DAPPS_RPC_ORIGINS_PERMISSION, + }), + ); + + rootMessenger.registerActionHandler( + 'SubjectMetadataController:getSubjectMetadata', + () => MOCK_SNAP_SUBJECT_METADATA, + ); + + const snap = snapController.getExpect(MOCK_SNAP_ID); + await expect( + snapController.handleRequest({ + snapId: snap.id, + origin: MOCK_SNAP_ID, + handler: HandlerType.OnRpcRequest, + request: { jsonrpc: '2.0', method: 'test' }, + }), + ).rejects.toThrow( + `Snap "${snap.id}" is not permitted to handle JSON-RPC requests from "${MOCK_SNAP_ID}".`, + ); + + snapController.destroy(); + }); + }); + describe('getRpcRequestHandler', () => { it('handlers populate the "jsonrpc" property if missing', async () => { const rootMessenger = getControllerMessenger(); diff --git a/packages/snaps-controllers/src/snaps/endowments/index.ts b/packages/snaps-controllers/src/snaps/endowments/index.ts index 68eb30da69..bd2e67a7e9 100644 --- a/packages/snaps-controllers/src/snaps/endowments/index.ts +++ b/packages/snaps-controllers/src/snaps/endowments/index.ts @@ -56,7 +56,7 @@ export const endowmentCaveatMappers: Record< [rpcEndowmentBuilder.targetKey]: getRpcCaveatMapper, }; -export const handlerEndowments = { +export const handlerEndowments: Record = { [HandlerType.OnRpcRequest]: rpcEndowmentBuilder.targetKey, [HandlerType.SnapKeyring]: keyringEndowmentBuilder.targetKey, [HandlerType.OnTransaction]: transactionInsightEndowmentBuilder.targetKey, diff --git a/packages/snaps-controllers/src/snaps/endowments/rpc.test.ts b/packages/snaps-controllers/src/snaps/endowments/rpc.test.ts new file mode 100644 index 0000000000..4ac30198c5 --- /dev/null +++ b/packages/snaps-controllers/src/snaps/endowments/rpc.test.ts @@ -0,0 +1,129 @@ +import { PermissionType } from '@metamask/permission-controller'; +import { SnapCaveatType } from '@metamask/snaps-utils'; + +import { SnapEndowments } from '.'; +import { + getRpcCaveatMapper, + getRpcCaveatOrigins, + rpcCaveatSpecifications, + rpcEndowmentBuilder, +} from './rpc'; + +describe('endowment:rpc', () => { + it('builds the expected permission specification', () => { + const specification = rpcEndowmentBuilder.specificationBuilder({}); + expect(specification).toStrictEqual({ + permissionType: PermissionType.Endowment, + targetKey: SnapEndowments.Rpc, + endowmentGetter: expect.any(Function), + allowedCaveats: [SnapCaveatType.RpcOrigin], + validator: expect.any(Function), + }); + + expect(specification.endowmentGetter()).toBeUndefined(); + }); + + describe('validator', () => { + it('throws if the caveat is not a single "transactionOrigin"', () => { + const specification = rpcEndowmentBuilder.specificationBuilder({}); + + expect(() => + specification.validator({ + // @ts-expect-error Missing other required permission types. + caveats: undefined, + }), + ).toThrow('Expected a single "rpcOrigin" caveat.'); + + expect(() => + // @ts-expect-error Missing other required permission types. + specification.validator({ + caveats: [{ type: 'foo', value: 'bar' }], + }), + ).toThrow('Expected a single "rpcOrigin" caveat.'); + + expect(() => + // @ts-expect-error Missing other required permission types. + specification.validator({ + caveats: [ + { type: 'rpcOrigin', value: { snaps: true, dapps: false } }, + { type: 'rpcOrigin', value: { snaps: true, dapps: false } }, + ], + }), + ).toThrow('Expected a single "rpcOrigin" caveat.'); + }); + }); +}); + +describe('getRpcCaveatMapper', () => { + it('maps a value to a caveat', () => { + expect(getRpcCaveatMapper({ snaps: true, dapps: false })).toStrictEqual({ + caveats: [ + { + type: SnapCaveatType.RpcOrigin, + value: { snaps: true, dapps: false }, + }, + ], + }); + }); +}); + +describe('getRpcCaveatOrigins', () => { + it('returns the origins from the caveat', () => { + expect( + // @ts-expect-error Missing other required permission types. + getRpcCaveatOrigins({ + caveats: [ + { + type: SnapCaveatType.RpcOrigin, + value: { snaps: true, dapps: false }, + }, + ], + }), + ).toStrictEqual({ snaps: true, dapps: false }); + }); + + it('throws if the caveat is not a single "rpcOrigin"', () => { + expect(() => + // @ts-expect-error Missing other required permission types. + getRpcCaveatOrigins({ + caveats: [{ type: 'foo', value: 'bar' }], + }), + ).toThrow('Assertion failed.'); + + expect(() => + // @ts-expect-error Missing other required permission types. + getRpcCaveatOrigins({ + caveats: [ + { type: 'rpcOrigin', value: { snaps: true, dapps: false } }, + { type: 'rpcOrigin', value: { snaps: true, dapps: false } }, + ], + }), + ).toThrow('Assertion failed.'); + }); +}); + +describe('rpcCaveatSpecifications', () => { + describe('validator', () => { + it('throws if the caveat values are invalid', () => { + expect(() => + rpcCaveatSpecifications[SnapCaveatType.RpcOrigin].validator?.( + // @ts-expect-error Missing value type. + { + type: SnapCaveatType.TransactionOrigin, + }, + ), + ).toThrow('Invalid JSON-RPC origins: Expected a plain object.'); + + expect(() => + rpcCaveatSpecifications[SnapCaveatType.RpcOrigin].validator?.({ + type: SnapCaveatType.TransactionOrigin, + value: { + foo: 'bar', + }, + }), + ).toThrow( + 'Invalid JSON-RPC origins: At path: foo -- Expected a value of type `never`, but received: `"bar"`.', + ); + }); + }); +}); diff --git a/packages/snaps-controllers/src/snaps/endowments/rpc.ts b/packages/snaps-controllers/src/snaps/endowments/rpc.ts index c8f59b7ec4..0d7b434da0 100644 --- a/packages/snaps-controllers/src/snaps/endowments/rpc.ts +++ b/packages/snaps-controllers/src/snaps/endowments/rpc.ts @@ -81,19 +81,12 @@ export const rpcEndowmentBuilder = Object.freeze({ function validateCaveatOrigins(caveat: Caveat) { if (!hasProperty(caveat, 'value') || !isPlainObject(caveat.value)) { throw ethErrors.rpc.invalidParams({ - message: 'Expected a plain object.', + message: 'Invalid JSON-RPC origins: Expected a plain object.', }); } const { value } = caveat; - - if (!hasProperty(value, 'origins') || !isPlainObject(value)) { - throw ethErrors.rpc.invalidParams({ - message: 'Expected a plain object.', - }); - } - - assertIsRpcOrigins(value.origins, ethErrors.rpc.invalidParams); + assertIsRpcOrigins(value, ethErrors.rpc.invalidParams); } /** @@ -127,10 +120,7 @@ export function getRpcCaveatMapper( export function getRpcCaveatOrigins( permission?: PermissionConstraint, ): RpcOrigins | null { - if (!permission?.caveats) { - return null; - } - + assert(permission?.caveats); assert(permission.caveats.length === 1); assert(permission.caveats[0].type === SnapCaveatType.RpcOrigin); diff --git a/packages/snaps-controllers/src/test-utils/controller.ts b/packages/snaps-controllers/src/test-utils/controller.ts index d84e3a96a5..961c2a92eb 100644 --- a/packages/snaps-controllers/src/test-utils/controller.ts +++ b/packages/snaps-controllers/src/test-utils/controller.ts @@ -8,6 +8,7 @@ import { SnapCaveatType } from '@metamask/snaps-utils'; import { getPersistedSnapObject, getTruncatedSnap, + MOCK_ORIGIN, MOCK_SNAP_ID, } from '@metamask/snaps-utils/test-utils'; import { @@ -75,7 +76,7 @@ export class MockControllerMessenger< } } -export const MOCK_SUBJECT_METADATA: SubjectMetadata = { +export const MOCK_SNAP_SUBJECT_METADATA: SubjectMetadata = { origin: MOCK_SNAP_ID, subjectType: SubjectType.Snap, name: 'foo', @@ -83,6 +84,14 @@ export const MOCK_SUBJECT_METADATA: SubjectMetadata = { iconUrl: 'baz', }; +export const MOCK_DAPP_SUBJECT_METADATA: SubjectMetadata = { + origin: MOCK_ORIGIN, + subjectType: SubjectType.Website, + name: 'foo', + extensionId: 'bar', + iconUrl: 'baz', +}; + export const MOCK_RPC_ORIGINS_PERMISSION: PermissionConstraint = { caveats: [ { type: SnapCaveatType.RpcOrigin, value: { snaps: true, dapps: false } }, @@ -93,6 +102,16 @@ export const MOCK_RPC_ORIGINS_PERMISSION: PermissionConstraint = { parentCapability: SnapEndowments.Rpc, }; +export const MOCK_DAPPS_RPC_ORIGINS_PERMISSION: PermissionConstraint = { + caveats: [ + { type: SnapCaveatType.RpcOrigin, value: { snaps: false, dapps: true } }, + ], + date: 1664187844588, + id: 'izn0WGUO8cvq_jqvLQuQP', + invoker: MOCK_SNAP_ID, + parentCapability: SnapEndowments.Rpc, +}; + export const getControllerMessenger = () => { const messenger = new MockControllerMessenger< SnapControllerActions | AllowedActions, @@ -153,7 +172,7 @@ export const getControllerMessenger = () => { messenger.registerActionHandler( 'SubjectMetadataController:getSubjectMetadata', - () => MOCK_SUBJECT_METADATA, + () => MOCK_SNAP_SUBJECT_METADATA, ); messenger.registerActionHandler('ExecutionService:executeSnap', asyncNoOp);