From bc86edc78a24db67fd8cbb7974edd929c2307242 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 16 Jun 2021 14:01:08 -0700 Subject: [PATCH 001/147] Begin work on PermissionsController refactor --- packages/controllers/.eslintrc.js | 2 + packages/controllers/package.json | 1 + .../controllers/src/permissions/Caveat.ts | 45 +++++++ .../controllers/src/permissions/Permission.ts | 109 +++++++++++++++++ .../src/permissions/PermissionController.ts | 65 +++++++++++ .../src/permissions/caveat-functions.ts | 110 ++++++++++++++++++ packages/controllers/src/types/is-subset.d.ts | 7 ++ packages/controllers/tsconfig.json | 2 +- 8 files changed, 340 insertions(+), 1 deletion(-) create mode 100644 packages/controllers/src/permissions/Caveat.ts create mode 100644 packages/controllers/src/permissions/Permission.ts create mode 100644 packages/controllers/src/permissions/PermissionController.ts create mode 100644 packages/controllers/src/permissions/caveat-functions.ts create mode 100644 packages/controllers/src/types/is-subset.d.ts diff --git a/packages/controllers/.eslintrc.js b/packages/controllers/.eslintrc.js index 884ff91cf1..ddb95ca6a9 100644 --- a/packages/controllers/.eslintrc.js +++ b/packages/controllers/.eslintrc.js @@ -3,5 +3,7 @@ module.exports = { rules: { '@typescript-eslint/consistent-type-definitions': 'off', + 'no-shadow': 'off', + '@typescript-eslint/no-shadow': ['error', { builtinGlobals: true }], }, }; diff --git a/packages/controllers/package.json b/packages/controllers/package.json index 8b060d459d..fc2ca0ec49 100644 --- a/packages/controllers/package.json +++ b/packages/controllers/package.json @@ -33,6 +33,7 @@ "@metamask/snap-workers": "^0.2.0", "eth-rpc-errors": "^4.0.2", "immer": "^9.0.6", + "is-subset": "^0.1.1", "json-rpc-engine": "^6.1.0", "json-rpc-middleware-stream": "^3.0.0", "nanoid": "^3.1.28", diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts new file mode 100644 index 0000000000..278b01c76b --- /dev/null +++ b/packages/controllers/src/permissions/Caveat.ts @@ -0,0 +1,45 @@ +import { Json } from 'json-rpc-engine'; + +export enum CaveatType { + FilterResponse = 'filterResponse', + ForceParams = 'forceParams', + LimitResponseLength = 'limitResponseLength', + RequireParamsIsSubset = 'requireParamsIsSubset', + RequireParamsIsSuperset = 'requireParamsIsSuperset', +} + +interface CaveatOptions { + type: CaveatType; + name?: string; + value: ValueType; +} + +/** + * A caveat is... + */ +export class Caveat { + /** + * The type of the caveat. + */ + public readonly type: CaveatType; + + /** + * The unique name of the caveat. + * + * TODO: Should be optional + */ + public readonly name: string | null; + + /** + * Any additional data necessary to enforce the caveat. + * + * TODO: Should be optional + */ + public readonly value: ValueType; + + constructor({ type, name, value }: CaveatOptions) { + this.type = type; + this.name = name || null; + this.value = value; + } +} diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts new file mode 100644 index 0000000000..855c3a5137 --- /dev/null +++ b/packages/controllers/src/permissions/Permission.ts @@ -0,0 +1,109 @@ +import { Json } from 'json-rpc-engine'; +import { nanoid } from 'nanoid'; +import { Caveat } from './Caveat'; + +/** + * The origin of an external domain. + * Effectively the GUID of an entity that can have permissions. + */ +export type OriginString = string; + +/** + * The name of a restricted method. + */ +export type RestrictedMethodName = string; + +interface PermissionOptions { + /** + * The method that the permission corresponds to. + */ + method: RestrictedMethodName; + + /** + * The origin string of the external domain that has the permission. + */ + origin: OriginString; + + /** + * The GUID of the permission object. + * Assigned if not provided. + */ + id?: string; + + /** + * The caveats of the permission. + * @see Caveat + */ + caveats?: Caveat[]; +} + +/** + * A permission is... + */ +export class Permission { + /** + * The GUID of the permission object. + */ + public readonly id: string; + + /** + * The creation date of the permission, in UNIX epoch time. + */ + public readonly date: number; + + /** + * The method that the permission corresponds to. + */ + public readonly method: string; + + /** + * The caveats of the permission. + * @see Caveat + * + * TODO: Should be optional + */ + public readonly caveats: Caveat[] | null; + + /** + * The origin string of the external domain that has the permission. + */ + public readonly invoker: OriginString; + + constructor({ method, origin, caveats, id }: PermissionOptions) { + this.id = id ?? nanoid(); + this.method = method; + this.invoker = origin; + this.caveats = caveats ?? null; + this.date = new Date().getTime(); + } +} + +type RequestedPermission = Pick; + +export type RequestedPermissions = Record; + +export interface DomainMetadata { + origin: OriginString; +} + +export interface PermissionsRequestMetadata extends DomainMetadata { + id: string; +} + +/** + * Used for prompting the user about a proposed new permission. + * Includes information about the domain granted, as well as the permissions + * assigned. + */ +export interface PermissionsRequest { + metadata: PermissionsRequestMetadata; + permissions: RequestedPermissions; +} + +export type UserApprovalPrompt = ( + permissionsRequest: PermissionsRequest, +) => Promise; + +export interface PermissionsDomainEntry extends DomainMetadata { + permissions: Record; +} diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts new file mode 100644 index 0000000000..c8865c72ca --- /dev/null +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -0,0 +1,65 @@ +import { + BaseControllerV2 as BaseController, + RestrictedControllerMessenger, +} from '@metamask/controllers'; +import { OriginString, PermissionsDomainEntry } from './Permission'; + +const name = 'PermissionsController'; + +export type PermissionsControllerDomains = Record< + OriginString, + PermissionsDomainEntry +>; + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type PermissionsControllerState = { + domains: PermissionsControllerDomains; +}; + +const metadata = { + domains: { persist: true, anonymous: true }, +}; + +const defaultState: PermissionsControllerState = { + domains: {}, +}; + +export interface GetDomains { + type: `${typeof name}:getDomains`; + handler: () => (keyof PermissionsControllerDomains)[]; +} + +export interface AccountsChanged { + type: `${typeof name}:accountsChanged`; + payload: [{ domain: OriginString; accounts: string[] }]; +} + +interface PermissionsControllerOptions { + messenger: RestrictedControllerMessenger< + typeof name, + GetDomains, + AccountsChanged, + never, + never + >; + state: Partial; + safeMethods: string[]; +} + +export class PermissionsController extends BaseController< + typeof name, + PermissionsControllerState +> { + private safeMethods: Set; + + constructor({ messenger, state = {}, safeMethods }: PermissionsControllerOptions) { + super({ + name, + metadata, + messenger, + state: { ...defaultState, ...state }, + }); + + this.safeMethods = new Set(safeMethods); + } +} diff --git a/packages/controllers/src/permissions/caveat-functions.ts b/packages/controllers/src/permissions/caveat-functions.ts new file mode 100644 index 0000000000..38954350c6 --- /dev/null +++ b/packages/controllers/src/permissions/caveat-functions.ts @@ -0,0 +1,110 @@ +import { JsonRpcMiddleware, Json } from 'json-rpc-engine'; +import isSubset from 'is-subset'; +import equal from 'fast-deep-equal'; +import { ethErrors } from 'eth-rpc-errors'; +import { Caveat } from './Caveat'; + +export type CaveatFunction = JsonRpcMiddleware; + +export type CaveatFunctionGenerator = ( + caveat: Caveat, +) => CaveatFunction; + +export const caveatFunctions = { + filterResponse, + forceParams, + limitResponseLength, + requireParamsIsSubset, + requireParamsIsSuperset, +}; + +/** + * Require that request.params is a subset of or equal to the caveat value. + * Arrays are order-dependent, objects are order-independent. + */ +export function requireParamsIsSubset( + serialized: Caveat>, +): CaveatFunction, unknown> { + const { value } = serialized; + return (req, res, next, end): void => { + // Ensure that the params are a subset of or equal to the caveat value + if (!isSubset(value, req.params)) { + res.error = ethErrors.provider.unauthorized({ data: req }); + return end(res.error); + } + + return next(); + }; +} + +/** + * Require that request.params is a superset of or equal to the caveat value. + * Arrays are order-dependent, objects are order-independent. + */ +export function requireParamsIsSuperset( + serialized: Caveat>, +): CaveatFunction, unknown> { + const { value } = serialized; + return (req, res, next, end): void => { + // Ensure that the params are a superset of or equal to the caveat value + if (!isSubset(req.params, value)) { + res.error = ethErrors.provider.unauthorized({ data: req }); + return end(res.error); + } + + return next(); + }; +} + +/* + * Filters array results deeply. + */ +export function filterResponse( + serialized: Caveat, +): CaveatFunction { + const { value } = serialized; + return (_req, res, next, _end): void => { + next((done) => { + if (Array.isArray(res.result)) { + res.result = res.result.filter((item) => { + const findResult = value.find((resultValue: Json) => { + return equal(resultValue, item); + }); + + return findResult !== undefined; + }); + } + done(); + }); + }; +} + +/* + * Limits array results to a specific integer length. + */ +export function limitResponseLength( + serialized: Caveat, +): CaveatFunction { + const { value } = serialized; + return (_req, res, next, _end): void => { + next((done) => { + if (Array.isArray(res.result)) { + res.result = res.result.slice(0, value); + } + done(); + }); + }; +} + +/* + * Forces the method to be called with given params. + */ +export function forceParams( + serialized: Caveat>, +): CaveatFunction { + const { value } = serialized; + return (req, _, next): void => { + req.params = Array.isArray(value) ? [...value] : { ...value }; + next(); + }; +} diff --git a/packages/controllers/src/types/is-subset.d.ts b/packages/controllers/src/types/is-subset.d.ts new file mode 100644 index 0000000000..0e25940129 --- /dev/null +++ b/packages/controllers/src/types/is-subset.d.ts @@ -0,0 +1,7 @@ +declare module 'is-subset' { + export type intersectObjects = ( + arg1?: Record, + arg2?: Record, + ) => Record; + export default function isSubset(superset: unknown, subset: unknown): boolean; +} diff --git a/packages/controllers/tsconfig.json b/packages/controllers/tsconfig.json index ddcb35b914..f5ca7a155b 100644 --- a/packages/controllers/tsconfig.json +++ b/packages/controllers/tsconfig.json @@ -8,7 +8,7 @@ "typeRoots": [ "../../node_modules/@types", "./node_modules/@types", - "./types" + "./src/types" ] }, "include": ["./src"], From 848f4c5b6a0d05e5f2de82b74638ae524b60d916 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Thu, 17 Jun 2021 18:32:55 -0700 Subject: [PATCH 002/147] Update function generators, add caveat types test --- packages/controllers/package.json | 1 + .../src/permissions/PermissionController.ts | 6 +- .../src/permissions/caveat-functions.test.ts | 15 +++++ .../src/permissions/caveat-functions.ts | 59 ++++++++++--------- 4 files changed, 53 insertions(+), 28 deletions(-) create mode 100644 packages/controllers/src/permissions/caveat-functions.test.ts diff --git a/packages/controllers/package.json b/packages/controllers/package.json index fc2ca0ec49..54ad9b8f9f 100644 --- a/packages/controllers/package.json +++ b/packages/controllers/package.json @@ -33,6 +33,7 @@ "@metamask/snap-workers": "^0.2.0", "eth-rpc-errors": "^4.0.2", "immer": "^9.0.6", + "fast-deep-equal": "^3.1.3", "is-subset": "^0.1.1", "json-rpc-engine": "^6.1.0", "json-rpc-middleware-stream": "^3.0.0", diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index c8865c72ca..b11719fee4 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -52,7 +52,11 @@ export class PermissionsController extends BaseController< > { private safeMethods: Set; - constructor({ messenger, state = {}, safeMethods }: PermissionsControllerOptions) { + constructor({ + messenger, + state = {}, + safeMethods, + }: PermissionsControllerOptions) { super({ name, metadata, diff --git a/packages/controllers/src/permissions/caveat-functions.test.ts b/packages/controllers/src/permissions/caveat-functions.test.ts new file mode 100644 index 0000000000..dc4cd6ca55 --- /dev/null +++ b/packages/controllers/src/permissions/caveat-functions.test.ts @@ -0,0 +1,15 @@ +import { CaveatType } from './Caveat'; +import { caveatFunctionGenerators } from './caveat-functions'; + +describe('caveat functions', () => { + it('implements all caveat types', () => { + const types = Object.values(CaveatType); + expect(types.length).toStrictEqual( + Object.keys(caveatFunctionGenerators).length, + ); + + for (const caveatType of types) { + expect(caveatFunctionGenerators).toHaveProperty(caveatType); + } + }); +}); diff --git a/packages/controllers/src/permissions/caveat-functions.ts b/packages/controllers/src/permissions/caveat-functions.ts index 38954350c6..ed93e3e1a6 100644 --- a/packages/controllers/src/permissions/caveat-functions.ts +++ b/packages/controllers/src/permissions/caveat-functions.ts @@ -6,11 +6,18 @@ import { Caveat } from './Caveat'; export type CaveatFunction = JsonRpcMiddleware; -export type CaveatFunctionGenerator = ( - caveat: Caveat, +export type CaveatFunctionGenerator = ( + caveat: Caveat, ) => CaveatFunction; -export const caveatFunctions = { +/** + * Caveat middleware function generators for all caveat types. + * + * A caveat function generator takes a caveat object and returns a caveat + * middleware function corresponding to the caveat's type and bound to its + * value. + */ +export const caveatFunctionGenerators = { filterResponse, forceParams, limitResponseLength, @@ -22,15 +29,14 @@ export const caveatFunctions = { * Require that request.params is a subset of or equal to the caveat value. * Arrays are order-dependent, objects are order-independent. */ -export function requireParamsIsSubset( - serialized: Caveat>, +function requireParamsIsSubset( + caveat: Caveat>, ): CaveatFunction, unknown> { - const { value } = serialized; - return (req, res, next, end): void => { + const { value } = caveat; + return (req, _res, next, _end): void => { // Ensure that the params are a subset of or equal to the caveat value if (!isSubset(value, req.params)) { - res.error = ethErrors.provider.unauthorized({ data: req }); - return end(res.error); + throw ethErrors.provider.unauthorized({ data: req }); } return next(); @@ -41,15 +47,14 @@ export function requireParamsIsSubset( * Require that request.params is a superset of or equal to the caveat value. * Arrays are order-dependent, objects are order-independent. */ -export function requireParamsIsSuperset( - serialized: Caveat>, +function requireParamsIsSuperset( + caveat: Caveat>, ): CaveatFunction, unknown> { - const { value } = serialized; - return (req, res, next, end): void => { + const { value } = caveat; + return (req, _res, next, _end): void => { // Ensure that the params are a superset of or equal to the caveat value if (!isSubset(req.params, value)) { - res.error = ethErrors.provider.unauthorized({ data: req }); - return end(res.error); + throw ethErrors.provider.unauthorized({ data: req }); } return next(); @@ -59,10 +64,10 @@ export function requireParamsIsSuperset( /* * Filters array results deeply. */ -export function filterResponse( - serialized: Caveat, +function filterResponse( + caveat: Caveat, ): CaveatFunction { - const { value } = serialized; + const { value } = caveat; return (_req, res, next, _end): void => { next((done) => { if (Array.isArray(res.result)) { @@ -82,10 +87,10 @@ export function filterResponse( /* * Limits array results to a specific integer length. */ -export function limitResponseLength( - serialized: Caveat, +function limitResponseLength( + caveat: Caveat, ): CaveatFunction { - const { value } = serialized; + const { value } = caveat; return (_req, res, next, _end): void => { next((done) => { if (Array.isArray(res.result)) { @@ -97,14 +102,14 @@ export function limitResponseLength( } /* - * Forces the method to be called with given params. + * Forces the method to be called with the specified params. */ -export function forceParams( - serialized: Caveat>, +function forceParams( + caveat: Caveat>, ): CaveatFunction { - const { value } = serialized; - return (req, _, next): void => { + const { value } = caveat; + return (req, _res, next, _end): void => { req.params = Array.isArray(value) ? [...value] : { ...value }; - next(); + return next(); }; } From 6ed5d49e9eeaeae4425efef92cde48b1dc15c71c Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 21 Jun 2021 09:21:41 -0700 Subject: [PATCH 003/147] Fixup caveat function gen. types and errors --- .../src/permissions/PermissionController.ts | 3 + .../src/permissions/caveat-functions.ts | 55 ++++++++++++------- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index b11719fee4..387e7748b9 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -46,6 +46,9 @@ interface PermissionsControllerOptions { safeMethods: string[]; } +// TODO: Use this error message. +// 'Unauthorized to perform action. Try requesting the required permission(s) first. For more information, see: https://docs.metamask.io/guide/rpc-api.html#permissions' + export class PermissionsController extends BaseController< typeof name, PermissionsControllerState diff --git a/packages/controllers/src/permissions/caveat-functions.ts b/packages/controllers/src/permissions/caveat-functions.ts index ed93e3e1a6..bbbcc5732b 100644 --- a/packages/controllers/src/permissions/caveat-functions.ts +++ b/packages/controllers/src/permissions/caveat-functions.ts @@ -30,13 +30,20 @@ export const caveatFunctionGenerators = { * Arrays are order-dependent, objects are order-independent. */ function requireParamsIsSubset( - caveat: Caveat>, -): CaveatFunction, unknown> { + caveat: Caveat>, +): CaveatFunction< + Json[] | Record, + Json[] | Record +> { const { value } = caveat; return (req, _res, next, _end): void => { // Ensure that the params are a subset of or equal to the caveat value if (!isSubset(value, req.params)) { - throw ethErrors.provider.unauthorized({ data: req }); + throw ethErrors.provider.unauthorized({ + data: { permittedParameters: value }, + message: + 'Unauthorized parameters. The request parameters must be a subset of or equal to the permitted parameters.', + }); } return next(); @@ -48,13 +55,20 @@ function requireParamsIsSubset( * Arrays are order-dependent, objects are order-independent. */ function requireParamsIsSuperset( - caveat: Caveat>, -): CaveatFunction, unknown> { + caveat: Caveat>, +): CaveatFunction< + Json[] | Record, + Json[] | Record +> { const { value } = caveat; return (req, _res, next, _end): void => { // Ensure that the params are a superset of or equal to the caveat value if (!isSubset(req.params, value)) { - throw ethErrors.provider.unauthorized({ data: req }); + throw ethErrors.provider.unauthorized({ + data: { requiredParameters: value }, + message: + 'Unauthorized parameters; missing required parameters. The request parameters must be a superset of or equal to the required parameters.', + }); } return next(); @@ -66,19 +80,18 @@ function requireParamsIsSuperset( */ function filterResponse( caveat: Caveat, -): CaveatFunction { +): CaveatFunction { const { value } = caveat; return (_req, res, next, _end): void => { next((done) => { - if (Array.isArray(res.result)) { - res.result = res.result.filter((item) => { - const findResult = value.find((resultValue: Json) => { - return equal(resultValue, item); - }); - - return findResult !== undefined; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + res.result = res.result!.filter((item) => { + const findResult = value.find((resultValue: Json) => { + return equal(resultValue, item); }); - } + + return findResult !== undefined; + }); done(); }); }; @@ -89,13 +102,12 @@ function filterResponse( */ function limitResponseLength( caveat: Caveat, -): CaveatFunction { +): CaveatFunction { const { value } = caveat; return (_req, res, next, _end): void => { next((done) => { - if (Array.isArray(res.result)) { - res.result = res.result.slice(0, value); - } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + res.result = res.result!.slice(0, value); done(); }); }; @@ -106,7 +118,10 @@ function limitResponseLength( */ function forceParams( caveat: Caveat>, -): CaveatFunction { +): CaveatFunction< + Json[] | Record, + Json[] | Record +> { const { value } = caveat; return (req, _res, next, _end): void => { req.params = Array.isArray(value) ? [...value] : { ...value }; From 746f1bfeeeeec93da7aaa6e63046a196ca155f15 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sun, 27 Jun 2021 10:49:45 -0700 Subject: [PATCH 004/147] Use modifed ZCAP-LD interfaces for caveats and permissions --- .../controllers/src/permissions/Caveat.ts | 19 +++-- .../controllers/src/permissions/Permission.ts | 72 +++++++++++++++++-- 2 files changed, 80 insertions(+), 11 deletions(-) diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index 278b01c76b..20a22ea033 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -15,9 +15,20 @@ interface CaveatOptions { } /** - * A caveat is... + * */ -export class Caveat { +export interface ZcapLdCaveat { + /** + * The type of the caveat, which is presumed to be meaningful in the context + * of the capability it is associated with. + */ + type: string; +} + +/** + * TODO: Document + */ +export class Caveat implements ZcapLdCaveat { /** * The type of the caveat. */ @@ -26,14 +37,14 @@ export class Caveat { /** * The unique name of the caveat. * - * TODO: Should be optional + * TODO: Make optional in typescript@4.4.x */ public readonly name: string | null; /** * Any additional data necessary to enforce the caveat. * - * TODO: Should be optional + * TODO: Make optional in typescript@4.4.x */ public readonly value: ValueType; diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 855c3a5137..dbb5a7ebab 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -1,6 +1,6 @@ import { Json } from 'json-rpc-engine'; import { nanoid } from 'nanoid'; -import { Caveat } from './Caveat'; +import { Caveat, ZcapLdCaveat } from './Caveat'; /** * The origin of an external domain. @@ -13,6 +13,64 @@ export type OriginString = string; */ export type RestrictedMethodName = string; +/** + * An interface + */ +interface ZcapLdCapability { + /** + * The context(s) in which this capability is meaningful. + * + * It is required by the standard, but we omit it because there is only one + * context (the user's MetaMask instance). + */ + '@context'?: string[]; + + /** + * The cryptograhically strong GUID of the capability. + */ + 'id': string; + + /** + * A pointer to the resource that possession of the capability grants + * access to. + * + * In the context of MetaMask, this is always the name of an RPC method. + */ + 'parentCapability': string; + + /** + * A pointer to the the entity that may invoke this capability. + * + * By the standard, this a link – usually some kind of URI – to a cryptographic + * key that the proof of the `proof` field "must validate against". + * + * In the context of MetaMask, this is simply the origin of an external domain. + */ + 'invoker': string; + + /** + * The issuing date, in UNIX epoch time. + */ + 'date'?: number; + + /** + * An array of caveat objects. See {@link ZcapLdCaveat}. + * + * TODO: Make optional in typescript@4.4.x + */ + 'caveats': ZcapLdCaveat[] | null; + + /** + * The proof that this capability was delegated to the specified invoker. + * By the standard, usually just a cryptographic signature of the capability + * object, excluding this field. + * + * In MetaMask, the "proof" of validity is the existence of a valid capability + * object in the designated part of our state tree, so this field is omitted. + */ + 'proof'?: string; +} + interface PermissionOptions { /** * The method that the permission corresponds to. @@ -32,15 +90,15 @@ interface PermissionOptions { /** * The caveats of the permission. - * @see Caveat + * See {@link Caveat}. */ caveats?: Caveat[]; } /** - * A permission is... + * TODO: Document */ -export class Permission { +export class Permission implements ZcapLdCapability { /** * The GUID of the permission object. */ @@ -54,13 +112,13 @@ export class Permission { /** * The method that the permission corresponds to. */ - public readonly method: string; + public readonly parentCapability: string; /** * The caveats of the permission. * @see Caveat * - * TODO: Should be optional + * TODO: Make optional in typescript@4.4.x */ public readonly caveats: Caveat[] | null; @@ -71,7 +129,7 @@ export class Permission { constructor({ method, origin, caveats, id }: PermissionOptions) { this.id = id ?? nanoid(); - this.method = method; + this.parentCapability = method; this.invoker = origin; this.caveats = caveats ?? null; this.date = new Date().getTime(); From d67885303ac60a24a5e307d42641b84755452f18 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 30 Jun 2021 20:47:17 -0700 Subject: [PATCH 005/147] Add errors, some basic functions --- .../controllers/src/permissions/Permission.ts | 42 +--- .../src/permissions/PermissionController.ts | 209 ++++++++++++++++-- .../src/permissions/caveat-functions.ts | 10 +- .../controllers/src/permissions/errors.ts | 34 +++ 4 files changed, 243 insertions(+), 52 deletions(-) create mode 100644 packages/controllers/src/permissions/errors.ts diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index dbb5a7ebab..cb5d0d6f61 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -11,7 +11,7 @@ export type OriginString = string; /** * The name of a restricted method. */ -export type RestrictedMethodName = string; +export type MethodName = string; /** * An interface @@ -75,12 +75,12 @@ interface PermissionOptions { /** * The method that the permission corresponds to. */ - method: RestrictedMethodName; + target: MethodName; /** * The origin string of the external domain that has the permission. */ - origin: OriginString; + invoker: OriginString; /** * The GUID of the permission object. @@ -127,41 +127,15 @@ export class Permission implements ZcapLdCapability { */ public readonly invoker: OriginString; - constructor({ method, origin, caveats, id }: PermissionOptions) { + constructor({ target, invoker, caveats, id }: PermissionOptions) { this.id = id ?? nanoid(); - this.parentCapability = method; - this.invoker = origin; + this.parentCapability = target; + this.invoker = invoker; this.caveats = caveats ?? null; this.date = new Date().getTime(); } } -type RequestedPermission = Pick; +type RequestedPermission = Pick; -export type RequestedPermissions = Record; - -export interface DomainMetadata { - origin: OriginString; -} - -export interface PermissionsRequestMetadata extends DomainMetadata { - id: string; -} - -/** - * Used for prompting the user about a proposed new permission. - * Includes information about the domain granted, as well as the permissions - * assigned. - */ -export interface PermissionsRequest { - metadata: PermissionsRequestMetadata; - permissions: RequestedPermissions; -} - -export type UserApprovalPrompt = ( - permissionsRequest: PermissionsRequest, -) => Promise; - -export interface PermissionsDomainEntry extends DomainMetadata { - permissions: Record; -} +export type RequestedPermissions = Record; diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 387e7748b9..4cebd8828c 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -2,9 +2,29 @@ import { BaseControllerV2 as BaseController, RestrictedControllerMessenger, } from '@metamask/controllers'; -import { OriginString, PermissionsDomainEntry } from './Permission'; +import { ethErrors } from 'eth-rpc-errors'; +import { + CaveatFunctionGenerator, + caveatFunctionGenerators, +} from './caveat-functions'; +import { + OriginString, + Permission, + MethodName, + RequestedPermissions, +} from './Permission'; +import { methodNotFound } from './errors'; +import { Caveat } from './Caveat'; + +export interface DomainMetadata { + origin: OriginString; +} -const name = 'PermissionsController'; +const controllerName = 'PermissionsController'; + +export interface PermissionsDomainEntry extends DomainMetadata { + permissions: Record; +} export type PermissionsControllerDomains = Record< OriginString, @@ -16,7 +36,7 @@ export type PermissionsControllerState = { domains: PermissionsControllerDomains; }; -const metadata = { +const stateMetadata = { domains: { persist: true, anonymous: true }, }; @@ -25,48 +45,209 @@ const defaultState: PermissionsControllerState = { }; export interface GetDomains { - type: `${typeof name}:getDomains`; + type: `${typeof controllerName}:getDomains`; handler: () => (keyof PermissionsControllerDomains)[]; } export interface AccountsChanged { - type: `${typeof name}:accountsChanged`; + type: `${typeof controllerName}:accountsChanged`; payload: [{ domain: OriginString; accounts: string[] }]; } interface PermissionsControllerOptions { messenger: RestrictedControllerMessenger< - typeof name, + typeof controllerName, GetDomains, AccountsChanged, never, never >; - state: Partial; + state?: Partial; + restrictedMethods: string[]; safeMethods: string[]; + methodPrefix: string; } -// TODO: Use this error message. -// 'Unauthorized to perform action. Try requesting the required permission(s) first. For more information, see: https://docs.metamask.io/guide/rpc-api.html#permissions' - export class PermissionsController extends BaseController< - typeof name, + typeof controllerName, PermissionsControllerState > { - private safeMethods: Set; + public readonly methodPrefix: string; + + /** + * Can be namespaced. + */ + private readonly restrictedMethods: Readonly>; + + private readonly safeMethods: Readonly>; + + private readonly internalMethods: Readonly>; + + private readonly caveatFunctionGenerators: Readonly< + Record> + > = { + ...caveatFunctionGenerators, + }; constructor({ messenger, state = {}, + methodPrefix, + restrictedMethods, safeMethods, }: PermissionsControllerOptions) { super({ - name, - metadata, + name: controllerName, + metadata: stateMetadata, messenger, state: { ...defaultState, ...state }, }); + this.methodPrefix = methodPrefix; + this.internalMethods = getInternalMethodNames(methodPrefix); + this.restrictedMethods = new Set(restrictedMethods); this.safeMethods = new Set(safeMethods); } + + /** + * Adds permissions to the given domain. Overwrites existing identical + * permissions (same domain, and method). Other existing permissions + * remain unaffected. + * + * @param {string} domainName - The grantee domain. + * @param {Array} newPermissions - The unique, new permissions for the grantee domain. + */ + updatePermissions( + origin: string, + permissions: Record, + ): void { + this.update((draftState) => { + if (!draftState.domains[origin]) { + draftState.domains[origin] = { origin, permissions: {} }; + } + Object.assign(draftState.domains[origin].permissions, permissions); + }); + } + + grantPermissions( + domain: DomainMetadata, + requestedPermissions: RequestedPermissions, + ): Record { + const { origin } = domain; + if (!origin || typeof origin !== 'string') { + throw ethErrors.rpc.invalidRequest(`Invalid domain: '${domain}'.`); + } + + // Enforce actual approving known methods: + for (const methodName in requestedPermissions) { + if (!this.getMethodKeyFor(methodName)) { + throw methodNotFound({ methodName }); + } + } + + const permissions: { [methodName: string]: Permission } = {}; + + for (const method of Object.keys(requestedPermissions)) { + const newPerm = new Permission({ + target: method, + invoker: origin, + caveats: requestedPermissions[method].caveats, + }); + + if (newPerm.caveats && !this.validateCaveats(newPerm.caveats)) { + throw ethErrors.rpc.internal({ + message: 'Invalid caveats.', + data: newPerm, + }); + } + + permissions[method] = newPerm; + } + + this.updatePermissions(origin, permissions); + return permissions; + } + + /** + * TODO: Implement. + * + * @param caveats The caveats to validate. + * @returns Whether the given caveats are valid. + */ + validateCaveats(_caveats: Caveat[]): boolean { + return true; + } + + /** + * Used for retrieving the key that manages the restricted method + * associated with the current RPC `method` key. + * + * Used to support our namespaced method feature, which allows blocks + * of methods to be hidden behind a restricted method with a trailing `_` character. + * + * @param method string - The requested RPC method. + * @returns The internal key of the method. + */ + getMethodKeyFor(method: string): string { + if (this.restrictedMethods.has(method)) { + return method; + } + + const wildCardMethodsWithoutWildCard: Record = {}; + for (const methodName of this.restrictedMethods) { + const wildCardMatch = methodName.match(/(.+)\*$/u); + if (wildCardMatch) { + wildCardMethodsWithoutWildCard[wildCardMatch[1]] = true; + } + } + + // Check for potentially nested namespaces: + // Ex: wildzone_ + // Ex: eth_plugin_ + const segments = method.split('_'); + let methodKey = ''; + + while ( + segments.length > 0 && + !this.restrictedMethods.has(methodKey) && + !wildCardMethodsWithoutWildCard[methodKey] + ) { + methodKey += `${segments.shift()}_`; + } + + if (this.restrictedMethods.has(methodKey)) { + return methodKey; + } else if (wildCardMethodsWithoutWildCard[methodKey]) { + return `${methodKey}*`; + } + + return ''; + } +} + +function getInternalMethodNames(methodPrefix: string) { + return new Set([ + `${methodPrefix}getPermissions`, + `${methodPrefix}requestPermissions`, + ]); +} + +// TODO: Are we using these? + +export interface PermissionsRequestMetadata extends DomainMetadata { + id: string; } + +/** + * Used for prompting the user about a proposed new permission. + * Includes information about the domain granted, as well as the permissions + * assigned. + */ +export interface PermissionsRequest { + stateMetadata: PermissionsRequestMetadata; + permissions: RequestedPermissions; +} + +export type UserApprovalPrompt = ( + permissionsRequest: PermissionsRequest, +) => Promise; diff --git a/packages/controllers/src/permissions/caveat-functions.ts b/packages/controllers/src/permissions/caveat-functions.ts index bbbcc5732b..ddbc3b68bf 100644 --- a/packages/controllers/src/permissions/caveat-functions.ts +++ b/packages/controllers/src/permissions/caveat-functions.ts @@ -4,11 +4,13 @@ import equal from 'fast-deep-equal'; import { ethErrors } from 'eth-rpc-errors'; import { Caveat } from './Caveat'; -export type CaveatFunction = JsonRpcMiddleware; +export type CaveatFunction = JsonRpcMiddleware; -export type CaveatFunctionGenerator = ( - caveat: Caveat, -) => CaveatFunction; +export type CaveatFunctionGenerator< + CaveatValue extends Json, + Params, + Result +> = (caveat: Caveat) => CaveatFunction; /** * Caveat middleware function generators for all caveat types. diff --git a/packages/controllers/src/permissions/errors.ts b/packages/controllers/src/permissions/errors.ts new file mode 100644 index 0000000000..677de8d73a --- /dev/null +++ b/packages/controllers/src/permissions/errors.ts @@ -0,0 +1,34 @@ +import { JsonRpcRequest } from 'json-rpc-engine'; +import { ethErrors, EthereumRpcError } from 'eth-rpc-errors'; + +interface ErrorArg { + message?: string; + data?: JsonRpcRequest | unknown; +} + +interface MethodNotFoundArg extends ErrorArg { + methodName?: string; +} + +export function unauthorized(arg?: ErrorArg) { + return ethErrors.provider.unauthorized({ + message: + arg?.message || + 'Unauthorized to perform action. Try requesting the required permission(s) first. For more information, see: https://docs.metamask.io/guide/rpc-api.html#permissions', + data: arg?.data || undefined, + }); +} + +export function methodNotFound(opts: MethodNotFoundArg) { + const message = opts.methodName + ? `The method '${opts.methodName}' does not exist / is not available.` + : undefined; + + return ethErrors.rpc.methodNotFound({ data: opts.data, message }); +} + +export function userRejectedRequest( + request?: JsonRpcRequest, +): EthereumRpcError> { + return ethErrors.provider.userRejectedRequest({ data: request }); +} From ff38cce7f910bb5ad2cf4a9b109d12f75215d50c Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Thu, 1 Jul 2021 08:40:47 -0700 Subject: [PATCH 006/147] Refactor caveats as middleware functions --- .../controllers/src/permissions/Caveat.ts | 26 +++------- .../src/permissions/PermissionController.ts | 24 +++++---- .../src/permissions/caveat-functions.test.ts | 15 ------ .../src/permissions/caveat-functions.ts | 6 +++ .../src/permissions/caveat-processor.ts | 50 +++++++++++++++++++ 5 files changed, 77 insertions(+), 44 deletions(-) delete mode 100644 packages/controllers/src/permissions/caveat-functions.test.ts create mode 100644 packages/controllers/src/permissions/caveat-processor.ts diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index 20a22ea033..41e8705d28 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -1,16 +1,7 @@ import { Json } from 'json-rpc-engine'; -export enum CaveatType { - FilterResponse = 'filterResponse', - ForceParams = 'forceParams', - LimitResponseLength = 'limitResponseLength', - RequireParamsIsSubset = 'requireParamsIsSubset', - RequireParamsIsSuperset = 'requireParamsIsSuperset', -} - interface CaveatOptions { - type: CaveatType; - name?: string; + type: string; value: ValueType; } @@ -30,16 +21,12 @@ export interface ZcapLdCaveat { */ export class Caveat implements ZcapLdCaveat { /** - * The type of the caveat. - */ - public readonly type: CaveatType; - - /** - * The unique name of the caveat. + * The type of the caveat. The type is presumed to be meaningful in the + * context of the capability it is associated with. * - * TODO: Make optional in typescript@4.4.x + * In MetaMask, every permission can only have one caveat of each type. */ - public readonly name: string | null; + public readonly type: string; /** * Any additional data necessary to enforce the caveat. @@ -48,9 +35,8 @@ export class Caveat implements ZcapLdCaveat { */ public readonly value: ValueType; - constructor({ type, name, value }: CaveatOptions) { + constructor({ type, value }: CaveatOptions) { this.type = type; - this.name = name || null; this.value = value; } } diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 4cebd8828c..122850d92d 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -4,9 +4,9 @@ import { } from '@metamask/controllers'; import { ethErrors } from 'eth-rpc-errors'; import { - CaveatFunctionGenerator, - caveatFunctionGenerators, -} from './caveat-functions'; + CaveatSpecification, + CaveatSpecifications, +} from './caveat-processor'; import { OriginString, Permission, @@ -63,9 +63,10 @@ interface PermissionsControllerOptions { never >; state?: Partial; + caveatSpecifications: CaveatSpecifications; + methodPrefix: string; restrictedMethods: string[]; safeMethods: string[]; - methodPrefix: string; } export class PermissionsController extends BaseController< @@ -83,15 +84,14 @@ export class PermissionsController extends BaseController< private readonly internalMethods: Readonly>; - private readonly caveatFunctionGenerators: Readonly< - Record> - > = { - ...caveatFunctionGenerators, - }; + private readonly caveatSpecifications: Readonly; + + private readonly caveatTypes: Readonly>; constructor({ messenger, state = {}, + caveatSpecifications, methodPrefix, restrictedMethods, safeMethods, @@ -103,6 +103,8 @@ export class PermissionsController extends BaseController< state: { ...defaultState, ...state }, }); + this.caveatSpecifications = caveatSpecifications; + this.caveatTypes = getCaveatTypes(caveatSpecifications); this.methodPrefix = methodPrefix; this.internalMethods = getInternalMethodNames(methodPrefix); this.restrictedMethods = new Set(restrictedMethods); @@ -225,6 +227,10 @@ export class PermissionsController extends BaseController< } } +function getCaveatTypes(caveatSpecifications: CaveatSpecifications) { + return new Set(Object.values(caveatSpecifications).map(specification => specification.type)) +} + function getInternalMethodNames(methodPrefix: string) { return new Set([ `${methodPrefix}getPermissions`, diff --git a/packages/controllers/src/permissions/caveat-functions.test.ts b/packages/controllers/src/permissions/caveat-functions.test.ts deleted file mode 100644 index dc4cd6ca55..0000000000 --- a/packages/controllers/src/permissions/caveat-functions.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { CaveatType } from './Caveat'; -import { caveatFunctionGenerators } from './caveat-functions'; - -describe('caveat functions', () => { - it('implements all caveat types', () => { - const types = Object.values(CaveatType); - expect(types.length).toStrictEqual( - Object.keys(caveatFunctionGenerators).length, - ); - - for (const caveatType of types) { - expect(caveatFunctionGenerators).toHaveProperty(caveatType); - } - }); -}); diff --git a/packages/controllers/src/permissions/caveat-functions.ts b/packages/controllers/src/permissions/caveat-functions.ts index ddbc3b68bf..74727260b6 100644 --- a/packages/controllers/src/permissions/caveat-functions.ts +++ b/packages/controllers/src/permissions/caveat-functions.ts @@ -4,6 +4,12 @@ import equal from 'fast-deep-equal'; import { ethErrors } from 'eth-rpc-errors'; import { Caveat } from './Caveat'; +// TODO: Decide what to do with these functions. +// Since caveats are now implemented by the consumer of the permissions +// controller, these will no longer serve their intended purpose. +// However, some of them may still be useful as utilities for constructing +// caveats, since some of them are battle-tested, and all of them have tests. + export type CaveatFunction = JsonRpcMiddleware; export type CaveatFunctionGenerator< diff --git a/packages/controllers/src/permissions/caveat-processor.ts b/packages/controllers/src/permissions/caveat-processor.ts new file mode 100644 index 0000000000..1fe88ba1d1 --- /dev/null +++ b/packages/controllers/src/permissions/caveat-processor.ts @@ -0,0 +1,50 @@ +import { Json, JsonRpcEngine, JsonRpcMiddleware } from 'json-rpc-engine'; +import { Permission } from './Permission'; + +type CaveatType = string; + +type CaveatImplementationFactory< + CaveatValue extends Json, + MethodParameters, + MethodResult +> = ( + caveatValue: CaveatValue, +) => JsonRpcMiddleware; + +export interface CaveatSpecification< + CaveatValue extends Json, + MethodParameters, + MethodResult +> { + type: CaveatType; + getImplementation: CaveatImplementationFactory< + CaveatValue, + MethodParameters, + MethodResult + >; +} + +export type CaveatSpecifications = Record< + CaveatType, + CaveatSpecification +>; + +export function applyCaveats( + caveatSpecifications: CaveatSpecifications, + permission: Permission, + engine: JsonRpcEngine, +): void { + if (!permission.caveats || permission.caveats.length === 0) { + return; + } + + permission.caveats.forEach((caveat) => { + if (!caveatSpecifications[caveat.type]) { + throw new Error(`Unrecognized caveat type: ${caveat.type}`); + } + + engine.push( + caveatSpecifications[caveat.type].getImplementation(caveat.value), + ); + }); +} From 87d3a0407655b5ecb5567a57315e784ea46e95d9 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sat, 3 Jul 2021 10:25:21 -0700 Subject: [PATCH 007/147] Implement state management primitives --- .../controllers/src/permissions/Permission.ts | 3 +- .../src/permissions/PermissionController.ts | 339 +++++++++++++++--- .../src/permissions/caveat-functions.ts | 138 ------- .../src/permissions/caveat-processing.ts | 125 +++++++ .../src/permissions/caveat-processor.ts | 50 --- .../controllers/src/permissions/errors.ts | 93 +++++ 6 files changed, 504 insertions(+), 244 deletions(-) delete mode 100644 packages/controllers/src/permissions/caveat-functions.ts create mode 100644 packages/controllers/src/permissions/caveat-processing.ts delete mode 100644 packages/controllers/src/permissions/caveat-processor.ts diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index cb5d0d6f61..19cfc4a21c 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -127,7 +127,8 @@ export class Permission implements ZcapLdCapability { */ public readonly invoker: OriginString; - constructor({ target, invoker, caveats, id }: PermissionOptions) { + constructor(options: PermissionOptions) { + const { id, target, invoker, caveats } = options; this.id = id ?? nanoid(); this.parentCapability = target; this.invoker = invoker; diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 122850d92d..76ab598420 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -2,18 +2,35 @@ import { BaseControllerV2 as BaseController, RestrictedControllerMessenger, } from '@metamask/controllers'; -import { ethErrors } from 'eth-rpc-errors'; +import deepEqual from 'fast-deep-equal'; import { - CaveatSpecification, - CaveatSpecifications, -} from './caveat-processor'; + Json, + JsonRpcEngineEndCallback, + JsonRpcEngineNextCallback, + JsonRpcMiddleware, + JsonRpcRequest, + PendingJsonRpcResponse, +} from 'json-rpc-engine'; +import { CaveatSpecifications } from './caveat-processing'; import { OriginString, Permission, MethodName, RequestedPermissions, } from './Permission'; -import { methodNotFound } from './errors'; +import { + PermissionDoesNotExistError, + methodNotFound, + UnrecognizedDomainError, + PermissionHasNoCaveatsError, + CaveatDoesNotExistError, + CaveatTypeDoesNotExistError, + InvalidCaveatJsonError, + PermissionTargetDoesNotExistError, + CaveatMissingValueError, + InvalidCaveatFieldsError, + InvalidDomainIdentifierError, +} from './errors'; import { Caveat } from './Caveat'; export interface DomainMetadata { @@ -54,6 +71,26 @@ export interface AccountsChanged { payload: [{ domain: OriginString; accounts: string[] }]; } +export interface PermittedJsonRpcMiddleware + extends JsonRpcMiddleware { + ( + req: JsonRpcRequest, + res: PendingJsonRpcResponse, + next: JsonRpcEngineNextCallback, + end: JsonRpcEngineEndCallback, + context?: Readonly< + { + origin: OriginString; + } & Record + >, + ): void; +} + +type RestrictedMethodsOption = Record< + MethodName, + PermittedJsonRpcMiddleware +>; + interface PermissionsControllerOptions { messenger: RestrictedControllerMessenger< typeof controllerName, @@ -65,7 +102,7 @@ interface PermissionsControllerOptions { state?: Partial; caveatSpecifications: CaveatSpecifications; methodPrefix: string; - restrictedMethods: string[]; + restrictedMethods: RestrictedMethodsOption; safeMethods: string[]; } @@ -78,13 +115,27 @@ export class PermissionsController extends BaseController< /** * Can be namespaced. */ - private readonly restrictedMethods: Readonly>; + private readonly restrictedMethods: Readonly< + Map> + >; - private readonly safeMethods: Readonly>; + private readonly _safeMethods: Readonly>; - private readonly internalMethods: Readonly>; + public get safeMethods(): Readonly> { + return this._safeMethods; + } - private readonly caveatSpecifications: Readonly; + private readonly _internalMethods: Readonly>; + + public get internalMethods(): Readonly> { + return this._internalMethods; + } + + private readonly _caveatSpecifications: Readonly; + + public get caveatSpecifications(): Readonly { + return this._caveatSpecifications; + } private readonly caveatTypes: Readonly>; @@ -103,12 +154,48 @@ export class PermissionsController extends BaseController< state: { ...defaultState, ...state }, }); - this.caveatSpecifications = caveatSpecifications; + this._caveatSpecifications = caveatSpecifications; this.caveatTypes = getCaveatTypes(caveatSpecifications); this.methodPrefix = methodPrefix; - this.internalMethods = getInternalMethodNames(methodPrefix); - this.restrictedMethods = new Set(restrictedMethods); - this.safeMethods = new Set(safeMethods); + this._internalMethods = getInternalMethodNames(methodPrefix); + this.restrictedMethods = getRestrictedMethodMap(restrictedMethods); + this._safeMethods = new Set(safeMethods); + } + + clearPermissions(): void { + this.update((_draftState) => { + return { ...defaultState }; + }); + } + + getDomains(): (keyof PermissionsControllerDomains)[] { + return Object.keys(this.state.domains); + } + + getPermission(origin: string, target: string): Permission { + return this.state.domains[origin]?.permissions[target]; + } + + getPermissions(origin: string): Record { + return this.state.domains[origin]?.permissions; + } + + hasPermission(origin: string, target: string): boolean { + return Boolean(this.getPermission(origin, target)); + } + + hasPermissions(origin: string): boolean { + return Boolean(this.state.domains[origin]); + } + + setPermission(origin: string, permission: Permission): void { + this.update((draftState) => { + if (!draftState.domains[origin]) { + draftState.domains[origin] = { origin, permissions: {} }; + } + const { parentCapability: target } = permission; + draftState.domains[origin].permissions[target] = permission as any; + }); } /** @@ -119,7 +206,7 @@ export class PermissionsController extends BaseController< * @param {string} domainName - The grantee domain. * @param {Array} newPermissions - The unique, new permissions for the grantee domain. */ - updatePermissions( + setPermissions( origin: string, permissions: Record, ): void { @@ -131,53 +218,99 @@ export class PermissionsController extends BaseController< }); } - grantPermissions( - domain: DomainMetadata, - requestedPermissions: RequestedPermissions, - ): Record { - const { origin } = domain; - if (!origin || typeof origin !== 'string') { - throw ethErrors.rpc.invalidRequest(`Invalid domain: '${domain}'.`); - } - - // Enforce actual approving known methods: - for (const methodName in requestedPermissions) { - if (!this.getMethodKeyFor(methodName)) { - throw methodNotFound({ methodName }); + revokePermission(origin: string, target: string): void { + this.update((draftState) => { + if (!draftState.domains[origin]) { + throw new UnrecognizedDomainError(origin); } - } - const permissions: { [methodName: string]: Permission } = {}; + const { permissions } = draftState.domains[origin]; + if (!permissions[target]) { + throw new PermissionDoesNotExistError(origin, target); + } - for (const method of Object.keys(requestedPermissions)) { - const newPerm = new Permission({ - target: method, - invoker: origin, - caveats: requestedPermissions[method].caveats, - }); + if (Object.keys(permissions).length > 1) { + delete permissions[target]; + } else { + delete draftState.domains[origin]; + } + }); + } - if (newPerm.caveats && !this.validateCaveats(newPerm.caveats)) { - throw ethErrors.rpc.internal({ - message: 'Invalid caveats.', - data: newPerm, - }); + revokeAllPermissions(origin: string): void { + this.update((draftState) => { + if (!draftState.domains[origin]) { + throw new UnrecognizedDomainError(origin); } + delete draftState.domains[origin]; + }); + } - permissions[method] = newPerm; - } + getCaveat( + origin: string, + target: string, + caveatType: string, + ): Caveat | undefined { + return this.getPermission(origin, target)?.caveats?.find( + (caveat) => caveat.type === caveatType, + ); + } - this.updatePermissions(origin, permissions); - return permissions; + hasCaveat(origin: string, target: string, caveatType: string): boolean { + return ( + this.getPermission(origin, target)?.caveats?.some( + (caveat) => caveat.type === caveatType, + ) ?? false + ); } - /** - * TODO: Implement. - * - * @param caveats The caveats to validate. - * @returns Whether the given caveats are valid. - */ - validateCaveats(_caveats: Caveat[]): boolean { - return true; + setCaveat(origin: string, target: string, caveat: Caveat): void { + this.update((draftState) => { + const permission = draftState.domains[origin]?.permissions[target]; + if (!permission) { + throw new PermissionDoesNotExistError(origin, target); + } + + if (permission.caveats) { + const existingIndex = permission.caveats.findIndex( + (existingCaveat) => existingCaveat.type === caveat.type, + ); + + if (existingIndex === -1) { + permission.caveats.push(caveat as any); + } else { + permission.caveats.splice(existingIndex, 1, caveat as any); + } + } else { + permission.caveats = [caveat as any]; + } + }); + } + + removeCaveat(origin: string, target: string, caveatType: string): void { + this.update((draftState) => { + const permission = draftState.domains[origin]?.permissions[target]; + if (!permission) { + throw new PermissionDoesNotExistError(origin, target); + } + + if (!permission.caveats) { + throw new PermissionHasNoCaveatsError(origin, target); + } + + const existingIndex = permission.caveats.findIndex( + (existingCaveat) => existingCaveat.type === caveatType, + ); + if (existingIndex === -1) { + throw new CaveatDoesNotExistError(origin, target, caveatType); + } + + if (permission.caveats.length > 1) { + permission.caveats.splice(existingIndex, 1); + } else { + permission.caveats = null; + } + }); } /** @@ -196,7 +329,7 @@ export class PermissionsController extends BaseController< } const wildCardMethodsWithoutWildCard: Record = {}; - for (const methodName of this.restrictedMethods) { + for (const methodName of this.restrictedMethods.keys()) { const wildCardMatch = methodName.match(/(.+)\*$/u); if (wildCardMatch) { wildCardMethodsWithoutWildCard[wildCardMatch[1]] = true; @@ -225,10 +358,90 @@ export class PermissionsController extends BaseController< return ''; } + + grantPermissions( + domain: DomainMetadata, + requestedPermissions: RequestedPermissions, + ): Record { + const { origin } = domain; + if (!origin || typeof origin !== 'string') { + throw new InvalidDomainIdentifierError(origin); + } + + // Enforce actual approving known methods: + for (const methodName in requestedPermissions) { + if (!this.getMethodKeyFor(methodName)) { + throw methodNotFound({ methodName }); + } + } + + const permissions: { [methodName: string]: Permission } = {}; + + for (const method of Object.keys(requestedPermissions)) { + if (!this.restrictedMethods.has(this.getMethodKeyFor(method))) { + throw new PermissionTargetDoesNotExistError(origin, method); + } + + permissions[method] = new Permission({ + target: method, + invoker: origin, + caveats: this.computeCaveats( + origin, + method, + requestedPermissions[method].caveats, + ), + }); + } + + this.setPermissions(origin, permissions); + return permissions; + } + + /** + * @param caveats The caveats to validate. + * @returns Whether the given caveats are valid. + */ + computeCaveats( + origin: string, + target: string, + caveats?: Caveat[], + ): Caveat[] | undefined { + const caveatArray = caveats?.map((caveat) => { + if (!this.caveatTypes.has(caveat.type)) { + throw new CaveatTypeDoesNotExistError(caveat.type, origin, target); + } + + if (!('value' in caveat)) { + throw new CaveatMissingValueError(caveat, origin, target); + } + + if (Object.keys(caveat).length !== 2) { + throw new InvalidCaveatFieldsError(caveat, origin, target); + } + + if (!isValidJson(caveat)) { + throw new InvalidCaveatJsonError(caveat, origin, target); + } + return new Caveat({ type: caveat.type, value: caveat.value }); + }); + return caveatArray && caveatArray.length > 0 ? caveatArray : undefined; + } +} + +function isValidJson(value: unknown): boolean { + try { + return deepEqual(value, JSON.parse(JSON.stringify(value))); + } catch (error) { + return false; + } } function getCaveatTypes(caveatSpecifications: CaveatSpecifications) { - return new Set(Object.values(caveatSpecifications).map(specification => specification.type)) + return new Set( + Object.values(caveatSpecifications).map( + (specification) => specification.type, + ), + ); } function getInternalMethodNames(methodPrefix: string) { @@ -238,6 +451,22 @@ function getInternalMethodNames(methodPrefix: string) { ]); } +function getRestrictedMethodMap( + restrictedMethods: RestrictedMethodsOption, +): Map> { + return Object.entries(restrictedMethods).reduce( + (methodMap, [methodName, implementation]) => { + if (!methodName || typeof methodName !== 'string') { + throw new Error(`Invalid method name: ${methodName}`); + } + + methodMap.set(methodName, implementation); + return methodMap; + }, + new Map(), + ); +} + // TODO: Are we using these? export interface PermissionsRequestMetadata extends DomainMetadata { diff --git a/packages/controllers/src/permissions/caveat-functions.ts b/packages/controllers/src/permissions/caveat-functions.ts deleted file mode 100644 index 74727260b6..0000000000 --- a/packages/controllers/src/permissions/caveat-functions.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { JsonRpcMiddleware, Json } from 'json-rpc-engine'; -import isSubset from 'is-subset'; -import equal from 'fast-deep-equal'; -import { ethErrors } from 'eth-rpc-errors'; -import { Caveat } from './Caveat'; - -// TODO: Decide what to do with these functions. -// Since caveats are now implemented by the consumer of the permissions -// controller, these will no longer serve their intended purpose. -// However, some of them may still be useful as utilities for constructing -// caveats, since some of them are battle-tested, and all of them have tests. - -export type CaveatFunction = JsonRpcMiddleware; - -export type CaveatFunctionGenerator< - CaveatValue extends Json, - Params, - Result -> = (caveat: Caveat) => CaveatFunction; - -/** - * Caveat middleware function generators for all caveat types. - * - * A caveat function generator takes a caveat object and returns a caveat - * middleware function corresponding to the caveat's type and bound to its - * value. - */ -export const caveatFunctionGenerators = { - filterResponse, - forceParams, - limitResponseLength, - requireParamsIsSubset, - requireParamsIsSuperset, -}; - -/** - * Require that request.params is a subset of or equal to the caveat value. - * Arrays are order-dependent, objects are order-independent. - */ -function requireParamsIsSubset( - caveat: Caveat>, -): CaveatFunction< - Json[] | Record, - Json[] | Record -> { - const { value } = caveat; - return (req, _res, next, _end): void => { - // Ensure that the params are a subset of or equal to the caveat value - if (!isSubset(value, req.params)) { - throw ethErrors.provider.unauthorized({ - data: { permittedParameters: value }, - message: - 'Unauthorized parameters. The request parameters must be a subset of or equal to the permitted parameters.', - }); - } - - return next(); - }; -} - -/** - * Require that request.params is a superset of or equal to the caveat value. - * Arrays are order-dependent, objects are order-independent. - */ -function requireParamsIsSuperset( - caveat: Caveat>, -): CaveatFunction< - Json[] | Record, - Json[] | Record -> { - const { value } = caveat; - return (req, _res, next, _end): void => { - // Ensure that the params are a superset of or equal to the caveat value - if (!isSubset(req.params, value)) { - throw ethErrors.provider.unauthorized({ - data: { requiredParameters: value }, - message: - 'Unauthorized parameters; missing required parameters. The request parameters must be a superset of or equal to the required parameters.', - }); - } - - return next(); - }; -} - -/* - * Filters array results deeply. - */ -function filterResponse( - caveat: Caveat, -): CaveatFunction { - const { value } = caveat; - return (_req, res, next, _end): void => { - next((done) => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - res.result = res.result!.filter((item) => { - const findResult = value.find((resultValue: Json) => { - return equal(resultValue, item); - }); - - return findResult !== undefined; - }); - done(); - }); - }; -} - -/* - * Limits array results to a specific integer length. - */ -function limitResponseLength( - caveat: Caveat, -): CaveatFunction { - const { value } = caveat; - return (_req, res, next, _end): void => { - next((done) => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - res.result = res.result!.slice(0, value); - done(); - }); - }; -} - -/* - * Forces the method to be called with the specified params. - */ -function forceParams( - caveat: Caveat>, -): CaveatFunction< - Json[] | Record, - Json[] | Record -> { - const { value } = caveat; - return (req, _res, next, _end): void => { - req.params = Array.isArray(value) ? [...value] : { ...value }; - return next(); - }; -} diff --git a/packages/controllers/src/permissions/caveat-processing.ts b/packages/controllers/src/permissions/caveat-processing.ts new file mode 100644 index 0000000000..593dd3a02d --- /dev/null +++ b/packages/controllers/src/permissions/caveat-processing.ts @@ -0,0 +1,125 @@ +import { Json, JsonRpcRequest, PendingJsonRpcResponse } from 'json-rpc-engine'; +import { Caveat } from './Caveat'; +import { Permission } from './Permission'; +import { PermittedJsonRpcMiddleware } from './PermissionController'; + +type CaveatType = string; + +/** + * The implementation of a caveat type. + */ +type CaveatFunction< + CaveatValue extends Json, + Params extends Json, + Result extends Json +> = ( + caveatObject: Caveat, + request: JsonRpcRequest, +) => CaveatReturnHandler | undefined; + +type CaveatImplementationFactory< + CaveatValue extends Json, + MethodParameters extends Json, + MethodResult extends Json +> = ( + caveatValue: CaveatValue, +) => CaveatFunction; + +export interface CaveatSpecification< + CaveatValue extends Json, + MethodParameters extends Json, + MethodResult extends Json +> { + type: CaveatType; + getImplementation: CaveatImplementationFactory< + CaveatValue, + MethodParameters, + MethodResult + >; +} + +export type CaveatSpecifications = Record< + CaveatType, + CaveatSpecification +>; + +/** + * A {@link CaveatFunction} whose `caveatObject` argument has been bound to + * the caveat object of a particular permission. + */ +type BoundCaveatFunction = ( + request: JsonRpcRequest, +) => CaveatReturnHandler | undefined; + +type CaveatReturnHandler = ( + response: PendingJsonRpcResponse, +) => void; + +/** + * Decorate a restricted method implementation with its caveats. + */ +export function decorateWithCaveats( + methodName: string, + methodImplementation: PermittedJsonRpcMiddleware, + getPermission: (method: string) => Permission, // bound to the requesting origin + caveatImplementations: Record>, // all caveat implementations +): PermittedJsonRpcMiddleware { + const { caveats } = getPermission(methodName); + + // If the permission has caveats, create an array of invocations of their + // corresponding functions bound to the caveat objects. + const caveatFunctions: BoundCaveatFunction[] = []; + if (caveats && caveats.length > 0) { + for (const caveat of caveats) { + if (!caveatImplementations[caveat.type]) { + throw new Error(`Unrecognized caveat type: ${caveat.type}`); + } + + caveatFunctions.push((request: JsonRpcRequest) => + caveatImplementations[caveat.type](caveat, request), + ); + } + } + + return ((req, res, next, end, context) => { + // If there are caveats, apply them to the request. + if (caveatFunctions.length > 0) { + const caveatReturnHandlers = applyCaveats(req, caveatFunctions); + + // If any of the caveats specified return handlers, wrap the "end" + // callback such that the return handlers are applied to the response + // before actually ending the request. + if (caveatReturnHandlers.length > 0) { + const endWithReturnHandlers = () => { + caveatReturnHandlers.forEach((returnHandler) => returnHandler(res)); + end(); + }; + + return methodImplementation( + req, + res, + next, + endWithReturnHandlers, + context, + ); + } + } + return methodImplementation(req, res, next, end, context); + }) as PermittedJsonRpcMiddleware; +} + +function applyCaveats( + req: JsonRpcRequest, + caveatFunctions: BoundCaveatFunction[], +): CaveatReturnHandler[] { + const returnHandlers: CaveatReturnHandler[] = []; + + for (const caveatFunction of caveatFunctions) { + const returnHandler = caveatFunction(req); + + if (returnHandler) { + returnHandlers.push(returnHandler); + } + } + return returnHandlers; +} diff --git a/packages/controllers/src/permissions/caveat-processor.ts b/packages/controllers/src/permissions/caveat-processor.ts deleted file mode 100644 index 1fe88ba1d1..0000000000 --- a/packages/controllers/src/permissions/caveat-processor.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Json, JsonRpcEngine, JsonRpcMiddleware } from 'json-rpc-engine'; -import { Permission } from './Permission'; - -type CaveatType = string; - -type CaveatImplementationFactory< - CaveatValue extends Json, - MethodParameters, - MethodResult -> = ( - caveatValue: CaveatValue, -) => JsonRpcMiddleware; - -export interface CaveatSpecification< - CaveatValue extends Json, - MethodParameters, - MethodResult -> { - type: CaveatType; - getImplementation: CaveatImplementationFactory< - CaveatValue, - MethodParameters, - MethodResult - >; -} - -export type CaveatSpecifications = Record< - CaveatType, - CaveatSpecification ->; - -export function applyCaveats( - caveatSpecifications: CaveatSpecifications, - permission: Permission, - engine: JsonRpcEngine, -): void { - if (!permission.caveats || permission.caveats.length === 0) { - return; - } - - permission.caveats.forEach((caveat) => { - if (!caveatSpecifications[caveat.type]) { - throw new Error(`Unrecognized caveat type: ${caveat.type}`); - } - - engine.push( - caveatSpecifications[caveat.type].getImplementation(caveat.value), - ); - }); -} diff --git a/packages/controllers/src/permissions/errors.ts b/packages/controllers/src/permissions/errors.ts index 677de8d73a..79c6907efc 100644 --- a/packages/controllers/src/permissions/errors.ts +++ b/packages/controllers/src/permissions/errors.ts @@ -1,5 +1,7 @@ import { JsonRpcRequest } from 'json-rpc-engine'; import { ethErrors, EthereumRpcError } from 'eth-rpc-errors'; +import { Permission } from './Permission'; +import { Caveat } from './Caveat'; interface ErrorArg { message?: string; @@ -32,3 +34,94 @@ export function userRejectedRequest( ): EthereumRpcError> { return ethErrors.provider.userRejectedRequest({ data: request }); } + +export class InvalidDomainIdentifierError extends Error { + constructor(origin: string) { + super(`Invalid external domain identifier: ${origin}`); + } +} + +export class UnrecognizedDomainError extends Error { + constructor(origin: string) { + super(`Unrecognized external domain: "${origin}" has no permissions.`); + } +} + +export class PermissionDoesNotExistError extends Error { + constructor(origin: string, target: string) { + super(`External domain "${origin}" has no permission for "${target}".`); + } +} + +export class PermissionHasNoCaveatsError extends Error { + constructor(origin: string, target: string) { + super( + `Permission for "${target}" of external domain "${origin}" has no caveats.`, + ); + } +} + +export class CaveatDoesNotExistError extends Error { + constructor(origin: string, target: string, caveatType: string) { + super( + `Permission for "${target}" of external domain "${origin}" has no caveat of type "${caveatType}".`, + ); + } +} + +export class PermissionTargetDoesNotExistError extends Error { + constructor(origin: string, target: string) { + super( + `Target "${target}" of requested permission for external domain "${origin}" does not exist.`, + ); + } +} + +export class InvalidPermissionJsonError extends Error { + public data: { origin: string; permission: Permission }; + + constructor(origin: string, permission: Permission) { + super( + `Permission object of external domain "${origin}" is not valid JSON.`, + ); + this.data = { origin, permission }; + } +} + +export class CaveatTypeDoesNotExistError extends Error { + public data: { origin: string; target: string }; + + constructor(caveatType: string, origin: string, target: string) { + super(`Caveat type "${caveatType}" does not exist.`); + this.data = { origin, target }; + } +} + +export class CaveatMissingValueError extends Error { + public data: { caveat: Caveat; origin: string; target: string }; + + constructor(caveat: Caveat, origin: string, target: string) { + super(`Caveat is missing "value" field.`); + this.data = { caveat, origin, target }; + } +} + +export class InvalidCaveatFieldsError extends Error { + public data: { caveat: Caveat; origin: string; target: string }; + + constructor(caveat: Caveat, origin: string, target: string) { + super( + `Caveat has unexpected number of fields: ${Object.keys(caveat).length}`, + ); + this.data = { caveat, origin, target }; + } +} + +export class InvalidCaveatJsonError extends Error { + public data: { caveat: Caveat; origin: string; target: string }; + + constructor(caveat: Caveat, origin: string, target: string) { + super(`Caveat object is not valid JSON.`); + this.data = { caveat, origin, target }; + } +} From cb86f11f9b6ff212b95bd94c0a44118510863e83 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sat, 3 Jul 2021 10:46:37 -0700 Subject: [PATCH 008/147] chore: lint, post-rebase fixup --- packages/controllers/.eslintrc.js | 11 +++++++++-- packages/controllers/src/permissions/Permission.ts | 12 ++++++------ .../src/permissions/PermissionController.ts | 1 - .../controllers/src/permissions/caveat-processing.ts | 6 +++--- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/controllers/.eslintrc.js b/packages/controllers/.eslintrc.js index ddb95ca6a9..c7e798f666 100644 --- a/packages/controllers/.eslintrc.js +++ b/packages/controllers/.eslintrc.js @@ -3,7 +3,14 @@ module.exports = { rules: { '@typescript-eslint/consistent-type-definitions': 'off', - 'no-shadow': 'off', - '@typescript-eslint/no-shadow': ['error', { builtinGlobals: true }], }, + + overrides: [ + { + files: ['**/*.d.ts'], + rules: { + 'import/unambiguous': 'off', + }, + }, + ], }; diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 19cfc4a21c..69f1b6c6a7 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -28,7 +28,7 @@ interface ZcapLdCapability { /** * The cryptograhically strong GUID of the capability. */ - 'id': string; + id: string; /** * A pointer to the resource that possession of the capability grants @@ -36,7 +36,7 @@ interface ZcapLdCapability { * * In the context of MetaMask, this is always the name of an RPC method. */ - 'parentCapability': string; + parentCapability: string; /** * A pointer to the the entity that may invoke this capability. @@ -46,19 +46,19 @@ interface ZcapLdCapability { * * In the context of MetaMask, this is simply the origin of an external domain. */ - 'invoker': string; + invoker: string; /** * The issuing date, in UNIX epoch time. */ - 'date'?: number; + date?: number; /** * An array of caveat objects. See {@link ZcapLdCaveat}. * * TODO: Make optional in typescript@4.4.x */ - 'caveats': ZcapLdCaveat[] | null; + caveats: ZcapLdCaveat[] | null; /** * The proof that this capability was delegated to the specified invoker. @@ -68,7 +68,7 @@ interface ZcapLdCapability { * In MetaMask, the "proof" of validity is the existence of a valid capability * object in the designated part of our state tree, so this field is omitted. */ - 'proof'?: string; + proof?: string; } interface PermissionOptions { diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 76ab598420..0348f405cd 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -48,7 +48,6 @@ export type PermissionsControllerDomains = Record< PermissionsDomainEntry >; -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions export type PermissionsControllerState = { domains: PermissionsControllerDomains; }; diff --git a/packages/controllers/src/permissions/caveat-processing.ts b/packages/controllers/src/permissions/caveat-processing.ts index 593dd3a02d..dc3004e375 100644 --- a/packages/controllers/src/permissions/caveat-processing.ts +++ b/packages/controllers/src/permissions/caveat-processing.ts @@ -11,7 +11,7 @@ type CaveatType = string; type CaveatFunction< CaveatValue extends Json, Params extends Json, - Result extends Json + Result extends Json, > = ( caveatObject: Caveat, request: JsonRpcRequest, @@ -20,7 +20,7 @@ type CaveatFunction< type CaveatImplementationFactory< CaveatValue extends Json, MethodParameters extends Json, - MethodResult extends Json + MethodResult extends Json, > = ( caveatValue: CaveatValue, ) => CaveatFunction; @@ -28,7 +28,7 @@ type CaveatImplementationFactory< export interface CaveatSpecification< CaveatValue extends Json, MethodParameters extends Json, - MethodResult extends Json + MethodResult extends Json, > { type: CaveatType; getImplementation: CaveatImplementationFactory< From fa793c3b498901a0659137ecd17687f88d70f395 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sat, 3 Jul 2021 17:36:10 -0700 Subject: [PATCH 009/147] domain -> actor, update actions/events, register action handlers --- .../controllers/src/permissions/Permission.ts | 8 +- .../src/permissions/PermissionController.ts | 199 +++++++++++------- .../src/permissions/caveat-processing.ts | 8 +- .../controllers/src/permissions/errors.ts | 22 +- .../src/plugins/PluginController.ts | 8 +- 5 files changed, 145 insertions(+), 100 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 69f1b6c6a7..40aa867a29 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -3,7 +3,7 @@ import { nanoid } from 'nanoid'; import { Caveat, ZcapLdCaveat } from './Caveat'; /** - * The origin of an external domain. + * The origin of an actor. * Effectively the GUID of an entity that can have permissions. */ export type OriginString = string; @@ -44,7 +44,7 @@ interface ZcapLdCapability { * By the standard, this a link – usually some kind of URI – to a cryptographic * key that the proof of the `proof` field "must validate against". * - * In the context of MetaMask, this is simply the origin of an external domain. + * In the context of MetaMask, this is simply the origin of an actor. */ invoker: string; @@ -78,7 +78,7 @@ interface PermissionOptions { target: MethodName; /** - * The origin string of the external domain that has the permission. + * The origin string of the actor that has the permission. */ invoker: OriginString; @@ -123,7 +123,7 @@ export class Permission implements ZcapLdCapability { public readonly caveats: Caveat[] | null; /** - * The origin string of the external domain that has the permission. + * The origin string of the actor that has the permission. */ public readonly invoker: OriginString; diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 0348f405cd..2406b2bd71 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -21,7 +21,7 @@ import { import { PermissionDoesNotExistError, methodNotFound, - UnrecognizedDomainError, + UnrecognizedActorError, PermissionHasNoCaveatsError, CaveatDoesNotExistError, CaveatTypeDoesNotExistError, @@ -29,104 +29,125 @@ import { PermissionTargetDoesNotExistError, CaveatMissingValueError, InvalidCaveatFieldsError, - InvalidDomainIdentifierError, + InvalidActorIdentifierError, } from './errors'; import { Caveat } from './Caveat'; -export interface DomainMetadata { +export interface ActorMetadata { origin: OriginString; } -const controllerName = 'PermissionsController'; +const controllerName = 'PermissionController'; -export interface PermissionsDomainEntry extends DomainMetadata { +export interface PermissionsActorEntry extends ActorMetadata { permissions: Record; } -export type PermissionsControllerDomains = Record< +export type PermissionControllerActors = Record< OriginString, - PermissionsDomainEntry + PermissionsActorEntry >; -export type PermissionsControllerState = { - domains: PermissionsControllerDomains; +export type PermissionControllerState = { + actors: PermissionControllerActors; }; const stateMetadata = { - domains: { persist: true, anonymous: true }, + actors: { persist: true, anonymous: true }, }; -const defaultState: PermissionsControllerState = { - domains: {}, +const defaultState: PermissionControllerState = { + actors: {}, }; -export interface GetDomains { - type: `${typeof controllerName}:getDomains`; - handler: () => (keyof PermissionsControllerDomains)[]; +export interface GetActorsAction { + type: `${typeof controllerName}:getActors`; + handler: () => (keyof PermissionControllerActors)[]; } -export interface AccountsChanged { - type: `${typeof controllerName}:accountsChanged`; - payload: [{ domain: OriginString; accounts: string[] }]; +export interface ClearPermissionsAction { + type: `${typeof controllerName}:clearPermissions`; + handler: () => void; } -export interface PermittedJsonRpcMiddleware +export interface PermissionChangedPayload { + actor: OriginString; + target: PermissionTarget; + permission: Permission; +} + +export interface PermissionChangedEvent { + type: `${typeof controllerName}:permissionChanged:${PermissionTarget}`; + payload: [PermissionChangedPayload]; +} + +export type PermissionControllerActions = + | GetActorsAction + | ClearPermissionsAction; + +export type PermissionControllerEvents = PermissionChangedEvent; + +export type PermissionControllerMessenger = RestrictedControllerMessenger< + typeof controllerName, + PermissionControllerActions, + PermissionControllerEvents, + never, + never +>; + +export interface RestrictedMethodImplementation extends JsonRpcMiddleware { ( req: JsonRpcRequest, res: PendingJsonRpcResponse, next: JsonRpcEngineNextCallback, end: JsonRpcEngineEndCallback, - context?: Readonly< - { - origin: OriginString; - } & Record - >, + context?: Readonly<{ + origin: OriginString; + [key: string]: unknown; + }>, ): void; } type RestrictedMethodsOption = Record< MethodName, - PermittedJsonRpcMiddleware + RestrictedMethodImplementation >; -interface PermissionsControllerOptions { - messenger: RestrictedControllerMessenger< - typeof controllerName, - GetDomains, - AccountsChanged, - never, - never - >; - state?: Partial; +interface PermissionControllerOptions { + messenger: PermissionControllerMessenger; caveatSpecifications: CaveatSpecifications; methodPrefix: string; restrictedMethods: RestrictedMethodsOption; safeMethods: string[]; + state?: Partial; } -export class PermissionsController extends BaseController< +export class PermissionController extends BaseController< typeof controllerName, - PermissionsControllerState + PermissionControllerState > { public readonly methodPrefix: string; + protected messagingSystem: PermissionControllerMessenger; + /** * Can be namespaced. */ - private readonly restrictedMethods: Readonly< - Map> + private readonly restrictedMethods: ReadonlyMap< + string, + RestrictedMethodImplementation >; - private readonly _safeMethods: Readonly>; + private readonly _safeMethods: ReadonlySet; - public get safeMethods(): Readonly> { + public get safeMethods(): ReadonlySet { return this._safeMethods; } - private readonly _internalMethods: Readonly>; + private readonly _internalMethods: ReadonlySet; - public get internalMethods(): Readonly> { + public get internalMethods(): ReadonlySet { return this._internalMethods; } @@ -136,7 +157,7 @@ export class PermissionsController extends BaseController< return this._caveatSpecifications; } - private readonly caveatTypes: Readonly>; + protected readonly caveatTypes: ReadonlySet; constructor({ messenger, @@ -145,7 +166,7 @@ export class PermissionsController extends BaseController< methodPrefix, restrictedMethods, safeMethods, - }: PermissionsControllerOptions) { + }: PermissionControllerOptions) { super({ name: controllerName, metadata: stateMetadata, @@ -153,12 +174,29 @@ export class PermissionsController extends BaseController< state: { ...defaultState, ...state }, }); - this._caveatSpecifications = caveatSpecifications; + this._caveatSpecifications = Object.freeze(caveatSpecifications); this.caveatTypes = getCaveatTypes(caveatSpecifications); this.methodPrefix = methodPrefix; this._internalMethods = getInternalMethodNames(methodPrefix); this.restrictedMethods = getRestrictedMethodMap(restrictedMethods); this._safeMethods = new Set(safeMethods); + + // This assignment is redundant, but TypeScript doesn't know that it becomes + // assigned if we don't do it. + this.messagingSystem = messenger; + + this.registerMessageHandlers(); + } + + protected registerMessageHandlers(): void { + this.messagingSystem.registerActionHandler( + `${controllerName}:getActors`, + () => this.getActors(), + ); + this.messagingSystem.registerActionHandler( + `${controllerName}:clearPermissions`, + () => this.clearPermissions(), + ); } clearPermissions(): void { @@ -167,16 +205,16 @@ export class PermissionsController extends BaseController< }); } - getDomains(): (keyof PermissionsControllerDomains)[] { - return Object.keys(this.state.domains); + getActors(): (keyof PermissionControllerActors)[] { + return Object.keys(this.state.actors); } getPermission(origin: string, target: string): Permission { - return this.state.domains[origin]?.permissions[target]; + return this.state.actors[origin]?.permissions[target]; } getPermissions(origin: string): Record { - return this.state.domains[origin]?.permissions; + return this.state.actors[origin]?.permissions; } hasPermission(origin: string, target: string): boolean { @@ -184,46 +222,46 @@ export class PermissionsController extends BaseController< } hasPermissions(origin: string): boolean { - return Boolean(this.state.domains[origin]); + return Boolean(this.state.actors[origin]); } setPermission(origin: string, permission: Permission): void { this.update((draftState) => { - if (!draftState.domains[origin]) { - draftState.domains[origin] = { origin, permissions: {} }; + if (!draftState.actors[origin]) { + draftState.actors[origin] = { origin, permissions: {} }; } const { parentCapability: target } = permission; - draftState.domains[origin].permissions[target] = permission as any; + draftState.actors[origin].permissions[target] = permission as any; }); } /** - * Adds permissions to the given domain. Overwrites existing identical - * permissions (same domain, and method). Other existing permissions + * Adds permissions to the given actor. Overwrites existing identical + * permissions (same actor and method). Other existing permissions * remain unaffected. * - * @param {string} domainName - The grantee domain. - * @param {Array} newPermissions - The unique, new permissions for the grantee domain. + * @param {string} origin - The origin of the grantee actor. + * @param {Array} newPermissions - The unique, new permissions for the grantee actor. */ setPermissions( origin: string, permissions: Record, ): void { this.update((draftState) => { - if (!draftState.domains[origin]) { - draftState.domains[origin] = { origin, permissions: {} }; + if (!draftState.actors[origin]) { + draftState.actors[origin] = { origin, permissions: {} }; } - Object.assign(draftState.domains[origin].permissions, permissions); + Object.assign(draftState.actors[origin].permissions, permissions); }); } revokePermission(origin: string, target: string): void { this.update((draftState) => { - if (!draftState.domains[origin]) { - throw new UnrecognizedDomainError(origin); + if (!draftState.actors[origin]) { + throw new UnrecognizedActorError(origin); } - const { permissions } = draftState.domains[origin]; + const { permissions } = draftState.actors[origin]; if (!permissions[target]) { throw new PermissionDoesNotExistError(origin, target); } @@ -231,17 +269,17 @@ export class PermissionsController extends BaseController< if (Object.keys(permissions).length > 1) { delete permissions[target]; } else { - delete draftState.domains[origin]; + delete draftState.actors[origin]; } }); } revokeAllPermissions(origin: string): void { this.update((draftState) => { - if (!draftState.domains[origin]) { - throw new UnrecognizedDomainError(origin); + if (!draftState.actors[origin]) { + throw new UnrecognizedActorError(origin); } - delete draftState.domains[origin]; + delete draftState.actors[origin]; }); } @@ -265,7 +303,7 @@ export class PermissionsController extends BaseController< setCaveat(origin: string, target: string, caveat: Caveat): void { this.update((draftState) => { - const permission = draftState.domains[origin]?.permissions[target]; + const permission = draftState.actors[origin]?.permissions[target]; if (!permission) { throw new PermissionDoesNotExistError(origin, target); } @@ -288,7 +326,7 @@ export class PermissionsController extends BaseController< removeCaveat(origin: string, target: string, caveatType: string): void { this.update((draftState) => { - const permission = draftState.domains[origin]?.permissions[target]; + const permission = draftState.actors[origin]?.permissions[target]; if (!permission) { throw new PermissionDoesNotExistError(origin, target); } @@ -359,12 +397,12 @@ export class PermissionsController extends BaseController< } grantPermissions( - domain: DomainMetadata, + actor: ActorMetadata, requestedPermissions: RequestedPermissions, ): Record { - const { origin } = domain; + const { origin } = actor; if (!origin || typeof origin !== 'string') { - throw new InvalidDomainIdentifierError(origin); + throw new InvalidActorIdentifierError(origin); } // Enforce actual approving known methods: @@ -452,7 +490,7 @@ function getInternalMethodNames(methodPrefix: string) { function getRestrictedMethodMap( restrictedMethods: RestrictedMethodsOption, -): Map> { +): ReadonlyMap> { return Object.entries(restrictedMethods).reduce( (methodMap, [methodName, implementation]) => { if (!methodName || typeof methodName !== 'string') { @@ -468,13 +506,24 @@ function getRestrictedMethodMap( // TODO: Are we using these? -export interface PermissionsRequestMetadata extends DomainMetadata { +export interface AccountsChangedPayload + extends PermissionChangedPayload<'eth_accounts'> { + accounts: string[]; +} + +export interface AccountsChangedEvent + extends PermissionChangedEvent<'eth_accounts'> { + type: `${typeof controllerName}:permissionChanged:eth_accounts`; + payload: [AccountsChangedPayload]; +} + +export interface PermissionsRequestMetadata extends ActorMetadata { id: string; } /** * Used for prompting the user about a proposed new permission. - * Includes information about the domain granted, as well as the permissions + * Includes information about the actor granted, as well as the permissions * assigned. */ export interface PermissionsRequest { diff --git a/packages/controllers/src/permissions/caveat-processing.ts b/packages/controllers/src/permissions/caveat-processing.ts index dc3004e375..bd24f4da04 100644 --- a/packages/controllers/src/permissions/caveat-processing.ts +++ b/packages/controllers/src/permissions/caveat-processing.ts @@ -1,7 +1,7 @@ import { Json, JsonRpcRequest, PendingJsonRpcResponse } from 'json-rpc-engine'; import { Caveat } from './Caveat'; import { Permission } from './Permission'; -import { PermittedJsonRpcMiddleware } from './PermissionController'; +import { RestrictedMethodImplementation } from './PermissionController'; type CaveatType = string; @@ -60,10 +60,10 @@ type CaveatReturnHandler = ( */ export function decorateWithCaveats( methodName: string, - methodImplementation: PermittedJsonRpcMiddleware, + methodImplementation: RestrictedMethodImplementation, getPermission: (method: string) => Permission, // bound to the requesting origin caveatImplementations: Record>, // all caveat implementations -): PermittedJsonRpcMiddleware { +): RestrictedMethodImplementation { const { caveats } = getPermission(methodName); // If the permission has caveats, create an array of invocations of their @@ -105,7 +105,7 @@ export function decorateWithCaveats( } } return methodImplementation(req, res, next, end, context); - }) as PermittedJsonRpcMiddleware; + }) as RestrictedMethodImplementation; } function applyCaveats( diff --git a/packages/controllers/src/permissions/errors.ts b/packages/controllers/src/permissions/errors.ts index 79c6907efc..45c2404877 100644 --- a/packages/controllers/src/permissions/errors.ts +++ b/packages/controllers/src/permissions/errors.ts @@ -35,36 +35,34 @@ export function userRejectedRequest( return ethErrors.provider.userRejectedRequest({ data: request }); } -export class InvalidDomainIdentifierError extends Error { +export class InvalidActorIdentifierError extends Error { constructor(origin: string) { - super(`Invalid external domain identifier: ${origin}`); + super(`Invalid actor identifier: ${origin}`); } } -export class UnrecognizedDomainError extends Error { +export class UnrecognizedActorError extends Error { constructor(origin: string) { - super(`Unrecognized external domain: "${origin}" has no permissions.`); + super(`Unrecognized actor: "${origin}" has no permissions.`); } } export class PermissionDoesNotExistError extends Error { constructor(origin: string, target: string) { - super(`External domain "${origin}" has no permission for "${target}".`); + super(`Actor "${origin}" has no permission for "${target}".`); } } export class PermissionHasNoCaveatsError extends Error { constructor(origin: string, target: string) { - super( - `Permission for "${target}" of external domain "${origin}" has no caveats.`, - ); + super(`Permission for "${target}" of actor "${origin}" has no caveats.`); } } export class CaveatDoesNotExistError extends Error { constructor(origin: string, target: string, caveatType: string) { super( - `Permission for "${target}" of external domain "${origin}" has no caveat of type "${caveatType}".`, + `Permission for "${target}" of actor "${origin}" has no caveat of type "${caveatType}".`, ); } } @@ -72,7 +70,7 @@ export class CaveatDoesNotExistError extends Error { export class PermissionTargetDoesNotExistError extends Error { constructor(origin: string, target: string) { super( - `Target "${target}" of requested permission for external domain "${origin}" does not exist.`, + `Target "${target}" of requested permission for actor "${origin}" does not exist.`, ); } } @@ -81,9 +79,7 @@ export class InvalidPermissionJsonError extends Error { public data: { origin: string; permission: Permission }; constructor(origin: string, permission: Permission) { - super( - `Permission object of external domain "${origin}" is not valid JSON.`, - ); + super(`Permission object of actor "${origin}" is not valid JSON.`); this.data = { origin, permission }; } } diff --git a/packages/controllers/src/plugins/PluginController.ts b/packages/controllers/src/plugins/PluginController.ts index 65933441cd..628a91230b 100644 --- a/packages/controllers/src/plugins/PluginController.ts +++ b/packages/controllers/src/plugins/PluginController.ts @@ -57,16 +57,16 @@ export type InstallPluginsResult = { // Types that probably should be defined elsewhere in prod type RemoveAllPermissionsFunction = (pluginIds: string[]) => void; -type CloseAllConnectionsFunction = (domain: string) => void; +type CloseAllConnectionsFunction = (origin: string) => void; type RequestPermissionsFunction = ( - domain: string, + origin: string, requestedPermissions: RequestedPluginPermissions, ) => Promise; type HasPermissionFunction = ( - domain: string, + origin: string, permissionName: string, ) => boolean; -type GetPermissionsFunction = (domain: string) => IOcapLdCapability[]; +type GetPermissionsFunction = (origin: string) => IOcapLdCapability[]; type PluginId = string; type StoredPlugins = Record; From 8a2f31306eeba9490be23c21a4014b7043f0fb01 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 13 Jul 2021 13:14:44 -0700 Subject: [PATCH 010/147] Add getCaveatValue static method --- .../controllers/src/permissions/Permission.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 40aa867a29..8967f5d0d1 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -135,6 +135,22 @@ export class Permission implements ZcapLdCapability { this.caveats = caveats ?? null; this.date = new Date().getTime(); } + + /** + * Gets the value of the caveat of the specified type belonging to the + * specified permission. + * + * @param permission The permission whose caveat value to retrieve. + * @param caveatType The type of the caveat to retrieve. + * @returns The caveat value, or undefined if no such caveat exists. + */ + public static getCaveatValue( + permission: Permission, + caveatType: string, + ): Json | undefined { + return permission.caveats?.find((caveat) => caveat.type === caveatType) + ?.value; + } } type RequestedPermission = Pick; From 724f8e19c6d1382cc942ae5f2dd61d6a56ad1421 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 13 Jul 2021 13:45:33 -0700 Subject: [PATCH 011/147] Fixups: return types, comments, code clarity, etc. --- .../src/permissions/PermissionController.ts | 55 +++++++++++-------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 2406b2bd71..f4cb5ac925 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -48,6 +48,7 @@ export type PermissionControllerActors = Record< PermissionsActorEntry >; +// TODO: TypeScript doesn't understand that a given actor may not exist. export type PermissionControllerState = { actors: PermissionControllerActors; }; @@ -134,7 +135,7 @@ export class PermissionController extends BaseController< /** * Can be namespaced. */ - private readonly restrictedMethods: ReadonlyMap< + private readonly _restrictedMethods: ReadonlyMap< string, RestrictedMethodImplementation >; @@ -157,7 +158,7 @@ export class PermissionController extends BaseController< return this._caveatSpecifications; } - protected readonly caveatTypes: ReadonlySet; + protected readonly _caveatTypes: ReadonlySet; constructor({ messenger, @@ -174,12 +175,12 @@ export class PermissionController extends BaseController< state: { ...defaultState, ...state }, }); - this._caveatSpecifications = Object.freeze(caveatSpecifications); - this.caveatTypes = getCaveatTypes(caveatSpecifications); - this.methodPrefix = methodPrefix; + this._caveatSpecifications = Object.freeze({ ...caveatSpecifications }); + this._caveatTypes = getCaveatTypes(this._caveatSpecifications); this._internalMethods = getInternalMethodNames(methodPrefix); - this.restrictedMethods = getRestrictedMethodMap(restrictedMethods); + this._restrictedMethods = getRestrictedMethodMap(restrictedMethods); this._safeMethods = new Set(safeMethods); + this.methodPrefix = methodPrefix; // This assignment is redundant, but TypeScript doesn't know that it becomes // assigned if we don't do it. @@ -188,6 +189,10 @@ export class PermissionController extends BaseController< this.registerMessageHandlers(); } + /** + * Constructor helper for registering this controller's messaging system + * actions. + */ protected registerMessageHandlers(): void { this.messagingSystem.registerActionHandler( `${controllerName}:getActors`, @@ -195,11 +200,11 @@ export class PermissionController extends BaseController< ); this.messagingSystem.registerActionHandler( `${controllerName}:clearPermissions`, - () => this.clearPermissions(), + () => this.clearState(), ); } - clearPermissions(): void { + protected clearState(): void { this.update((_draftState) => { return { ...defaultState }; }); @@ -209,11 +214,11 @@ export class PermissionController extends BaseController< return Object.keys(this.state.actors); } - getPermission(origin: string, target: string): Permission { + getPermission(origin: string, target: string): Permission | undefined { return this.state.actors[origin]?.permissions[target]; } - getPermissions(origin: string): Record { + getPermissions(origin: string): Record | undefined { return this.state.actors[origin]?.permissions; } @@ -231,6 +236,7 @@ export class PermissionController extends BaseController< draftState.actors[origin] = { origin, permissions: {} }; } const { parentCapability: target } = permission; + // Typecast: ts(2589) draftState.actors[origin].permissions[target] = permission as any; }); } @@ -308,15 +314,16 @@ export class PermissionController extends BaseController< throw new PermissionDoesNotExistError(origin, target); } + // Typecasts: ts(2589) if (permission.caveats) { - const existingIndex = permission.caveats.findIndex( + const caveatIndex = permission.caveats.findIndex( (existingCaveat) => existingCaveat.type === caveat.type, ); - if (existingIndex === -1) { + if (caveatIndex === -1) { permission.caveats.push(caveat as any); } else { - permission.caveats.splice(existingIndex, 1, caveat as any); + permission.caveats.splice(caveatIndex, 1, caveat as any); } } else { permission.caveats = [caveat as any]; @@ -335,17 +342,17 @@ export class PermissionController extends BaseController< throw new PermissionHasNoCaveatsError(origin, target); } - const existingIndex = permission.caveats.findIndex( + const caveatIndex = permission.caveats.findIndex( (existingCaveat) => existingCaveat.type === caveatType, ); - if (existingIndex === -1) { + if (caveatIndex === -1) { throw new CaveatDoesNotExistError(origin, target, caveatType); } - if (permission.caveats.length > 1) { - permission.caveats.splice(existingIndex, 1); - } else { + if (permission.caveats.length === 1) { permission.caveats = null; + } else { + permission.caveats.splice(caveatIndex, 1); } }); } @@ -361,12 +368,12 @@ export class PermissionController extends BaseController< * @returns The internal key of the method. */ getMethodKeyFor(method: string): string { - if (this.restrictedMethods.has(method)) { + if (this._restrictedMethods.has(method)) { return method; } const wildCardMethodsWithoutWildCard: Record = {}; - for (const methodName of this.restrictedMethods.keys()) { + for (const methodName of this._restrictedMethods.keys()) { const wildCardMatch = methodName.match(/(.+)\*$/u); if (wildCardMatch) { wildCardMethodsWithoutWildCard[wildCardMatch[1]] = true; @@ -381,13 +388,13 @@ export class PermissionController extends BaseController< while ( segments.length > 0 && - !this.restrictedMethods.has(methodKey) && + !this._restrictedMethods.has(methodKey) && !wildCardMethodsWithoutWildCard[methodKey] ) { methodKey += `${segments.shift()}_`; } - if (this.restrictedMethods.has(methodKey)) { + if (this._restrictedMethods.has(methodKey)) { return methodKey; } else if (wildCardMethodsWithoutWildCard[methodKey]) { return `${methodKey}*`; @@ -415,7 +422,7 @@ export class PermissionController extends BaseController< const permissions: { [methodName: string]: Permission } = {}; for (const method of Object.keys(requestedPermissions)) { - if (!this.restrictedMethods.has(this.getMethodKeyFor(method))) { + if (!this._restrictedMethods.has(this.getMethodKeyFor(method))) { throw new PermissionTargetDoesNotExistError(origin, method); } @@ -444,7 +451,7 @@ export class PermissionController extends BaseController< caveats?: Caveat[], ): Caveat[] | undefined { const caveatArray = caveats?.map((caveat) => { - if (!this.caveatTypes.has(caveat.type)) { + if (!this._caveatTypes.has(caveat.type)) { throw new CaveatTypeDoesNotExistError(caveat.type, origin, target); } From bd46ea5d3b56c3be5b5c52d380ad6f28a20fc7f3 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Thu, 5 Aug 2021 08:01:33 -0700 Subject: [PATCH 012/147] Clean up events and actions --- .../src/permissions/PermissionController.ts | 49 ++++++++----------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index f4cb5ac925..d3915307dc 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -1,8 +1,9 @@ +import type { Patch } from 'immer'; +import deepEqual from 'fast-deep-equal'; import { BaseControllerV2 as BaseController, RestrictedControllerMessenger, } from '@metamask/controllers'; -import deepEqual from 'fast-deep-equal'; import { Json, JsonRpcEngineEndCallback, @@ -11,6 +12,7 @@ import { JsonRpcRequest, PendingJsonRpcResponse, } from 'json-rpc-engine'; + import { CaveatSpecifications } from './caveat-processing'; import { OriginString, @@ -61,32 +63,32 @@ const defaultState: PermissionControllerState = { actors: {}, }; -export interface GetActorsAction { +export type GetPermissionsState = { + type: `${typeof controllerName}:getState`; + handler: () => PermissionControllerState; +}; + +export type GetActors = { type: `${typeof controllerName}:getActors`; handler: () => (keyof PermissionControllerActors)[]; -} +}; -export interface ClearPermissionsAction { +export type ClearPermissions = { type: `${typeof controllerName}:clearPermissions`; handler: () => void; -} - -export interface PermissionChangedPayload { - actor: OriginString; - target: PermissionTarget; - permission: Permission; -} +}; -export interface PermissionChangedEvent { - type: `${typeof controllerName}:permissionChanged:${PermissionTarget}`; - payload: [PermissionChangedPayload]; -} +export type PermissionsStateChange = { + type: `${typeof controllerName}:stateChange`; + payload: [PermissionControllerState, Patch[]]; +}; export type PermissionControllerActions = - | GetActorsAction - | ClearPermissionsAction; + | GetPermissionsState + | GetActors + | ClearPermissions; -export type PermissionControllerEvents = PermissionChangedEvent; +export type PermissionControllerEvents = PermissionsStateChange; export type PermissionControllerMessenger = RestrictedControllerMessenger< typeof controllerName, @@ -513,17 +515,6 @@ function getRestrictedMethodMap( // TODO: Are we using these? -export interface AccountsChangedPayload - extends PermissionChangedPayload<'eth_accounts'> { - accounts: string[]; -} - -export interface AccountsChangedEvent - extends PermissionChangedEvent<'eth_accounts'> { - type: `${typeof controllerName}:permissionChanged:eth_accounts`; - payload: [AccountsChangedPayload]; -} - export interface PermissionsRequestMetadata extends ActorMetadata { id: string; } From dc19143e89e67d16e6f3316423cfcce7bb8ac275 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Thu, 5 Aug 2021 08:06:01 -0700 Subject: [PATCH 013/147] actors -> subjects --- .../controllers/src/permissions/Permission.ts | 8 +- .../src/permissions/PermissionController.ts | 90 +++++++++---------- .../controllers/src/permissions/errors.ts | 18 ++-- 3 files changed, 58 insertions(+), 58 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 8967f5d0d1..0e7b9e3abc 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -3,7 +3,7 @@ import { nanoid } from 'nanoid'; import { Caveat, ZcapLdCaveat } from './Caveat'; /** - * The origin of an actor. + * The origin of a subject. * Effectively the GUID of an entity that can have permissions. */ export type OriginString = string; @@ -44,7 +44,7 @@ interface ZcapLdCapability { * By the standard, this a link – usually some kind of URI – to a cryptographic * key that the proof of the `proof` field "must validate against". * - * In the context of MetaMask, this is simply the origin of an actor. + * In the context of MetaMask, this is simply the origin of subject. */ invoker: string; @@ -78,7 +78,7 @@ interface PermissionOptions { target: MethodName; /** - * The origin string of the actor that has the permission. + * The origin string of the subject that has the permission. */ invoker: OriginString; @@ -123,7 +123,7 @@ export class Permission implements ZcapLdCapability { public readonly caveats: Caveat[] | null; /** - * The origin string of the actor that has the permission. + * The origin string of the subject that has the permission. */ public readonly invoker: OriginString; diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index d3915307dc..ce9f98e457 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -23,7 +23,7 @@ import { import { PermissionDoesNotExistError, methodNotFound, - UnrecognizedActorError, + UnrecognizedSubjectError, PermissionHasNoCaveatsError, CaveatDoesNotExistError, CaveatTypeDoesNotExistError, @@ -31,36 +31,36 @@ import { PermissionTargetDoesNotExistError, CaveatMissingValueError, InvalidCaveatFieldsError, - InvalidActorIdentifierError, + InvalidSubjectIdentifierError, } from './errors'; import { Caveat } from './Caveat'; -export interface ActorMetadata { +export interface SubjectMetadata { origin: OriginString; } const controllerName = 'PermissionController'; -export interface PermissionsActorEntry extends ActorMetadata { +export interface PermissionsSubjectEntry extends SubjectMetadata { permissions: Record; } -export type PermissionControllerActors = Record< +export type PermissionControllerSubjects = Record< OriginString, - PermissionsActorEntry + PermissionsSubjectEntry >; -// TODO: TypeScript doesn't understand that a given actor may not exist. +// TODO: TypeScript doesn't understand that a given subject may not exist. export type PermissionControllerState = { - actors: PermissionControllerActors; + subjects: PermissionControllerSubjects; }; const stateMetadata = { - actors: { persist: true, anonymous: true }, + subjects: { persist: true, anonymous: true }, }; const defaultState: PermissionControllerState = { - actors: {}, + subjects: {}, }; export type GetPermissionsState = { @@ -68,9 +68,9 @@ export type GetPermissionsState = { handler: () => PermissionControllerState; }; -export type GetActors = { - type: `${typeof controllerName}:getActors`; - handler: () => (keyof PermissionControllerActors)[]; +export type GetSubjects = { + type: `${typeof controllerName}:getSubjects`; + handler: () => (keyof PermissionControllerSubjects)[]; }; export type ClearPermissions = { @@ -85,7 +85,7 @@ export type PermissionsStateChange = { export type PermissionControllerActions = | GetPermissionsState - | GetActors + | GetSubjects | ClearPermissions; export type PermissionControllerEvents = PermissionsStateChange; @@ -197,8 +197,8 @@ export class PermissionController extends BaseController< */ protected registerMessageHandlers(): void { this.messagingSystem.registerActionHandler( - `${controllerName}:getActors`, - () => this.getActors(), + `${controllerName}:getSubjects`, + () => this.getSubjects(), ); this.messagingSystem.registerActionHandler( `${controllerName}:clearPermissions`, @@ -212,16 +212,16 @@ export class PermissionController extends BaseController< }); } - getActors(): (keyof PermissionControllerActors)[] { - return Object.keys(this.state.actors); + getSubjects(): (keyof PermissionControllerSubjects)[] { + return Object.keys(this.state.subjects); } getPermission(origin: string, target: string): Permission | undefined { - return this.state.actors[origin]?.permissions[target]; + return this.state.subjects[origin]?.permissions[target]; } getPermissions(origin: string): Record | undefined { - return this.state.actors[origin]?.permissions; + return this.state.subjects[origin]?.permissions; } hasPermission(origin: string, target: string): boolean { @@ -229,47 +229,47 @@ export class PermissionController extends BaseController< } hasPermissions(origin: string): boolean { - return Boolean(this.state.actors[origin]); + return Boolean(this.state.subjects[origin]); } setPermission(origin: string, permission: Permission): void { this.update((draftState) => { - if (!draftState.actors[origin]) { - draftState.actors[origin] = { origin, permissions: {} }; + if (!draftState.subjects[origin]) { + draftState.subjects[origin] = { origin, permissions: {} }; } const { parentCapability: target } = permission; // Typecast: ts(2589) - draftState.actors[origin].permissions[target] = permission as any; + draftState.subjects[origin].permissions[target] = permission as any; }); } /** - * Adds permissions to the given actor. Overwrites existing identical - * permissions (same actor and method). Other existing permissions + * Adds permissions to the given subject. Overwrites existing identical + * permissions (same subject and method). Other existing permissions * remain unaffected. * - * @param {string} origin - The origin of the grantee actor. - * @param {Array} newPermissions - The unique, new permissions for the grantee actor. + * @param {string} origin - The origin of the grantee subject. + * @param {Array} newPermissions - The unique, new permissions for the grantee subject. */ setPermissions( origin: string, permissions: Record, ): void { this.update((draftState) => { - if (!draftState.actors[origin]) { - draftState.actors[origin] = { origin, permissions: {} }; + if (!draftState.subjects[origin]) { + draftState.subjects[origin] = { origin, permissions: {} }; } - Object.assign(draftState.actors[origin].permissions, permissions); + Object.assign(draftState.subjects[origin].permissions, permissions); }); } revokePermission(origin: string, target: string): void { this.update((draftState) => { - if (!draftState.actors[origin]) { - throw new UnrecognizedActorError(origin); + if (!draftState.subjects[origin]) { + throw new UnrecognizedSubjectError(origin); } - const { permissions } = draftState.actors[origin]; + const { permissions } = draftState.subjects[origin]; if (!permissions[target]) { throw new PermissionDoesNotExistError(origin, target); } @@ -277,17 +277,17 @@ export class PermissionController extends BaseController< if (Object.keys(permissions).length > 1) { delete permissions[target]; } else { - delete draftState.actors[origin]; + delete draftState.subjects[origin]; } }); } revokeAllPermissions(origin: string): void { this.update((draftState) => { - if (!draftState.actors[origin]) { - throw new UnrecognizedActorError(origin); + if (!draftState.subjects[origin]) { + throw new UnrecognizedSubjectError(origin); } - delete draftState.actors[origin]; + delete draftState.subjects[origin]; }); } @@ -311,7 +311,7 @@ export class PermissionController extends BaseController< setCaveat(origin: string, target: string, caveat: Caveat): void { this.update((draftState) => { - const permission = draftState.actors[origin]?.permissions[target]; + const permission = draftState.subjects[origin]?.permissions[target]; if (!permission) { throw new PermissionDoesNotExistError(origin, target); } @@ -335,7 +335,7 @@ export class PermissionController extends BaseController< removeCaveat(origin: string, target: string, caveatType: string): void { this.update((draftState) => { - const permission = draftState.actors[origin]?.permissions[target]; + const permission = draftState.subjects[origin]?.permissions[target]; if (!permission) { throw new PermissionDoesNotExistError(origin, target); } @@ -406,12 +406,12 @@ export class PermissionController extends BaseController< } grantPermissions( - actor: ActorMetadata, + subject: SubjectMetadata, requestedPermissions: RequestedPermissions, ): Record { - const { origin } = actor; + const { origin } = subject; if (!origin || typeof origin !== 'string') { - throw new InvalidActorIdentifierError(origin); + throw new InvalidSubjectIdentifierError(origin); } // Enforce actual approving known methods: @@ -515,13 +515,13 @@ function getRestrictedMethodMap( // TODO: Are we using these? -export interface PermissionsRequestMetadata extends ActorMetadata { +export interface PermissionsRequestMetadata extends SubjectMetadata { id: string; } /** * Used for prompting the user about a proposed new permission. - * Includes information about the actor granted, as well as the permissions + * Includes information about the subject granted, as well as the permissions * assigned. */ export interface PermissionsRequest { diff --git a/packages/controllers/src/permissions/errors.ts b/packages/controllers/src/permissions/errors.ts index 45c2404877..d558e2bbe8 100644 --- a/packages/controllers/src/permissions/errors.ts +++ b/packages/controllers/src/permissions/errors.ts @@ -35,34 +35,34 @@ export function userRejectedRequest( return ethErrors.provider.userRejectedRequest({ data: request }); } -export class InvalidActorIdentifierError extends Error { +export class InvalidSubjectIdentifierError extends Error { constructor(origin: string) { - super(`Invalid actor identifier: ${origin}`); + super(`Invalid subject identifier: ${origin}`); } } -export class UnrecognizedActorError extends Error { +export class UnrecognizedSubjectError extends Error { constructor(origin: string) { - super(`Unrecognized actor: "${origin}" has no permissions.`); + super(`Unrecognized subject: "${origin}" has no permissions.`); } } export class PermissionDoesNotExistError extends Error { constructor(origin: string, target: string) { - super(`Actor "${origin}" has no permission for "${target}".`); + super(`Subject "${origin}" has no permission for "${target}".`); } } export class PermissionHasNoCaveatsError extends Error { constructor(origin: string, target: string) { - super(`Permission for "${target}" of actor "${origin}" has no caveats.`); + super(`Permission for "${target}" of subject "${origin}" has no caveats.`); } } export class CaveatDoesNotExistError extends Error { constructor(origin: string, target: string, caveatType: string) { super( - `Permission for "${target}" of actor "${origin}" has no caveat of type "${caveatType}".`, + `Permission for "${target}" of subject "${origin}" has no caveat of type "${caveatType}".`, ); } } @@ -70,7 +70,7 @@ export class CaveatDoesNotExistError extends Error { export class PermissionTargetDoesNotExistError extends Error { constructor(origin: string, target: string) { super( - `Target "${target}" of requested permission for actor "${origin}" does not exist.`, + `Target "${target}" of requested permission for subject "${origin}" does not exist.`, ); } } @@ -79,7 +79,7 @@ export class InvalidPermissionJsonError extends Error { public data: { origin: string; permission: Permission }; constructor(origin: string, permission: Permission) { - super(`Permission object of actor "${origin}" is not valid JSON.`); + super(`Permission object of subject "${origin}" is not valid JSON.`); this.data = { origin, permission }; } } From 718e8b7460b028de84350f3f4a727135bd6c48eb Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Thu, 5 Aug 2021 08:38:07 -0700 Subject: [PATCH 014/147] Clean up caveat processing --- .../src/permissions/caveat-processing.ts | 54 ++++++++++++------- .../controllers/src/permissions/errors.ts | 6 +++ 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/packages/controllers/src/permissions/caveat-processing.ts b/packages/controllers/src/permissions/caveat-processing.ts index bd24f4da04..6436254507 100644 --- a/packages/controllers/src/permissions/caveat-processing.ts +++ b/packages/controllers/src/permissions/caveat-processing.ts @@ -1,5 +1,6 @@ import { Json, JsonRpcRequest, PendingJsonRpcResponse } from 'json-rpc-engine'; import { Caveat } from './Caveat'; +import { UnrecognizedCaveatTypeError } from './errors'; import { Permission } from './Permission'; import { RestrictedMethodImplementation } from './PermissionController'; @@ -66,48 +67,61 @@ export function decorateWithCaveats( ): RestrictedMethodImplementation { const { caveats } = getPermission(methodName); - // If the permission has caveats, create an array of invocations of their - // corresponding functions bound to the caveat objects. - const caveatFunctions: BoundCaveatFunction[] = []; + // If the permission has caveats, create an array of functions that call the + // corresponding caveat functions with the caveat object and request as + // arguments. if (caveats && caveats.length > 0) { + const caveatFunctions: BoundCaveatFunction[] = []; + for (const caveat of caveats) { if (!caveatImplementations[caveat.type]) { - throw new Error(`Unrecognized caveat type: ${caveat.type}`); + throw new UnrecognizedCaveatTypeError(caveat.type); } caveatFunctions.push((request: JsonRpcRequest) => caveatImplementations[caveat.type](caveat, request), ); } - } - return ((req, res, next, end, context) => { - // If there are caveats, apply them to the request. - if (caveatFunctions.length > 0) { + return ((req, res, next, end, context) => { + let _end = end; const caveatReturnHandlers = applyCaveats(req, caveatFunctions); // If any of the caveats specified return handlers, wrap the "end" // callback such that the return handlers are applied to the response // before actually ending the request. if (caveatReturnHandlers.length > 0) { - const endWithReturnHandlers = () => { + _end = () => { caveatReturnHandlers.forEach((returnHandler) => returnHandler(res)); end(); }; - - return methodImplementation( - req, - res, - next, - endWithReturnHandlers, - context, - ); } - } - return methodImplementation(req, res, next, end, context); - }) as RestrictedMethodImplementation; + + return methodImplementation(req, res, next, _end, context); + }) as RestrictedMethodImplementation; + } + + // If there are no caveats, return a function that calls the restricted method + // implementation directly, without applying any caveats. + return ((req, res, next, end, context) => + methodImplementation( + req, + res, + next, + end, + context, + )) as RestrictedMethodImplementation; } +/** + * Applies the specified caveats to the specified request. Specifically, every + * caveat function in the array is called with the request object as an argument. + * + * @param req - The request to apply caveats to. + * @param caveatFunctions - The caveats functions to call with the request as + * an argument. + * @returns Any return handlers returned by the caveat functions. + */ function applyCaveats( req: JsonRpcRequest, caveatFunctions: BoundCaveatFunction[], diff --git a/packages/controllers/src/permissions/errors.ts b/packages/controllers/src/permissions/errors.ts index d558e2bbe8..4252222497 100644 --- a/packages/controllers/src/permissions/errors.ts +++ b/packages/controllers/src/permissions/errors.ts @@ -59,6 +59,12 @@ export class PermissionHasNoCaveatsError extends Error { } } +export class UnrecognizedCaveatTypeError extends Error { + constructor(caveatType: string) { + super(`Unrecognized caveat type: ${caveatType}`); + } +} + export class CaveatDoesNotExistError extends Error { constructor(origin: string, target: string, caveatType: string) { super( From fc544c5fd3e70a0f20631b5b5c88f9d374c487b2 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Thu, 5 Aug 2021 16:47:04 -0700 Subject: [PATCH 015/147] Add PermissionEnforcer --- packages/controllers/package.json | 3 +- .../src/permissions/PermissionController.ts | 52 +++-- .../src/permissions/PermissionEnforcer.ts | 192 ++++++++++++++++++ .../src/permissions/caveat-processing.ts | 41 ++-- .../controllers/src/permissions/errors.ts | 6 +- yarn.lock | 10 + 6 files changed, 253 insertions(+), 51 deletions(-) create mode 100644 packages/controllers/src/permissions/PermissionEnforcer.ts diff --git a/packages/controllers/package.json b/packages/controllers/package.json index 54ad9b8f9f..0e932636e9 100644 --- a/packages/controllers/package.json +++ b/packages/controllers/package.json @@ -31,10 +31,11 @@ "@metamask/post-message-stream": "4.0.0", "@metamask/safe-event-emitter": "^2.0.0", "@metamask/snap-workers": "^0.2.0", + "@types/deep-freeze-strict": "^1.1.0", + "deep-freeze-strict": "^1.1.1", "eth-rpc-errors": "^4.0.2", "immer": "^9.0.6", "fast-deep-equal": "^3.1.3", - "is-subset": "^0.1.1", "json-rpc-engine": "^6.1.0", "json-rpc-middleware-stream": "^3.0.0", "nanoid": "^3.1.28", diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index ce9f98e457..3858c904f9 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -1,5 +1,6 @@ import type { Patch } from 'immer'; import deepEqual from 'fast-deep-equal'; +import deepFreeze from 'deep-freeze-strict'; import { BaseControllerV2 as BaseController, RestrictedControllerMessenger, @@ -50,7 +51,7 @@ export type PermissionControllerSubjects = Record< PermissionsSubjectEntry >; -// TODO: TypeScript doesn't understand that a given subject may not exist. +// TODO: TypeScript doesn't understand that a given subject may not exist. Can we fix it? export type PermissionControllerState = { subjects: PermissionControllerSubjects; }; @@ -109,7 +110,7 @@ export interface RestrictedMethodImplementation origin: OriginString; [key: string]: unknown; }>, - ): void; + ): void | Promise; } type RestrictedMethodsOption = Record< @@ -148,12 +149,6 @@ export class PermissionController extends BaseController< return this._safeMethods; } - private readonly _internalMethods: ReadonlySet; - - public get internalMethods(): ReadonlySet { - return this._internalMethods; - } - private readonly _caveatSpecifications: Readonly; public get caveatSpecifications(): Readonly { @@ -177,9 +172,8 @@ export class PermissionController extends BaseController< state: { ...defaultState, ...state }, }); - this._caveatSpecifications = Object.freeze({ ...caveatSpecifications }); + this._caveatSpecifications = deepFreeze({ ...caveatSpecifications }); this._caveatTypes = getCaveatTypes(this._caveatSpecifications); - this._internalMethods = getInternalMethodNames(methodPrefix); this._restrictedMethods = getRestrictedMethodMap(restrictedMethods); this._safeMethods = new Set(safeMethods); this.methodPrefix = methodPrefix; @@ -216,6 +210,14 @@ export class PermissionController extends BaseController< return Object.keys(this.state.subjects); } + /** + * Gets the permission for the specified target of the subject corresponding + * to the specified origin. + * + * @param origin - The origin of the subject. + * @param target - The method name as invoked by a third party (i.e., not a method key). + * @returns The permission if it exists, or undefined otherwise. + */ getPermission(origin: string, target: string): Permission | undefined { return this.state.subjects[origin]?.permissions[target]; } @@ -405,6 +407,27 @@ export class PermissionController extends BaseController< return ''; } + getRestrictedMethodImplementation( + method: string, + ): RestrictedMethodImplementation | undefined { + const methodKey = this.getMethodKeyFor(method); + if (!methodKey) { + return undefined; + } + + const methodImplementation = this._restrictedMethods.get(methodKey); + /* istanbul ignore if */ // This should be impossible + if (!methodImplementation) { + throw new Error( + `Method "${method}" with method key "${methodKey}" has no implementation.`, + ); + } + return methodImplementation as RestrictedMethodImplementation< + Params, + Result + >; + } + grantPermissions( subject: SubjectMetadata, requestedPermissions: RequestedPermissions, @@ -417,7 +440,7 @@ export class PermissionController extends BaseController< // Enforce actual approving known methods: for (const methodName in requestedPermissions) { if (!this.getMethodKeyFor(methodName)) { - throw methodNotFound({ methodName }); + throw methodNotFound({ method: methodName }); } } @@ -490,13 +513,6 @@ function getCaveatTypes(caveatSpecifications: CaveatSpecifications) { ); } -function getInternalMethodNames(methodPrefix: string) { - return new Set([ - `${methodPrefix}getPermissions`, - `${methodPrefix}requestPermissions`, - ]); -} - function getRestrictedMethodMap( restrictedMethods: RestrictedMethodsOption, ): ReadonlyMap> { diff --git a/packages/controllers/src/permissions/PermissionEnforcer.ts b/packages/controllers/src/permissions/PermissionEnforcer.ts new file mode 100644 index 0000000000..c2ae5955d2 --- /dev/null +++ b/packages/controllers/src/permissions/PermissionEnforcer.ts @@ -0,0 +1,192 @@ +import { nanoid } from 'nanoid'; +import { + JsonRpcEngineEndCallback, + JsonRpcEngineNextCallback, + JsonRpcRequest, + PendingJsonRpcResponse, +} from 'json-rpc-engine'; +import { CaveatSpecifications, decorateWithCaveats } from './caveat-processing'; +import { methodNotFound, unauthorized } from './errors'; +import type { + PermissionController, + RestrictedMethodImplementation, + SubjectMetadata, +} from './PermissionController'; + +interface PermissionEnforcerArgs { + caveatSpecifications: CaveatSpecifications; + isSafeMethod: (method: string) => boolean; + getPermission: PermissionController['getPermission']; + getRestrictedMethodImplementation: PermissionController['getRestrictedMethodImplementation']; +} + +class PermissionEnforcer { + private isSafeMethod: (method: string) => boolean; + + private getPermission: PermissionController['getPermission']; + + private getRestrictedMethodImplementation: PermissionController['getRestrictedMethodImplementation']; + + private caveatSpecifications: CaveatSpecifications; + + constructor({ + caveatSpecifications, + isSafeMethod, + getPermission, + getRestrictedMethodImplementation, + }: PermissionEnforcerArgs) { + this.caveatSpecifications = caveatSpecifications; + this.isSafeMethod = isSafeMethod; + this.getPermission = getPermission; + this.getRestrictedMethodImplementation = getRestrictedMethodImplementation; + } + + async safelyExecuteRestrictedMethodAs( + origin: string, + method: string, + params: Params, + ): Promise { + try { + return await this.executeRestrictedMethodAs(origin, method, params); + } catch (error) { + console.error(error); + return undefined; + } + } + + executeRestrictedMethodAs( + origin: string, + method: string, + params: Params, + ): Promise { + return new Promise((resolve) => { + const id = nanoid(); + const req: JsonRpcRequest = { id, jsonrpc: '2.0', method }; + if (params !== undefined) { + req.params = params; + } + + const res: PendingJsonRpcResponse = { id, jsonrpc: '2.0' }; + + // no-op + const next = () => undefined; + + const end = () => { + if (res.error) { + throw res.error; + } else if (res.result === undefined) { + throw new Error( + `Internal request for method "${method}" as origin "${origin}" has no result.`, + ); + } else { + resolve(res.result); + } + }; + + // if the method also is not a restricted method, the method does not exist + const methodImplementation = + this.getRestrictedMethodImplementation(method); + if (!methodImplementation) { + throw methodNotFound({ method, data: { request: req } }); + } + this.executeRestrictedMethod( + methodImplementation, + { origin }, + req, + res, + next, + end, + ); + }); + } + + getPermissionsMiddleware() { + const permissionsMiddleware = ( + req: JsonRpcRequest, + res: PendingJsonRpcResponse, + next: JsonRpcEngineNextCallback, + end: JsonRpcEngineEndCallback, + subject: SubjectMetadata, + ): void => { + const { method } = req; + + // skip registered safe/passthrough methods. + if (this.isSafeMethod(method)) { + return next(); + } + + // if the method also is not a restricted method, the method does not exist + const methodImplementation = + this.getRestrictedMethodImplementation(method); + if (!methodImplementation) { + return end(methodNotFound({ method, data: { request: req } })); + } + + const permission = this.getPermission(subject.origin, method); + if (!permission) { + return end(unauthorized({ data: { request: req } })); + } + + this.executeRestrictedMethod( + methodImplementation, + subject, + req, + res, + next, + end, + ); + return undefined; + }; + return permissionsMiddleware; + } + + /** + * If called as a result of an RPC request, the existence of the method + * should already be verified. + * + * @param subject + * @param req + * @param res + * @param next + * @param end + * @returns + */ + private executeRestrictedMethod( + methodImplementation: RestrictedMethodImplementation, + subject: SubjectMetadata, + req: JsonRpcRequest, + res: PendingJsonRpcResponse, + next: JsonRpcEngineNextCallback, + end: JsonRpcEngineEndCallback, + ): void | Promise { + const { origin } = subject; + const { method } = req; + + const permission = this.getPermission(origin, method); + if (!permission) { + throw unauthorized({ data: { origin, method } }); + } + + return decorateWithCaveats( + methodImplementation, + permission, + this.caveatSpecifications, + )(req, res, next, end, { origin }); + } +} + +export type { PermissionEnforcer }; + +export function getPermissionEnforcer( + controller: PermissionController, +): PermissionEnforcer { + return new PermissionEnforcer({ + caveatSpecifications: controller.caveatSpecifications, + isSafeMethod: (method: string) => { + return controller.safeMethods.has(method); + }, + getPermission: controller.getPermission.bind(controller), + getRestrictedMethodImplementation: + controller.getRestrictedMethodImplementation.bind(controller), + }); +} diff --git a/packages/controllers/src/permissions/caveat-processing.ts b/packages/controllers/src/permissions/caveat-processing.ts index 6436254507..1eb9c57fda 100644 --- a/packages/controllers/src/permissions/caveat-processing.ts +++ b/packages/controllers/src/permissions/caveat-processing.ts @@ -18,25 +18,13 @@ type CaveatFunction< request: JsonRpcRequest, ) => CaveatReturnHandler | undefined; -type CaveatImplementationFactory< - CaveatValue extends Json, - MethodParameters extends Json, - MethodResult extends Json, -> = ( - caveatValue: CaveatValue, -) => CaveatFunction; - export interface CaveatSpecification< CaveatValue extends Json, MethodParameters extends Json, MethodResult extends Json, > { type: CaveatType; - getImplementation: CaveatImplementationFactory< - CaveatValue, - MethodParameters, - MethodResult - >; + implementation: CaveatFunction; } export type CaveatSpecifications = Record< @@ -59,13 +47,14 @@ type CaveatReturnHandler = ( /** * Decorate a restricted method implementation with its caveats. */ -export function decorateWithCaveats( - methodName: string, - methodImplementation: RestrictedMethodImplementation, - getPermission: (method: string) => Permission, // bound to the requesting origin - caveatImplementations: Record>, // all caveat implementations -): RestrictedMethodImplementation { - const { caveats } = getPermission(methodName); +export function decorateWithCaveats< + MethodImplementation extends RestrictedMethodImplementation, +>( + methodImplementation: MethodImplementation, + permission: Readonly, // bound to the requesting origin + caveatImplementations: CaveatSpecifications, // all caveat implementations +): MethodImplementation { + const { caveats } = permission; // If the permission has caveats, create an array of functions that call the // corresponding caveat functions with the caveat object and request as @@ -79,7 +68,7 @@ export function decorateWithCaveats( } caveatFunctions.push((request: JsonRpcRequest) => - caveatImplementations[caveat.type](caveat, request), + caveatImplementations[caveat.type].implementation(caveat, request), ); } @@ -98,19 +87,13 @@ export function decorateWithCaveats( } return methodImplementation(req, res, next, _end, context); - }) as RestrictedMethodImplementation; + }) as MethodImplementation; } // If there are no caveats, return a function that calls the restricted method // implementation directly, without applying any caveats. return ((req, res, next, end, context) => - methodImplementation( - req, - res, - next, - end, - context, - )) as RestrictedMethodImplementation; + methodImplementation(req, res, next, end, context)) as MethodImplementation; } /** diff --git a/packages/controllers/src/permissions/errors.ts b/packages/controllers/src/permissions/errors.ts index 4252222497..d513ea2a0b 100644 --- a/packages/controllers/src/permissions/errors.ts +++ b/packages/controllers/src/permissions/errors.ts @@ -9,7 +9,7 @@ interface ErrorArg { } interface MethodNotFoundArg extends ErrorArg { - methodName?: string; + method?: string; } export function unauthorized(arg?: ErrorArg) { @@ -22,8 +22,8 @@ export function unauthorized(arg?: ErrorArg) { } export function methodNotFound(opts: MethodNotFoundArg) { - const message = opts.methodName - ? `The method '${opts.methodName}' does not exist / is not available.` + const message = opts.method + ? `The method '${opts.method}' does not exist / is not available.` : undefined; return ethErrors.rpc.methodNotFound({ data: opts.data, message }); diff --git a/yarn.lock b/yarn.lock index 904bd70d69..79040945cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2631,6 +2631,11 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/deep-freeze-strict@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@types/deep-freeze-strict/-/deep-freeze-strict-1.1.0.tgz#447a6a2576191344aa42310131dd3df5c41492c4" + integrity sha1-RHpqJXYZE0SqQjEBMd099cQUksQ= + "@types/graceful-fs@^4.1.2": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" @@ -4843,6 +4848,11 @@ deep-equal@^1.0.0: object-keys "^1.1.1" regexp.prototype.flags "^1.2.0" +deep-freeze-strict@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/deep-freeze-strict/-/deep-freeze-strict-1.1.1.tgz#77d0583ca24a69be4bbd9ac2fae415d55523e5b0" + integrity sha1-d9BYPKJKab5LvZrC+uQV1VUj5bA= + deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" From 3f7532a266cef96f8356e1f48422b2f280ae5ac0 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Thu, 5 Aug 2021 17:05:29 -0700 Subject: [PATCH 016/147] Add consumer-specified caveat value validation --- packages/controllers/package.json | 1 - .../src/permissions/PermissionController.ts | 34 +++++-------------- .../src/permissions/caveat-processing.ts | 5 +++ .../controllers/src/permissions/errors.ts | 6 ++++ 4 files changed, 20 insertions(+), 26 deletions(-) diff --git a/packages/controllers/package.json b/packages/controllers/package.json index 0e932636e9..ffbd7c28dd 100644 --- a/packages/controllers/package.json +++ b/packages/controllers/package.json @@ -35,7 +35,6 @@ "deep-freeze-strict": "^1.1.1", "eth-rpc-errors": "^4.0.2", "immer": "^9.0.6", - "fast-deep-equal": "^3.1.3", "json-rpc-engine": "^6.1.0", "json-rpc-middleware-stream": "^3.0.0", "nanoid": "^3.1.28", diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 3858c904f9..a4a4ae253a 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -1,5 +1,4 @@ import type { Patch } from 'immer'; -import deepEqual from 'fast-deep-equal'; import deepFreeze from 'deep-freeze-strict'; import { BaseControllerV2 as BaseController, @@ -28,11 +27,11 @@ import { PermissionHasNoCaveatsError, CaveatDoesNotExistError, CaveatTypeDoesNotExistError, - InvalidCaveatJsonError, PermissionTargetDoesNotExistError, CaveatMissingValueError, InvalidCaveatFieldsError, InvalidSubjectIdentifierError, + InvalidCaveatTypeError, } from './errors'; import { Caveat } from './Caveat'; @@ -155,8 +154,6 @@ export class PermissionController extends BaseController< return this._caveatSpecifications; } - protected readonly _caveatTypes: ReadonlySet; - constructor({ messenger, state = {}, @@ -173,7 +170,6 @@ export class PermissionController extends BaseController< }); this._caveatSpecifications = deepFreeze({ ...caveatSpecifications }); - this._caveatTypes = getCaveatTypes(this._caveatSpecifications); this._restrictedMethods = getRestrictedMethodMap(restrictedMethods); this._safeMethods = new Set(safeMethods); this.methodPrefix = methodPrefix; @@ -476,43 +472,31 @@ export class PermissionController extends BaseController< caveats?: Caveat[], ): Caveat[] | undefined { const caveatArray = caveats?.map((caveat) => { - if (!this._caveatTypes.has(caveat.type)) { + if (caveat.type !== 'string') { + throw new InvalidCaveatTypeError(caveat.type); + } + + const specification = this.caveatSpecifications[caveat.type]; + if (!specification) { throw new CaveatTypeDoesNotExistError(caveat.type, origin, target); } if (!('value' in caveat)) { throw new CaveatMissingValueError(caveat, origin, target); } + // Should throw an error if validation fails. + specification.validator(caveat.value); if (Object.keys(caveat).length !== 2) { throw new InvalidCaveatFieldsError(caveat, origin, target); } - if (!isValidJson(caveat)) { - throw new InvalidCaveatJsonError(caveat, origin, target); - } return new Caveat({ type: caveat.type, value: caveat.value }); }); return caveatArray && caveatArray.length > 0 ? caveatArray : undefined; } } -function isValidJson(value: unknown): boolean { - try { - return deepEqual(value, JSON.parse(JSON.stringify(value))); - } catch (error) { - return false; - } -} - -function getCaveatTypes(caveatSpecifications: CaveatSpecifications) { - return new Set( - Object.values(caveatSpecifications).map( - (specification) => specification.type, - ), - ); -} - function getRestrictedMethodMap( restrictedMethods: RestrictedMethodsOption, ): ReadonlyMap> { diff --git a/packages/controllers/src/permissions/caveat-processing.ts b/packages/controllers/src/permissions/caveat-processing.ts index 1eb9c57fda..2a173887a4 100644 --- a/packages/controllers/src/permissions/caveat-processing.ts +++ b/packages/controllers/src/permissions/caveat-processing.ts @@ -18,6 +18,10 @@ type CaveatFunction< request: JsonRpcRequest, ) => CaveatReturnHandler | undefined; +type CaveatValueValidator = ( + caveatValue: CaveatValue, +) => void; + export interface CaveatSpecification< CaveatValue extends Json, MethodParameters extends Json, @@ -25,6 +29,7 @@ export interface CaveatSpecification< > { type: CaveatType; implementation: CaveatFunction; + validator: CaveatValueValidator; } export type CaveatSpecifications = Record< diff --git a/packages/controllers/src/permissions/errors.ts b/packages/controllers/src/permissions/errors.ts index d513ea2a0b..6006a86bce 100644 --- a/packages/controllers/src/permissions/errors.ts +++ b/packages/controllers/src/permissions/errors.ts @@ -90,6 +90,12 @@ export class InvalidPermissionJsonError extends Error { } } +export class InvalidCaveatTypeError extends Error { + constructor(caveatType: string) { + super(`Caveat types must be strings. Received: ${typeof caveatType}`); + } +} + export class CaveatTypeDoesNotExistError extends Error { public data: { origin: string; target: string }; From 741cb76a0f0083905cb9fd9b31a15fabd7b29b33 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Thu, 5 Aug 2021 17:35:04 -0700 Subject: [PATCH 017/147] Add revokePermissionForAllSubjects --- .../src/permissions/PermissionController.ts | 55 ++++++++++++++++--- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index a4a4ae253a..e1459a9fd3 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -41,9 +41,11 @@ export interface SubjectMetadata { const controllerName = 'PermissionController'; -export interface PermissionsSubjectEntry extends SubjectMetadata { - permissions: Record; -} +type SubjectPermissions = Record; + +export type PermissionsSubjectEntry = { + permissions: SubjectPermissions; +} & SubjectMetadata; export type PermissionControllerSubjects = Record< OriginString, @@ -272,14 +274,51 @@ export class PermissionController extends BaseController< throw new PermissionDoesNotExistError(origin, target); } - if (Object.keys(permissions).length > 1) { - delete permissions[target]; - } else { - delete draftState.subjects[origin]; - } + // Typecast: ts(2589) + this._deletePermission( + draftState as any, + permissions as any, + origin, + target, + ); }); } + revokePermissionForAllSubjects(target: string): void { + if (this.getSubjects().length === 0) { + return; + } + + this.update((draftState) => { + Object.values(draftState.subjects).forEach((subject) => { + const { permissions } = subject; + + // Typecast: ts(2589) + if (permissions[target]) { + this._deletePermission( + draftState as any, + permissions as any, + origin, + target, + ); + } + }); + }); + } + + private _deletePermission( + draftState: PermissionControllerState, + permissions: SubjectPermissions, + origin: string, + target: string, + ): void { + if (Object.keys(permissions).length > 1) { + delete permissions[target]; + } else { + delete draftState.subjects[origin]; + } + } + revokeAllPermissions(origin: string): void { this.update((draftState) => { if (!draftState.subjects[origin]) { From 8ea589f0b98bc29fefcc2ae3fcf9b8b25fbe91fe Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 9 Aug 2021 12:05:29 -0700 Subject: [PATCH 018/147] Rename some stuff in the PermissionEnforcer --- .../src/permissions/PermissionController.ts | 10 +++++----- .../src/permissions/PermissionEnforcer.ts | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index e1459a9fd3..35810aabed 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -80,16 +80,16 @@ export type ClearPermissions = { handler: () => void; }; -export type PermissionsStateChange = { - type: `${typeof controllerName}:stateChange`; - payload: [PermissionControllerState, Patch[]]; -}; - export type PermissionControllerActions = | GetPermissionsState | GetSubjects | ClearPermissions; +export type PermissionsStateChange = { + type: `${typeof controllerName}:stateChange`; + payload: [PermissionControllerState, Patch[]]; +}; + export type PermissionControllerEvents = PermissionsStateChange; export type PermissionControllerMessenger = RestrictedControllerMessenger< diff --git a/packages/controllers/src/permissions/PermissionEnforcer.ts b/packages/controllers/src/permissions/PermissionEnforcer.ts index c2ae5955d2..68e29e170b 100644 --- a/packages/controllers/src/permissions/PermissionEnforcer.ts +++ b/packages/controllers/src/permissions/PermissionEnforcer.ts @@ -41,20 +41,20 @@ class PermissionEnforcer { this.getRestrictedMethodImplementation = getRestrictedMethodImplementation; } - async safelyExecuteRestrictedMethodAs( + async safelyExecuteRestrictedMethod( origin: string, method: string, params: Params, ): Promise { try { - return await this.executeRestrictedMethodAs(origin, method, params); + return await this.executeRestrictedMethod(origin, method, params); } catch (error) { console.error(error); return undefined; } } - executeRestrictedMethodAs( + executeRestrictedMethod( origin: string, method: string, params: Params, @@ -89,7 +89,7 @@ class PermissionEnforcer { if (!methodImplementation) { throw methodNotFound({ method, data: { request: req } }); } - this.executeRestrictedMethod( + this._executeRestrictedMethod( methodImplementation, { origin }, req, @@ -127,7 +127,7 @@ class PermissionEnforcer { return end(unauthorized({ data: { request: req } })); } - this.executeRestrictedMethod( + this._executeRestrictedMethod( methodImplementation, subject, req, @@ -151,7 +151,7 @@ class PermissionEnforcer { * @param end * @returns */ - private executeRestrictedMethod( + private _executeRestrictedMethod( methodImplementation: RestrictedMethodImplementation, subject: SubjectMetadata, req: JsonRpcRequest, From e59568d2bd4b6df0204d0b785300535106f6a827 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 16 Aug 2021 20:14:38 -0700 Subject: [PATCH 019/147] Post-rebase fixup --- packages/controllers/src/permissions/PermissionController.ts | 1 + packages/controllers/src/permissions/PermissionEnforcer.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 35810aabed..91819fc722 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -192,6 +192,7 @@ export class PermissionController extends BaseController< `${controllerName}:getSubjects`, () => this.getSubjects(), ); + this.messagingSystem.registerActionHandler( `${controllerName}:clearPermissions`, () => this.clearState(), diff --git a/packages/controllers/src/permissions/PermissionEnforcer.ts b/packages/controllers/src/permissions/PermissionEnforcer.ts index 68e29e170b..b10d808972 100644 --- a/packages/controllers/src/permissions/PermissionEnforcer.ts +++ b/packages/controllers/src/permissions/PermissionEnforcer.ts @@ -89,6 +89,7 @@ class PermissionEnforcer { if (!methodImplementation) { throw methodNotFound({ method, data: { request: req } }); } + this._executeRestrictedMethod( methodImplementation, { origin }, From 7e1ea6d35d6b0e659f03f51b59d597fb9df1f6ae Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Fri, 20 Aug 2021 12:50:50 -0700 Subject: [PATCH 020/147] DRY up some restricted method validation --- .../src/permissions/PermissionEnforcer.ts | 51 +++++++++++++------ 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionEnforcer.ts b/packages/controllers/src/permissions/PermissionEnforcer.ts index b10d808972..f8d7a24f84 100644 --- a/packages/controllers/src/permissions/PermissionEnforcer.ts +++ b/packages/controllers/src/permissions/PermissionEnforcer.ts @@ -4,6 +4,7 @@ import { JsonRpcEngineNextCallback, JsonRpcRequest, PendingJsonRpcResponse, + JsonRpcEngineCallbackError, } from 'json-rpc-engine'; import { CaveatSpecifications, decorateWithCaveats } from './caveat-processing'; import { methodNotFound, unauthorized } from './errors'; @@ -25,7 +26,7 @@ class PermissionEnforcer { private getPermission: PermissionController['getPermission']; - private getRestrictedMethodImplementation: PermissionController['getRestrictedMethodImplementation']; + private _getRestrictedMethodImplementation: PermissionController['getRestrictedMethodImplementation']; private caveatSpecifications: CaveatSpecifications; @@ -38,7 +39,7 @@ class PermissionEnforcer { this.caveatSpecifications = caveatSpecifications; this.isSafeMethod = isSafeMethod; this.getPermission = getPermission; - this.getRestrictedMethodImplementation = getRestrictedMethodImplementation; + this._getRestrictedMethodImplementation = getRestrictedMethodImplementation; } async safelyExecuteRestrictedMethod( @@ -71,9 +72,9 @@ class PermissionEnforcer { // no-op const next = () => undefined; - const end = () => { - if (res.error) { - throw res.error; + const end = (error?: JsonRpcEngineCallbackError) => { + if (error || res.error) { + throw error || res.error; } else if (res.result === undefined) { throw new Error( `Internal request for method "${method}" as origin "${origin}" has no result.`, @@ -83,12 +84,10 @@ class PermissionEnforcer { } }; - // if the method also is not a restricted method, the method does not exist - const methodImplementation = - this.getRestrictedMethodImplementation(method); - if (!methodImplementation) { - throw methodNotFound({ method, data: { request: req } }); - } + const methodImplementation = this.getRestrictedMethodImplementation( + method, + req, + ); this._executeRestrictedMethod( methodImplementation, @@ -117,11 +116,10 @@ class PermissionEnforcer { } // if the method also is not a restricted method, the method does not exist - const methodImplementation = - this.getRestrictedMethodImplementation(method); - if (!methodImplementation) { - return end(methodNotFound({ method, data: { request: req } })); - } + const methodImplementation = this.getRestrictedMethodImplementation( + method, + req, + ); const permission = this.getPermission(subject.origin, method); if (!permission) { @@ -141,6 +139,27 @@ class PermissionEnforcer { return permissionsMiddleware; } + /** + * Get the implementation of the given method. + * + * @param method - The name of the method whose implementation to retrieve. + * @param request - The JSON-RPC request object calling the restricted method. + * @returns The restricted method implementation, or throws an error if no + * method exists. + */ + private getRestrictedMethodImplementation( + method: string, + request: JsonRpcRequest, + ): RestrictedMethodImplementation { + const methodImplementation = + this._getRestrictedMethodImplementation(method); + if (!methodImplementation) { + throw methodNotFound({ method, data: { request } }); + } + + return methodImplementation; + } + /** * If called as a result of an RPC request, the existence of the method * should already be verified. From 360fc34509f041d032ba2127416fdd31b6d2c51f Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Fri, 20 Aug 2021 13:05:07 -0700 Subject: [PATCH 021/147] Remove safelyExecuteRestrictedMethod --- .../src/permissions/PermissionEnforcer.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionEnforcer.ts b/packages/controllers/src/permissions/PermissionEnforcer.ts index f8d7a24f84..c660912b60 100644 --- a/packages/controllers/src/permissions/PermissionEnforcer.ts +++ b/packages/controllers/src/permissions/PermissionEnforcer.ts @@ -42,19 +42,6 @@ class PermissionEnforcer { this._getRestrictedMethodImplementation = getRestrictedMethodImplementation; } - async safelyExecuteRestrictedMethod( - origin: string, - method: string, - params: Params, - ): Promise { - try { - return await this.executeRestrictedMethod(origin, method, params); - } catch (error) { - console.error(error); - return undefined; - } - } - executeRestrictedMethod( origin: string, method: string, From f1b65bba393e6f49f3a5231dba132a532f61020f Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sat, 21 Aug 2021 12:12:20 -0700 Subject: [PATCH 022/147] Re-implement caveat decoration per suggestion by @Gudahtt --- .../src/permissions/PermissionController.ts | 61 ++++---- .../src/permissions/PermissionEnforcer.ts | 146 +++++++++--------- .../src/permissions/caveat-decoration.ts | 77 +++++++++ .../src/permissions/caveat-processing.ts | 127 --------------- .../controllers/src/permissions/errors.ts | 7 + 5 files changed, 188 insertions(+), 230 deletions(-) create mode 100644 packages/controllers/src/permissions/caveat-decoration.ts delete mode 100644 packages/controllers/src/permissions/caveat-processing.ts diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 91819fc722..769b0dbdfe 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -4,16 +4,10 @@ import { BaseControllerV2 as BaseController, RestrictedControllerMessenger, } from '@metamask/controllers'; -import { - Json, - JsonRpcEngineEndCallback, - JsonRpcEngineNextCallback, - JsonRpcMiddleware, - JsonRpcRequest, - PendingJsonRpcResponse, -} from 'json-rpc-engine'; - -import { CaveatSpecifications } from './caveat-processing'; +import { Json } from 'json-rpc-engine'; + +import { EthereumRpcError } from 'eth-rpc-errors'; +import { CaveatSpecifications } from './caveat-decoration'; import { OriginString, Permission, @@ -100,23 +94,28 @@ export type PermissionControllerMessenger = RestrictedControllerMessenger< never >; -export interface RestrictedMethodImplementation - extends JsonRpcMiddleware { - ( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - next: JsonRpcEngineNextCallback, - end: JsonRpcEngineEndCallback, - context?: Readonly<{ - origin: OriginString; - [key: string]: unknown; - }>, - ): void | Promise; -} +export type RestrictedMethodContext = Readonly<{ + origin: OriginString; + [key: string]: any; +}>; + +export type RestrictedMethodRequest = { + method: string; + params?: Params; + [key: string]: any; +}; + +export type RestrictedMethodImplementation< + Params extends Json, + Result extends Json | Promise, +> = ( + request: RestrictedMethodRequest, + context: RestrictedMethodContext, +) => Result | Error | EthereumRpcError; type RestrictedMethodsOption = Record< MethodName, - RestrictedMethodImplementation + RestrictedMethodImplementation >; interface PermissionControllerOptions { @@ -141,7 +140,7 @@ export class PermissionController extends BaseController< */ private readonly _restrictedMethods: ReadonlyMap< string, - RestrictedMethodImplementation + RestrictedMethodImplementation >; private readonly _safeMethods: ReadonlySet; @@ -443,9 +442,9 @@ export class PermissionController extends BaseController< return ''; } - getRestrictedMethodImplementation( + getRestrictedMethodImplementation( method: string, - ): RestrictedMethodImplementation | undefined { + ): RestrictedMethodImplementation | undefined { const methodKey = this.getMethodKeyFor(method); if (!methodKey) { return undefined; @@ -458,10 +457,8 @@ export class PermissionController extends BaseController< `Method "${method}" with method key "${methodKey}" has no implementation.`, ); } - return methodImplementation as RestrictedMethodImplementation< - Params, - Result - >; + + return methodImplementation; } grantPermissions( @@ -539,7 +536,7 @@ export class PermissionController extends BaseController< function getRestrictedMethodMap( restrictedMethods: RestrictedMethodsOption, -): ReadonlyMap> { +): ReadonlyMap> { return Object.entries(restrictedMethods).reduce( (methodMap, [methodName, implementation]) => { if (!methodName || typeof methodName !== 'string') { diff --git a/packages/controllers/src/permissions/PermissionEnforcer.ts b/packages/controllers/src/permissions/PermissionEnforcer.ts index c660912b60..7d6795cb13 100644 --- a/packages/controllers/src/permissions/PermissionEnforcer.ts +++ b/packages/controllers/src/permissions/PermissionEnforcer.ts @@ -1,13 +1,14 @@ import { nanoid } from 'nanoid'; import { - JsonRpcEngineEndCallback, - JsonRpcEngineNextCallback, + AsyncJsonRpcEngineNextCallback, + createAsyncMiddleware, + Json, JsonRpcRequest, + JsonRpcMiddleware, PendingJsonRpcResponse, - JsonRpcEngineCallbackError, } from 'json-rpc-engine'; -import { CaveatSpecifications, decorateWithCaveats } from './caveat-processing'; -import { methodNotFound, unauthorized } from './errors'; +import { CaveatSpecifications, decorateWithCaveats } from './caveat-decoration'; +import { internalError, methodNotFound, unauthorized } from './errors'; import type { PermissionController, RestrictedMethodImplementation, @@ -42,59 +43,49 @@ class PermissionEnforcer { this._getRestrictedMethodImplementation = getRestrictedMethodImplementation; } - executeRestrictedMethod( + async executeRestrictedMethod( origin: string, method: string, params: Params, ): Promise { - return new Promise((resolve) => { - const id = nanoid(); - const req: JsonRpcRequest = { id, jsonrpc: '2.0', method }; - if (params !== undefined) { - req.params = params; - } + const id = nanoid(); + const req: JsonRpcRequest = { id, jsonrpc: '2.0', method }; + if (params !== undefined) { + req.params = params; + } - const res: PendingJsonRpcResponse = { id, jsonrpc: '2.0' }; + const methodImplementation = this.getRestrictedMethodImplementation( + method, + req, + ); - // no-op - const next = () => undefined; + const result = await this._executeRestrictedMethod( + methodImplementation, + { origin }, + req, + ); - const end = (error?: JsonRpcEngineCallbackError) => { - if (error || res.error) { - throw error || res.error; - } else if (res.result === undefined) { - throw new Error( - `Internal request for method "${method}" as origin "${origin}" has no result.`, - ); - } else { - resolve(res.result); - } - }; + if (resultIsError(result)) { + throw result; + } - const methodImplementation = this.getRestrictedMethodImplementation( - method, - req, + if (result === undefined) { + throw new Error( + `Internal request for method "${method}" as origin "${origin}" has no result.`, ); + } - this._executeRestrictedMethod( - methodImplementation, - { origin }, - req, - res, - next, - end, - ); - }); + return result as Result; } - getPermissionsMiddleware() { - const permissionsMiddleware = ( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - next: JsonRpcEngineNextCallback, - end: JsonRpcEngineEndCallback, - subject: SubjectMetadata, - ): void => { + getPermissionsMiddleware( + subject: SubjectMetadata, + ): JsonRpcMiddleware { + const permissionsMiddleware = async ( + req: JsonRpcRequest, + res: PendingJsonRpcResponse, + next: AsyncJsonRpcEngineNextCallback, + ): Promise => { const { method } = req; // skip registered safe/passthrough methods. @@ -108,22 +99,30 @@ class PermissionEnforcer { req, ); - const permission = this.getPermission(subject.origin, method); - if (!permission) { - return end(unauthorized({ data: { request: req } })); - } - - this._executeRestrictedMethod( + const result = await this._executeRestrictedMethod( methodImplementation, subject, req, - res, - next, - end, ); + + if (resultIsError(result)) { + res.error = result; + return undefined; + } + + if (result === undefined) { + res.error = internalError( + `Request for method "${req.method}" returned undefined result.`, + req, + ); + return undefined; + } + + res.result = result as Json; return undefined; }; - return permissionsMiddleware; + + return createAsyncMiddleware(permissionsMiddleware); } /** @@ -136,8 +135,8 @@ class PermissionEnforcer { */ private getRestrictedMethodImplementation( method: string, - request: JsonRpcRequest, - ): RestrictedMethodImplementation { + request: JsonRpcRequest, + ): RestrictedMethodImplementation { const methodImplementation = this._getRestrictedMethodImplementation(method); if (!methodImplementation) { @@ -151,34 +150,29 @@ class PermissionEnforcer { * If called as a result of an RPC request, the existence of the method * should already be verified. * - * @param subject - * @param req - * @param res - * @param next - * @param end + * @param methodImplementation - The implementation of the method to call. + * @param subject - Metadata about the subject that made the request. + * @param req - The request object associated with the request. * @returns */ private _executeRestrictedMethod( - methodImplementation: RestrictedMethodImplementation, + methodImplementation: RestrictedMethodImplementation, subject: SubjectMetadata, - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - next: JsonRpcEngineNextCallback, - end: JsonRpcEngineEndCallback, - ): void | Promise { + req: JsonRpcRequest, + ): Json | Error | Promise { const { origin } = subject; const { method } = req; const permission = this.getPermission(origin, method); if (!permission) { - throw unauthorized({ data: { origin, method } }); + return unauthorized({ data: { origin, method } }); } return decorateWithCaveats( methodImplementation, permission, this.caveatSpecifications, - )(req, res, next, end, { origin }); + )(req, { origin }); } } @@ -197,3 +191,13 @@ export function getPermissionEnforcer( controller.getRestrictedMethodImplementation.bind(controller), }); } + +function resultIsError(resultOrError: Json | Error): resultOrError is Error { + return Boolean( + resultOrError instanceof Error || + (resultOrError && + typeof resultOrError === 'object' && + 'message' in resultOrError && + 'code' in resultOrError), + ); +} diff --git a/packages/controllers/src/permissions/caveat-decoration.ts b/packages/controllers/src/permissions/caveat-decoration.ts new file mode 100644 index 0000000000..36410b588b --- /dev/null +++ b/packages/controllers/src/permissions/caveat-decoration.ts @@ -0,0 +1,77 @@ +import { Json } from 'json-rpc-engine'; +import { Caveat } from './Caveat'; +import { UnrecognizedCaveatTypeError } from './errors'; +import { Permission } from './Permission'; +import { RestrictedMethodImplementation } from './PermissionController'; + +type CaveatType = string; + +type CaveatValueValidator = ( + caveatValue: CaveatValue, +) => void; + +type CaveatDecorator = ( + decorated: RestrictedMethodImplementation, + caveat: Caveat, +) => RestrictedMethodImplementation; + +export interface CaveatSpecification { + type: CaveatType; + decorator: CaveatDecorator; + validator: CaveatValueValidator; +} + +export type CaveatSpecifications = Readonly< + Record> +>; + +/** + * Decorate a restricted method implementation with its caveats. + * + * TODO: Should all caveat functions be async? + * TODO: Document how to create caveats using below examples. + */ +export function decorateWithCaveats< + MethodImplementation extends RestrictedMethodImplementation, +>( + methodImplementation: MethodImplementation, + permission: Readonly, // bound to the requesting origin + caveatSpecifications: CaveatSpecifications, // all caveat implementations +): MethodImplementation { + const caveats = permission.caveats ?? []; + + let decorated: RestrictedMethodImplementation = + methodImplementation; + for (const caveat of caveats) { + const specification = caveatSpecifications[caveat.type]; + if (!specification) { + throw new UnrecognizedCaveatTypeError(caveat.type); + } + + decorated = specification.decorator(decorated, caveat); + } + return decorated as MethodImplementation; +} + +// Return next example. +// +// export function ifMetaMask(next: MethodImplementation) { +// return (req: JsonRpcRequest, context: Record) => { +// if (context.origin !== 'metamask.io') { +// return new EthereumJsonRpcError(); +// } +// +// return next(req, context); +// }; +// } + +// "Return handler" example +// +// export function eth_accounts(next: MethodImplementation) { +// return (req: JsonRpcRequest, context: Record) => { +// const accounts = next(req, context); +// return accounts.filter(() => { +// // filter +// }); +// }; +// } diff --git a/packages/controllers/src/permissions/caveat-processing.ts b/packages/controllers/src/permissions/caveat-processing.ts deleted file mode 100644 index 2a173887a4..0000000000 --- a/packages/controllers/src/permissions/caveat-processing.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { Json, JsonRpcRequest, PendingJsonRpcResponse } from 'json-rpc-engine'; -import { Caveat } from './Caveat'; -import { UnrecognizedCaveatTypeError } from './errors'; -import { Permission } from './Permission'; -import { RestrictedMethodImplementation } from './PermissionController'; - -type CaveatType = string; - -/** - * The implementation of a caveat type. - */ -type CaveatFunction< - CaveatValue extends Json, - Params extends Json, - Result extends Json, -> = ( - caveatObject: Caveat, - request: JsonRpcRequest, -) => CaveatReturnHandler | undefined; - -type CaveatValueValidator = ( - caveatValue: CaveatValue, -) => void; - -export interface CaveatSpecification< - CaveatValue extends Json, - MethodParameters extends Json, - MethodResult extends Json, -> { - type: CaveatType; - implementation: CaveatFunction; - validator: CaveatValueValidator; -} - -export type CaveatSpecifications = Record< - CaveatType, - CaveatSpecification ->; - -/** - * A {@link CaveatFunction} whose `caveatObject` argument has been bound to - * the caveat object of a particular permission. - */ -type BoundCaveatFunction = ( - request: JsonRpcRequest, -) => CaveatReturnHandler | undefined; - -type CaveatReturnHandler = ( - response: PendingJsonRpcResponse, -) => void; - -/** - * Decorate a restricted method implementation with its caveats. - */ -export function decorateWithCaveats< - MethodImplementation extends RestrictedMethodImplementation, ->( - methodImplementation: MethodImplementation, - permission: Readonly, // bound to the requesting origin - caveatImplementations: CaveatSpecifications, // all caveat implementations -): MethodImplementation { - const { caveats } = permission; - - // If the permission has caveats, create an array of functions that call the - // corresponding caveat functions with the caveat object and request as - // arguments. - if (caveats && caveats.length > 0) { - const caveatFunctions: BoundCaveatFunction[] = []; - - for (const caveat of caveats) { - if (!caveatImplementations[caveat.type]) { - throw new UnrecognizedCaveatTypeError(caveat.type); - } - - caveatFunctions.push((request: JsonRpcRequest) => - caveatImplementations[caveat.type].implementation(caveat, request), - ); - } - - return ((req, res, next, end, context) => { - let _end = end; - const caveatReturnHandlers = applyCaveats(req, caveatFunctions); - - // If any of the caveats specified return handlers, wrap the "end" - // callback such that the return handlers are applied to the response - // before actually ending the request. - if (caveatReturnHandlers.length > 0) { - _end = () => { - caveatReturnHandlers.forEach((returnHandler) => returnHandler(res)); - end(); - }; - } - - return methodImplementation(req, res, next, _end, context); - }) as MethodImplementation; - } - - // If there are no caveats, return a function that calls the restricted method - // implementation directly, without applying any caveats. - return ((req, res, next, end, context) => - methodImplementation(req, res, next, end, context)) as MethodImplementation; -} - -/** - * Applies the specified caveats to the specified request. Specifically, every - * caveat function in the array is called with the request object as an argument. - * - * @param req - The request to apply caveats to. - * @param caveatFunctions - The caveats functions to call with the request as - * an argument. - * @returns Any return handlers returned by the caveat functions. - */ -function applyCaveats( - req: JsonRpcRequest, - caveatFunctions: BoundCaveatFunction[], -): CaveatReturnHandler[] { - const returnHandlers: CaveatReturnHandler[] = []; - - for (const caveatFunction of caveatFunctions) { - const returnHandler = caveatFunction(req); - - if (returnHandler) { - returnHandlers.push(returnHandler); - } - } - return returnHandlers; -} diff --git a/packages/controllers/src/permissions/errors.ts b/packages/controllers/src/permissions/errors.ts index 6006a86bce..d82f98f55a 100644 --- a/packages/controllers/src/permissions/errors.ts +++ b/packages/controllers/src/permissions/errors.ts @@ -35,6 +35,13 @@ export function userRejectedRequest( return ethErrors.provider.userRejectedRequest({ data: request }); } +export function internalError( + message: string, + request?: JsonRpcRequest, +): EthereumRpcError> { + return ethErrors.rpc.internal({ message, data: request }); +} + export class InvalidSubjectIdentifierError extends Error { constructor(origin: string) { super(`Invalid subject identifier: ${origin}`); From 6c66ab60be276d52bf3da287beae52592e271a1f Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sat, 21 Aug 2021 13:21:45 -0700 Subject: [PATCH 023/147] Require that caveat functions are async --- .../src/permissions/PermissionController.ts | 19 +++++++++-- .../src/permissions/PermissionEnforcer.ts | 14 ++++---- .../src/permissions/caveat-decoration.ts | 34 +++++++++++-------- 3 files changed, 43 insertions(+), 24 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 769b0dbdfe..cd63000688 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -105,14 +105,29 @@ export type RestrictedMethodRequest = { [key: string]: any; }; -export type RestrictedMethodImplementation< +export type SyncRestrictedMethodImplementation< Params extends Json, - Result extends Json | Promise, + Result extends Json, > = ( request: RestrictedMethodRequest, context: RestrictedMethodContext, ) => Result | Error | EthereumRpcError; +export type AsyncRestrictedMethodImplementation< + Params extends Json, + Result extends Json, +> = ( + request: RestrictedMethodRequest, + context: RestrictedMethodContext, +) => Promise>; + +export type RestrictedMethodImplementation< + Params extends Json, + Result extends Json, +> = + | SyncRestrictedMethodImplementation + | AsyncRestrictedMethodImplementation; + type RestrictedMethodsOption = Record< MethodName, RestrictedMethodImplementation diff --git a/packages/controllers/src/permissions/PermissionEnforcer.ts b/packages/controllers/src/permissions/PermissionEnforcer.ts index 7d6795cb13..48b1abdd3c 100644 --- a/packages/controllers/src/permissions/PermissionEnforcer.ts +++ b/packages/controllers/src/permissions/PermissionEnforcer.ts @@ -43,13 +43,13 @@ class PermissionEnforcer { this._getRestrictedMethodImplementation = getRestrictedMethodImplementation; } - async executeRestrictedMethod( + async executeRestrictedMethod( origin: string, method: string, - params: Params, - ): Promise { + params: Json, + ): Promise { const id = nanoid(); - const req: JsonRpcRequest = { id, jsonrpc: '2.0', method }; + const req: JsonRpcRequest = { id, jsonrpc: '2.0', method }; if (params !== undefined) { req.params = params; } @@ -75,7 +75,7 @@ class PermissionEnforcer { ); } - return result as Result; + return result; } getPermissionsMiddleware( @@ -118,7 +118,7 @@ class PermissionEnforcer { return undefined; } - res.result = result as Json; + res.result = result; return undefined; }; @@ -159,7 +159,7 @@ class PermissionEnforcer { methodImplementation: RestrictedMethodImplementation, subject: SubjectMetadata, req: JsonRpcRequest, - ): Json | Error | Promise { + ): ReturnType> { const { origin } = subject; const { method } = req; diff --git a/packages/controllers/src/permissions/caveat-decoration.ts b/packages/controllers/src/permissions/caveat-decoration.ts index 36410b588b..18eb425707 100644 --- a/packages/controllers/src/permissions/caveat-decoration.ts +++ b/packages/controllers/src/permissions/caveat-decoration.ts @@ -2,7 +2,10 @@ import { Json } from 'json-rpc-engine'; import { Caveat } from './Caveat'; import { UnrecognizedCaveatTypeError } from './errors'; import { Permission } from './Permission'; -import { RestrictedMethodImplementation } from './PermissionController'; +import { + AsyncRestrictedMethodImplementation, + RestrictedMethodImplementation, +} from './PermissionController'; type CaveatType = string; @@ -11,9 +14,9 @@ type CaveatValueValidator = ( ) => void; type CaveatDecorator = ( - decorated: RestrictedMethodImplementation, + decorated: AsyncRestrictedMethodImplementation, caveat: Caveat, -) => RestrictedMethodImplementation; +) => AsyncRestrictedMethodImplementation; export interface CaveatSpecification { type: CaveatType; @@ -28,20 +31,21 @@ export type CaveatSpecifications = Readonly< /** * Decorate a restricted method implementation with its caveats. * - * TODO: Should all caveat functions be async? * TODO: Document how to create caveats using below examples. + * Note that all caveat functions (i.e. the argument and return value of the decorator) + * must be awaited. */ -export function decorateWithCaveats< - MethodImplementation extends RestrictedMethodImplementation, ->( - methodImplementation: MethodImplementation, +export function decorateWithCaveats( + methodImplementation: RestrictedMethodImplementation, permission: Readonly, // bound to the requesting origin caveatSpecifications: CaveatSpecifications, // all caveat implementations -): MethodImplementation { +): RestrictedMethodImplementation { const caveats = permission.caveats ?? []; - let decorated: RestrictedMethodImplementation = - methodImplementation; + let decorated = methodImplementation as AsyncRestrictedMethodImplementation< + Json, + Json + >; for (const caveat of caveats) { const specification = caveatSpecifications[caveat.type]; if (!specification) { @@ -50,13 +54,13 @@ export function decorateWithCaveats< decorated = specification.decorator(decorated, caveat); } - return decorated as MethodImplementation; + return decorated; } // Return next example. // // export function ifMetaMask(next: MethodImplementation) { -// return (req: JsonRpcRequest, context: Record) => { +// return async (req: JsonRpcRequest, context: Record) => { // if (context.origin !== 'metamask.io') { // return new EthereumJsonRpcError(); // } @@ -68,8 +72,8 @@ export function decorateWithCaveats< // "Return handler" example // // export function eth_accounts(next: MethodImplementation) { -// return (req: JsonRpcRequest, context: Record) => { -// const accounts = next(req, context); +// return async (req: JsonRpcRequest, context: Record) => { +// const accounts = await next(req, context); // return accounts.filter(() => { // // filter // }); From 40884217a037d218900ff1dfdfbb71d6fb1d94e7 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sat, 21 Aug 2021 21:58:35 -0700 Subject: [PATCH 024/147] Update caveat state management --- .../controllers/src/permissions/Permission.ts | 7 +-- .../src/permissions/PermissionController.ts | 63 ++++++++++++++++--- .../controllers/src/permissions/errors.ts | 8 +++ 3 files changed, 64 insertions(+), 14 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 0e7b9e3abc..d184b17823 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -144,12 +144,11 @@ export class Permission implements ZcapLdCapability { * @param caveatType The type of the caveat to retrieve. * @returns The caveat value, or undefined if no such caveat exists. */ - public static getCaveatValue( + public static getCaveat( permission: Permission, caveatType: string, - ): Json | undefined { - return permission.caveats?.find((caveat) => caveat.type === caveatType) - ?.value; + ): Caveat | undefined { + return permission.caveats?.find((caveat) => caveat.type === caveatType); } } diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index cd63000688..771634babf 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -26,6 +26,7 @@ import { InvalidCaveatFieldsError, InvalidSubjectIdentifierError, InvalidCaveatTypeError, + CaveatAlreadyExistsError, } from './errors'; import { Caveat } from './Caveat'; @@ -343,27 +344,69 @@ export class PermissionController extends BaseController< }); } - getCaveat( + hasCaveat(origin: string, target: string, caveatType: string): boolean { + return ( + this.getPermission(origin, target)?.caveats?.some( + (caveat) => caveat.type === caveatType, + ) ?? false + ); + } + + addCaveat( origin: string, target: string, caveatType: string, - ): Caveat | undefined { - return this.getPermission(origin, target)?.caveats?.find( - (caveat) => caveat.type === caveatType, + caveatValue: Json, + ): void { + if (this._getCaveat(origin, target, caveatType)) { + throw new CaveatAlreadyExistsError(origin, target, caveatType); + } + + this._setCaveat( + origin, + target, + new Caveat({ type: caveatType, value: caveatValue }), ); } - hasCaveat(origin: string, target: string, caveatType: string): boolean { - return ( - this.getPermission(origin, target)?.caveats?.some( - (caveat) => caveat.type === caveatType, - ) ?? false + updateCaveat( + origin: string, + target: string, + caveatType: string, + caveatValue: Json, + ): void { + if (!this._getCaveat(origin, target, caveatType)) { + throw new CaveatDoesNotExistError(origin, target, caveatType); + } + + this._setCaveat( + origin, + target, + new Caveat({ type: caveatType, value: caveatValue }), ); } - setCaveat(origin: string, target: string, caveat: Caveat): void { + private _getCaveat( + origin: string, + target: string, + caveatType: string, + ): Caveat | undefined { + const permission = this.getPermission(origin, target); + if (!permission) { + throw new PermissionDoesNotExistError(origin, target); + } + + return Permission.getCaveat(permission, caveatType); + } + + private _setCaveat( + origin: string, + target: string, + caveat: Caveat, + ): void { this.update((draftState) => { const permission = draftState.subjects[origin]?.permissions[target]; + /* istanbul ignore if */ // This should be impossible if (!permission) { throw new PermissionDoesNotExistError(origin, target); } diff --git a/packages/controllers/src/permissions/errors.ts b/packages/controllers/src/permissions/errors.ts index d82f98f55a..911d142dd8 100644 --- a/packages/controllers/src/permissions/errors.ts +++ b/packages/controllers/src/permissions/errors.ts @@ -80,6 +80,14 @@ export class CaveatDoesNotExistError extends Error { } } +export class CaveatAlreadyExistsError extends Error { + constructor(origin: string, target: string, caveatType: string) { + super( + `Permission for "${target}" of subject "${origin}" already has a caveat of type "${caveatType}".`, + ); + } +} + export class PermissionTargetDoesNotExistError extends Error { constructor(origin: string, target: string) { super( From 2024b51ec776a6cc9956aded474475194497ceec Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sat, 21 Aug 2021 22:32:35 -0700 Subject: [PATCH 025/147] Fixup / tweak --- .../src/permissions/PermissionController.ts | 25 ++++++++----------- .../src/permissions/caveat-decoration.ts | 2 ++ .../controllers/src/permissions/errors.ts | 8 ------ 3 files changed, 13 insertions(+), 22 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 771634babf..a0f37d870d 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -21,7 +21,6 @@ import { PermissionHasNoCaveatsError, CaveatDoesNotExistError, CaveatTypeDoesNotExistError, - PermissionTargetDoesNotExistError, CaveatMissingValueError, InvalidCaveatFieldsError, InvalidSubjectIdentifierError, @@ -195,14 +194,14 @@ export class PermissionController extends BaseController< // assigned if we don't do it. this.messagingSystem = messenger; - this.registerMessageHandlers(); + this._registerMessageHandlers(); } /** * Constructor helper for registering this controller's messaging system * actions. */ - protected registerMessageHandlers(): void { + private _registerMessageHandlers(): void { this.messagingSystem.registerActionHandler( `${controllerName}:getSubjects`, () => this.getSubjects(), @@ -214,7 +213,7 @@ export class PermissionController extends BaseController< ); } - protected clearState(): void { + clearState(): void { this.update((_draftState) => { return { ...defaultState }; }); @@ -522,6 +521,7 @@ export class PermissionController extends BaseController< grantPermissions( subject: SubjectMetadata, requestedPermissions: RequestedPermissions, + shouldPreserveExistingPermissions = true, ): Record { const { origin } = subject; if (!origin || typeof origin !== 'string') { @@ -535,13 +535,10 @@ export class PermissionController extends BaseController< } } - const permissions: { [methodName: string]: Permission } = {}; + const permissions: { [methodName: string]: Permission } = + (shouldPreserveExistingPermissions && this.getPermissions(origin)) || {}; for (const method of Object.keys(requestedPermissions)) { - if (!this._restrictedMethods.has(this.getMethodKeyFor(method))) { - throw new PermissionTargetDoesNotExistError(origin, method); - } - permissions[method] = new Permission({ target: method, invoker: origin, @@ -567,6 +564,10 @@ export class PermissionController extends BaseController< caveats?: Caveat[], ): Caveat[] | undefined { const caveatArray = caveats?.map((caveat) => { + if (Object.keys(caveat).length !== 2) { + throw new InvalidCaveatFieldsError(caveat, origin, target); + } + if (caveat.type !== 'string') { throw new InvalidCaveatTypeError(caveat.type); } @@ -580,11 +581,7 @@ export class PermissionController extends BaseController< throw new CaveatMissingValueError(caveat, origin, target); } // Should throw an error if validation fails. - specification.validator(caveat.value); - - if (Object.keys(caveat).length !== 2) { - throw new InvalidCaveatFieldsError(caveat, origin, target); - } + specification.validator(caveat.value, origin, target); return new Caveat({ type: caveat.type, value: caveat.value }); }); diff --git a/packages/controllers/src/permissions/caveat-decoration.ts b/packages/controllers/src/permissions/caveat-decoration.ts index 18eb425707..74929a93b5 100644 --- a/packages/controllers/src/permissions/caveat-decoration.ts +++ b/packages/controllers/src/permissions/caveat-decoration.ts @@ -11,6 +11,8 @@ type CaveatType = string; type CaveatValueValidator = ( caveatValue: CaveatValue, + origin?: string, + target?: string, ) => void; type CaveatDecorator = ( diff --git a/packages/controllers/src/permissions/errors.ts b/packages/controllers/src/permissions/errors.ts index 911d142dd8..4a7114178e 100644 --- a/packages/controllers/src/permissions/errors.ts +++ b/packages/controllers/src/permissions/errors.ts @@ -88,14 +88,6 @@ export class CaveatAlreadyExistsError extends Error { } } -export class PermissionTargetDoesNotExistError extends Error { - constructor(origin: string, target: string) { - super( - `Target "${target}" of requested permission for subject "${origin}" does not exist.`, - ); - } -} - export class InvalidPermissionJsonError extends Error { public data: { origin: string; permission: Permission }; From 747f12068e636ad06440a641ad8869c74d6850e7 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sun, 22 Aug 2021 22:06:16 -0700 Subject: [PATCH 026/147] Implement RPC methods, use types over interfaces Most of wallet_requestPermissions is implemented as the method requestpermissions on the PermissionEnforcer. --- packages/controllers/.eslintrc.js | 3 + .../controllers/src/permissions/Caveat.ts | 25 +- .../controllers/src/permissions/Permission.ts | 15 +- .../src/permissions/PermissionController.ts | 22 +- .../src/permissions/PermissionEnforcer.ts | 131 +++++++- .../src/permissions/caveat-decoration.ts | 4 +- .../controllers/src/permissions/errors.ts | 31 +- .../permissions/rpc-methods/getPermissions.ts | 37 +++ .../src/permissions/rpc-methods/index.ts | 10 + .../rpc-methods/requestPermissions.ts | 60 ++++ .../permissions/temp/ApprovalController.d.ts | 292 ++++++++++++++++++ .../permissions/temp/BaseControllerV2.d.ts | 185 +++++++++++ 12 files changed, 761 insertions(+), 54 deletions(-) create mode 100644 packages/controllers/src/permissions/rpc-methods/getPermissions.ts create mode 100644 packages/controllers/src/permissions/rpc-methods/index.ts create mode 100644 packages/controllers/src/permissions/rpc-methods/requestPermissions.ts create mode 100644 packages/controllers/src/permissions/temp/ApprovalController.d.ts create mode 100644 packages/controllers/src/permissions/temp/BaseControllerV2.d.ts diff --git a/packages/controllers/.eslintrc.js b/packages/controllers/.eslintrc.js index c7e798f666..51331125a9 100644 --- a/packages/controllers/.eslintrc.js +++ b/packages/controllers/.eslintrc.js @@ -5,6 +5,9 @@ module.exports = { '@typescript-eslint/consistent-type-definitions': 'off', }, + // TODO:temp + ignorePatterns: ['**/temp/**'], + overrides: [ { files: ['**/*.d.ts'], diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index 41e8705d28..b73538e57f 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -1,25 +1,30 @@ import { Json } from 'json-rpc-engine'; -interface CaveatOptions { +type CaveatOptions = { type: string; value: ValueType; -} +}; -/** - * - */ -export interface ZcapLdCaveat { +export type ZcapLdCaveat = { /** * The type of the caveat, which is presumed to be meaningful in the context * of the capability it is associated with. */ type: string; -} +}; + +/** + * Identical to instances of the Caveat class, useful for when TypeScript + * has a meltdown over assigning classes to the Json type. + */ +export type CaveatInterface = ZcapLdCaveat & { + value: Value; +}; /** * TODO: Document */ -export class Caveat implements ZcapLdCaveat { +export class Caveat implements CaveatInterface { /** * The type of the caveat. The type is presumed to be meaningful in the * context of the capability it is associated with. @@ -33,9 +38,9 @@ export class Caveat implements ZcapLdCaveat { * * TODO: Make optional in typescript@4.4.x */ - public readonly value: ValueType; + public readonly value: Value; - constructor({ type, value }: CaveatOptions) { + constructor({ type, value }: CaveatOptions) { this.type = type; this.value = value; } diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index d184b17823..ba5c9a252f 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -1,6 +1,6 @@ import { Json } from 'json-rpc-engine'; import { nanoid } from 'nanoid'; -import { Caveat, ZcapLdCaveat } from './Caveat'; +import { Caveat, CaveatInterface, ZcapLdCaveat } from './Caveat'; /** * The origin of a subject. @@ -16,7 +16,7 @@ export type MethodName = string; /** * An interface */ -interface ZcapLdCapability { +type ZcapLdCapability = { /** * The context(s) in which this capability is meaningful. * @@ -69,9 +69,9 @@ interface ZcapLdCapability { * object in the designated part of our state tree, so this field is omitted. */ proof?: string; -} +}; -interface PermissionOptions { +type PermissionOptions = { /** * The method that the permission corresponds to. */ @@ -93,7 +93,7 @@ interface PermissionOptions { * See {@link Caveat}. */ caveats?: Caveat[]; -} +}; /** * TODO: Document @@ -152,6 +152,9 @@ export class Permission implements ZcapLdCapability { } } -type RequestedPermission = Pick; +type RequestedPermission = { + target: MethodName; + caveats: CaveatInterface[] | null; +}; export type RequestedPermissions = Record; diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index a0f37d870d..87ed4f1065 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -29,13 +29,13 @@ import { } from './errors'; import { Caveat } from './Caveat'; -export interface SubjectMetadata { +export type SubjectMetadata = { origin: OriginString; -} +}; const controllerName = 'PermissionController'; -type SubjectPermissions = Record; +export type SubjectPermissions = Record; export type PermissionsSubjectEntry = { permissions: SubjectPermissions; @@ -133,14 +133,14 @@ type RestrictedMethodsOption = Record< RestrictedMethodImplementation >; -interface PermissionControllerOptions { +type PermissionControllerOptions = { messenger: PermissionControllerMessenger; caveatSpecifications: CaveatSpecifications; methodPrefix: string; restrictedMethods: RestrictedMethodsOption; safeMethods: string[]; state?: Partial; -} +}; export class PermissionController extends BaseController< typeof controllerName, @@ -545,7 +545,7 @@ export class PermissionController extends BaseController< caveats: this.computeCaveats( origin, method, - requestedPermissions[method].caveats, + requestedPermissions[method].caveats || undefined, ), }); } @@ -607,19 +607,19 @@ function getRestrictedMethodMap( // TODO: Are we using these? -export interface PermissionsRequestMetadata extends SubjectMetadata { +export type PermissionsRequestMetadata = SubjectMetadata & { id: string; -} +}; /** * Used for prompting the user about a proposed new permission. * Includes information about the subject granted, as well as the permissions * assigned. */ -export interface PermissionsRequest { - stateMetadata: PermissionsRequestMetadata; +export type PermissionsRequest = { + metadata: PermissionsRequestMetadata; permissions: RequestedPermissions; -} +}; export type UserApprovalPrompt = ( permissionsRequest: PermissionsRequest, diff --git a/packages/controllers/src/permissions/PermissionEnforcer.ts b/packages/controllers/src/permissions/PermissionEnforcer.ts index 48b1abdd3c..9ad7e9ef33 100644 --- a/packages/controllers/src/permissions/PermissionEnforcer.ts +++ b/packages/controllers/src/permissions/PermissionEnforcer.ts @@ -8,38 +8,67 @@ import { PendingJsonRpcResponse, } from 'json-rpc-engine'; import { CaveatSpecifications, decorateWithCaveats } from './caveat-decoration'; -import { internalError, methodNotFound, unauthorized } from './errors'; +import { + internalError, + invalidParams, + methodNotFound, + unauthorized, +} from './errors'; +import type { Permission, RequestedPermissions } from './Permission'; import type { PermissionController, + PermissionsRequest, RestrictedMethodImplementation, SubjectMetadata, } from './PermissionController'; +import type { ApprovalController } from './temp/ApprovalController'; + +type IsRestrictedMethod = (method: string) => boolean; +type IsSafeMethod = (method: string) => boolean; +type RequestUserApproval = ( + permissionsRequest: PermissionsRequest, +) => Promise; -interface PermissionEnforcerArgs { +type PermissionEnforcerArgs = { caveatSpecifications: CaveatSpecifications; - isSafeMethod: (method: string) => boolean; + isRestrictedMethod: IsRestrictedMethod; + isSafeMethod: IsSafeMethod; getPermission: PermissionController['getPermission']; getRestrictedMethodImplementation: PermissionController['getRestrictedMethodImplementation']; -} + grantPermissions: PermissionController['grantPermissions']; + requestUserApproval: RequestUserApproval; +}; class PermissionEnforcer { - private isSafeMethod: (method: string) => boolean; + private caveatSpecifications: CaveatSpecifications; + + private isRestrictedMethod: IsRestrictedMethod; + + private isSafeMethod: IsSafeMethod; private getPermission: PermissionController['getPermission']; - private _getRestrictedMethodImplementation: PermissionController['getRestrictedMethodImplementation']; + private grantPermissions: PermissionController['grantPermissions']; - private caveatSpecifications: CaveatSpecifications; + private requestUserApproval: RequestUserApproval; + + private _getRestrictedMethodImplementation: PermissionController['getRestrictedMethodImplementation']; constructor({ caveatSpecifications, + isRestrictedMethod, isSafeMethod, getPermission, getRestrictedMethodImplementation, + grantPermissions, + requestUserApproval, }: PermissionEnforcerArgs) { this.caveatSpecifications = caveatSpecifications; + this.isRestrictedMethod = isRestrictedMethod; this.isSafeMethod = isSafeMethod; this.getPermission = getPermission; + this.grantPermissions = grantPermissions; + this.requestUserApproval = requestUserApproval; this._getRestrictedMethodImplementation = getRestrictedMethodImplementation; } @@ -78,6 +107,64 @@ class PermissionEnforcer { return result; } + async requestPermissions( + origin: string, + requestedPermissions: RequestedPermissions, + id: string = nanoid(), + ): Promise<[Record, { id: string; origin: string }]> { + this.validateRequestedPermissions(origin, requestedPermissions); + const metadata: PermissionsRequest['metadata'] = { + id, + origin, + }; + + const permissionsRequest: PermissionsRequest = { + metadata, + permissions: requestedPermissions, + }; + + const approved = await this.requestUserApproval(permissionsRequest); + if (Object.keys(approved).length === 0) { + throw internalError( + `Approved permissions request for origin "${origin}" contains no permissions.`, + { requestedPermissions }, + ); + } + + return [this.grantPermissions({ origin }, approved), metadata]; + } + + private validateRequestedPermissions( + origin: string, + requestedPermissions: RequestedPermissions, + ) { + if ( + !requestedPermissions || + typeof requestedPermissions !== 'object' || + Array.isArray(requestedPermissions) + ) { + throw invalidParams({ data: { origin, requestedPermissions } }); + } + + const perms: RequestedPermissions = requestedPermissions; + + for (const methodName of Object.keys(perms)) { + const target = + perms[methodName].target || + (perms[methodName] as unknown as Permission).parentCapability; + if (target !== undefined && methodName !== target) { + throw invalidParams({ data: { origin, requestedPermissions } }); + } + + if (!this.isRestrictedMethod(methodName)) { + throw methodNotFound({ + method: methodName, + data: { origin, requestedPermissions }, + }); + } + } + } + getPermissionsMiddleware( subject: SubjectMetadata, ): JsonRpcMiddleware { @@ -113,7 +200,7 @@ class PermissionEnforcer { if (result === undefined) { res.error = internalError( `Request for method "${req.method}" returned undefined result.`, - req, + { request: req }, ); return undefined; } @@ -179,16 +266,34 @@ class PermissionEnforcer { export type { PermissionEnforcer }; export function getPermissionEnforcer( - controller: PermissionController, + permissionController: PermissionController, + addAndShowApprovalRequest: ApprovalController['addAndShowApprovalRequest'], ): PermissionEnforcer { return new PermissionEnforcer({ - caveatSpecifications: controller.caveatSpecifications, + caveatSpecifications: permissionController.caveatSpecifications, + isRestrictedMethod: (method: string) => { + return Boolean(permissionController.getMethodKeyFor(method)); + }, isSafeMethod: (method: string) => { - return controller.safeMethods.has(method); + return permissionController.safeMethods.has(method); }, - getPermission: controller.getPermission.bind(controller), + getPermission: + permissionController.getPermission.bind(permissionController), getRestrictedMethodImplementation: - controller.getRestrictedMethodImplementation.bind(controller), + permissionController.getRestrictedMethodImplementation.bind( + permissionController, + ), + grantPermissions: + permissionController.grantPermissions.bind(permissionController), + requestUserApproval: async (permissionsRequest: PermissionsRequest) => { + const { origin, id } = permissionsRequest.metadata; + return (await addAndShowApprovalRequest({ + id, + origin, + requestData: permissionsRequest, + type: 'wallet_requestPermissions', // TODO: Establish enum somehow + })) as RequestedPermissions; + }, }); } diff --git a/packages/controllers/src/permissions/caveat-decoration.ts b/packages/controllers/src/permissions/caveat-decoration.ts index 74929a93b5..508abac049 100644 --- a/packages/controllers/src/permissions/caveat-decoration.ts +++ b/packages/controllers/src/permissions/caveat-decoration.ts @@ -20,11 +20,11 @@ type CaveatDecorator = ( caveat: Caveat, ) => AsyncRestrictedMethodImplementation; -export interface CaveatSpecification { +export type CaveatSpecification = { type: CaveatType; decorator: CaveatDecorator; validator: CaveatValueValidator; -} +}; export type CaveatSpecifications = Readonly< Record> diff --git a/packages/controllers/src/permissions/errors.ts b/packages/controllers/src/permissions/errors.ts index 4a7114178e..118996b2a2 100644 --- a/packages/controllers/src/permissions/errors.ts +++ b/packages/controllers/src/permissions/errors.ts @@ -3,14 +3,14 @@ import { ethErrors, EthereumRpcError } from 'eth-rpc-errors'; import { Permission } from './Permission'; import { Caveat } from './Caveat'; -interface ErrorArg { +type ErrorArg = { message?: string; data?: JsonRpcRequest | unknown; -} +}; -interface MethodNotFoundArg extends ErrorArg { +type MethodNotFoundArg = ErrorArg & { method?: string; -} +}; export function unauthorized(arg?: ErrorArg) { return ethErrors.provider.unauthorized({ @@ -29,17 +29,24 @@ export function methodNotFound(opts: MethodNotFoundArg) { return ethErrors.rpc.methodNotFound({ data: opts.data, message }); } -export function userRejectedRequest( - request?: JsonRpcRequest, -): EthereumRpcError> { - return ethErrors.provider.userRejectedRequest({ data: request }); +export function invalidParams(opts: ErrorArg) { + return ethErrors.rpc.invalidParams({ + data: opts.data, + message: opts.message, + }); +} + +export function userRejectedRequest>( + data?: Data, +): EthereumRpcError { + return ethErrors.provider.userRejectedRequest({ data }); } -export function internalError( +export function internalError>( message: string, - request?: JsonRpcRequest, -): EthereumRpcError> { - return ethErrors.rpc.internal({ message, data: request }); + data?: Data, +): EthereumRpcError { + return ethErrors.rpc.internal({ message, data }); } export class InvalidSubjectIdentifierError extends Error { diff --git a/packages/controllers/src/permissions/rpc-methods/getPermissions.ts b/packages/controllers/src/permissions/rpc-methods/getPermissions.ts new file mode 100644 index 0000000000..b216df62ea --- /dev/null +++ b/packages/controllers/src/permissions/rpc-methods/getPermissions.ts @@ -0,0 +1,37 @@ +import type { PermittedHandlerExport } from '@mm-snap/rpc-methods/types'; +import type { + JsonRpcEngineEndCallback, + PendingJsonRpcResponse, +} from 'json-rpc-engine'; + +import type { Permission } from '../Permission'; +import type { SubjectPermissions } from '../PermissionController'; + +export const getPermissionsHandler: PermittedHandlerExport< + GetPermissionsHooks, + void, + Permission[] +> = { + methodNames: ['wallet_getPermissions'], + implementation: getPermissionsImplementation, + methodDescription: 'Get permissions.', + hookNames: { + getPermissions: true, + }, +}; + +export type GetPermissionsHooks = { + getPermissions: () => SubjectPermissions; +}; + +async function getPermissionsImplementation( + _req: unknown, + res: PendingJsonRpcResponse, + _next: unknown, + end: JsonRpcEngineEndCallback, + { getPermissions }: GetPermissionsHooks, +): Promise { + // getPermissions is already bound to the origin + res.result = Object.values(getPermissions() || {}); + return end(); +} diff --git a/packages/controllers/src/permissions/rpc-methods/index.ts b/packages/controllers/src/permissions/rpc-methods/index.ts new file mode 100644 index 0000000000..6b32b8efd8 --- /dev/null +++ b/packages/controllers/src/permissions/rpc-methods/index.ts @@ -0,0 +1,10 @@ +import { + requestPermissionsHandler, + RequestPermissionsHooks, +} from './requestPermissions'; +import { getPermissionsHandler, GetPermissionsHooks } from './getPermissions'; + +export type PermittedRpcMethodHooks = RequestPermissionsHooks & + GetPermissionsHooks; + +export const handlers = [requestPermissionsHandler, getPermissionsHandler]; diff --git a/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts b/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts new file mode 100644 index 0000000000..34bdd8a648 --- /dev/null +++ b/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts @@ -0,0 +1,60 @@ +import type { PermittedHandlerExport } from '@mm-snap/rpc-methods/types'; +import type { + JsonRpcEngineEndCallback, + JsonRpcRequest, + PendingJsonRpcResponse, +} from 'json-rpc-engine'; +import { nanoid } from 'nanoid'; + +import { invalidParams } from '../errors'; +import type { Permission, RequestedPermissions } from '../Permission'; + +export const requestPermissionsHandler: PermittedHandlerExport< + RequestPermissionsHooks, + [RequestedPermissions], + Permission[] +> = { + methodNames: ['wallet_requestPermissions'], + implementation: requestPermissionsImplementation, + methodDescription: 'Request permissions.', + hookNames: { + requestPermissions: true, + }, +}; + +// PermissionEnforcer['requestPermissions'] but with the origin parameter bound +type RequestPermissions = ( + requestedPermissions: RequestedPermissions, + id: string, +) => Promise<[Record, { id: string; origin: string }]>; + +export type RequestPermissionsHooks = { + requestPermissions: RequestPermissions; +}; + +async function requestPermissionsImplementation( + req: JsonRpcRequest<[RequestedPermissions]>, + res: PendingJsonRpcResponse, + _next: unknown, + end: JsonRpcEngineEndCallback, + { requestPermissions }: RequestPermissionsHooks, +): Promise { + if ( + !Array.isArray(req.params) || + !req.params[0] || + typeof req.params[0] !== 'object' || + Array.isArray(req.params[0]) + ) { + return end(invalidParams({ data: { request: req } })); + } + + const id = + typeof req.id === 'number' || req.id ? req.id.toString() : nanoid(); + + try { + res.result = Object.values(requestPermissions(req.params[0], id)); + return end(); + } catch (error) { + return end(error); + } +} diff --git a/packages/controllers/src/permissions/temp/ApprovalController.d.ts b/packages/controllers/src/permissions/temp/ApprovalController.d.ts new file mode 100644 index 0000000000..4ccb11c04d --- /dev/null +++ b/packages/controllers/src/permissions/temp/ApprovalController.d.ts @@ -0,0 +1,292 @@ +import type { Patch } from 'immer'; +import { BaseController, Json } from './BaseControllerV2'; +import type { RestrictedControllerMessenger } from '@metamask/controllers'; + +declare const controllerName = "ApprovalController"; +declare type ApprovalRequestData = Record | null; +export declare type ApprovalRequest = { + /** + * The ID of the approval request. + */ + id: string; + /** + * The origin of the approval request. + */ + origin: string; + /** + * The time that the request was received, per Date.now(). + */ + time: number; + /** + * The type of the approval request. + */ + type: string; + /** + * Additional data associated with the request. + * TODO:TS4.4 make optional + */ + requestData: RequestData; +}; +declare type ShowApprovalRequest = () => void | Promise; +export declare type ApprovalControllerState = { + pendingApprovals: Record>>; + pendingApprovalCount: number; +}; +export declare type GetApprovalsState = { + type: `${typeof controllerName}:getState`; + handler: () => ApprovalControllerState; +}; +export declare type ClearApprovalRequests = { + type: `${typeof controllerName}:clearRequests`; + handler: () => void; +}; +declare type AddApprovalOptions = { + id?: string; + origin: string; + type: string; + requestData?: Record; +}; +export declare type AddApprovalRequest = { + type: `${typeof controllerName}:addRequest`; + handler: (opts: AddApprovalOptions, shouldShowRequest: boolean) => ReturnType; +}; +export declare type HasApprovalRequest = { + type: `${typeof controllerName}:hasRequest`; + handler: ApprovalController['has']; +}; +export declare type AcceptRequest = { + type: `${typeof controllerName}:acceptRequest`; + handler: ApprovalController['accept']; +}; +export declare type RejectRequest = { + type: `${typeof controllerName}:rejectRequest`; + handler: ApprovalController['reject']; +}; +export declare type ApprovalControllerActions = GetApprovalsState | ClearApprovalRequests | AddApprovalRequest | HasApprovalRequest | AcceptRequest | RejectRequest; +export declare type ApprovalStateChange = { + type: `${typeof controllerName}:stateChange`; + payload: [ApprovalControllerState, Patch[]]; +}; +export declare type ApprovalControllerEvents = ApprovalStateChange; +export declare type ApprovalControllerMessenger = RestrictedControllerMessenger; +declare type ApprovalControllerOptions = { + messenger: ApprovalControllerMessenger; + showApprovalRequest: ShowApprovalRequest; + state?: Partial; +}; +/** + * Controller for managing requests that require user approval. + * + * Enables limiting the number of pending requests by origin and type, counting + * pending requests, and more. + * + * Adding a request returns a promise that resolves or rejects when the request + * is approved or denied, respectively. + */ +export declare class ApprovalController extends BaseController { + private _approvals; + private _origins; + private _showApprovalRequest; + /** + * @param opts - Options bag + * @param opts.showApprovalRequest - Function for opening the UI such that + * the request can be displayed to the user. + */ + constructor({ messenger, showApprovalRequest, state, }: ApprovalControllerOptions); + /** + * Constructor helper for registering this controller's messaging system + * actions. + */ + private registerMessageHandlers; + /** + * Adds an approval request per the given arguments, calls the show approval + * request function, and returns the associated approval promise. + * + * There can only be one approval per origin and type. An error is thrown if + * attempting to add an invalid or duplicate request. + * + * @param opts - Options bag. + * @param opts.id - The id of the approval request. A random id will be + * generated if none is provided. + * @param opts.origin - The origin of the approval request. + * @param opts.type - The type associated with the approval request. + * @param opts.requestData - Additional data associated with the request, + * if any. + * @returns The approval promise. + */ + addAndShowApprovalRequest(opts: AddApprovalOptions): Promise; + /** + * Adds an approval request per the given arguments and returns the approval + * promise. + * + * There can only be one approval per origin and type. An error is thrown if + * attempting to add an invalid or duplicate request. + * + * @param opts - Options bag. + * @param opts.id - The id of the approval request. A random id will be + * generated if none is provided. + * @param opts.origin - The origin of the approval request. + * @param opts.type - The type associated with the approval request. + * @param opts.requestData - Additional data associated with the request, + * if any. + * @returns The approval promise. + */ + add(opts: AddApprovalOptions): Promise; + /** + * Gets the info for the approval request with the given id. + * + * @param id - The id of the approval request. + * @returns The approval request data associated with the id. + */ + get(id: string): ApprovalRequest | undefined; + /** + * Gets the number of pending approvals, by origin and/or type. + * + * If only `origin` is specified, all approvals for that origin will be + * counted, regardless of type. + * If only `type` is specified, all approvals for that type will be counted, + * regardless of origin. + * If both `origin` and `type` are specified, 0 or 1 will be returned. + * + * @param opts.origin - An approval origin. + * @param opts.type - The type of the approval request. + * @returns The current approval request count for the given origin and/or + * type. + */ + getApprovalCount(opts?: { + origin?: string; + type?: string; + }): number; + /** + * @returns The current total approval request count, for all types and + * origins. + */ + getTotalApprovalCount(): number; + /** + * Checks if there's a pending approval request with the given ID. + * + * @param opts - Options bag. + * @param opts.id - The ID to check for. + * @returns `true` if a matching approval is found, and `false` otherwise. + */ + has(opts: { + id: string; + }): boolean; + /** + * Checks if there's any pending approval request with the given origin. + * + * @param opts - Options bag. + * @param opts.origin - The origin to check for. + * @returns `true` if a matching approval is found, and `false` otherwise. + */ + has(opts: { + origin: string; + }): boolean; + /** + * Checks if there's any pending approval request with the given type. + * + * @param opts - Options bag. + * @param opts.type - The type to check for. + * @returns `true` if a matching approval is found, and `false` otherwise. + */ + has(opts: { + type: string; + }): boolean; + /** + * Checks if there's any pending approval request with the given origin and + * type. + * + * @param opts - Options bag. + * @param opts.origin - The origin to check for. + * @param opts.type - The type to check for. + * @returns `true` if a matching approval is found, and `false` otherwise. + */ + has(opts: { + origin: string; + type: string; + }): boolean; + /** + * Resolves the promise of the approval with the given id, and deletes the + * approval. Throws an error if no such approval exists. + * + * @param id - The id of the approval request. + * @param value - The value to resolve the approval promise with. + */ + accept(id: string, value?: unknown): void; + /** + * Rejects the promise of the approval with the given id, and deletes the + * approval. Throws an error if no such approval exists. + * + * @param id - The id of the approval request. + * @param error - The error to reject the approval promise with. + */ + reject(id: string, error: Error): void; + /** + * Rejects and deletes all approval requests. + */ + clear(): void; + /** + * Implementation of add operation. + * + * @param origin - The origin of the approval request. + * @param type - The type associated with the approval request. + * @param id - The id of the approval request. + * @param requestData - The request data associated with the approval request. + * @returns The approval promise. + */ + private _add; + /** + * Validates parameters to the add method. + * + * @param id - The id of the approval request. + * @param origin - The origin of the approval request. + * @param type - The type associated with the approval request. + * @param requestData - The request data associated with the approval request. + */ + private _validateAddParams; + /** + * Adds an entry to _origins. + * Performs no validation. + * + * @param origin - The origin of the approval request. + * @param type - The type associated with the approval request. + */ + private _addPendingApprovalOrigin; + /** + * Adds an entry to the store. + * Performs no validation. + * + * @param id - The id of the approval request. + * @param origin - The origin of the approval request. + * @param type - The type associated with the approval request. + * @param requestData - The request data associated with the approval request. + */ + private _addToStore; + /** + * Deletes the approval with the given id. The approval promise must be + * resolved or reject before this method is called. + * Deletion is an internal operation because approval state is solely + * managed by this controller. + * + * @param id - The id of the approval request to be deleted. + */ + private _delete; + /** + * Gets the approval callbacks for the given id, deletes the entry, and then + * returns the callbacks for promise resolution. + * Throws an error if no approval is found for the given id. + * + * @param id - The id of the approval request. + * @returns The promise callbacks associated with the approval request. + */ + private _deleteApprovalAndGetCallbacks; + /** + * Checks whether there are any approvals associated with the given + * origin. + * + * @param origin - The origin to check. + * @returns True if the origin has no approvals, false otherwise. + */ + private _isEmptyOrigin; +} +export default ApprovalController; diff --git a/packages/controllers/src/permissions/temp/BaseControllerV2.d.ts b/packages/controllers/src/permissions/temp/BaseControllerV2.d.ts new file mode 100644 index 0000000000..85ad61591f --- /dev/null +++ b/packages/controllers/src/permissions/temp/BaseControllerV2.d.ts @@ -0,0 +1,185 @@ +import type { Draft, Patch } from 'immer'; +import type { RestrictedControllerMessenger } from '@metamask/controllers'; +/** + * A state change listener. + * + * This function will get called for each state change, and is given a copy of + * the new state along with a set of patches describing the changes since the + * last update. + * + * @param state - The new controller state + * @param patches - A list of patches describing any changes (see here for more + * information: https://immerjs.github.io/immer/docs/patches) + */ +export declare type Listener = (state: T, patches: Patch[]) => void; +declare type primitive = null | boolean | number | string; +declare type DefinitelyNotJsonable = ((...args: any[]) => any) | undefined; +export declare type IsJsonable = [ + Extract, +] extends [never] + ? T extends primitive + ? T + : T extends (infer U)[] + ? IsJsonable[] + : T extends object + ? { + [P in keyof T]: P extends string ? IsJsonable : never; + } + : never + : never; +/** + * An function to derive state. + * + * This function will accept one piece of the controller state (one property), + * and will return some derivation of that state. + * + * @param value - A piece of controller state + * @returns Something derived from controller state + */ +export declare type StateDeriver = ( + value: IsJsonable, +) => IsJsonable; +/** + * State metadata. + * + * This metadata describes which parts of state should be persisted, and how to + * get an anonymized representation of the state. + */ +export declare type StateMetadata = { + [P in keyof T]: StatePropertyMetadata; +}; +/** + * Metadata for a single state property + * + * @property persist - Indicates whether this property should be persisted + * (`true` for persistent, `false` for transient), or is set to a function + * that derives the persistent state from the state. + * @property anonymous - Indicates whether this property is already anonymous, + * (`true` for anonymous, `false` if it has potential to be personally + * identifiable), or is set to a function that returns an anonymized + * representation of this state. + */ +export type StatePropertyMetadata = { + persist: boolean | StateDeriver; + anonymous: boolean | StateDeriver; +} +export declare type Json = + | null + | boolean + | number + | string + | Json[] + | { + [prop: string]: Json; + }; +/** + * Controller class that provides state management, subscriptions, and state metadata + */ +export declare class BaseController< + N extends string, + S extends Record, + messenger extends RestrictedControllerMessenger, +> { + private internalState; + + protected messagingSystem: messenger; + + /** + * The name of the controller. + * + * This is used by the ComposableController to construct a composed application state. + */ + readonly name: N; + + readonly metadata: StateMetadata; + + /** + * The existence of the `subscribe` property is how the ComposableController detects whether a + * controller extends the old BaseController or the new one. We set it to `never` here to ensure + * this property is never used for new BaseController-based controllers, to ensure the + * ComposableController never mistakes them for an older style controller. + */ + readonly subscribe: never; + + /** + * Creates a BaseController instance. + * + * @param options + * @param options.messenger - Controller messaging system + * @param options.metadata - State metadata, describing how to "anonymize" the state, and which + * parts should be persisted. + * @param options.name - The name of the controller, used as a namespace for events and actions + * @param options.state - Initial controller state + */ + constructor({ + messenger, + metadata, + name, + state, + }: { + messenger: messenger; + metadata: StateMetadata; + name: N; + state: IsJsonable; + }); + + /** + * Retrieves current controller state + * + * @returns - Current state + */ + get state(): IsJsonable; + + set state(_: IsJsonable); + + /** + * Updates controller state. Accepts a callback that is passed a draft copy + * of the controller state. If a value is returned, it is set as the new + * state. Otherwise, any changes made within that callback to the draft are + * applied to the controller state. + * + * @param callback - Callback for updating state, passed a draft state + * object. Return a new state object or mutate the draft to update state. + */ + protected update( + callback: (state: Draft>) => void | IsJsonable, + ): void; + + /** + * Prepares the controller for garbage collection. This should be extended + * by any subclasses to clean up any additional connections or events. + * + * The only cleanup performed here is to remove listeners. While technically + * this is not required to ensure this instance is garbage collected, it at + * least ensures this instance won't be responsible for preventing the + * listeners from being garbage collected. + */ + protected destroy(): void; +} +/** + * Returns an anonymized representation of the controller state. + * + * By "anonymized" we mean that it should not contain any information that could be personally + * identifiable. + * + * @param state - The controller state + * @param metadata - The controller state metadata, which describes how to derive the + * anonymized state + * @returns The anonymized controller state + */ +export declare function getAnonymizedState>( + state: IsJsonable, + metadata: StateMetadata, +): IsJsonable>; +/** + * Returns the subset of state that should be persisted + * + * @param state - The controller state + * @param metadata - The controller state metadata, which describes which pieces of state should be persisted + * @returns The subset of controller state that should be persisted + */ +export declare function getPersistentState>( + state: IsJsonable, + metadata: StateMetadata, +): IsJsonable>; +export {}; From 3849fc14975617a18cd08b37bcd40a37e81bf8d1 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 23 Aug 2021 13:37:35 -0700 Subject: [PATCH 027/147] Fix requestPermissions return value --- .../src/permissions/rpc-methods/requestPermissions.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts b/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts index 34bdd8a648..e9b50f2e70 100644 --- a/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts +++ b/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts @@ -52,7 +52,9 @@ async function requestPermissionsImplementation( typeof req.id === 'number' || req.id ? req.id.toString() : nanoid(); try { - res.result = Object.values(requestPermissions(req.params[0], id)); + res.result = Object.values( + (await requestPermissions(req.params[0], id))[0], + ); return end(); } catch (error) { return end(error); From 7e00e61911adaff773ae36e0cca17eea89860eb2 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 23 Aug 2021 16:01:49 -0700 Subject: [PATCH 028/147] Add permission specifications and accept/reject hooks Adds permission specifications, in the vein of caveat specifications, to allow consumer-specified permission factory and validator functions. Now that we have those, we also call caveat and permission validators (if they exist) whenever a caveat and therefore permission is mutated, or when either is instantiated. Also adds methos for accepting and rejecting permission requests to the permission enforcer. --- .../controllers/src/permissions/Caveat.ts | 117 +++++- .../controllers/src/permissions/Permission.ts | 86 ++++- .../src/permissions/PermissionController.ts | 349 ++++++++---------- .../src/permissions/PermissionEnforcer.ts | 275 ++++++++++---- .../src/permissions/caveat-decoration.ts | 83 ----- .../controllers/src/permissions/errors.ts | 12 +- packages/controllers/src/utils.ts | 5 + 7 files changed, 572 insertions(+), 355 deletions(-) delete mode 100644 packages/controllers/src/permissions/caveat-decoration.ts create mode 100644 packages/controllers/src/utils.ts diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index b73538e57f..fd047b623b 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -1,8 +1,17 @@ import { Json } from 'json-rpc-engine'; -type CaveatOptions = { - type: string; - value: ValueType; +import { UnrecognizedCaveatTypeError } from './errors'; +import { + Permission, + AsyncRestrictedMethodImplementation, + RestrictedMethodImplementation, +} from './Permission'; + +type CaveatType = string; + +type CaveatOptions = { + type: CaveatType; + value: Value; }; export type ZcapLdCaveat = { @@ -10,7 +19,7 @@ export type ZcapLdCaveat = { * The type of the caveat, which is presumed to be meaningful in the context * of the capability it is associated with. */ - type: string; + type: CaveatType; }; /** @@ -31,7 +40,7 @@ export class Caveat implements CaveatInterface { * * In MetaMask, every permission can only have one caveat of each type. */ - public readonly type: string; + public readonly type: CaveatType; /** * Any additional data necessary to enforce the caveat. @@ -45,3 +54,101 @@ export class Caveat implements CaveatInterface { this.value = value; } } + +// Next, we define types used for specifying caveats at the consumer layer, +// and a function for applying caveats to a restricted method request. This is +// Accomplished by decorating the restricted method implementation with the +// the corresponding caveat functions. + +type CaveatDecorator = ( + decorated: AsyncRestrictedMethodImplementation, + caveat: Caveat, +) => AsyncRestrictedMethodImplementation; + +type CaveatValidator = ( + origin: string, + target: string, + caveat: Caveat, +) => void; + +export type CaveatSpecification = { + /** + * The string type of the caveat. + */ + type: CaveatType; + + /** + * The decorator function used to apply the caveat to restricted method + * requests. + */ + decorator: CaveatDecorator; + + /** + * The validator function used to validate caveats of the associated type + * whenever they are instantiated. Caveats are instantiated whenever they are + * created or mutated. + * + * The validator should throw an appropriate JSON-RPC error if validation fails. + * + * If no validator is specified, no validation of caveat values will be + * performed. Although caveats can also be validated by permission validators, + * validating caveat values separately is strongly recommended. + */ + validator?: CaveatValidator; +}; + +export type CaveatSpecifications = Readonly< + Record +>; + +/** + * Decorate a restricted method implementation with its caveats. + * + * TODO: Document how to create caveats using below examples. + * Note that all caveat functions (i.e. the argument and return value of the decorator) + * must be awaited. + */ +export function decorateWithCaveats( + methodImplementation: RestrictedMethodImplementation, + permission: Readonly, // bound to the requesting origin + caveatSpecifications: CaveatSpecifications, // all caveat implementations +): RestrictedMethodImplementation { + const caveats = permission.caveats ?? []; + + let decorated = methodImplementation as AsyncRestrictedMethodImplementation< + Json, + Json + >; + for (const caveat of caveats) { + const specification = caveatSpecifications[caveat.type]; + if (!specification) { + throw new UnrecognizedCaveatTypeError(caveat.type); + } + + decorated = specification.decorator(decorated, caveat); + } + return decorated; +} + +// Return next example. +// +// export function ifMetaMask(next: MethodImplementation) { +// return async (req: JsonRpcRequest, context: Record) => { +// if (context.origin !== 'metamask.io') { +// return new EthereumJsonRpcError(); +// } +// +// return next(req, context); +// }; +// } + +// "Return handler" example +// +// export function eth_accounts(next: MethodImplementation) { +// return async (req: JsonRpcRequest, context: Record) => { +// const accounts = await next(req, context); +// return accounts.filter(() => { +// // filter +// }); +// }; +// } diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index ba5c9a252f..be02876a49 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -1,3 +1,4 @@ +import type { EthereumRpcError } from 'eth-rpc-errors'; import { Json } from 'json-rpc-engine'; import { nanoid } from 'nanoid'; import { Caveat, CaveatInterface, ZcapLdCaveat } from './Caveat'; @@ -128,7 +129,7 @@ export class Permission implements ZcapLdCapability { public readonly invoker: OriginString; constructor(options: PermissionOptions) { - const { id, target, invoker, caveats } = options; + const { caveats, id, invoker, target } = options; this.id = id ?? nanoid(); this.parentCapability = target; this.invoker = invoker; @@ -158,3 +159,86 @@ type RequestedPermission = { }; export type RequestedPermissions = Record; + +export type RestrictedMethodContext = Readonly<{ + origin: OriginString; + [key: string]: any; +}>; + +export type RestrictedMethodRequest = { + method: string; + params?: Params; + [key: string]: any; +}; + +export type SyncRestrictedMethodImplementation< + Params extends Json, + Result extends Json, +> = ( + request: RestrictedMethodRequest, + context: RestrictedMethodContext, +) => Result | Error | EthereumRpcError; + +export type AsyncRestrictedMethodImplementation< + Params extends Json, + Result extends Json, +> = ( + request: RestrictedMethodRequest, + context: RestrictedMethodContext, +) => Promise>; + +export type RestrictedMethodImplementation< + Params extends Json, + Result extends Json, +> = + | SyncRestrictedMethodImplementation + | AsyncRestrictedMethodImplementation; + +export type PermissionSpecification< + FactoryOptions extends PermissionOptions, + RequestData extends Record, + MethodImplementation extends RestrictedMethodImplementation, +> = { + /** + * The target resource of the permission. In other words, at the time of + * writing, the RPC method name. + */ + target: string; + + /** + * The factory function used to get permission objects. + * + * If no factory is specified, the {@link Permission} constructor will be + * used. + * + * The validator function will be called on newly created permissions whether + * a factory is specified or not. + */ + factory?: (options: FactoryOptions, requestData?: RequestData) => Permission; + + /** + * The implementation of the restricted method that the permission + * corresponds to. + */ + methodImplementation: MethodImplementation; + + /** + * The validator function used to validate permissions of the associated type + * whenever they are instantiated. Permissions are instantiated whenever they + * are created or mutated. The only way a permission can be legally mutated is + * when its caveat array or its contents are modified by the permission + * controller. + * + * The validator should throw an appropriate JSON-RPC error if validation fails. + */ + validator?: (permission: Permission) => void; +}; + +export type PermissionSpecifications = Record< + MethodName, + PermissionSpecification< + PermissionOptions, + Record, + RestrictedMethodImplementation + > +>; diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 87ed4f1065..253d7dc9b9 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -1,18 +1,20 @@ -import type { Patch } from 'immer'; -import deepFreeze from 'deep-freeze-strict'; import { BaseControllerV2 as BaseController, RestrictedControllerMessenger, } from '@metamask/controllers'; +import type { Patch } from 'immer'; +import deepFreeze from 'deep-freeze-strict'; import { Json } from 'json-rpc-engine'; -import { EthereumRpcError } from 'eth-rpc-errors'; -import { CaveatSpecifications } from './caveat-decoration'; +import { isPlainObject } from '../utils'; +import { Caveat, CaveatSpecifications } from './Caveat'; import { OriginString, Permission, MethodName, RequestedPermissions, + RestrictedMethodImplementation, + PermissionSpecifications, } from './Permission'; import { PermissionDoesNotExistError, @@ -26,8 +28,8 @@ import { InvalidSubjectIdentifierError, InvalidCaveatTypeError, CaveatAlreadyExistsError, + InvalidCaveatError, } from './errors'; -import { Caveat } from './Caveat'; export type SubjectMetadata = { origin: OriginString; @@ -47,6 +49,7 @@ export type PermissionControllerSubjects = Record< >; // TODO: TypeScript doesn't understand that a given subject may not exist. Can we fix it? +// Yes we can, in TS4.4 export type PermissionControllerState = { subjects: PermissionControllerSubjects; }; @@ -94,54 +97,22 @@ export type PermissionControllerMessenger = RestrictedControllerMessenger< never >; -export type RestrictedMethodContext = Readonly<{ - origin: OriginString; - [key: string]: any; -}>; - -export type RestrictedMethodRequest = { - method: string; - params?: Params; - [key: string]: any; -}; - -export type SyncRestrictedMethodImplementation< - Params extends Json, - Result extends Json, -> = ( - request: RestrictedMethodRequest, - context: RestrictedMethodContext, -) => Result | Error | EthereumRpcError; - -export type AsyncRestrictedMethodImplementation< - Params extends Json, - Result extends Json, -> = ( - request: RestrictedMethodRequest, - context: RestrictedMethodContext, -) => Promise>; - -export type RestrictedMethodImplementation< - Params extends Json, - Result extends Json, -> = - | SyncRestrictedMethodImplementation - | AsyncRestrictedMethodImplementation; - -type RestrictedMethodsOption = Record< - MethodName, - RestrictedMethodImplementation ->; - type PermissionControllerOptions = { messenger: PermissionControllerMessenger; caveatSpecifications: CaveatSpecifications; methodPrefix: string; - restrictedMethods: RestrictedMethodsOption; + permissionSpecifications: PermissionSpecifications; safeMethods: string[]; state?: Partial; }; +type GrantPermissionsOptions = { + approvedPermissions: RequestedPermissions; + subject: SubjectMetadata; + shouldPreserveExistingPermissions?: boolean; + requestData?: Record; +}; + export class PermissionController extends BaseController< typeof controllerName, PermissionControllerState @@ -150,13 +121,11 @@ export class PermissionController extends BaseController< protected messagingSystem: PermissionControllerMessenger; - /** - * Can be namespaced. - */ - private readonly _restrictedMethods: ReadonlyMap< - string, - RestrictedMethodImplementation - >; + private readonly _permissionSpecifications: Readonly; + + public get permissionSpecifications(): Readonly { + return this._permissionSpecifications; + } private readonly _safeMethods: ReadonlySet; @@ -175,7 +144,7 @@ export class PermissionController extends BaseController< state = {}, caveatSpecifications, methodPrefix, - restrictedMethods, + permissionSpecifications, safeMethods, }: PermissionControllerOptions) { super({ @@ -186,7 +155,9 @@ export class PermissionController extends BaseController< }); this._caveatSpecifications = deepFreeze({ ...caveatSpecifications }); - this._restrictedMethods = getRestrictedMethodMap(restrictedMethods); + this._permissionSpecifications = deepFreeze({ + ...permissionSpecifications, + }); this._safeMethods = new Set(safeMethods); this.methodPrefix = methodPrefix; @@ -194,14 +165,14 @@ export class PermissionController extends BaseController< // assigned if we don't do it. this.messagingSystem = messenger; - this._registerMessageHandlers(); + this.registerMessageHandlers(); } /** * Constructor helper for registering this controller's messaging system * actions. */ - private _registerMessageHandlers(): void { + private registerMessageHandlers(): void { this.messagingSystem.registerActionHandler( `${controllerName}:getSubjects`, () => this.getSubjects(), @@ -247,15 +218,11 @@ export class PermissionController extends BaseController< return Boolean(this.state.subjects[origin]); } - setPermission(origin: string, permission: Permission): void { - this.update((draftState) => { - if (!draftState.subjects[origin]) { - draftState.subjects[origin] = { origin, permissions: {} }; - } - const { parentCapability: target } = permission; - // Typecast: ts(2589) - draftState.subjects[origin].permissions[target] = permission as any; - }); + private validatePermission(permission: Permission): void { + const specification = this.getPermissionSpecification( + this.getTargetKey(permission.parentCapability), + ); + specification.validator?.(permission); } /** @@ -263,18 +230,27 @@ export class PermissionController extends BaseController< * permissions (same subject and method). Other existing permissions * remain unaffected. * - * @param {string} origin - The origin of the grantee subject. - * @param {Array} newPermissions - The unique, new permissions for the grantee subject. + * **Note: Assumes that the new permissions have been validated.** + * + * @param origin - The origin of the grantee subject. + * @param permissions - The new permissions for the grantee subject. */ - setPermissions( + private setValidatedPermissions( origin: string, permissions: Record, + shouldPreserveExistingPermissions = false, ): void { this.update((draftState) => { if (!draftState.subjects[origin]) { draftState.subjects[origin] = { origin, permissions: {} }; } - Object.assign(draftState.subjects[origin].permissions, permissions); + + if (shouldPreserveExistingPermissions) { + Object.assign(draftState.subjects[origin].permissions, permissions); + } else { + // Typecast: ts(2589) + draftState.subjects[origin].permissions = permissions as any; + } }); } @@ -290,7 +266,7 @@ export class PermissionController extends BaseController< } // Typecast: ts(2589) - this._deletePermission( + this.deletePermission( draftState as any, permissions as any, origin, @@ -310,7 +286,7 @@ export class PermissionController extends BaseController< // Typecast: ts(2589) if (permissions[target]) { - this._deletePermission( + this.deletePermission( draftState as any, permissions as any, origin, @@ -321,7 +297,7 @@ export class PermissionController extends BaseController< }); } - private _deletePermission( + private deletePermission( draftState: PermissionControllerState, permissions: SubjectPermissions, origin: string, @@ -357,15 +333,11 @@ export class PermissionController extends BaseController< caveatType: string, caveatValue: Json, ): void { - if (this._getCaveat(origin, target, caveatType)) { + if (this.hasCaveat(origin, target, caveatType)) { throw new CaveatAlreadyExistsError(origin, target, caveatType); } - this._setCaveat( - origin, - target, - new Caveat({ type: caveatType, value: caveatValue }), - ); + this.setCaveat(origin, target, caveatType, caveatValue); } updateCaveat( @@ -374,18 +346,14 @@ export class PermissionController extends BaseController< caveatType: string, caveatValue: Json, ): void { - if (!this._getCaveat(origin, target, caveatType)) { + if (!this.hasCaveat(origin, target, caveatType)) { throw new CaveatDoesNotExistError(origin, target, caveatType); } - this._setCaveat( - origin, - target, - new Caveat({ type: caveatType, value: caveatValue }), - ); + this.setCaveat(origin, target, caveatType, caveatValue); } - private _getCaveat( + private getCaveat( origin: string, target: string, caveatType: string, @@ -398,18 +366,24 @@ export class PermissionController extends BaseController< return Permission.getCaveat(permission, caveatType); } - private _setCaveat( + private setCaveat( origin: string, target: string, - caveat: Caveat, + caveatType: string, + caveatValue: Json, ): void { this.update((draftState) => { const permission = draftState.subjects[origin]?.permissions[target]; - /* istanbul ignore if */ // This should be impossible + /* istanbul ignore if: This should be impossible, but TypeScript wants it */ if (!permission) { throw new PermissionDoesNotExistError(origin, target); } + const caveat = this.computeCaveat(origin, target, { + type: caveatType, + value: caveatValue, + }); + // Typecasts: ts(2589) if (permission.caveats) { const caveatIndex = permission.caveats.findIndex( @@ -424,6 +398,9 @@ export class PermissionController extends BaseController< } else { permission.caveats = [caveat as any]; } + + // Typecast: ts(2589) + this.validatePermission(permission as any); }); } @@ -450,6 +427,9 @@ export class PermissionController extends BaseController< } else { permission.caveats.splice(caveatIndex, 1); } + + // Typecast: ts(2589) + this.validatePermission(permission as any); }); } @@ -463,14 +443,14 @@ export class PermissionController extends BaseController< * @param method string - The requested RPC method. * @returns The internal key of the method. */ - getMethodKeyFor(method: string): string { - if (this._restrictedMethods.has(method)) { + getTargetKey(method: string): string { + if (method in this._permissionSpecifications) { return method; } const wildCardMethodsWithoutWildCard: Record = {}; - for (const methodName of this._restrictedMethods.keys()) { - const wildCardMatch = methodName.match(/(.+)\*$/u); + for (const targetKey of Object.keys(this._permissionSpecifications)) { + const wildCardMatch = targetKey.match(/(.+)\*$/u); if (wildCardMatch) { wildCardMethodsWithoutWildCard[wildCardMatch[1]] = true; } @@ -480,20 +460,20 @@ export class PermissionController extends BaseController< // Ex: wildzone_ // Ex: eth_plugin_ const segments = method.split('_'); - let methodKey = ''; + let targetKey = ''; while ( segments.length > 0 && - !this._restrictedMethods.has(methodKey) && - !wildCardMethodsWithoutWildCard[methodKey] + !(targetKey in this._permissionSpecifications) && + !wildCardMethodsWithoutWildCard[targetKey] ) { - methodKey += `${segments.shift()}_`; + targetKey += `${segments.shift()}_`; } - if (this._restrictedMethods.has(methodKey)) { - return methodKey; - } else if (wildCardMethodsWithoutWildCard[methodKey]) { - return `${methodKey}*`; + if (targetKey in this._permissionSpecifications) { + return targetKey; + } else if (wildCardMethodsWithoutWildCard[targetKey]) { + return `${targetKey}*`; } return ''; @@ -502,55 +482,70 @@ export class PermissionController extends BaseController< getRestrictedMethodImplementation( method: string, ): RestrictedMethodImplementation | undefined { - const methodKey = this.getMethodKeyFor(method); - if (!methodKey) { + const targetKey = this.getTargetKey(method); + if (!targetKey) { return undefined; } - const methodImplementation = this._restrictedMethods.get(methodKey); - /* istanbul ignore if */ // This should be impossible - if (!methodImplementation) { + return this.getPermissionSpecification(method).methodImplementation; + } + + private getPermissionSpecification(targetKey: string) { + const specification = this._permissionSpecifications[targetKey]; + /* istanbul ignore if: This should be impossible, but TypeScript wants it */ + if (!specification) { throw new Error( - `Method "${method}" with method key "${methodKey}" has no implementation.`, + `Existing permission target key "${targetKey}" has no specification.`, ); } - return methodImplementation; + return specification; } - grantPermissions( - subject: SubjectMetadata, - requestedPermissions: RequestedPermissions, - shouldPreserveExistingPermissions = true, - ): Record { + grantPermissions(opts: GrantPermissionsOptions): SubjectPermissions { + const { + requestData, + approvedPermissions, + shouldPreserveExistingPermissions = false, + subject, + } = opts; const { origin } = subject; + if (!origin || typeof origin !== 'string') { throw new InvalidSubjectIdentifierError(origin); } - // Enforce actual approving known methods: - for (const methodName in requestedPermissions) { - if (!this.getMethodKeyFor(methodName)) { - throw methodNotFound({ method: methodName }); + const permissions: { [methodName: string]: Permission } = + (shouldPreserveExistingPermissions && { + ...this.getPermissions(origin), + }) || + {}; + + for (const target of Object.keys(approvedPermissions)) { + const targetKey = this.getTargetKey(target); + if (!targetKey) { + throw methodNotFound({ method: target }); } - } - const permissions: { [methodName: string]: Permission } = - (shouldPreserveExistingPermissions && this.getPermissions(origin)) || {}; - - for (const method of Object.keys(requestedPermissions)) { - permissions[method] = new Permission({ - target: method, - invoker: origin, - caveats: this.computeCaveats( - origin, - method, - requestedPermissions[method].caveats || undefined, - ), - }); + const specification = this.getPermissionSpecification(targetKey); + + const caveats = this.computeCaveats( + origin, + target, + approvedPermissions[target].caveats || undefined, + ); + + const permissionOptions = { caveats, invoker: origin, target }; + + const permission = + specification.factory?.(permissionOptions, requestData) || + new Permission(permissionOptions); + + specification.validator?.(permission); + permissions[targetKey] = permission; } - this.setPermissions(origin, permissions); + this.setValidatedPermissions(origin, permissions); return permissions; } @@ -558,69 +553,53 @@ export class PermissionController extends BaseController< * @param caveats The caveats to validate. * @returns Whether the given caveats are valid. */ - computeCaveats( + private computeCaveats( origin: string, target: string, caveats?: Caveat[], ): Caveat[] | undefined { - const caveatArray = caveats?.map((caveat) => { - if (Object.keys(caveat).length !== 2) { - throw new InvalidCaveatFieldsError(caveat, origin, target); - } - - if (caveat.type !== 'string') { - throw new InvalidCaveatTypeError(caveat.type); - } - - const specification = this.caveatSpecifications[caveat.type]; - if (!specification) { - throw new CaveatTypeDoesNotExistError(caveat.type, origin, target); - } - - if (!('value' in caveat)) { - throw new CaveatMissingValueError(caveat, origin, target); - } - // Should throw an error if validation fails. - specification.validator(caveat.value, origin, target); - - return new Caveat({ type: caveat.type, value: caveat.value }); - }); + const caveatArray = caveats?.map((requestedCaveat) => + this.computeCaveat(origin, target, requestedCaveat), + ); return caveatArray && caveatArray.length > 0 ? caveatArray : undefined; } -} -function getRestrictedMethodMap( - restrictedMethods: RestrictedMethodsOption, -): ReadonlyMap> { - return Object.entries(restrictedMethods).reduce( - (methodMap, [methodName, implementation]) => { - if (!methodName || typeof methodName !== 'string') { - throw new Error(`Invalid method name: ${methodName}`); - } + private computeCaveat( + origin: string, + target: string, + requestedCaveat: Caveat, + ): Caveat { + if (!isPlainObject(requestedCaveat)) { + throw new InvalidCaveatError(requestedCaveat); + } - methodMap.set(methodName, implementation); - return methodMap; - }, - new Map(), - ); -} + if (Object.keys(requestedCaveat).length !== 2) { + throw new InvalidCaveatFieldsError(requestedCaveat, origin, target); + } -// TODO: Are we using these? + if (requestedCaveat.type !== 'string') { + throw new InvalidCaveatTypeError(requestedCaveat.type); + } -export type PermissionsRequestMetadata = SubjectMetadata & { - id: string; -}; + const specification = this.caveatSpecifications[requestedCaveat.type]; + if (!specification) { + throw new CaveatTypeDoesNotExistError( + requestedCaveat.type, + origin, + target, + ); + } -/** - * Used for prompting the user about a proposed new permission. - * Includes information about the subject granted, as well as the permissions - * assigned. - */ -export type PermissionsRequest = { - metadata: PermissionsRequestMetadata; - permissions: RequestedPermissions; -}; + if (!('value' in requestedCaveat)) { + throw new CaveatMissingValueError(requestedCaveat, origin, target); + } + + const caveat = new Caveat({ + type: requestedCaveat.type, + value: requestedCaveat.value, + }); -export type UserApprovalPrompt = ( - permissionsRequest: PermissionsRequest, -) => Promise; + specification.validator?.(origin, target, caveat); + return caveat; + } +} diff --git a/packages/controllers/src/permissions/PermissionEnforcer.ts b/packages/controllers/src/permissions/PermissionEnforcer.ts index 9ad7e9ef33..a46dfc32b4 100644 --- a/packages/controllers/src/permissions/PermissionEnforcer.ts +++ b/packages/controllers/src/permissions/PermissionEnforcer.ts @@ -7,27 +7,44 @@ import { JsonRpcMiddleware, PendingJsonRpcResponse, } from 'json-rpc-engine'; -import { CaveatSpecifications, decorateWithCaveats } from './caveat-decoration'; +import { CaveatSpecifications, decorateWithCaveats } from './Caveat'; import { internalError, invalidParams, methodNotFound, unauthorized, + userRejectedRequest, } from './errors'; -import type { Permission, RequestedPermissions } from './Permission'; import type { - PermissionController, - PermissionsRequest, + Permission, + RequestedPermissions, RestrictedMethodImplementation, +} from './Permission'; +import type { + PermissionController, SubjectMetadata, } from './PermissionController'; import type { ApprovalController } from './temp/ApprovalController'; +export type PermissionsRequestMetadata = SubjectMetadata & { + id: string; +}; + +/** + * Used for prompting the user about a proposed new permission. + * Includes information about the subject granted, as well as the permissions + * assigned. + */ +export type PermissionsRequest = { + metadata: PermissionsRequestMetadata; + permissions: RequestedPermissions; +}; + type IsRestrictedMethod = (method: string) => boolean; type IsSafeMethod = (method: string) => boolean; type RequestUserApproval = ( permissionsRequest: PermissionsRequest, -) => Promise; +) => Promise; type PermissionEnforcerArgs = { caveatSpecifications: CaveatSpecifications; @@ -37,6 +54,9 @@ type PermissionEnforcerArgs = { getRestrictedMethodImplementation: PermissionController['getRestrictedMethodImplementation']; grantPermissions: PermissionController['grantPermissions']; requestUserApproval: RequestUserApproval; + acceptPermissionsRequest: ApprovalController['accept']; + rejectPermissionsRequest: ApprovalController['reject']; + hasApprovalRequest: ApprovalController['has']; }; class PermissionEnforcer { @@ -52,6 +72,12 @@ class PermissionEnforcer { private requestUserApproval: RequestUserApproval; + private _acceptPermissionsRequest: ApprovalController['accept']; + + private _rejectPermissionsRequest: ApprovalController['reject']; + + private _hasApprovalRequest: ApprovalController['has']; + private _getRestrictedMethodImplementation: PermissionController['getRestrictedMethodImplementation']; constructor({ @@ -62,6 +88,9 @@ class PermissionEnforcer { getRestrictedMethodImplementation, grantPermissions, requestUserApproval, + acceptPermissionsRequest, + rejectPermissionsRequest, + hasApprovalRequest, }: PermissionEnforcerArgs) { this.caveatSpecifications = caveatSpecifications; this.isRestrictedMethod = isRestrictedMethod; @@ -69,48 +98,33 @@ class PermissionEnforcer { this.getPermission = getPermission; this.grantPermissions = grantPermissions; this.requestUserApproval = requestUserApproval; + this._acceptPermissionsRequest = acceptPermissionsRequest; + this._rejectPermissionsRequest = rejectPermissionsRequest; + this._hasApprovalRequest = hasApprovalRequest; this._getRestrictedMethodImplementation = getRestrictedMethodImplementation; } - async executeRestrictedMethod( + public async requestPermissions( origin: string, - method: string, - params: Json, - ): Promise { - const id = nanoid(); - const req: JsonRpcRequest = { id, jsonrpc: '2.0', method }; - if (params !== undefined) { - req.params = params; - } - - const methodImplementation = this.getRestrictedMethodImplementation( - method, - req, - ); - - const result = await this._executeRestrictedMethod( - methodImplementation, - { origin }, - req, - ); - - if (resultIsError(result)) { - throw result; - } - - if (result === undefined) { - throw new Error( - `Internal request for method "${method}" as origin "${origin}" has no result.`, - ); - } + requestedPermissions: RequestedPermissions, + id: string = nanoid(), + ) { + return this._requestPermissions(origin, requestedPermissions, id, false); + } - return result; + public async requestAndPreservePermissions( + origin: string, + requestedPermissions: RequestedPermissions, + id: string = nanoid(), + ) { + return this._requestPermissions(origin, requestedPermissions, id, true); } - async requestPermissions( + private async _requestPermissions( origin: string, requestedPermissions: RequestedPermissions, id: string = nanoid(), + shouldPreserveExistingPermissions = false, ): Promise<[Record, { id: string; origin: string }]> { this.validateRequestedPermissions(origin, requestedPermissions); const metadata: PermissionsRequest['metadata'] = { @@ -123,15 +137,25 @@ class PermissionEnforcer { permissions: requestedPermissions, }; - const approved = await this.requestUserApproval(permissionsRequest); - if (Object.keys(approved).length === 0) { + const { permissions: approvedPermissions, metadata: approvedMetadata } = + await this.requestUserApproval(permissionsRequest); + if (Object.keys(approvedPermissions).length === 0) { + // The request should already have been rejected if this is the case. throw internalError( `Approved permissions request for origin "${origin}" contains no permissions.`, { requestedPermissions }, ); } - return [this.grantPermissions({ origin }, approved), metadata]; + return [ + this.grantPermissions({ + subject: { origin }, + approvedPermissions, + shouldPreserveExistingPermissions, + requestData: approvedMetadata, + }), + metadata, + ]; } private validateRequestedPermissions( @@ -165,51 +189,89 @@ class PermissionEnforcer { } } - getPermissionsMiddleware( - subject: SubjectMetadata, - ): JsonRpcMiddleware { - const permissionsMiddleware = async ( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - next: AsyncJsonRpcEngineNextCallback, - ): Promise => { - const { method } = req; + /** + * User approval callback. Resolves the Promise for the permissions request + * waited upon by rpc-cap, see requestUserApproval in _initializePermissions. + * The request will be rejected if finalizePermissionsRequest fails. + * Idempotent for a given request id. + * + * @param approved - The request object approved by the user + * @param accounts - The accounts to expose, if any + */ + async acceptPermissionsRequest(request: PermissionsRequest) { + const { id } = request.metadata; - // skip registered safe/passthrough methods. - if (this.isSafeMethod(method)) { - return next(); + if (!this._hasApprovalRequest({ id })) { + console.debug(`Permissions request with id '${id}' not found.`); + return; + } + + try { + if (Object.keys(request.permissions).length === 0) { + this._rejectPermissionsRequest( + id, + invalidParams({ + message: 'Must request at least one permission.', + }), + ); + } else { + this._acceptPermissionsRequest(id, request); } + } catch (err) { + // if finalization fails, reject the request + this._rejectPermissionsRequest(id, err); + } + } - // if the method also is not a restricted method, the method does not exist - const methodImplementation = this.getRestrictedMethodImplementation( - method, - req, - ); + /** + * User rejection callback. Rejects the Promise for the permissions request + * waited upon by rpc-cap, see requestUserApproval in _initializePermissions. + * Idempotent for a given id. + * + * @param id - The id of the request rejected by the user + */ + async rejectPermissionsRequest(id: string) { + if (!this._hasApprovalRequest({ id })) { + console.debug(`Permissions request with id '${id}' not found.`); + return; + } - const result = await this._executeRestrictedMethod( - methodImplementation, - subject, - req, - ); + this._rejectPermissionsRequest(id, userRejectedRequest()); + } - if (resultIsError(result)) { - res.error = result; - return undefined; - } + async executeRestrictedMethod( + origin: string, + method: string, + params: Json, + ): Promise { + const id = nanoid(); + const req: JsonRpcRequest = { id, jsonrpc: '2.0', method }; + if (params !== undefined) { + req.params = params; + } - if (result === undefined) { - res.error = internalError( - `Request for method "${req.method}" returned undefined result.`, - { request: req }, - ); - return undefined; - } + const methodImplementation = this.getRestrictedMethodImplementation( + method, + req, + ); - res.result = result; - return undefined; - }; + const result = await this._executeRestrictedMethod( + methodImplementation, + { origin }, + req, + ); - return createAsyncMiddleware(permissionsMiddleware); + if (resultIsError(result)) { + throw result; + } + + if (result === undefined) { + throw new Error( + `Internal request for method "${method}" as origin "${origin}" has no result.`, + ); + } + + return result; } /** @@ -261,6 +323,53 @@ class PermissionEnforcer { this.caveatSpecifications, )(req, { origin }); } + + getPermissionsMiddleware( + subject: SubjectMetadata, + ): JsonRpcMiddleware { + const permissionsMiddleware = async ( + req: JsonRpcRequest, + res: PendingJsonRpcResponse, + next: AsyncJsonRpcEngineNextCallback, + ): Promise => { + const { method } = req; + + // skip registered safe/passthrough methods. + if (this.isSafeMethod(method)) { + return next(); + } + + // if the method also is not a restricted method, the method does not exist + const methodImplementation = this.getRestrictedMethodImplementation( + method, + req, + ); + + const result = await this._executeRestrictedMethod( + methodImplementation, + subject, + req, + ); + + if (resultIsError(result)) { + res.error = result; + return undefined; + } + + if (result === undefined) { + res.error = internalError( + `Request for method "${req.method}" returned undefined result.`, + { request: req }, + ); + return undefined; + } + + res.result = result; + return undefined; + }; + + return createAsyncMiddleware(permissionsMiddleware); + } } export type { PermissionEnforcer }; @@ -268,11 +377,14 @@ export type { PermissionEnforcer }; export function getPermissionEnforcer( permissionController: PermissionController, addAndShowApprovalRequest: ApprovalController['addAndShowApprovalRequest'], + acceptPermissionsRequest: ApprovalController['accept'], + rejectPermissionsRequest: ApprovalController['reject'], + hasApprovalRequest: ApprovalController['has'], ): PermissionEnforcer { return new PermissionEnforcer({ caveatSpecifications: permissionController.caveatSpecifications, isRestrictedMethod: (method: string) => { - return Boolean(permissionController.getMethodKeyFor(method)); + return Boolean(permissionController.getTargetKey(method)); }, isSafeMethod: (method: string) => { return permissionController.safeMethods.has(method); @@ -291,9 +403,12 @@ export function getPermissionEnforcer( id, origin, requestData: permissionsRequest, - type: 'wallet_requestPermissions', // TODO: Establish enum somehow - })) as RequestedPermissions; + type: 'wallet_requestPermissions', // TODO: Define enum for this + })) as PermissionsRequest; }, + acceptPermissionsRequest, + rejectPermissionsRequest, + hasApprovalRequest, }); } diff --git a/packages/controllers/src/permissions/caveat-decoration.ts b/packages/controllers/src/permissions/caveat-decoration.ts deleted file mode 100644 index 508abac049..0000000000 --- a/packages/controllers/src/permissions/caveat-decoration.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { Json } from 'json-rpc-engine'; -import { Caveat } from './Caveat'; -import { UnrecognizedCaveatTypeError } from './errors'; -import { Permission } from './Permission'; -import { - AsyncRestrictedMethodImplementation, - RestrictedMethodImplementation, -} from './PermissionController'; - -type CaveatType = string; - -type CaveatValueValidator = ( - caveatValue: CaveatValue, - origin?: string, - target?: string, -) => void; - -type CaveatDecorator = ( - decorated: AsyncRestrictedMethodImplementation, - caveat: Caveat, -) => AsyncRestrictedMethodImplementation; - -export type CaveatSpecification = { - type: CaveatType; - decorator: CaveatDecorator; - validator: CaveatValueValidator; -}; - -export type CaveatSpecifications = Readonly< - Record> ->; - -/** - * Decorate a restricted method implementation with its caveats. - * - * TODO: Document how to create caveats using below examples. - * Note that all caveat functions (i.e. the argument and return value of the decorator) - * must be awaited. - */ -export function decorateWithCaveats( - methodImplementation: RestrictedMethodImplementation, - permission: Readonly, // bound to the requesting origin - caveatSpecifications: CaveatSpecifications, // all caveat implementations -): RestrictedMethodImplementation { - const caveats = permission.caveats ?? []; - - let decorated = methodImplementation as AsyncRestrictedMethodImplementation< - Json, - Json - >; - for (const caveat of caveats) { - const specification = caveatSpecifications[caveat.type]; - if (!specification) { - throw new UnrecognizedCaveatTypeError(caveat.type); - } - - decorated = specification.decorator(decorated, caveat); - } - return decorated; -} - -// Return next example. -// -// export function ifMetaMask(next: MethodImplementation) { -// return async (req: JsonRpcRequest, context: Record) => { -// if (context.origin !== 'metamask.io') { -// return new EthereumJsonRpcError(); -// } -// -// return next(req, context); -// }; -// } - -// "Return handler" example -// -// export function eth_accounts(next: MethodImplementation) { -// return async (req: JsonRpcRequest, context: Record) => { -// const accounts = await next(req, context); -// return accounts.filter(() => { -// // filter -// }); -// }; -// } diff --git a/packages/controllers/src/permissions/errors.ts b/packages/controllers/src/permissions/errors.ts index 118996b2a2..ee6dd54ab3 100644 --- a/packages/controllers/src/permissions/errors.ts +++ b/packages/controllers/src/permissions/errors.ts @@ -1,5 +1,5 @@ import { JsonRpcRequest } from 'json-rpc-engine'; -import { ethErrors, EthereumRpcError } from 'eth-rpc-errors'; +import { errorCodes, ethErrors, EthereumRpcError } from 'eth-rpc-errors'; import { Permission } from './Permission'; import { Caveat } from './Caveat'; @@ -104,6 +104,16 @@ export class InvalidPermissionJsonError extends Error { } } +export class InvalidCaveatError extends EthereumRpcError { + constructor(receivedCaveat: unknown) { + super( + errorCodes.rpc.invalidParams, + `Invalid caveat. Caveats must be plain objects.`, + { receivedCaveat }, + ); + } +} + export class InvalidCaveatTypeError extends Error { constructor(caveatType: string) { super(`Caveat types must be strings. Received: ${typeof caveatType}`); diff --git a/packages/controllers/src/utils.ts b/packages/controllers/src/utils.ts new file mode 100644 index 0000000000..adfefcb8ed --- /dev/null +++ b/packages/controllers/src/utils.ts @@ -0,0 +1,5 @@ +export function isPlainObject( + value: unknown, +): value is Record { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} From 48289f739636a4c9589dded9a5a715e8c8229c3d Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 23 Aug 2021 18:38:17 -0700 Subject: [PATCH 029/147] Remove some cruft --- packages/controllers/src/types/is-subset.d.ts | 7 ------- packages/controllers/tsconfig.json | 3 +-- 2 files changed, 1 insertion(+), 9 deletions(-) delete mode 100644 packages/controllers/src/types/is-subset.d.ts diff --git a/packages/controllers/src/types/is-subset.d.ts b/packages/controllers/src/types/is-subset.d.ts deleted file mode 100644 index 0e25940129..0000000000 --- a/packages/controllers/src/types/is-subset.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare module 'is-subset' { - export type intersectObjects = ( - arg1?: Record, - arg2?: Record, - ) => Record; - export default function isSubset(superset: unknown, subset: unknown): boolean; -} diff --git a/packages/controllers/tsconfig.json b/packages/controllers/tsconfig.json index f5ca7a155b..a4d4b216bc 100644 --- a/packages/controllers/tsconfig.json +++ b/packages/controllers/tsconfig.json @@ -7,8 +7,7 @@ "rootDir": "src", "typeRoots": [ "../../node_modules/@types", - "./node_modules/@types", - "./src/types" + "./node_modules/@types" ] }, "include": ["./src"], From b517440f7822cb34b1a0b5a3cd7f79b49cddc299 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 23 Aug 2021 18:34:59 -0700 Subject: [PATCH 030/147] Add enums, export controller, refactor internal exports Adds an index.ts in each controller subdirectory to simplify exports in main index.ts file. --- packages/controllers/src/index.ts | 8 ++++---- .../controllers/src/permissions/PermissionEnforcer.ts | 3 ++- packages/controllers/src/permissions/enums.ts | 4 ++++ packages/controllers/src/permissions/index.ts | 6 ++++++ .../src/permissions/rpc-methods/getPermissions.ts | 3 ++- .../src/permissions/rpc-methods/requestPermissions.ts | 3 ++- packages/controllers/src/plugins/index.ts | 1 + packages/controllers/src/resource/index.ts | 1 + packages/controllers/src/services/index.ts | 2 ++ 9 files changed, 24 insertions(+), 7 deletions(-) create mode 100644 packages/controllers/src/permissions/enums.ts create mode 100644 packages/controllers/src/permissions/index.ts create mode 100644 packages/controllers/src/plugins/index.ts create mode 100644 packages/controllers/src/resource/index.ts create mode 100644 packages/controllers/src/services/index.ts diff --git a/packages/controllers/src/index.ts b/packages/controllers/src/index.ts index 90595681f8..38a58b9756 100644 --- a/packages/controllers/src/index.ts +++ b/packages/controllers/src/index.ts @@ -1,4 +1,4 @@ -export * from './services/WebWorkerExecutionEnvironmentService'; -export * from './plugins/PluginController'; -export * from './resource/ExternalResourceController'; -export * from './services/ExecutionEnvironmentService'; +export * from './permissions'; +export * from './plugins'; +export * from './resource'; +export * from './services'; diff --git a/packages/controllers/src/permissions/PermissionEnforcer.ts b/packages/controllers/src/permissions/PermissionEnforcer.ts index a46dfc32b4..a0759d3e38 100644 --- a/packages/controllers/src/permissions/PermissionEnforcer.ts +++ b/packages/controllers/src/permissions/PermissionEnforcer.ts @@ -25,6 +25,7 @@ import type { SubjectMetadata, } from './PermissionController'; import type { ApprovalController } from './temp/ApprovalController'; +import { MethodNames } from './enums'; export type PermissionsRequestMetadata = SubjectMetadata & { id: string; @@ -403,7 +404,7 @@ export function getPermissionEnforcer( id, origin, requestData: permissionsRequest, - type: 'wallet_requestPermissions', // TODO: Define enum for this + type: MethodNames.requestPermissions, })) as PermissionsRequest; }, acceptPermissionsRequest, diff --git a/packages/controllers/src/permissions/enums.ts b/packages/controllers/src/permissions/enums.ts new file mode 100644 index 0000000000..41bc718318 --- /dev/null +++ b/packages/controllers/src/permissions/enums.ts @@ -0,0 +1,4 @@ +export enum MethodNames { + requestPermissions = 'wallet_requestPermissions', + getPermissions = 'wallet_getPermissions', +} diff --git a/packages/controllers/src/permissions/index.ts b/packages/controllers/src/permissions/index.ts new file mode 100644 index 0000000000..75700e9fee --- /dev/null +++ b/packages/controllers/src/permissions/index.ts @@ -0,0 +1,6 @@ +export * from './Caveat'; +export * from './enums'; +export * from './Permission'; +export * from './PermissionController'; +export * from './PermissionEnforcer'; +export * as rpcMethods from './rpc-methods'; diff --git a/packages/controllers/src/permissions/rpc-methods/getPermissions.ts b/packages/controllers/src/permissions/rpc-methods/getPermissions.ts index b216df62ea..fbe2c8bfbd 100644 --- a/packages/controllers/src/permissions/rpc-methods/getPermissions.ts +++ b/packages/controllers/src/permissions/rpc-methods/getPermissions.ts @@ -3,6 +3,7 @@ import type { JsonRpcEngineEndCallback, PendingJsonRpcResponse, } from 'json-rpc-engine'; +import { MethodNames } from '../enums'; import type { Permission } from '../Permission'; import type { SubjectPermissions } from '../PermissionController'; @@ -12,7 +13,7 @@ export const getPermissionsHandler: PermittedHandlerExport< void, Permission[] > = { - methodNames: ['wallet_getPermissions'], + methodNames: [MethodNames.getPermissions], implementation: getPermissionsImplementation, methodDescription: 'Get permissions.', hookNames: { diff --git a/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts b/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts index e9b50f2e70..bb6e6179a7 100644 --- a/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts +++ b/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts @@ -5,6 +5,7 @@ import type { PendingJsonRpcResponse, } from 'json-rpc-engine'; import { nanoid } from 'nanoid'; +import { MethodNames } from '../enums'; import { invalidParams } from '../errors'; import type { Permission, RequestedPermissions } from '../Permission'; @@ -14,7 +15,7 @@ export const requestPermissionsHandler: PermittedHandlerExport< [RequestedPermissions], Permission[] > = { - methodNames: ['wallet_requestPermissions'], + methodNames: [MethodNames.requestPermissions], implementation: requestPermissionsImplementation, methodDescription: 'Request permissions.', hookNames: { diff --git a/packages/controllers/src/plugins/index.ts b/packages/controllers/src/plugins/index.ts new file mode 100644 index 0000000000..bb09430e92 --- /dev/null +++ b/packages/controllers/src/plugins/index.ts @@ -0,0 +1 @@ +export * from './PluginController'; diff --git a/packages/controllers/src/resource/index.ts b/packages/controllers/src/resource/index.ts new file mode 100644 index 0000000000..533c38bd0a --- /dev/null +++ b/packages/controllers/src/resource/index.ts @@ -0,0 +1 @@ +export * from './ExternalResourceController'; diff --git a/packages/controllers/src/services/index.ts b/packages/controllers/src/services/index.ts new file mode 100644 index 0000000000..3a2505dda6 --- /dev/null +++ b/packages/controllers/src/services/index.ts @@ -0,0 +1,2 @@ +export * from './ExecutionEnvironmentService'; +export * from './WebWorkerExecutionEnvironmentService'; From 5fef679e331b7bb3e8c94b2179e0b15005f66a02 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 23 Aug 2021 23:54:14 -0700 Subject: [PATCH 031/147] Rename SubjectMetadata to PermissionsSubjectMetadata --- .../controllers/src/permissions/PermissionController.ts | 6 +++--- .../controllers/src/permissions/PermissionEnforcer.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 253d7dc9b9..838993d601 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -31,7 +31,7 @@ import { InvalidCaveatError, } from './errors'; -export type SubjectMetadata = { +export type PermissionsSubjectMetadata = { origin: OriginString; }; @@ -41,7 +41,7 @@ export type SubjectPermissions = Record; export type PermissionsSubjectEntry = { permissions: SubjectPermissions; -} & SubjectMetadata; +} & PermissionsSubjectMetadata; export type PermissionControllerSubjects = Record< OriginString, @@ -108,7 +108,7 @@ type PermissionControllerOptions = { type GrantPermissionsOptions = { approvedPermissions: RequestedPermissions; - subject: SubjectMetadata; + subject: PermissionsSubjectMetadata; shouldPreserveExistingPermissions?: boolean; requestData?: Record; }; diff --git a/packages/controllers/src/permissions/PermissionEnforcer.ts b/packages/controllers/src/permissions/PermissionEnforcer.ts index a0759d3e38..2ebb9e9dd1 100644 --- a/packages/controllers/src/permissions/PermissionEnforcer.ts +++ b/packages/controllers/src/permissions/PermissionEnforcer.ts @@ -22,12 +22,12 @@ import type { } from './Permission'; import type { PermissionController, - SubjectMetadata, + PermissionsSubjectMetadata, } from './PermissionController'; import type { ApprovalController } from './temp/ApprovalController'; import { MethodNames } from './enums'; -export type PermissionsRequestMetadata = SubjectMetadata & { +export type PermissionsRequestMetadata = PermissionsSubjectMetadata & { id: string; }; @@ -307,7 +307,7 @@ class PermissionEnforcer { */ private _executeRestrictedMethod( methodImplementation: RestrictedMethodImplementation, - subject: SubjectMetadata, + subject: PermissionsSubjectMetadata, req: JsonRpcRequest, ): ReturnType> { const { origin } = subject; @@ -326,7 +326,7 @@ class PermissionEnforcer { } getPermissionsMiddleware( - subject: SubjectMetadata, + subject: PermissionsSubjectMetadata, ): JsonRpcMiddleware { const permissionsMiddleware = async ( req: JsonRpcRequest, From 44ef0b6b8fb7eae8054299d6669014aef52854f8 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 23 Aug 2021 23:58:22 -0700 Subject: [PATCH 032/147] fixup! Rename SubjectMetadata to PermissionsSubjectMetadata --- packages/controllers/tsconfig.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/controllers/tsconfig.json b/packages/controllers/tsconfig.json index a4d4b216bc..df6fea5545 100644 --- a/packages/controllers/tsconfig.json +++ b/packages/controllers/tsconfig.json @@ -5,10 +5,7 @@ "composite": true, "outDir": "dist", "rootDir": "src", - "typeRoots": [ - "../../node_modules/@types", - "./node_modules/@types" - ] + "typeRoots": ["../../node_modules/@types", "./node_modules/@types"] }, "include": ["./src"], "exclude": ["**/*.test.ts"] From 047e787233dcc82dd3b223df80d2b7567ced9c46 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 24 Aug 2021 14:17:16 -0700 Subject: [PATCH 033/147] Assume that permission factories produce valid outputs --- .../controllers/src/permissions/Permission.ts | 17 ++++++++--------- .../src/permissions/PermissionController.ts | 11 +++++++---- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index be02876a49..d4b7555759 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -206,13 +206,14 @@ export type PermissionSpecification< target: string; /** - * The factory function used to get permission objects. + * The factory function used to get permission objects. Permissions returned + * by this function are presumed to valid, and they will not be passed to the + * validator function associated with this specification (if any). In other + * words, the factory function should validate the permissions it creates. * * If no factory is specified, the {@link Permission} constructor will be - * used. - * - * The validator function will be called on newly created permissions whether - * a factory is specified or not. + * used, and the validator function (if specified) will be called on newly + * constructed permissions. */ factory?: (options: FactoryOptions, requestData?: RequestData) => Permission; @@ -224,10 +225,8 @@ export type PermissionSpecification< /** * The validator function used to validate permissions of the associated type - * whenever they are instantiated. Permissions are instantiated whenever they - * are created or mutated. The only way a permission can be legally mutated is - * when its caveat array or its contents are modified by the permission - * controller. + * whenever they are mutated. The only way a permission can be legally mutated + * is when its caveats are modified by the permission controller. * * The validator should throw an appropriate JSON-RPC error if validation fails. */ diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 838993d601..4c57a4a11a 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -537,11 +537,14 @@ export class PermissionController extends BaseController< const permissionOptions = { caveats, invoker: origin, target }; - const permission = - specification.factory?.(permissionOptions, requestData) || - new Permission(permissionOptions); + let permission: Permission; + if (specification.factory) { + permission = specification.factory(permissionOptions, requestData); + } else { + permission = new Permission(permissionOptions); + specification.validator?.(permission); + } - specification.validator?.(permission); permissions[targetKey] = permission; } From 2214b7c9246aebbd58085daa70b9a8f462ff4bf0 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 30 Aug 2021 17:49:00 -0700 Subject: [PATCH 034/147] Various tweaks and fixups * Remove PermissionController.methodPrefix * Replace Caveat and Permission classes with types and factory functions * Add first test case * Various type fixups --- packages/controllers/jest.config.js | 27 ++++- .../controllers/src/permissions/Caveat.ts | 68 +++++------- .../controllers/src/permissions/Permission.ts | 103 ++++++++++-------- .../permissions/PermissionController.test.ts | 77 +++++++++++++ .../src/permissions/PermissionController.ts | 18 +-- .../src/permissions/PermissionEnforcer.ts | 4 +- 6 files changed, 191 insertions(+), 106 deletions(-) create mode 100644 packages/controllers/src/permissions/PermissionController.test.ts diff --git a/packages/controllers/jest.config.js b/packages/controllers/jest.config.js index 239f4d3d9c..4055994997 100644 --- a/packages/controllers/jest.config.js +++ b/packages/controllers/jest.config.js @@ -10,9 +10,26 @@ module.exports = { // statements: 100, // }, // }, - preset: 'ts-jest', - runner: '@jest-runner/electron', - testEnvironment: '@jest-runner/electron/environment', - testRegex: ['\\.test\\.ts$'], - testTimeout: 5000, + silent: true, + testTimeout: 2500, + projects: [ + { + displayName: 'runner: electron', + testMatch: [ + '/src/plugins/**/*.test.ts', + '/src/services/**/*.test.ts', + ], + preset: 'ts-jest', + runner: '@jest-runner/electron', + testEnvironment: '@jest-runner/electron/environment', + }, + { + displayName: 'runner: default', + testPathIgnorePatterns: [ + '/src/plugins/*', + '/src/services/*', + ], + preset: 'ts-jest', + }, + ], }; diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index fd047b623b..b1726e407c 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -9,50 +9,42 @@ import { type CaveatType = string; -type CaveatOptions = { - type: CaveatType; - value: Value; -}; - export type ZcapLdCaveat = { /** - * The type of the caveat, which is presumed to be meaningful in the context - * of the capability it is associated with. + * The type of the caveat. The type is presumed to be meaningful in the + * context of the capability it is associated with. + * + * In MetaMask, every permission can only have one caveat of each type. */ - type: CaveatType; + readonly type: CaveatType; }; /** * Identical to instances of the Caveat class, useful for when TypeScript * has a meltdown over assigning classes to the Json type. */ -export type CaveatInterface = ZcapLdCaveat & { - value: Value; -}; - -/** - * TODO: Document - */ -export class Caveat implements CaveatInterface { - /** - * The type of the caveat. The type is presumed to be meaningful in the - * context of the capability it is associated with. - * - * In MetaMask, every permission can only have one caveat of each type. - */ - public readonly type: CaveatType; - +export type Caveat = ZcapLdCaveat & { /** * Any additional data necessary to enforce the caveat. * * TODO: Make optional in typescript@4.4.x */ - public readonly value: Value; + readonly value: Value | null; +}; - constructor({ type, value }: CaveatOptions) { - this.type = type; - this.value = value; - } +/** + * The {@link Caveat} factory function. Naively constructs a new caveat from the + * inputs. Sets `value` to `null` if no value is provided. + * + * @param type - The type of the caveat. + * @param value - The value associated with the caveat, if any. + * @returns The new caveat object. + */ +export function constructCaveat( + type: string, + value?: Value, +): Caveat { + return { type, value: value === undefined ? null : value }; } // Next, we define types used for specifying caveats at the consumer layer, @@ -60,18 +52,18 @@ export class Caveat implements CaveatInterface { // Accomplished by decorating the restricted method implementation with the // the corresponding caveat functions. -type CaveatDecorator = ( +export type CaveatDecorator = ( decorated: AsyncRestrictedMethodImplementation, - caveat: Caveat, + caveat: Caveat, ) => AsyncRestrictedMethodImplementation; -type CaveatValidator = ( +export type CaveatValidator = ( origin: string, target: string, caveat: Caveat, ) => void; -export type CaveatSpecification = { +export type CaveatSpecification = { /** * The string type of the caveat. */ @@ -81,7 +73,7 @@ export type CaveatSpecification = { * The decorator function used to apply the caveat to restricted method * requests. */ - decorator: CaveatDecorator; + decorator: CaveatDecorator; /** * The validator function used to validate caveats of the associated type @@ -98,7 +90,7 @@ export type CaveatSpecification = { }; export type CaveatSpecifications = Readonly< - Record + Record> >; /** @@ -144,11 +136,9 @@ export function decorateWithCaveats( // "Return handler" example // -// export function eth_accounts(next: MethodImplementation) { +// export function eth_accounts(next: MethodImplementation, caveat: Caveat) { // return async (req: JsonRpcRequest, context: Record) => { // const accounts = await next(req, context); -// return accounts.filter(() => { -// // filter -// }); +// return accounts.filter((account: string) => caveat.value.includes(account)) // }; // } diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index d4b7555759..4d14c7d5f0 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -1,7 +1,7 @@ import type { EthereumRpcError } from 'eth-rpc-errors'; import { Json } from 'json-rpc-engine'; import { nanoid } from 'nanoid'; -import { Caveat, CaveatInterface, ZcapLdCaveat } from './Caveat'; +import { Caveat, ZcapLdCaveat } from './Caveat'; /** * The origin of a subject. @@ -24,12 +24,12 @@ type ZcapLdCapability = { * It is required by the standard, but we omit it because there is only one * context (the user's MetaMask instance). */ - '@context'?: string[]; + readonly '@context'?: string[]; /** * The cryptograhically strong GUID of the capability. */ - id: string; + readonly id: string; /** * A pointer to the resource that possession of the capability grants @@ -37,7 +37,7 @@ type ZcapLdCapability = { * * In the context of MetaMask, this is always the name of an RPC method. */ - parentCapability: string; + readonly parentCapability: string; /** * A pointer to the the entity that may invoke this capability. @@ -47,19 +47,19 @@ type ZcapLdCapability = { * * In the context of MetaMask, this is simply the origin of subject. */ - invoker: string; + readonly invoker: string; /** * The issuing date, in UNIX epoch time. */ - date?: number; + readonly date?: number; /** * An array of caveat objects. See {@link ZcapLdCaveat}. * * TODO: Make optional in typescript@4.4.x */ - caveats: ZcapLdCaveat[] | null; + readonly caveats: ZcapLdCaveat[] | null; /** * The proof that this capability was delegated to the specified invoker. @@ -69,7 +69,7 @@ type ZcapLdCapability = { * In MetaMask, the "proof" of validity is the existence of a valid capability * object in the designated part of our state tree, so this field is omitted. */ - proof?: string; + readonly proof?: string; }; type PermissionOptions = { @@ -99,21 +99,19 @@ type PermissionOptions = { /** * TODO: Document */ -export class Permission implements ZcapLdCapability { +export type Permission = Omit< + ZcapLdCapability, + '@context' | 'caveats' | 'proof' +> & { /** * The GUID of the permission object. */ - public readonly id: string; + readonly id: string; /** * The creation date of the permission, in UNIX epoch time. */ - public readonly date: number; - - /** - * The method that the permission corresponds to. - */ - public readonly parentCapability: string; + readonly date: number; /** * The caveats of the permission. @@ -121,41 +119,51 @@ export class Permission implements ZcapLdCapability { * * TODO: Make optional in typescript@4.4.x */ - public readonly caveats: Caveat[] | null; + readonly caveats: Caveat[] | null; /** * The origin string of the subject that has the permission. */ - public readonly invoker: OriginString; + readonly invoker: OriginString; +}; - constructor(options: PermissionOptions) { - const { caveats, id, invoker, target } = options; - this.id = id ?? nanoid(); - this.parentCapability = target; - this.invoker = invoker; - this.caveats = caveats ?? null; - this.date = new Date().getTime(); - } +/** + * The default {@link Permission} factory function. Naively constructs a permission from + * the inputs. Sets a default, random `id` if none is provided. + * + * @param options - The options for the permission. + * @returns The new permission object. + */ +export function constructPermission(options: PermissionOptions) { + const { caveats, id, invoker, target } = options; + + return { + id: id ?? nanoid(), + parentCapability: target, + invoker, + caveats: caveats ?? null, + date: new Date().getTime(), + }; +} - /** - * Gets the value of the caveat of the specified type belonging to the - * specified permission. - * - * @param permission The permission whose caveat value to retrieve. - * @param caveatType The type of the caveat to retrieve. - * @returns The caveat value, or undefined if no such caveat exists. - */ - public static getCaveat( - permission: Permission, - caveatType: string, - ): Caveat | undefined { - return permission.caveats?.find((caveat) => caveat.type === caveatType); - } +/** + * Gets the value of the caveat of the specified type belonging to the + * specified permission. + * + * @param permission The permission whose caveat value to retrieve. + * @param caveatType The type of the caveat to retrieve. + * @returns The caveat value, or undefined if no such caveat exists. + */ +export function findCaveat( + permission: Permission, + caveatType: string, +): Caveat | undefined { + return permission.caveats?.find((caveat) => caveat.type === caveatType); } type RequestedPermission = { - target: MethodName; - caveats: CaveatInterface[] | null; + target?: MethodName; + caveats: Caveat[] | null; }; export type RequestedPermissions = Record; @@ -165,26 +173,25 @@ export type RestrictedMethodContext = Readonly<{ [key: string]: any; }>; -export type RestrictedMethodRequest = { +// TODO: Should restricted methods just take (method, params, context) instead? +export type RestrictedMethodArgs = { method: string; params?: Params; - [key: string]: any; + context: RestrictedMethodContext; }; export type SyncRestrictedMethodImplementation< Params extends Json, Result extends Json, > = ( - request: RestrictedMethodRequest, - context: RestrictedMethodContext, + args: RestrictedMethodArgs, ) => Result | Error | EthereumRpcError; export type AsyncRestrictedMethodImplementation< Params extends Json, Result extends Json, > = ( - request: RestrictedMethodRequest, - context: RestrictedMethodContext, + args: RestrictedMethodArgs, ) => Promise>; export type RestrictedMethodImplementation< diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts new file mode 100644 index 0000000000..1bcf8250c1 --- /dev/null +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -0,0 +1,77 @@ +import { ControllerMessenger, Json } from '@metamask/controllers'; +import { + AsyncRestrictedMethodImplementation, + CaveatDecorator, + CaveatSpecifications, + PermissionController, + PermissionControllerActions, + PermissionControllerEvents, + PermissionSpecifications, + RestrictedMethodArgs, +} from '.'; + +const controllerName = 'PermissionController' as const; + +function getRestrictedMessenger() { + const controllerMessenger = new ControllerMessenger< + PermissionControllerActions, + PermissionControllerEvents + >(); + return controllerMessenger.getRestricted( + { + name: controllerName, + }, + ); +} + +enum CaveatTypes { + filterArrayResponse = 'filterArrayResponse', +} + +function getCaveatSpecifications(): CaveatSpecifications { + return { + filterArrayResponse: { + type: CaveatTypes.filterArrayResponse, + decorator: (( + method: AsyncRestrictedMethodImplementation, + caveat: { type: CaveatTypes.filterArrayResponse; value: string[] }, + ) => + async (args: RestrictedMethodArgs) => { + const result = await method(args); + return Array.isArray(result) + ? result.filter(caveat.value.includes) + : result; + }) as CaveatDecorator, + }, + }; +} + +function getPermissionSpecifications(): PermissionSpecifications { + return { + wallet_getSecretArray: { + target: 'wallet_getSecretArray', + methodImplementation: (_args: RestrictedMethodArgs) => { + return ['secret1', 'secret2', 'secret3']; + }, + }, + }; +} + +describe('PermissionController', () => { + describe('constructor', () => { + it('initializes a new PermissionController', () => { + const controller = new PermissionController({ + caveatSpecifications: getCaveatSpecifications(), + messenger: getRestrictedMessenger(), + permissionSpecifications: getPermissionSpecifications(), + safeMethods: ['wallet_safeMethod'], + }); + + expect(controller.state).toStrictEqual({ subjects: {} }); + }); + }); + + describe('foo', () => { + it.todo('bar'); + }); +}); diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 4c57a4a11a..ba6e81de8e 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -7,7 +7,7 @@ import deepFreeze from 'deep-freeze-strict'; import { Json } from 'json-rpc-engine'; import { isPlainObject } from '../utils'; -import { Caveat, CaveatSpecifications } from './Caveat'; +import { Caveat, CaveatSpecifications, constructCaveat } from './Caveat'; import { OriginString, Permission, @@ -15,6 +15,8 @@ import { RequestedPermissions, RestrictedMethodImplementation, PermissionSpecifications, + findCaveat, + constructPermission, } from './Permission'; import { PermissionDoesNotExistError, @@ -100,7 +102,6 @@ export type PermissionControllerMessenger = RestrictedControllerMessenger< type PermissionControllerOptions = { messenger: PermissionControllerMessenger; caveatSpecifications: CaveatSpecifications; - methodPrefix: string; permissionSpecifications: PermissionSpecifications; safeMethods: string[]; state?: Partial; @@ -117,8 +118,6 @@ export class PermissionController extends BaseController< typeof controllerName, PermissionControllerState > { - public readonly methodPrefix: string; - protected messagingSystem: PermissionControllerMessenger; private readonly _permissionSpecifications: Readonly; @@ -143,7 +142,6 @@ export class PermissionController extends BaseController< messenger, state = {}, caveatSpecifications, - methodPrefix, permissionSpecifications, safeMethods, }: PermissionControllerOptions) { @@ -159,7 +157,6 @@ export class PermissionController extends BaseController< ...permissionSpecifications, }); this._safeMethods = new Set(safeMethods); - this.methodPrefix = methodPrefix; // This assignment is redundant, but TypeScript doesn't know that it becomes // assigned if we don't do it. @@ -363,7 +360,7 @@ export class PermissionController extends BaseController< throw new PermissionDoesNotExistError(origin, target); } - return Permission.getCaveat(permission, caveatType); + return findCaveat(permission, caveatType); } private setCaveat( @@ -541,7 +538,7 @@ export class PermissionController extends BaseController< if (specification.factory) { permission = specification.factory(permissionOptions, requestData); } else { - permission = new Permission(permissionOptions); + permission = constructPermission(permissionOptions); specification.validator?.(permission); } @@ -597,10 +594,7 @@ export class PermissionController extends BaseController< throw new CaveatMissingValueError(requestedCaveat, origin, target); } - const caveat = new Caveat({ - type: requestedCaveat.type, - value: requestedCaveat.value, - }); + const caveat = constructCaveat(requestedCaveat.type, requestedCaveat.value); specification.validator?.(origin, target, caveat); return caveat; diff --git a/packages/controllers/src/permissions/PermissionEnforcer.ts b/packages/controllers/src/permissions/PermissionEnforcer.ts index 2ebb9e9dd1..61507a80ef 100644 --- a/packages/controllers/src/permissions/PermissionEnforcer.ts +++ b/packages/controllers/src/permissions/PermissionEnforcer.ts @@ -311,7 +311,7 @@ class PermissionEnforcer { req: JsonRpcRequest, ): ReturnType> { const { origin } = subject; - const { method } = req; + const { method, params } = req; const permission = this.getPermission(origin, method); if (!permission) { @@ -322,7 +322,7 @@ class PermissionEnforcer { methodImplementation, permission, this.caveatSpecifications, - )(req, { origin }); + )({ method, params, context: { origin } }); } getPermissionsMiddleware( From 19219b3adbb5f93eede5e45e724d15ce1d00714e Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 30 Aug 2021 22:31:26 -0700 Subject: [PATCH 035/147] @metamask/controllers@15.0.0 --- .../controllers/src/permissions/PermissionController.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index ba6e81de8e..6c94665a59 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -116,10 +116,9 @@ type GrantPermissionsOptions = { export class PermissionController extends BaseController< typeof controllerName, - PermissionControllerState + PermissionControllerState, + PermissionControllerMessenger > { - protected messagingSystem: PermissionControllerMessenger; - private readonly _permissionSpecifications: Readonly; public get permissionSpecifications(): Readonly { @@ -158,10 +157,6 @@ export class PermissionController extends BaseController< }); this._safeMethods = new Set(safeMethods); - // This assignment is redundant, but TypeScript doesn't know that it becomes - // assigned if we don't do it. - this.messagingSystem = messenger; - this.registerMessageHandlers(); } From b262c886c3f5d6469281eb49060d3a62fbc005fb Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 30 Aug 2021 23:40:05 -0700 Subject: [PATCH 036/147] Make PermissionEnforcer a PermissionController property --- .../src/permissions/PermissionController.ts | 83 +++++++++++++++++-- .../src/permissions/PermissionEnforcer.ts | 43 +--------- 2 files changed, 76 insertions(+), 50 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 6c94665a59..66c84d6bcc 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -1,6 +1,10 @@ import { BaseControllerV2 as BaseController, RestrictedControllerMessenger, + AddApprovalRequest, + AcceptRequest as AcceptApprovalRequest, + RejectRequest as RejectApprovalRequest, + HasApprovalRequest, } from '@metamask/controllers'; import type { Patch } from 'immer'; import deepFreeze from 'deep-freeze-strict'; @@ -32,6 +36,8 @@ import { CaveatAlreadyExistsError, InvalidCaveatError, } from './errors'; +import { PermissionEnforcer, PermissionsRequest } from './PermissionEnforcer'; +import { MethodNames } from './enums'; export type PermissionsSubjectMetadata = { origin: OriginString; @@ -91,11 +97,17 @@ export type PermissionsStateChange = { export type PermissionControllerEvents = PermissionsStateChange; +type AllowedActions = + | AddApprovalRequest + | HasApprovalRequest + | AcceptApprovalRequest + | RejectApprovalRequest; + export type PermissionControllerMessenger = RestrictedControllerMessenger< typeof controllerName, - PermissionControllerActions, + PermissionControllerActions | AllowedActions, PermissionControllerEvents, - never, + AllowedActions['type'], never >; @@ -119,6 +131,18 @@ export class PermissionController extends BaseController< PermissionControllerState, PermissionControllerMessenger > { + private readonly _caveatSpecifications: Readonly; + + public get caveatSpecifications(): Readonly { + return this._caveatSpecifications; + } + + private readonly _enforcer: Readonly; + + public get enforcer(): Readonly { + return this._enforcer; + } + private readonly _permissionSpecifications: Readonly; public get permissionSpecifications(): Readonly { @@ -131,12 +155,6 @@ export class PermissionController extends BaseController< return this._safeMethods; } - private readonly _caveatSpecifications: Readonly; - - public get caveatSpecifications(): Readonly { - return this._caveatSpecifications; - } - constructor({ messenger, state = {}, @@ -158,6 +176,7 @@ export class PermissionController extends BaseController< this._safeMethods = new Set(safeMethods); this.registerMessageHandlers(); + this._enforcer = this.constructEnforcer(); } /** @@ -594,4 +613,52 @@ export class PermissionController extends BaseController< specification.validator?.(origin, target, caveat); return caveat; } + + private constructEnforcer(): PermissionEnforcer { + return new PermissionEnforcer({ + caveatSpecifications: this.caveatSpecifications, + getPermission: this.getPermission.bind(this), + getRestrictedMethodImplementation: + this.getRestrictedMethodImplementation.bind(this), + grantPermissions: this.grantPermissions.bind(this), + isRestrictedMethod: (method: string) => { + return Boolean(this.getTargetKey(method)); + }, + isSafeMethod: (method: string) => { + return this.safeMethods.has(method); + }, + requestUserApproval: async (permissionsRequest: PermissionsRequest) => { + const { origin, id } = permissionsRequest.metadata; + return (await this.messagingSystem.call( + 'ApprovalController:addRequest', + { + id, + origin, + requestData: permissionsRequest, + type: MethodNames.requestPermissions, + }, + true, + )) as PermissionsRequest; + }, + acceptPermissionsRequest: (id: string, value?: unknown) => + this.messagingSystem.call( + 'ApprovalController:acceptRequest', + id, + value, + ), + rejectPermissionsRequest: (id: string, error: Error) => + this.messagingSystem.call( + 'ApprovalController:rejectRequest', + id, + error, + ), + hasApprovalRequest: (opts: { + id?: string; + origin?: string; + type?: string; + }) => + // Typecast: It can't handle the overloads + this.messagingSystem.call('ApprovalController:hasRequest', opts as any), + }); + } } diff --git a/packages/controllers/src/permissions/PermissionEnforcer.ts b/packages/controllers/src/permissions/PermissionEnforcer.ts index 61507a80ef..45ba240ba3 100644 --- a/packages/controllers/src/permissions/PermissionEnforcer.ts +++ b/packages/controllers/src/permissions/PermissionEnforcer.ts @@ -25,7 +25,6 @@ import type { PermissionsSubjectMetadata, } from './PermissionController'; import type { ApprovalController } from './temp/ApprovalController'; -import { MethodNames } from './enums'; export type PermissionsRequestMetadata = PermissionsSubjectMetadata & { id: string; @@ -60,7 +59,7 @@ type PermissionEnforcerArgs = { hasApprovalRequest: ApprovalController['has']; }; -class PermissionEnforcer { +export class PermissionEnforcer { private caveatSpecifications: CaveatSpecifications; private isRestrictedMethod: IsRestrictedMethod; @@ -373,46 +372,6 @@ class PermissionEnforcer { } } -export type { PermissionEnforcer }; - -export function getPermissionEnforcer( - permissionController: PermissionController, - addAndShowApprovalRequest: ApprovalController['addAndShowApprovalRequest'], - acceptPermissionsRequest: ApprovalController['accept'], - rejectPermissionsRequest: ApprovalController['reject'], - hasApprovalRequest: ApprovalController['has'], -): PermissionEnforcer { - return new PermissionEnforcer({ - caveatSpecifications: permissionController.caveatSpecifications, - isRestrictedMethod: (method: string) => { - return Boolean(permissionController.getTargetKey(method)); - }, - isSafeMethod: (method: string) => { - return permissionController.safeMethods.has(method); - }, - getPermission: - permissionController.getPermission.bind(permissionController), - getRestrictedMethodImplementation: - permissionController.getRestrictedMethodImplementation.bind( - permissionController, - ), - grantPermissions: - permissionController.grantPermissions.bind(permissionController), - requestUserApproval: async (permissionsRequest: PermissionsRequest) => { - const { origin, id } = permissionsRequest.metadata; - return (await addAndShowApprovalRequest({ - id, - origin, - requestData: permissionsRequest, - type: MethodNames.requestPermissions, - })) as PermissionsRequest; - }, - acceptPermissionsRequest, - rejectPermissionsRequest, - hasApprovalRequest, - }); -} - function resultIsError(resultOrError: Json | Error): resultOrError is Error { return Boolean( resultOrError instanceof Error || From 14474a8a95ff67971c0ce6e725fd713e89969203 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 31 Aug 2021 00:19:18 -0700 Subject: [PATCH 037/147] WIP: Tests --- .../controllers/src/permissions/Permission.ts | 2 +- .../permissions/PermissionController.test.ts | 52 +++++++++++++++---- .../src/permissions/PermissionController.ts | 2 +- .../permissions/PermissionEnforcer.test.ts | 7 +++ 4 files changed, 52 insertions(+), 11 deletions(-) create mode 100644 packages/controllers/src/permissions/PermissionEnforcer.test.ts diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 4d14c7d5f0..767439d3d2 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -163,7 +163,7 @@ export function findCaveat( type RequestedPermission = { target?: MethodName; - caveats: Caveat[] | null; + caveats?: Caveat[]; }; export type RequestedPermissions = Record; diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 1bcf8250c1..336e0d2cf0 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -6,6 +6,7 @@ import { PermissionController, PermissionControllerActions, PermissionControllerEvents, + PermissionEnforcer, PermissionSpecifications, RestrictedMethodArgs, } from '.'; @@ -57,21 +58,54 @@ function getPermissionSpecifications(): PermissionSpecifications { }; } +function getPermissionController(): PermissionController { + return new PermissionController({ + caveatSpecifications: getCaveatSpecifications(), + messenger: getRestrictedMessenger(), + permissionSpecifications: getPermissionSpecifications(), + safeMethods: ['wallet_safeMethod'], + }); +} + describe('PermissionController', () => { describe('constructor', () => { it('initializes a new PermissionController', () => { - const controller = new PermissionController({ - caveatSpecifications: getCaveatSpecifications(), - messenger: getRestrictedMessenger(), - permissionSpecifications: getPermissionSpecifications(), - safeMethods: ['wallet_safeMethod'], - }); - + const controller = getPermissionController(); expect(controller.state).toStrictEqual({ subjects: {} }); + expect(controller.enforcer instanceof PermissionEnforcer).toStrictEqual( + true, + ); }); }); - describe('foo', () => { - it.todo('bar'); + describe('grantPermissions', () => { + it('grants new permissions', () => { + const controller = getPermissionController(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + wallet_getSecretArray: {}, + }, + }); + + expect(controller.state).toStrictEqual({ + subjects: { + [origin]: { + origin, + permissions: { + wallet_getSecretArray: { + id: expect.any(String), + parentCapability: 'wallet_getSecretArray', + invoker: origin, + caveats: null, + date: expect.any(Number), + }, + }, + }, + }, + }); + }); }); }); diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 66c84d6bcc..ca9c34c5bb 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -543,7 +543,7 @@ export class PermissionController extends BaseController< const caveats = this.computeCaveats( origin, target, - approvedPermissions[target].caveats || undefined, + approvedPermissions[target].caveats, ); const permissionOptions = { caveats, invoker: origin, target }; diff --git a/packages/controllers/src/permissions/PermissionEnforcer.test.ts b/packages/controllers/src/permissions/PermissionEnforcer.test.ts new file mode 100644 index 0000000000..49e5b53c91 --- /dev/null +++ b/packages/controllers/src/permissions/PermissionEnforcer.test.ts @@ -0,0 +1,7 @@ +/* eslint-disable import/unambiguous */ + +describe('PermissionEnforcer', () => { + describe('executeRestrictedMethod', () => { + it.todo('can execute restricted methods'); + }); +}); From 4b4efbeca5d18d5ff73a7dc6290677fa863d001e Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 31 Aug 2021 17:07:58 -0700 Subject: [PATCH 038/147] Remove temporary type declaration files --- packages/controllers/.eslintrc.js | 3 - .../src/permissions/PermissionEnforcer.ts | 5 +- .../permissions/temp/ApprovalController.d.ts | 292 ------------------ .../permissions/temp/BaseControllerV2.d.ts | 185 ----------- 4 files changed, 3 insertions(+), 482 deletions(-) delete mode 100644 packages/controllers/src/permissions/temp/ApprovalController.d.ts delete mode 100644 packages/controllers/src/permissions/temp/BaseControllerV2.d.ts diff --git a/packages/controllers/.eslintrc.js b/packages/controllers/.eslintrc.js index 51331125a9..c7e798f666 100644 --- a/packages/controllers/.eslintrc.js +++ b/packages/controllers/.eslintrc.js @@ -5,9 +5,6 @@ module.exports = { '@typescript-eslint/consistent-type-definitions': 'off', }, - // TODO:temp - ignorePatterns: ['**/temp/**'], - overrides: [ { files: ['**/*.d.ts'], diff --git a/packages/controllers/src/permissions/PermissionEnforcer.ts b/packages/controllers/src/permissions/PermissionEnforcer.ts index 45ba240ba3..05e9cea32a 100644 --- a/packages/controllers/src/permissions/PermissionEnforcer.ts +++ b/packages/controllers/src/permissions/PermissionEnforcer.ts @@ -1,4 +1,4 @@ -import { nanoid } from 'nanoid'; +import type { ApprovalController } from '@metamask/controllers'; import { AsyncJsonRpcEngineNextCallback, createAsyncMiddleware, @@ -7,6 +7,8 @@ import { JsonRpcMiddleware, PendingJsonRpcResponse, } from 'json-rpc-engine'; +import { nanoid } from 'nanoid'; + import { CaveatSpecifications, decorateWithCaveats } from './Caveat'; import { internalError, @@ -24,7 +26,6 @@ import type { PermissionController, PermissionsSubjectMetadata, } from './PermissionController'; -import type { ApprovalController } from './temp/ApprovalController'; export type PermissionsRequestMetadata = PermissionsSubjectMetadata & { id: string; diff --git a/packages/controllers/src/permissions/temp/ApprovalController.d.ts b/packages/controllers/src/permissions/temp/ApprovalController.d.ts deleted file mode 100644 index 4ccb11c04d..0000000000 --- a/packages/controllers/src/permissions/temp/ApprovalController.d.ts +++ /dev/null @@ -1,292 +0,0 @@ -import type { Patch } from 'immer'; -import { BaseController, Json } from './BaseControllerV2'; -import type { RestrictedControllerMessenger } from '@metamask/controllers'; - -declare const controllerName = "ApprovalController"; -declare type ApprovalRequestData = Record | null; -export declare type ApprovalRequest = { - /** - * The ID of the approval request. - */ - id: string; - /** - * The origin of the approval request. - */ - origin: string; - /** - * The time that the request was received, per Date.now(). - */ - time: number; - /** - * The type of the approval request. - */ - type: string; - /** - * Additional data associated with the request. - * TODO:TS4.4 make optional - */ - requestData: RequestData; -}; -declare type ShowApprovalRequest = () => void | Promise; -export declare type ApprovalControllerState = { - pendingApprovals: Record>>; - pendingApprovalCount: number; -}; -export declare type GetApprovalsState = { - type: `${typeof controllerName}:getState`; - handler: () => ApprovalControllerState; -}; -export declare type ClearApprovalRequests = { - type: `${typeof controllerName}:clearRequests`; - handler: () => void; -}; -declare type AddApprovalOptions = { - id?: string; - origin: string; - type: string; - requestData?: Record; -}; -export declare type AddApprovalRequest = { - type: `${typeof controllerName}:addRequest`; - handler: (opts: AddApprovalOptions, shouldShowRequest: boolean) => ReturnType; -}; -export declare type HasApprovalRequest = { - type: `${typeof controllerName}:hasRequest`; - handler: ApprovalController['has']; -}; -export declare type AcceptRequest = { - type: `${typeof controllerName}:acceptRequest`; - handler: ApprovalController['accept']; -}; -export declare type RejectRequest = { - type: `${typeof controllerName}:rejectRequest`; - handler: ApprovalController['reject']; -}; -export declare type ApprovalControllerActions = GetApprovalsState | ClearApprovalRequests | AddApprovalRequest | HasApprovalRequest | AcceptRequest | RejectRequest; -export declare type ApprovalStateChange = { - type: `${typeof controllerName}:stateChange`; - payload: [ApprovalControllerState, Patch[]]; -}; -export declare type ApprovalControllerEvents = ApprovalStateChange; -export declare type ApprovalControllerMessenger = RestrictedControllerMessenger; -declare type ApprovalControllerOptions = { - messenger: ApprovalControllerMessenger; - showApprovalRequest: ShowApprovalRequest; - state?: Partial; -}; -/** - * Controller for managing requests that require user approval. - * - * Enables limiting the number of pending requests by origin and type, counting - * pending requests, and more. - * - * Adding a request returns a promise that resolves or rejects when the request - * is approved or denied, respectively. - */ -export declare class ApprovalController extends BaseController { - private _approvals; - private _origins; - private _showApprovalRequest; - /** - * @param opts - Options bag - * @param opts.showApprovalRequest - Function for opening the UI such that - * the request can be displayed to the user. - */ - constructor({ messenger, showApprovalRequest, state, }: ApprovalControllerOptions); - /** - * Constructor helper for registering this controller's messaging system - * actions. - */ - private registerMessageHandlers; - /** - * Adds an approval request per the given arguments, calls the show approval - * request function, and returns the associated approval promise. - * - * There can only be one approval per origin and type. An error is thrown if - * attempting to add an invalid or duplicate request. - * - * @param opts - Options bag. - * @param opts.id - The id of the approval request. A random id will be - * generated if none is provided. - * @param opts.origin - The origin of the approval request. - * @param opts.type - The type associated with the approval request. - * @param opts.requestData - Additional data associated with the request, - * if any. - * @returns The approval promise. - */ - addAndShowApprovalRequest(opts: AddApprovalOptions): Promise; - /** - * Adds an approval request per the given arguments and returns the approval - * promise. - * - * There can only be one approval per origin and type. An error is thrown if - * attempting to add an invalid or duplicate request. - * - * @param opts - Options bag. - * @param opts.id - The id of the approval request. A random id will be - * generated if none is provided. - * @param opts.origin - The origin of the approval request. - * @param opts.type - The type associated with the approval request. - * @param opts.requestData - Additional data associated with the request, - * if any. - * @returns The approval promise. - */ - add(opts: AddApprovalOptions): Promise; - /** - * Gets the info for the approval request with the given id. - * - * @param id - The id of the approval request. - * @returns The approval request data associated with the id. - */ - get(id: string): ApprovalRequest | undefined; - /** - * Gets the number of pending approvals, by origin and/or type. - * - * If only `origin` is specified, all approvals for that origin will be - * counted, regardless of type. - * If only `type` is specified, all approvals for that type will be counted, - * regardless of origin. - * If both `origin` and `type` are specified, 0 or 1 will be returned. - * - * @param opts.origin - An approval origin. - * @param opts.type - The type of the approval request. - * @returns The current approval request count for the given origin and/or - * type. - */ - getApprovalCount(opts?: { - origin?: string; - type?: string; - }): number; - /** - * @returns The current total approval request count, for all types and - * origins. - */ - getTotalApprovalCount(): number; - /** - * Checks if there's a pending approval request with the given ID. - * - * @param opts - Options bag. - * @param opts.id - The ID to check for. - * @returns `true` if a matching approval is found, and `false` otherwise. - */ - has(opts: { - id: string; - }): boolean; - /** - * Checks if there's any pending approval request with the given origin. - * - * @param opts - Options bag. - * @param opts.origin - The origin to check for. - * @returns `true` if a matching approval is found, and `false` otherwise. - */ - has(opts: { - origin: string; - }): boolean; - /** - * Checks if there's any pending approval request with the given type. - * - * @param opts - Options bag. - * @param opts.type - The type to check for. - * @returns `true` if a matching approval is found, and `false` otherwise. - */ - has(opts: { - type: string; - }): boolean; - /** - * Checks if there's any pending approval request with the given origin and - * type. - * - * @param opts - Options bag. - * @param opts.origin - The origin to check for. - * @param opts.type - The type to check for. - * @returns `true` if a matching approval is found, and `false` otherwise. - */ - has(opts: { - origin: string; - type: string; - }): boolean; - /** - * Resolves the promise of the approval with the given id, and deletes the - * approval. Throws an error if no such approval exists. - * - * @param id - The id of the approval request. - * @param value - The value to resolve the approval promise with. - */ - accept(id: string, value?: unknown): void; - /** - * Rejects the promise of the approval with the given id, and deletes the - * approval. Throws an error if no such approval exists. - * - * @param id - The id of the approval request. - * @param error - The error to reject the approval promise with. - */ - reject(id: string, error: Error): void; - /** - * Rejects and deletes all approval requests. - */ - clear(): void; - /** - * Implementation of add operation. - * - * @param origin - The origin of the approval request. - * @param type - The type associated with the approval request. - * @param id - The id of the approval request. - * @param requestData - The request data associated with the approval request. - * @returns The approval promise. - */ - private _add; - /** - * Validates parameters to the add method. - * - * @param id - The id of the approval request. - * @param origin - The origin of the approval request. - * @param type - The type associated with the approval request. - * @param requestData - The request data associated with the approval request. - */ - private _validateAddParams; - /** - * Adds an entry to _origins. - * Performs no validation. - * - * @param origin - The origin of the approval request. - * @param type - The type associated with the approval request. - */ - private _addPendingApprovalOrigin; - /** - * Adds an entry to the store. - * Performs no validation. - * - * @param id - The id of the approval request. - * @param origin - The origin of the approval request. - * @param type - The type associated with the approval request. - * @param requestData - The request data associated with the approval request. - */ - private _addToStore; - /** - * Deletes the approval with the given id. The approval promise must be - * resolved or reject before this method is called. - * Deletion is an internal operation because approval state is solely - * managed by this controller. - * - * @param id - The id of the approval request to be deleted. - */ - private _delete; - /** - * Gets the approval callbacks for the given id, deletes the entry, and then - * returns the callbacks for promise resolution. - * Throws an error if no approval is found for the given id. - * - * @param id - The id of the approval request. - * @returns The promise callbacks associated with the approval request. - */ - private _deleteApprovalAndGetCallbacks; - /** - * Checks whether there are any approvals associated with the given - * origin. - * - * @param origin - The origin to check. - * @returns True if the origin has no approvals, false otherwise. - */ - private _isEmptyOrigin; -} -export default ApprovalController; diff --git a/packages/controllers/src/permissions/temp/BaseControllerV2.d.ts b/packages/controllers/src/permissions/temp/BaseControllerV2.d.ts deleted file mode 100644 index 85ad61591f..0000000000 --- a/packages/controllers/src/permissions/temp/BaseControllerV2.d.ts +++ /dev/null @@ -1,185 +0,0 @@ -import type { Draft, Patch } from 'immer'; -import type { RestrictedControllerMessenger } from '@metamask/controllers'; -/** - * A state change listener. - * - * This function will get called for each state change, and is given a copy of - * the new state along with a set of patches describing the changes since the - * last update. - * - * @param state - The new controller state - * @param patches - A list of patches describing any changes (see here for more - * information: https://immerjs.github.io/immer/docs/patches) - */ -export declare type Listener = (state: T, patches: Patch[]) => void; -declare type primitive = null | boolean | number | string; -declare type DefinitelyNotJsonable = ((...args: any[]) => any) | undefined; -export declare type IsJsonable = [ - Extract, -] extends [never] - ? T extends primitive - ? T - : T extends (infer U)[] - ? IsJsonable[] - : T extends object - ? { - [P in keyof T]: P extends string ? IsJsonable : never; - } - : never - : never; -/** - * An function to derive state. - * - * This function will accept one piece of the controller state (one property), - * and will return some derivation of that state. - * - * @param value - A piece of controller state - * @returns Something derived from controller state - */ -export declare type StateDeriver = ( - value: IsJsonable, -) => IsJsonable; -/** - * State metadata. - * - * This metadata describes which parts of state should be persisted, and how to - * get an anonymized representation of the state. - */ -export declare type StateMetadata = { - [P in keyof T]: StatePropertyMetadata; -}; -/** - * Metadata for a single state property - * - * @property persist - Indicates whether this property should be persisted - * (`true` for persistent, `false` for transient), or is set to a function - * that derives the persistent state from the state. - * @property anonymous - Indicates whether this property is already anonymous, - * (`true` for anonymous, `false` if it has potential to be personally - * identifiable), or is set to a function that returns an anonymized - * representation of this state. - */ -export type StatePropertyMetadata = { - persist: boolean | StateDeriver; - anonymous: boolean | StateDeriver; -} -export declare type Json = - | null - | boolean - | number - | string - | Json[] - | { - [prop: string]: Json; - }; -/** - * Controller class that provides state management, subscriptions, and state metadata - */ -export declare class BaseController< - N extends string, - S extends Record, - messenger extends RestrictedControllerMessenger, -> { - private internalState; - - protected messagingSystem: messenger; - - /** - * The name of the controller. - * - * This is used by the ComposableController to construct a composed application state. - */ - readonly name: N; - - readonly metadata: StateMetadata; - - /** - * The existence of the `subscribe` property is how the ComposableController detects whether a - * controller extends the old BaseController or the new one. We set it to `never` here to ensure - * this property is never used for new BaseController-based controllers, to ensure the - * ComposableController never mistakes them for an older style controller. - */ - readonly subscribe: never; - - /** - * Creates a BaseController instance. - * - * @param options - * @param options.messenger - Controller messaging system - * @param options.metadata - State metadata, describing how to "anonymize" the state, and which - * parts should be persisted. - * @param options.name - The name of the controller, used as a namespace for events and actions - * @param options.state - Initial controller state - */ - constructor({ - messenger, - metadata, - name, - state, - }: { - messenger: messenger; - metadata: StateMetadata; - name: N; - state: IsJsonable; - }); - - /** - * Retrieves current controller state - * - * @returns - Current state - */ - get state(): IsJsonable; - - set state(_: IsJsonable); - - /** - * Updates controller state. Accepts a callback that is passed a draft copy - * of the controller state. If a value is returned, it is set as the new - * state. Otherwise, any changes made within that callback to the draft are - * applied to the controller state. - * - * @param callback - Callback for updating state, passed a draft state - * object. Return a new state object or mutate the draft to update state. - */ - protected update( - callback: (state: Draft>) => void | IsJsonable, - ): void; - - /** - * Prepares the controller for garbage collection. This should be extended - * by any subclasses to clean up any additional connections or events. - * - * The only cleanup performed here is to remove listeners. While technically - * this is not required to ensure this instance is garbage collected, it at - * least ensures this instance won't be responsible for preventing the - * listeners from being garbage collected. - */ - protected destroy(): void; -} -/** - * Returns an anonymized representation of the controller state. - * - * By "anonymized" we mean that it should not contain any information that could be personally - * identifiable. - * - * @param state - The controller state - * @param metadata - The controller state metadata, which describes how to derive the - * anonymized state - * @returns The anonymized controller state - */ -export declare function getAnonymizedState>( - state: IsJsonable, - metadata: StateMetadata, -): IsJsonable>; -/** - * Returns the subset of state that should be persisted - * - * @param state - The controller state - * @param metadata - The controller state metadata, which describes which pieces of state should be persisted - * @returns The subset of controller state that should be persisted - */ -export declare function getPersistentState>( - state: IsJsonable, - metadata: StateMetadata, -): IsJsonable>; -export {}; From a761273a42fd9738e43092dbf52f5e7da81dd242 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 1 Sep 2021 15:52:53 -0700 Subject: [PATCH 039/147] Add permission system readme, rename some things --- .../permissions/PermissionController.test.ts | 4 +- .../src/permissions/PermissionController.ts | 16 +- .../src/permissions/PermissionEnforcer.ts | 16 +- .../controllers/src/permissions/README.md | 153 ++++++++++++++++++ 4 files changed, 172 insertions(+), 17 deletions(-) create mode 100644 packages/controllers/src/permissions/README.md diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 336e0d2cf0..7f35ebcfc2 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -39,6 +39,7 @@ function getCaveatSpecifications(): CaveatSpecifications { ) => async (args: RestrictedMethodArgs) => { const result = await method(args); + // TODO: Throw error if this isn't an array return Array.isArray(result) ? result.filter(caveat.value.includes) : result; @@ -63,7 +64,7 @@ function getPermissionController(): PermissionController { caveatSpecifications: getCaveatSpecifications(), messenger: getRestrictedMessenger(), permissionSpecifications: getPermissionSpecifications(), - safeMethods: ['wallet_safeMethod'], + unrestrictedMethods: ['wallet_unrestrictedMethod'], }); } @@ -72,6 +73,7 @@ describe('PermissionController', () => { it('initializes a new PermissionController', () => { const controller = getPermissionController(); expect(controller.state).toStrictEqual({ subjects: {} }); + controller.enforcer.executeRestrictedMethod('foo', 'foo'); expect(controller.enforcer instanceof PermissionEnforcer).toStrictEqual( true, ); diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index ca9c34c5bb..7eabba4ee9 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -115,7 +115,7 @@ type PermissionControllerOptions = { messenger: PermissionControllerMessenger; caveatSpecifications: CaveatSpecifications; permissionSpecifications: PermissionSpecifications; - safeMethods: string[]; + unrestrictedMethods: string[]; state?: Partial; }; @@ -149,10 +149,10 @@ export class PermissionController extends BaseController< return this._permissionSpecifications; } - private readonly _safeMethods: ReadonlySet; + private readonly _unrestrictedMethods: ReadonlySet; - public get safeMethods(): ReadonlySet { - return this._safeMethods; + public get unrestrictedMethods(): ReadonlySet { + return this._unrestrictedMethods; } constructor({ @@ -160,7 +160,7 @@ export class PermissionController extends BaseController< state = {}, caveatSpecifications, permissionSpecifications, - safeMethods, + unrestrictedMethods, }: PermissionControllerOptions) { super({ name: controllerName, @@ -173,7 +173,7 @@ export class PermissionController extends BaseController< this._permissionSpecifications = deepFreeze({ ...permissionSpecifications, }); - this._safeMethods = new Set(safeMethods); + this._unrestrictedMethods = new Set(unrestrictedMethods); this.registerMessageHandlers(); this._enforcer = this.constructEnforcer(); @@ -624,8 +624,8 @@ export class PermissionController extends BaseController< isRestrictedMethod: (method: string) => { return Boolean(this.getTargetKey(method)); }, - isSafeMethod: (method: string) => { - return this.safeMethods.has(method); + isUnrestrictedMethod: (method: string) => { + return this.unrestrictedMethods.has(method); }, requestUserApproval: async (permissionsRequest: PermissionsRequest) => { const { origin, id } = permissionsRequest.metadata; diff --git a/packages/controllers/src/permissions/PermissionEnforcer.ts b/packages/controllers/src/permissions/PermissionEnforcer.ts index 05e9cea32a..12e75aa2c9 100644 --- a/packages/controllers/src/permissions/PermissionEnforcer.ts +++ b/packages/controllers/src/permissions/PermissionEnforcer.ts @@ -42,7 +42,7 @@ export type PermissionsRequest = { }; type IsRestrictedMethod = (method: string) => boolean; -type IsSafeMethod = (method: string) => boolean; +type IsUnrestrictedMethod = (method: string) => boolean; type RequestUserApproval = ( permissionsRequest: PermissionsRequest, ) => Promise; @@ -50,7 +50,7 @@ type RequestUserApproval = ( type PermissionEnforcerArgs = { caveatSpecifications: CaveatSpecifications; isRestrictedMethod: IsRestrictedMethod; - isSafeMethod: IsSafeMethod; + isUnrestrictedMethod: IsUnrestrictedMethod; getPermission: PermissionController['getPermission']; getRestrictedMethodImplementation: PermissionController['getRestrictedMethodImplementation']; grantPermissions: PermissionController['grantPermissions']; @@ -65,7 +65,7 @@ export class PermissionEnforcer { private isRestrictedMethod: IsRestrictedMethod; - private isSafeMethod: IsSafeMethod; + private isUnrestrictedMethod: IsUnrestrictedMethod; private getPermission: PermissionController['getPermission']; @@ -84,7 +84,7 @@ export class PermissionEnforcer { constructor({ caveatSpecifications, isRestrictedMethod, - isSafeMethod, + isUnrestrictedMethod, getPermission, getRestrictedMethodImplementation, grantPermissions, @@ -95,7 +95,7 @@ export class PermissionEnforcer { }: PermissionEnforcerArgs) { this.caveatSpecifications = caveatSpecifications; this.isRestrictedMethod = isRestrictedMethod; - this.isSafeMethod = isSafeMethod; + this.isUnrestrictedMethod = isUnrestrictedMethod; this.getPermission = getPermission; this.grantPermissions = grantPermissions; this.requestUserApproval = requestUserApproval; @@ -243,7 +243,7 @@ export class PermissionEnforcer { async executeRestrictedMethod( origin: string, method: string, - params: Json, + params?: Json, ): Promise { const id = nanoid(); const req: JsonRpcRequest = { id, jsonrpc: '2.0', method }; @@ -325,7 +325,7 @@ export class PermissionEnforcer { )({ method, params, context: { origin } }); } - getPermissionsMiddleware( + createPermissionMiddleware( subject: PermissionsSubjectMetadata, ): JsonRpcMiddleware { const permissionsMiddleware = async ( @@ -336,7 +336,7 @@ export class PermissionEnforcer { const { method } = req; // skip registered safe/passthrough methods. - if (this.isSafeMethod(method)) { + if (this.isUnrestrictedMethod(method)) { return next(); } diff --git a/packages/controllers/src/permissions/README.md b/packages/controllers/src/permissions/README.md new file mode 100644 index 0000000000..c1185366a1 --- /dev/null +++ b/packages/controllers/src/permissions/README.md @@ -0,0 +1,153 @@ +# PermissionController + +The `PermissionController` is the heart of an object capability-inspired permission system. +It is the successor of the original MetaMask permission system, [`rpc-cap`](https://github.com/MetaMask/rpc-cap). + +## Conceptual Overview + +The permission system itself belongs to a **host**, and it mediates the access to resources – called **targets** – of distinct **subjects**. +A target can belong to the host itself, or another subject. + +When a subject requests access to a target, we say that they **invoke** it. +The system ensures that subjects can only invoke a target if they have the **permission** to do so. +Permissions are associated with a subject and target, and they are part of the state of the permission system. + +Permissions can have **caveats**, which are host-defined attenuations of the authority a permission grants over a particular target. + +## Implementation Overview + +Permission system concepts correspond to components of the MetaMask stack as follows: + +| Concept | Implementation | +| :---------------- | :--------------------------------------------------------------------------------------- | +| Host | The MetaMask application | +| Subjects | Websites, Snaps, or other extensions | +| Targets | JSON-RPC methods | +| Invocations | JSON-RPC requests | +| Permissions | Permission objects | +| Caveats | Caveat objects | +| Permission system | The `PermissionController`, `PermissionEnforcer`, and their `json-rpc-engine` middleware | + +For the time being, JSON-RPC methods are the only kind of permission target that exists. +Therefore, our implementation of the permission system revolves around JSON-RPC requests and MetaMask's `json-rpc-engine` stack. +To use the permission system, every JSON-RPC method must be enumerated and designated as either "restricted" or "unrestricted". +Unrestricted methods can always be called by anyone. +Restricted methods require the requisite permission in order to be called. + +At any given moment, the `PermissionController` state tree describes the complete state of the permissions of all subjects known to the current MetaMask instance. +The `PermissionController` also provides methods for adding, updating, and removing permissions. +The controller has a sibling module, the `PermissionEnforcer`, which furnishes methods for applying the rules described by the controller state to incoming JSON-RPC requests. +The also provides a `json-rpc-engine` middleware so that we can apply its logic to incoming JSON-RPC requests. + +Once the permission middleware is injected into our middleware stack, every JSON-RPC request will be handled in one of the following ways: + +- If the requested method is neither restricted nor unrestricted, the request will be rejected with a `methodNotFound` error. +- If the requested method is unrestricted, it will pass through the middleware unmodified. +- If the requested method is restricted, the enforcer will attempt to get the permission corresponding to the subject and target, and: + - If a permission is found, call the corresponding method with the request parameters. + - If a permission is not found, reject the request with an `unauthorized` error. + +### Caveats + +Caveats are objects associated with permissions that impose arbitrary restrictions on restricted method requests. +Every permission has a `caveats` field, which is either an array of caveats or `null`. +Every caveat has a string `type`, and every type has an associated function that is used to apply the caveat to a restricted method request. +When the `PermissionController` is constructed, the consumer specifies the available caveat types and their implementations. + +> In `rpc-cap`, caveat types were specified by `rpc-cap` itself, and the consumer had to choose from the available types. +> This did not end up being easy to reason about or work with in practice. +> Now, caveat types and their implementations are provided by the consumer, just like the restricted methods and their implementations. + +## Examples + +### Construction + +```typescript +// To construct a permission controller, we first need to define the caveat +// types and restricted methods + +const caveatSpecifications = { + filterArrayResponse: { + type: 'filterArrayResponse', + // If a permission has any caveats, its corresponding restricted method + // implementation is decorated / wrapped with the implementations of its + // caveats, using the caveat's decorator function. + decorator: (( + // Restricted methods and other caveats can be async, so we have to + // assume that the method is async. + method: AsyncRestrictedMethodImplementation, + caveat: { type: 'filterArrayResponse'; value: string[] }, + ) => + async (args: RestrictedMethodArgs) => { + const result = await method(args); + return Array.isArray(result) + ? result.filter(caveat.value.includes) + : result; + }) as CaveatDecorator, + }, +}; + +const permissionSpecifications = { + wallet_getSecretArray: { + // i.e. the restricted method name + target: 'wallet_getSecretArray', + methodImplementation: (_args: RestrictedMethodArgs) => { + return ['secret1', 'secret2', 'secret3']; + }, + }, +}; + +const permissionController = new PermissionController({ + caveatSpecifications, + messenger: controllerMessenger, // assume this was given + permissionSpecifications, + unrestrictedMethods: ['wallet_unrestrictedMethod'], +}); + +const permissionEnforcer = permissionController.enforcer; +``` + +### Adding the Permission Middleware + +```typescript +// This should take place where a middleware stack is created for a particular +// subject. + +// The subject could be a port, stream, socket, etc. +const origin = getOrigin(subject); + +const engine = new JsonRpcEngine(); +engine.push(/* your various middleware*/); +engine.push(permissionEnforcer.createPermissionMiddleware({ origin })); +// Your middleware stack is now permissioned +``` + +### Calling a Restricted Method Internally + +```typescript +// Sometimes, we need to call a restricted method internally, as a particular subject. +permissionEnforcer.executeRestrictedMethod(origin, 'wallet_getSecretArray'); + +// If the restricted method has any parameters, they are given as the third +// argument to executeRestrictedMethod(). +permissionEnforcer.executeRestrictedMethod(origin, 'wallet_getSecret', { + secretType: 'array', +}); +``` + +### Requesting and Getting Permissions + +```typescript +// From the perspective of subjects, requesting and getting permissions +// works the same as it does with `rpc-cap`. +const approvedPermissions = await ethereum.request({ + method: 'wallet_requestPermissions', + params: [{ + wallet_getSecretArray: {}, + }] +}) + +const existingPermissions = await ethereum.request({ + method: 'wallet_getPermissions', +) +``` From db1fd30fc2d766c8e4f6c84c667e9b9887242f8b Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sat, 11 Sep 2021 14:06:07 -0700 Subject: [PATCH 040/147] Post-rebase fixup --- packages/controllers/.eslintrc.js | 9 --------- packages/controllers/jest.config.js | 2 +- .../src/permissions/PermissionController.test.ts | 1 - .../src/permissions/rpc-methods/getPermissions.ts | 2 +- .../src/permissions/rpc-methods/requestPermissions.ts | 2 +- 5 files changed, 3 insertions(+), 13 deletions(-) diff --git a/packages/controllers/.eslintrc.js b/packages/controllers/.eslintrc.js index c7e798f666..884ff91cf1 100644 --- a/packages/controllers/.eslintrc.js +++ b/packages/controllers/.eslintrc.js @@ -4,13 +4,4 @@ module.exports = { rules: { '@typescript-eslint/consistent-type-definitions': 'off', }, - - overrides: [ - { - files: ['**/*.d.ts'], - rules: { - 'import/unambiguous': 'off', - }, - }, - ], }; diff --git a/packages/controllers/jest.config.js b/packages/controllers/jest.config.js index 4055994997..f459441f41 100644 --- a/packages/controllers/jest.config.js +++ b/packages/controllers/jest.config.js @@ -11,7 +11,7 @@ module.exports = { // }, // }, silent: true, - testTimeout: 2500, + testTimeout: 5000, projects: [ { displayName: 'runner: electron', diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 7f35ebcfc2..69db571cba 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -73,7 +73,6 @@ describe('PermissionController', () => { it('initializes a new PermissionController', () => { const controller = getPermissionController(); expect(controller.state).toStrictEqual({ subjects: {} }); - controller.enforcer.executeRestrictedMethod('foo', 'foo'); expect(controller.enforcer instanceof PermissionEnforcer).toStrictEqual( true, ); diff --git a/packages/controllers/src/permissions/rpc-methods/getPermissions.ts b/packages/controllers/src/permissions/rpc-methods/getPermissions.ts index fbe2c8bfbd..2dadb56a91 100644 --- a/packages/controllers/src/permissions/rpc-methods/getPermissions.ts +++ b/packages/controllers/src/permissions/rpc-methods/getPermissions.ts @@ -1,4 +1,4 @@ -import type { PermittedHandlerExport } from '@mm-snap/rpc-methods/types'; +import type { PermittedHandlerExport } from '@metamask/rpc-methods/types'; import type { JsonRpcEngineEndCallback, PendingJsonRpcResponse, diff --git a/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts b/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts index bb6e6179a7..927c67571e 100644 --- a/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts +++ b/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts @@ -1,4 +1,4 @@ -import type { PermittedHandlerExport } from '@mm-snap/rpc-methods/types'; +import type { PermittedHandlerExport } from '@metamask/rpc-methods/types'; import type { JsonRpcEngineEndCallback, JsonRpcRequest, From fc99e7dbddd6e1e62338028ee1210d2e48d27511 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 21 Sep 2021 16:25:56 -0700 Subject: [PATCH 041/147] Stop using 'in' operator --- .../src/permissions/PermissionController.ts | 10 +++++----- .../controllers/src/permissions/PermissionEnforcer.ts | 7 ++++--- packages/controllers/src/utils.ts | 11 ++++++++--- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 7eabba4ee9..d6a8ce5502 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -10,7 +10,7 @@ import type { Patch } from 'immer'; import deepFreeze from 'deep-freeze-strict'; import { Json } from 'json-rpc-engine'; -import { isPlainObject } from '../utils'; +import { isPlainObject, hasProperty } from '../utils'; import { Caveat, CaveatSpecifications, constructCaveat } from './Caveat'; import { OriginString, @@ -455,7 +455,7 @@ export class PermissionController extends BaseController< * @returns The internal key of the method. */ getTargetKey(method: string): string { - if (method in this._permissionSpecifications) { + if (hasProperty(this._permissionSpecifications, method)) { return method; } @@ -475,13 +475,13 @@ export class PermissionController extends BaseController< while ( segments.length > 0 && - !(targetKey in this._permissionSpecifications) && + !hasProperty(this._permissionSpecifications, targetKey) && !wildCardMethodsWithoutWildCard[targetKey] ) { targetKey += `${segments.shift()}_`; } - if (targetKey in this._permissionSpecifications) { + if (hasProperty(this._permissionSpecifications, targetKey)) { return targetKey; } else if (wildCardMethodsWithoutWildCard[targetKey]) { return `${targetKey}*`; @@ -604,7 +604,7 @@ export class PermissionController extends BaseController< ); } - if (!('value' in requestedCaveat)) { + if (!hasProperty(requestedCaveat, 'value')) { throw new CaveatMissingValueError(requestedCaveat, origin, target); } diff --git a/packages/controllers/src/permissions/PermissionEnforcer.ts b/packages/controllers/src/permissions/PermissionEnforcer.ts index 12e75aa2c9..aaf7d3b4d1 100644 --- a/packages/controllers/src/permissions/PermissionEnforcer.ts +++ b/packages/controllers/src/permissions/PermissionEnforcer.ts @@ -9,6 +9,7 @@ import { } from 'json-rpc-engine'; import { nanoid } from 'nanoid'; +import { isPlainObject, hasProperty } from '../utils'; import { CaveatSpecifications, decorateWithCaveats } from './Caveat'; import { internalError, @@ -377,8 +378,8 @@ function resultIsError(resultOrError: Json | Error): resultOrError is Error { return Boolean( resultOrError instanceof Error || (resultOrError && - typeof resultOrError === 'object' && - 'message' in resultOrError && - 'code' in resultOrError), + isPlainObject(resultOrError) && + hasProperty(resultOrError, 'message') && + hasProperty(resultOrError, 'code')), ); } diff --git a/packages/controllers/src/utils.ts b/packages/controllers/src/utils.ts index adfefcb8ed..53b5a4f5ab 100644 --- a/packages/controllers/src/utils.ts +++ b/packages/controllers/src/utils.ts @@ -1,5 +1,10 @@ -export function isPlainObject( - value: unknown, -): value is Record { +type PlainObject = Record; + +export function isPlainObject(value: unknown): value is PlainObject { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); } + +export const hasProperty = ( + object: PlainObject, + key: string | number | symbol, +) => Reflect.hasOwnProperty.call(object, key); From 8a07f6488c54a73db73ac6d242e7ed332920cf1d Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 21 Sep 2021 21:47:42 -0700 Subject: [PATCH 042/147] WIP: Improved type safety 1 (unstable) --- .../controllers/src/permissions/Caveat.ts | 26 +- .../controllers/src/permissions/Permission.ts | 37 ++- .../src/permissions/PermissionController.ts | 255 +++++++++++++----- 3 files changed, 237 insertions(+), 81 deletions(-) diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index b1726e407c..e35a9c90c6 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -63,7 +63,10 @@ export type CaveatValidator = ( caveat: Caveat, ) => void; -export type CaveatSpecification = { +export type CaveatSpecification< + CaveatType extends string, + CaveatValue extends Json, +> = { /** * The string type of the caveat. */ @@ -90,9 +93,28 @@ export type CaveatSpecification = { }; export type CaveatSpecifications = Readonly< - Record> + Record> >; +export type GenericCaveat = Caveat; + +export type CaveatSpec> = CaveatSpecification< + Caveats['type'], + ExtractCaveatValue +>; + +export type CaveatSpecs> = Record< + Caveats['type'], + CaveatSpec +>; + +export type ExtractCaveatValue = Caveat extends { + type: CaveatType; + value: infer CaveatValue; +} + ? CaveatValue + : never; + /** * Decorate a restricted method implementation with its caveats. * diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 767439d3d2..83ec3da784 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -1,7 +1,7 @@ import type { EthereumRpcError } from 'eth-rpc-errors'; import { Json } from 'json-rpc-engine'; import { nanoid } from 'nanoid'; -import { Caveat, ZcapLdCaveat } from './Caveat'; +import { Caveat, GenericCaveat, ZcapLdCaveat } from './Caveat'; /** * The origin of a subject. @@ -72,7 +72,7 @@ type ZcapLdCapability = { readonly proof?: string; }; -type PermissionOptions = { +export type PermissionOptions = { /** * The method that the permission corresponds to. */ @@ -154,11 +154,14 @@ export function constructPermission(options: PermissionOptions) { * @param caveatType The type of the caveat to retrieve. * @returns The caveat value, or undefined if no such caveat exists. */ -export function findCaveat( +export function findCaveat>( permission: Permission, - caveatType: string, -): Caveat | undefined { - return permission.caveats?.find((caveat) => caveat.type === caveatType); + caveatType: TargetCaveat['type'], +): TargetCaveat | undefined { + // TODO:types + return permission.caveats?.find( + (caveat) => caveat.type === caveatType, + ) as any; } type RequestedPermission = { @@ -202,6 +205,7 @@ export type RestrictedMethodImplementation< | AsyncRestrictedMethodImplementation; export type PermissionSpecification< + Target extends string, FactoryOptions extends PermissionOptions, RequestData extends Record, MethodImplementation extends RestrictedMethodImplementation, @@ -210,7 +214,7 @@ export type PermissionSpecification< * The target resource of the permission. In other words, at the time of * writing, the RPC method name. */ - target: string; + target: Target | `${Target}*`; /** * The factory function used to get permission objects. Permissions returned @@ -243,8 +247,27 @@ export type PermissionSpecification< export type PermissionSpecifications = Record< MethodName, PermissionSpecification< + string, PermissionOptions, Record, RestrictedMethodImplementation > >; + +export type PermConstraint = Omit< + Permission, + 'caveats' +> & { + caveats: AllowedCaveat extends never ? null : AllowedCaveat[]; +}; + +export type GenericPerm = PermConstraint; + +export type PermSpec = PermissionSpecification< + Target, + PermissionOptions, + Record, + RestrictedMethodImplementation +>; + +export type PermSpecs = Record>; diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index d6a8ce5502..3d404dccda 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -11,16 +11,27 @@ import deepFreeze from 'deep-freeze-strict'; import { Json } from 'json-rpc-engine'; import { isPlainObject, hasProperty } from '../utils'; -import { Caveat, CaveatSpecifications, constructCaveat } from './Caveat'; +import { + Caveat as CaveatConstraint, + CaveatSpecs, + constructCaveat, + ExtractCaveatValue, + GenericCaveat, +} from './Caveat'; import { OriginString, - Permission, + Permission as PermissionConstraint, MethodName, RequestedPermissions, RestrictedMethodImplementation, - PermissionSpecifications, + PermissionOptions, + PermissionSpecifications as PermissionSpecificationsConstraint, + PermissionSpecification as PermissionSpecificationConstraint, findCaveat, constructPermission, + GenericPerm, + PermSpec, + PermSpecs, } from './Permission'; import { PermissionDoesNotExistError, @@ -39,45 +50,47 @@ import { import { PermissionEnforcer, PermissionsRequest } from './PermissionEnforcer'; import { MethodNames } from './enums'; -export type PermissionsSubjectMetadata = { +export type PermissionSubjectMetadata = { origin: OriginString; }; const controllerName = 'PermissionController'; -export type SubjectPermissions = Record; +export type SubjectPermissions = Record< + Permission['parentCapability'], + Permission +>; -export type PermissionsSubjectEntry = { - permissions: SubjectPermissions; -} & PermissionsSubjectMetadata; +export type PermissionSubjectEntry = { + permissions: SubjectPermissions; +} & PermissionSubjectMetadata; -export type PermissionControllerSubjects = Record< - OriginString, - PermissionsSubjectEntry ->; +export type PermissionControllerSubjects = + Record>; // TODO: TypeScript doesn't understand that a given subject may not exist. Can we fix it? // Yes we can, in TS4.4 -export type PermissionControllerState = { - subjects: PermissionControllerSubjects; +export type PermissionControllerState = { + subjects: PermissionControllerSubjects; }; const stateMetadata = { subjects: { persist: true, anonymous: true }, }; -const defaultState: PermissionControllerState = { +// TODO:types +const defaultState: PermissionControllerState = { subjects: {}, }; export type GetPermissionsState = { type: `${typeof controllerName}:getState`; - handler: () => PermissionControllerState; + handler: () => PermissionControllerState; }; export type GetSubjects = { type: `${typeof controllerName}:getSubjects`; - handler: () => (keyof PermissionControllerSubjects)[]; + handler: () => (keyof PermissionControllerSubjects)[]; }; export type ClearPermissions = { @@ -92,7 +105,7 @@ export type PermissionControllerActions = export type PermissionsStateChange = { type: `${typeof controllerName}:stateChange`; - payload: [PermissionControllerState, Patch[]]; + payload: [PermissionControllerState, Patch[]]; }; export type PermissionControllerEvents = PermissionsStateChange; @@ -111,29 +124,86 @@ export type PermissionControllerMessenger = RestrictedControllerMessenger< never >; -type PermissionControllerOptions = { +type ExtractPermissionOptions< + PSpec, + Perm extends GenericPerm, +> = PSpec extends PermissionSpecificationConstraint< + Perm['parentCapability'], + infer PermOpts, + Record, + RestrictedMethodImplementation +> + ? PermOpts + : never; + +type ExtractRequestData< + PSpec, + Perm extends GenericPerm, +> = PSpec extends PermissionSpecificationConstraint< + Perm['parentCapability'], + PermissionOptions, + infer RequestData, + RestrictedMethodImplementation +> + ? RequestData + : never; + +type ExtractMethodImplementation< + PSpec, + Perm extends GenericPerm, +> = PSpec extends PermissionSpecificationConstraint< + Perm['parentCapability'], + PermissionOptions, + Record, + infer Impl +> + ? Impl + : never; + +type ExtractValidCaveats< + Perm extends GenericPerm, + Target extends string, +> = Perm extends { + parentCapability: Target; + caveats: (infer ValidCaveats)[]; +} + ? ValidCaveats + : never; + +type PermissionControllerOptions< + Targets extends string, + Permission extends GenericPerm, + Caveat extends GenericCaveat, +> = { messenger: PermissionControllerMessenger; - caveatSpecifications: CaveatSpecifications; - permissionSpecifications: PermissionSpecifications; + caveatSpecifications: CaveatSpecs; + permissionSpecifications: Record< + Permission['parentCapability'], + PermSpec + >; unrestrictedMethods: string[]; - state?: Partial; + state?: Partial>; }; type GrantPermissionsOptions = { approvedPermissions: RequestedPermissions; - subject: PermissionsSubjectMetadata; + subject: PermissionSubjectMetadata; shouldPreserveExistingPermissions?: boolean; requestData?: Record; }; -export class PermissionController extends BaseController< +export class PermissionController< + Targets extends string, + Permission extends GenericPerm, + Caveat extends CaveatConstraint, +> extends BaseController< typeof controllerName, - PermissionControllerState, + PermissionControllerState, PermissionControllerMessenger > { - private readonly _caveatSpecifications: Readonly; + private readonly _caveatSpecifications: Readonly>; - public get caveatSpecifications(): Readonly { + public get caveatSpecifications(): Readonly> { return this._caveatSpecifications; } @@ -143,9 +213,9 @@ export class PermissionController extends BaseController< return this._enforcer; } - private readonly _permissionSpecifications: Readonly; + private readonly _permissionSpecifications: Readonly>; - public get permissionSpecifications(): Readonly { + public get permissionSpecifications(): Readonly> { return this._permissionSpecifications; } @@ -161,12 +231,15 @@ export class PermissionController extends BaseController< caveatSpecifications, permissionSpecifications, unrestrictedMethods, - }: PermissionControllerOptions) { + }: PermissionControllerOptions) { super({ name: controllerName, metadata: stateMetadata, messenger, - state: { ...defaultState, ...state }, + state: { + ...(defaultState as PermissionControllerState), + ...state, + }, }); this._caveatSpecifications = deepFreeze({ ...caveatSpecifications }); @@ -201,7 +274,7 @@ export class PermissionController extends BaseController< }); } - getSubjects(): (keyof PermissionControllerSubjects)[] { + getSubjects(): OriginString[] { return Object.keys(this.state.subjects); } @@ -213,15 +286,23 @@ export class PermissionController extends BaseController< * @param target - The method name as invoked by a third party (i.e., not a method key). * @returns The permission if it exists, or undefined otherwise. */ - getPermission(origin: string, target: string): Permission | undefined { - return this.state.subjects[origin]?.permissions[target]; + getPermission( + origin: string, + target: SubjectPermission['parentCapability'], + ): SubjectPermission | undefined { + return this.state.subjects[origin]?.permissions[ + target + ] as SubjectPermission; } getPermissions(origin: string): Record | undefined { return this.state.subjects[origin]?.permissions; } - hasPermission(origin: string, target: string): boolean { + hasPermission( + origin: string, + target: Permission['parentCapability'], + ): boolean { return Boolean(this.getPermission(origin, target)); } @@ -231,7 +312,7 @@ export class PermissionController extends BaseController< private validatePermission(permission: Permission): void { const specification = this.getPermissionSpecification( - this.getTargetKey(permission.parentCapability), + this.getTargetKey(permission.parentCapability) as Targets, ); specification.validator?.(permission); } @@ -253,7 +334,8 @@ export class PermissionController extends BaseController< ): void { this.update((draftState) => { if (!draftState.subjects[origin]) { - draftState.subjects[origin] = { origin, permissions: {} }; + // TODO:types + draftState.subjects[origin] = { origin, permissions: {} as any }; } if (shouldPreserveExistingPermissions) { @@ -265,14 +347,17 @@ export class PermissionController extends BaseController< }); } - revokePermission(origin: string, target: string): void { + revokePermission( + origin: string, + target: Permission['parentCapability'], + ): void { this.update((draftState) => { if (!draftState.subjects[origin]) { throw new UnrecognizedSubjectError(origin); } const { permissions } = draftState.subjects[origin]; - if (!permissions[target]) { + if (!hasProperty(permissions as Record, target)) { throw new PermissionDoesNotExistError(origin, target); } @@ -286,7 +371,7 @@ export class PermissionController extends BaseController< }); } - revokePermissionForAllSubjects(target: string): void { + revokePermissionForAllSubjects(target: Permission['parentCapability']): void { if (this.getSubjects().length === 0) { return; } @@ -295,9 +380,9 @@ export class PermissionController extends BaseController< Object.values(draftState.subjects).forEach((subject) => { const { permissions } = subject; - // Typecast: ts(2589) - if (permissions[target]) { + if (hasProperty(permissions as Record, target)) { this.deletePermission( + // Typecast: ts(2589) draftState as any, permissions as any, origin, @@ -309,10 +394,10 @@ export class PermissionController extends BaseController< } private deletePermission( - draftState: PermissionControllerState, - permissions: SubjectPermissions, + draftState: PermissionControllerState, + permissions: SubjectPermissions, origin: string, - target: string, + target: Permission['parentCapability'], ): void { if (Object.keys(permissions).length > 1) { delete permissions[target]; @@ -330,7 +415,10 @@ export class PermissionController extends BaseController< }); } - hasCaveat(origin: string, target: string, caveatType: string): boolean { + hasCaveat< + Target extends Permission['parentCapability'], + CaveatType extends ExtractValidCaveats, + >(origin: string, target: Target, caveatType: CaveatType['type']): boolean { return ( this.getPermission(origin, target)?.caveats?.some( (caveat) => caveat.type === caveatType, @@ -338,11 +426,14 @@ export class PermissionController extends BaseController< ); } - addCaveat( + addCaveat< + Target extends Permission['parentCapability'], + CaveatType extends ExtractValidCaveats, + >( origin: string, - target: string, - caveatType: string, - caveatValue: Json, + target: Target, + caveatType: CaveatType['type'], + caveatValue: ExtractCaveatValue, ): void { if (this.hasCaveat(origin, target, caveatType)) { throw new CaveatAlreadyExistsError(origin, target, caveatType); @@ -351,11 +442,14 @@ export class PermissionController extends BaseController< this.setCaveat(origin, target, caveatType, caveatValue); } - updateCaveat( + updateCaveat< + Target extends Permission['parentCapability'], + CaveatType extends ExtractValidCaveats, + >( origin: string, - target: string, - caveatType: string, - caveatValue: Json, + target: Target, + caveatType: CaveatType['type'], + caveatValue: ExtractCaveatValue, ): void { if (!this.hasCaveat(origin, target, caveatType)) { throw new CaveatDoesNotExistError(origin, target, caveatType); @@ -364,11 +458,14 @@ export class PermissionController extends BaseController< this.setCaveat(origin, target, caveatType, caveatValue); } - private getCaveat( + private getCaveat< + Target extends Permission['parentCapability'], + CaveatType extends ExtractValidCaveats, + >( origin: string, - target: string, - caveatType: string, - ): Caveat | undefined { + target: Target, + caveatType: CaveatType['type'], + ): CaveatType | undefined { const permission = this.getPermission(origin, target); if (!permission) { throw new PermissionDoesNotExistError(origin, target); @@ -377,14 +474,20 @@ export class PermissionController extends BaseController< return findCaveat(permission, caveatType); } - private setCaveat( + private setCaveat< + Target extends Permission['parentCapability'], + CaveatType extends ExtractValidCaveats, + >( origin: string, - target: string, - caveatType: string, - caveatValue: Json, + target: Target, + caveatType: CaveatType['type'], + caveatValue: ExtractCaveatValue, ): void { this.update((draftState) => { - const permission = draftState.subjects[origin]?.permissions[target]; + // TODO:types + const permission = (draftState.subjects[origin] as any)?.permissions[ + target + ] as Permission; /* istanbul ignore if: This should be impossible, but TypeScript wants it */ if (!permission) { throw new PermissionDoesNotExistError(origin, target); @@ -415,9 +518,14 @@ export class PermissionController extends BaseController< }); } - removeCaveat(origin: string, target: string, caveatType: string): void { + removeCaveat< + Target extends Permission['parentCapability'], + CaveatType extends ExtractValidCaveats, + >(origin: string, target: Target, caveatType: CaveatType['type']): void { this.update((draftState) => { - const permission = draftState.subjects[origin]?.permissions[target]; + // TODO:types + const permission: Permission = (draftState.subjects[origin] as any) + ?.permissions[target]; if (!permission) { throw new PermissionDoesNotExistError(origin, target); } @@ -454,9 +562,9 @@ export class PermissionController extends BaseController< * @param method string - The requested RPC method. * @returns The internal key of the method. */ - getTargetKey(method: string): string { + getTargetKey(method: Permission['parentCapability']): Targets | '' { if (hasProperty(this._permissionSpecifications, method)) { - return method; + return method as Targets; } const wildCardMethodsWithoutWildCard: Record = {}; @@ -482,9 +590,9 @@ export class PermissionController extends BaseController< } if (hasProperty(this._permissionSpecifications, targetKey)) { - return targetKey; + return targetKey as Targets; } else if (wildCardMethodsWithoutWildCard[targetKey]) { - return `${targetKey}*`; + return `${targetKey}*` as Targets; } return ''; @@ -498,10 +606,11 @@ export class PermissionController extends BaseController< return undefined; } - return this.getPermissionSpecification(method).methodImplementation; + return this.getPermissionSpecification(method as Targets) + .methodImplementation; } - private getPermissionSpecification(targetKey: string) { + private getPermissionSpecification(targetKey: Targets) { const specification = this._permissionSpecifications[targetKey]; /* istanbul ignore if: This should be impossible, but TypeScript wants it */ if (!specification) { @@ -513,7 +622,9 @@ export class PermissionController extends BaseController< return specification; } - grantPermissions(opts: GrantPermissionsOptions): SubjectPermissions { + grantPermissions( + opts: GrantPermissionsOptions, + ): SubjectPermissions { const { requestData, approvedPermissions, From fd98a4af297edd73b0c3a51cc2a8f0d0930b5ab9 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 22 Sep 2021 16:22:43 -0700 Subject: [PATCH 043/147] WIP: Improved type safety 2 (builds) --- .../controllers/src/permissions/Caveat.ts | 35 +- .../controllers/src/permissions/Permission.ts | 172 +++++--- .../permissions/PermissionController.test.ts | 37 +- .../src/permissions/PermissionController.ts | 412 +++++++++--------- .../src/permissions/PermissionEnforcer.ts | 87 +++- .../controllers/src/permissions/errors.ts | 18 +- .../permissions/rpc-methods/getPermissions.ts | 3 +- 7 files changed, 451 insertions(+), 313 deletions(-) diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index e35a9c90c6..e52715dcca 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -7,8 +7,6 @@ import { RestrictedMethodImplementation, } from './Permission'; -type CaveatType = string; - export type ZcapLdCaveat = { /** * The type of the caveat. The type is presumed to be meaningful in the @@ -16,7 +14,7 @@ export type ZcapLdCaveat = { * * In MetaMask, every permission can only have one caveat of each type. */ - readonly type: CaveatType; + readonly type: string; }; /** @@ -29,7 +27,7 @@ export type Caveat = ZcapLdCaveat & { * * TODO: Make optional in typescript@4.4.x */ - readonly value: Value | null; + readonly value: Value; }; /** @@ -40,11 +38,11 @@ export type Caveat = ZcapLdCaveat & { * @param value - The value associated with the caveat, if any. * @returns The new caveat object. */ -export function constructCaveat( +export function constructCaveat( type: string, - value?: Value, -): Caveat { - return { type, value: value === undefined ? null : value }; + value: unknown, +): { type: string; value: unknown } { + return { type, value }; } // Next, we define types used for specifying caveats at the consumer layer, @@ -57,11 +55,11 @@ export type CaveatDecorator = ( caveat: Caveat, ) => AsyncRestrictedMethodImplementation; -export type CaveatValidator = ( +export type CaveatValidator = ( origin: string, target: string, - caveat: Caveat, -) => void; + caveat: { type: string; value: unknown }, +) => caveat is Caveats; export type CaveatSpecification< CaveatType extends string, @@ -89,11 +87,11 @@ export type CaveatSpecification< * performed. Although caveats can also be validated by permission validators, * validating caveat values separately is strongly recommended. */ - validator?: CaveatValidator; + validator?: CaveatValidator>; }; export type CaveatSpecifications = Readonly< - Record> + Record> >; export type GenericCaveat = Caveat; @@ -108,7 +106,7 @@ export type CaveatSpecs> = Record< CaveatSpec >; -export type ExtractCaveatValue = Caveat extends { +export type ExtractCaveatValue = Caveats extends { type: CaveatType; value: infer CaveatValue; } @@ -122,10 +120,10 @@ export type ExtractCaveatValue = Caveat extends { * Note that all caveat functions (i.e. the argument and return value of the decorator) * must be awaited. */ -export function decorateWithCaveats( +export function decorateWithCaveats( methodImplementation: RestrictedMethodImplementation, permission: Readonly, // bound to the requesting origin - caveatSpecifications: CaveatSpecifications, // all caveat implementations + caveatSpecifications: CaveatSpecs, // all caveat implementations ): RestrictedMethodImplementation { const caveats = permission.caveats ?? []; @@ -133,13 +131,14 @@ export function decorateWithCaveats( Json, Json >; + // TODO:types fix the base caveat types for (const caveat of caveats) { - const specification = caveatSpecifications[caveat.type]; + const specification = caveatSpecifications[caveat.type as Caveats['type']]; if (!specification) { throw new UnrecognizedCaveatTypeError(caveat.type); } - decorated = specification.decorator(decorated, caveat); + decorated = specification.decorator(decorated, caveat as any); } return decorated; } diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 83ec3da784..3ef349a4f0 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -205,69 +205,115 @@ export type RestrictedMethodImplementation< | AsyncRestrictedMethodImplementation; export type PermissionSpecification< - Target extends string, + TargetKey extends string, + Perm extends PermConstraint, FactoryOptions extends PermissionOptions, RequestData extends Record, MethodImplementation extends RestrictedMethodImplementation, -> = { - /** - * The target resource of the permission. In other words, at the time of - * writing, the RPC method name. - */ - target: Target | `${Target}*`; - - /** - * The factory function used to get permission objects. Permissions returned - * by this function are presumed to valid, and they will not be passed to the - * validator function associated with this specification (if any). In other - * words, the factory function should validate the permissions it creates. - * - * If no factory is specified, the {@link Permission} constructor will be - * used, and the validator function (if specified) will be called on newly - * constructed permissions. - */ - factory?: (options: FactoryOptions, requestData?: RequestData) => Permission; - - /** - * The implementation of the restricted method that the permission - * corresponds to. - */ - methodImplementation: MethodImplementation; - - /** - * The validator function used to validate permissions of the associated type - * whenever they are mutated. The only way a permission can be legally mutated - * is when its caveats are modified by the permission controller. - * - * The validator should throw an appropriate JSON-RPC error if validation fails. - */ - validator?: (permission: Permission) => void; -}; - -export type PermissionSpecifications = Record< - MethodName, - PermissionSpecification< - string, - PermissionOptions, - Record, - RestrictedMethodImplementation - > ->; - -export type PermConstraint = Omit< - Permission, - 'caveats' -> & { - caveats: AllowedCaveat extends never ? null : AllowedCaveat[]; -}; - -export type GenericPerm = PermConstraint; - -export type PermSpec = PermissionSpecification< - Target, - PermissionOptions, - Record, - RestrictedMethodImplementation ->; - -export type PermSpecs = Record>; +> = TargetKeyConstraint extends never + ? never + : { + /** + * The target resource of the permission. In other words, at the time of + * writing, the RPC method name. + */ + target: TargetKey; + + /** + * The factory function used to get permission objects. Permissions returned + * by this function are presumed to valid, and they will not be passed to the + * validator function associated with this specification (if any). In other + * words, the factory function should validate the permissions it creates. + * + * If no factory is specified, the {@link Permission} constructor will be + * used, and the validator function (if specified) will be called on newly + * constructed permissions. + */ + factory?: (options: FactoryOptions, requestData?: RequestData) => Perm; + + /** + * The implementation of the restricted method that the permission + * corresponds to. + */ + methodImplementation: MethodImplementation; + + /** + * The validator function used to validate permissions of the associated type + * whenever they are mutated. The only way a permission can be legally mutated + * is when its caveats are modified by the permission controller. + * + * The validator should throw an appropriate JSON-RPC error if validation fails. + */ + validator?: (permission: Permission) => void; + }; + +// export type PermissionSpecifications = Record< +// MethodName, +// PermissionSpecification< +// string, +// GenericPerm, +// PermissionOptions, +// Record, +// RestrictedMethodImplementation +// > +// >; + +export type TargetKeyConstraint = Key extends `${string}_*` + ? Key + : Key extends `${string}_` + ? never + : Key extends `${string}*` + ? never + : Key; + +export type TargetNameConstraint = + Name extends `${string}*` ? never : Name extends `${string}_` ? never : Name; + +// type NotWildCard = Name extends `${string}*` +// ? never +// : Name; + +export type ExtractPermissionTargetNames = + TargetKey extends `${infer Base}_*` ? `${Base}_${string}` : TargetKey; + +// type MyKeys = 'foo' | 'foo2' | 'bar_*' | 'excluded1_' | 'excluded2*'; + +// type MyConstrainedKeys = TargetKeyConstraint + +// type MyNames = TargetNameConstraint>; + +// let foo: NotWildCard<'foo*'>; +// foo. + +// let bar: PermConstraint<'foo*', any>; +// bar. + +export type PermConstraint< + TargetName extends string, + AllowedCaveat extends GenericCaveat | never, +> = TargetNameConstraint extends never + ? never + : Omit & { + caveats: AllowedCaveat extends never ? null : AllowedCaveat[] | null; + parentCapability: TargetName extends `${string}_` + ? `${TargetName}${string}` + : TargetName; + }; + +export type GenericPerm = PermConstraint; + +export type PermSpec = + TargetKeyConstraint extends never + ? never + : PermissionSpecification< + TargetKey, + PermConstraint, + PermissionOptions, + Record, + RestrictedMethodImplementation + >; + +export type PermSpecs = + TargetKeyConstraint extends never + ? never + : Record>; diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 69db571cba..35394a9088 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -1,13 +1,15 @@ import { ControllerMessenger, Json } from '@metamask/controllers'; import { AsyncRestrictedMethodImplementation, + Caveat, CaveatDecorator, - CaveatSpecifications, + CaveatSpecs, + PermConstraint, PermissionController, PermissionControllerActions, PermissionControllerEvents, PermissionEnforcer, - PermissionSpecifications, + PermSpecs, RestrictedMethodArgs, } from '.'; @@ -29,7 +31,13 @@ enum CaveatTypes { filterArrayResponse = 'filterArrayResponse', } -function getCaveatSpecifications(): CaveatSpecifications { +type DefaultTargetNames = 'wallet_getSecretArray'; + +type DefaultCaveats = Caveat; + +type DefaultPermissions = PermConstraint; + +function getCaveatSpecifications(): CaveatSpecs { return { filterArrayResponse: { type: CaveatTypes.filterArrayResponse, @@ -39,16 +47,19 @@ function getCaveatSpecifications(): CaveatSpecifications { ) => async (args: RestrictedMethodArgs) => { const result = await method(args); - // TODO: Throw error if this isn't an array - return Array.isArray(result) - ? result.filter(caveat.value.includes) - : result; - }) as CaveatDecorator, + if (!Array.isArray(result)) { + throw Error('not an array'); + } + + return result.filter(caveat.value.includes); + }) as CaveatDecorator, }, }; } -function getPermissionSpecifications(): PermissionSpecifications { +function getPermissionSpecifications(): PermSpecs< + DefaultPermissions['parentCapability'] +> { return { wallet_getSecretArray: { target: 'wallet_getSecretArray', @@ -59,7 +70,11 @@ function getPermissionSpecifications(): PermissionSpecifications { }; } -function getPermissionController(): PermissionController { +function getPermissionController(): PermissionController< + DefaultTargetNames, + DefaultCaveats, + DefaultPermissions +> { return new PermissionController({ caveatSpecifications: getCaveatSpecifications(), messenger: getRestrictedMessenger(), @@ -91,6 +106,8 @@ describe('PermissionController', () => { }, }); + // controller.addCaveat('foo', 'wallet_getSecretArray', 'filterArrayResponse', ['foo']) + expect(controller.state).toStrictEqual({ subjects: { [origin]: { diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 3d404dccda..8ca8214087 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -12,7 +12,6 @@ import { Json } from 'json-rpc-engine'; import { isPlainObject, hasProperty } from '../utils'; import { - Caveat as CaveatConstraint, CaveatSpecs, constructCaveat, ExtractCaveatValue, @@ -20,18 +19,14 @@ import { } from './Caveat'; import { OriginString, - Permission as PermissionConstraint, MethodName, RequestedPermissions, RestrictedMethodImplementation, - PermissionOptions, - PermissionSpecifications as PermissionSpecificationsConstraint, - PermissionSpecification as PermissionSpecificationConstraint, - findCaveat, constructPermission, GenericPerm, - PermSpec, PermSpecs, + PermConstraint, + ExtractPermissionTargetNames, } from './Permission'; import { PermissionDoesNotExistError, @@ -68,8 +63,7 @@ export type PermissionSubjectEntry = { export type PermissionControllerSubjects = Record>; -// TODO: TypeScript doesn't understand that a given subject may not exist. Can we fix it? -// Yes we can, in TS4.4 +// TODO:TS4.4 Enable compiler flags to forbid unchecked member access export type PermissionControllerState = { subjects: PermissionControllerSubjects; }; @@ -78,7 +72,7 @@ const stateMetadata = { subjects: { persist: true, anonymous: true }, }; -// TODO:types +// Typecast: We can't reference the class generics, so we must resort to "any" const defaultState: PermissionControllerState = { subjects: {}, }; @@ -98,6 +92,7 @@ export type ClearPermissions = { handler: () => void; }; +// TODO: Implement all desired actions export type PermissionControllerActions = | GetPermissionsState | GetSubjects @@ -108,6 +103,10 @@ export type PermissionsStateChange = { payload: [PermissionControllerState, Patch[]]; }; +/** + * The controller only emits its generic state change events. Consumers should + * use selector subscriptions to subscribe to relevant substate. + */ export type PermissionControllerEvents = PermissionsStateChange; type AllowedActions = @@ -124,63 +123,71 @@ export type PermissionControllerMessenger = RestrictedControllerMessenger< never >; -type ExtractPermissionOptions< - PSpec, +// type ExtractPermissionOptions< +// PSpec, +// Perm extends GenericPerm, +// > = PSpec extends PermissionSpecificationConstraint< +// Perm['parentCapability'], +// PermConstraint<>, +// infer PermOpts, +// Record, +// RestrictedMethodImplementation +// > +// ? PermOpts +// : never; + +// type ExtractRequestData< +// PSpec, +// Perm extends GenericPerm, +// > = PSpec extends PermissionSpecificationConstraint< +// Perm['parentCapability'], +// PermissionOptions, +// infer RequestData, +// RestrictedMethodImplementation +// > +// ? RequestData +// : never; + +// type ExtractMethodImplementation< +// PSpec, +// Perm extends GenericPerm, +// > = PSpec extends PermissionSpecificationConstraint< +// Perm['parentCapability'], +// PermissionOptions, +// Record, +// infer Impl +// > +// ? Impl +// : never; + +type ExtractValidCaveatTypes< Perm extends GenericPerm, -> = PSpec extends PermissionSpecificationConstraint< - Perm['parentCapability'], - infer PermOpts, - Record, - RestrictedMethodImplementation -> - ? PermOpts - : never; - -type ExtractRequestData< - PSpec, - Perm extends GenericPerm, -> = PSpec extends PermissionSpecificationConstraint< - Perm['parentCapability'], - PermissionOptions, - infer RequestData, - RestrictedMethodImplementation -> - ? RequestData - : never; - -type ExtractMethodImplementation< - PSpec, - Perm extends GenericPerm, -> = PSpec extends PermissionSpecificationConstraint< - Perm['parentCapability'], - PermissionOptions, - Record, - infer Impl -> - ? Impl - : never; - -type ExtractValidCaveats< - Perm extends GenericPerm, - Target extends string, + TargetName extends string, > = Perm extends { - parentCapability: Target; - caveats: (infer ValidCaveats)[]; + parentCapability: TargetName; + caveats: null; } - ? ValidCaveats + ? never + : Perm extends { + parentCapability: TargetName; + caveats: (infer ValidCaveats)[]; + } + ? ValidCaveats extends GenericCaveat + ? ValidCaveats['type'] + : never : never; type PermissionControllerOptions< - Targets extends string, - Permission extends GenericPerm, + TargetKey extends string, + Permission extends PermConstraint< + ExtractPermissionTargetNames, + GenericCaveat + >, Caveat extends GenericCaveat, > = { messenger: PermissionControllerMessenger; caveatSpecifications: CaveatSpecs; - permissionSpecifications: Record< - Permission['parentCapability'], - PermSpec - >; + permissionSpecifications: PermSpecs; unrestrictedMethods: string[]; state?: Partial>; }; @@ -193,9 +200,12 @@ type GrantPermissionsOptions = { }; export class PermissionController< - Targets extends string, - Permission extends GenericPerm, - Caveat extends CaveatConstraint, + TargetKey extends string, + Caveat extends GenericCaveat, + Permission extends PermConstraint< + ExtractPermissionTargetNames, + Caveat + >, > extends BaseController< typeof controllerName, PermissionControllerState, @@ -207,15 +217,19 @@ export class PermissionController< return this._caveatSpecifications; } - private readonly _enforcer: Readonly; + private readonly _enforcer: Readonly< + PermissionEnforcer + >; - public get enforcer(): Readonly { + public get enforcer(): Readonly< + PermissionEnforcer + > { return this._enforcer; } - private readonly _permissionSpecifications: Readonly>; + private readonly _permissionSpecifications: Readonly>; - public get permissionSpecifications(): Readonly> { + public get permissionSpecifications(): Readonly> { return this._permissionSpecifications; } @@ -231,7 +245,7 @@ export class PermissionController< caveatSpecifications, permissionSpecifications, unrestrictedMethods, - }: PermissionControllerOptions) { + }: PermissionControllerOptions) { super({ name: controllerName, metadata: stateMetadata, @@ -287,7 +301,7 @@ export class PermissionController< * @returns The permission if it exists, or undefined otherwise. */ getPermission( - origin: string, + origin: OriginString, target: SubjectPermission['parentCapability'], ): SubjectPermission | undefined { return this.state.subjects[origin]?.permissions[ @@ -295,26 +309,32 @@ export class PermissionController< ] as SubjectPermission; } - getPermissions(origin: string): Record | undefined { + getPermissions( + origin: OriginString, + ): SubjectPermissions | undefined { return this.state.subjects[origin]?.permissions; } hasPermission( - origin: string, + origin: OriginString, target: Permission['parentCapability'], ): boolean { return Boolean(this.getPermission(origin, target)); } - hasPermissions(origin: string): boolean { + hasPermissions(origin: OriginString): boolean { return Boolean(this.state.subjects[origin]); } - private validatePermission(permission: Permission): void { - const specification = this.getPermissionSpecification( - this.getTargetKey(permission.parentCapability) as Targets, - ); - specification.validator?.(permission); + private validateModifiedPermission(permission: Permission): void { + const targetKey = this.getTargetKey(permission.parentCapability); + /* istanbul ignore next: this should be impossible */ + if (!targetKey) { + throw new Error( + `Fatal: Existing permission target key "${targetKey}" has no specification.`, + ); + } + this.permissionSpecifications[targetKey].validator?.(permission); } /** @@ -322,19 +342,19 @@ export class PermissionController< * permissions (same subject and method). Other existing permissions * remain unaffected. * - * **Note: Assumes that the new permissions have been validated.** + * **ATTN: Assumes that the new permissions have been validated.** * * @param origin - The origin of the grantee subject. * @param permissions - The new permissions for the grantee subject. */ private setValidatedPermissions( - origin: string, + origin: OriginString, permissions: Record, shouldPreserveExistingPermissions = false, ): void { this.update((draftState) => { if (!draftState.subjects[origin]) { - // TODO:types + // Typecast: immer's WritableDraft is incompatible with our generics draftState.subjects[origin] = { origin, permissions: {} as any }; } @@ -348,7 +368,7 @@ export class PermissionController< } revokePermission( - origin: string, + origin: OriginString, target: Permission['parentCapability'], ): void { this.update((draftState) => { @@ -396,7 +416,7 @@ export class PermissionController< private deletePermission( draftState: PermissionControllerState, permissions: SubjectPermissions, - origin: string, + origin: OriginString, target: Permission['parentCapability'], ): void { if (Object.keys(permissions).length > 1) { @@ -406,7 +426,7 @@ export class PermissionController< } } - revokeAllPermissions(origin: string): void { + revokeAllPermissions(origin: OriginString): void { this.update((draftState) => { if (!draftState.subjects[origin]) { throw new UnrecognizedSubjectError(origin); @@ -416,9 +436,9 @@ export class PermissionController< } hasCaveat< - Target extends Permission['parentCapability'], - CaveatType extends ExtractValidCaveats, - >(origin: string, target: Target, caveatType: CaveatType['type']): boolean { + TargetName extends Permission['parentCapability'], + CaveatType extends ExtractValidCaveatTypes, + >(origin: OriginString, target: TargetName, caveatType: CaveatType): boolean { return ( this.getPermission(origin, target)?.caveats?.some( (caveat) => caveat.type === caveatType, @@ -427,12 +447,12 @@ export class PermissionController< } addCaveat< - Target extends Permission['parentCapability'], - CaveatType extends ExtractValidCaveats, + TargetName extends Permission['parentCapability'], + CaveatType extends ExtractValidCaveatTypes, >( - origin: string, - target: Target, - caveatType: CaveatType['type'], + origin: OriginString, + target: TargetName, + caveatType: CaveatType, caveatValue: ExtractCaveatValue, ): void { if (this.hasCaveat(origin, target, caveatType)) { @@ -443,12 +463,12 @@ export class PermissionController< } updateCaveat< - Target extends Permission['parentCapability'], - CaveatType extends ExtractValidCaveats, + TargetName extends Permission['parentCapability'], + CaveatType extends ExtractValidCaveatTypes, >( - origin: string, - target: Target, - caveatType: CaveatType['type'], + origin: OriginString, + target: TargetName, + caveatType: CaveatType, caveatValue: ExtractCaveatValue, ): void { if (!this.hasCaveat(origin, target, caveatType)) { @@ -458,36 +478,36 @@ export class PermissionController< this.setCaveat(origin, target, caveatType, caveatValue); } - private getCaveat< - Target extends Permission['parentCapability'], - CaveatType extends ExtractValidCaveats, - >( - origin: string, - target: Target, - caveatType: CaveatType['type'], - ): CaveatType | undefined { - const permission = this.getPermission(origin, target); - if (!permission) { - throw new PermissionDoesNotExistError(origin, target); - } - - return findCaveat(permission, caveatType); - } + // private getCaveat< + // TargetName extends Permission['parentCapability'], + // CaveatType extends ExtractValidCaveatTypes, + // >( + // origin: OriginString, + // target: TargetName, + // caveatType: CaveatType, + // ): CaveatType | undefined { + // const permission = this.getPermission(origin, target); + // if (!permission) { + // throw new PermissionDoesNotExistError(origin, target); + // } + + // return findCaveat(permission, caveatType); + // } private setCaveat< - Target extends Permission['parentCapability'], - CaveatType extends ExtractValidCaveats, + TargetName extends Permission['parentCapability'], + CaveatType extends ExtractValidCaveatTypes, >( - origin: string, - target: Target, - caveatType: CaveatType['type'], + origin: OriginString, + target: TargetName, + caveatType: CaveatType, caveatValue: ExtractCaveatValue, ): void { this.update((draftState) => { - // TODO:types - const permission = (draftState.subjects[origin] as any)?.permissions[ - target - ] as Permission; + // Typecast: immer's WritableDraft is incompatible with our generics + const permission = ( + draftState.subjects as PermissionControllerSubjects + )[origin]?.permissions[target] as Permission; /* istanbul ignore if: This should be impossible, but TypeScript wants it */ if (!permission) { throw new PermissionDoesNotExistError(origin, target); @@ -510,22 +530,26 @@ export class PermissionController< permission.caveats.splice(caveatIndex, 1, caveat as any); } } else { - permission.caveats = [caveat as any]; + // Typecast: At this point, we don't know if the specific permission + // is allowed to have caveats, but it should be impossible to call + // this method for a permission that may not have any caveats. + // If all else fails, the permission validator is also called. + permission.caveats = [caveat] as any; } - // Typecast: ts(2589) - this.validatePermission(permission as any); + this.validateModifiedPermission(permission); }); } removeCaveat< - Target extends Permission['parentCapability'], - CaveatType extends ExtractValidCaveats, - >(origin: string, target: Target, caveatType: CaveatType['type']): void { + TargetName extends Permission['parentCapability'], + CaveatType extends ExtractValidCaveatTypes, + >(origin: OriginString, target: TargetName, caveatType: CaveatType): void { this.update((draftState) => { - // TODO:types - const permission: Permission = (draftState.subjects[origin] as any) - ?.permissions[target]; + // Typecast: immer's WritableDraft is incompatible with our generics + const permission: Permission = ( + draftState.subjects as PermissionControllerSubjects + )[origin]?.permissions[target]; if (!permission) { throw new PermissionDoesNotExistError(origin, target); } @@ -547,8 +571,7 @@ export class PermissionController< permission.caveats.splice(caveatIndex, 1); } - // Typecast: ts(2589) - this.validatePermission(permission as any); + this.validateModifiedPermission(permission); }); } @@ -559,12 +582,12 @@ export class PermissionController< * Used to support our namespaced method feature, which allows blocks * of methods to be hidden behind a restricted method with a trailing `_` character. * - * @param method string - The requested RPC method. + * @param method - The requested RPC method. * @returns The internal key of the method. */ - getTargetKey(method: Permission['parentCapability']): Targets | '' { + getTargetKey(method: Permission['parentCapability']): TargetKey | undefined { if (hasProperty(this._permissionSpecifications, method)) { - return method as Targets; + return method as TargetKey; } const wildCardMethodsWithoutWildCard: Record = {}; @@ -590,84 +613,75 @@ export class PermissionController< } if (hasProperty(this._permissionSpecifications, targetKey)) { - return targetKey as Targets; + return targetKey as TargetKey; } else if (wildCardMethodsWithoutWildCard[targetKey]) { - return `${targetKey}*` as Targets; + return `${targetKey}*` as TargetKey; } - return ''; + return undefined; } getRestrictedMethodImplementation( - method: string, + method: Permission['parentCapability'], ): RestrictedMethodImplementation | undefined { const targetKey = this.getTargetKey(method); if (!targetKey) { return undefined; } - return this.getPermissionSpecification(method as Targets) - .methodImplementation; + return this.permissionSpecifications[targetKey].methodImplementation; } - private getPermissionSpecification(targetKey: Targets) { - const specification = this._permissionSpecifications[targetKey]; - /* istanbul ignore if: This should be impossible, but TypeScript wants it */ - if (!specification) { - throw new Error( - `Existing permission target key "${targetKey}" has no specification.`, - ); - } - - return specification; - } - - grantPermissions( - opts: GrantPermissionsOptions, - ): SubjectPermissions { - const { - requestData, - approvedPermissions, - shouldPreserveExistingPermissions = false, - subject, - } = opts; + grantPermissions({ + requestData, + approvedPermissions, + shouldPreserveExistingPermissions = false, + subject, + }: GrantPermissionsOptions): SubjectPermissions { const { origin } = subject; if (!origin || typeof origin !== 'string') { throw new InvalidSubjectIdentifierError(origin); } - const permissions: { [methodName: string]: Permission } = - (shouldPreserveExistingPermissions && { + const permissions: SubjectPermissions = + ((shouldPreserveExistingPermissions && { ...this.getPermissions(origin), }) || - {}; + {}) as SubjectPermissions; - for (const target of Object.keys(approvedPermissions)) { - const targetKey = this.getTargetKey(target); + for (const requestedTarget of Object.keys(approvedPermissions)) { + const targetKey = this.getTargetKey( + requestedTarget as Permission['parentCapability'], + ); if (!targetKey) { - throw methodNotFound({ method: target }); + throw methodNotFound({ method: requestedTarget }); } - const specification = this.getPermissionSpecification(targetKey); + // The requested target must be a valid target name if we found its key. + // We reassign it to change its type. + const targetName = requestedTarget as Permission['parentCapability']; + const specification = this.permissionSpecifications[targetKey]; const caveats = this.computeCaveats( origin, - target, - approvedPermissions[target].caveats, + targetName, + approvedPermissions[targetName].caveats, ); - const permissionOptions = { caveats, invoker: origin, target }; - - let permission: Permission; - if (specification.factory) { - permission = specification.factory(permissionOptions, requestData); - } else { - permission = constructPermission(permissionOptions); - specification.validator?.(permission); - } - - permissions[targetKey] = permission; + const permissionOptions = { + caveats, + invoker: origin, + target: targetName, + }; + + const permission = ( + specification.factory + ? specification.factory(permissionOptions, requestData) + : constructPermission(permissionOptions) + ) as Permission; + specification.validator?.(permission); + permissions[targetName] = permission; } this.setValidatedPermissions(origin, permissions); @@ -679,10 +693,10 @@ export class PermissionController< * @returns Whether the given caveats are valid. */ private computeCaveats( - origin: string, - target: string, - caveats?: Caveat[], - ): Caveat[] | undefined { + origin: OriginString, + target: Permission['parentCapability'], + caveats?: unknown[], + ): Caveat[] | undefined { const caveatArray = caveats?.map((requestedCaveat) => this.computeCaveat(origin, target, requestedCaveat), ); @@ -690,10 +704,10 @@ export class PermissionController< } private computeCaveat( - origin: string, - target: string, - requestedCaveat: Caveat, - ): Caveat { + origin: OriginString, + target: Permission['parentCapability'], + requestedCaveat: unknown, + ): Caveat { if (!isPlainObject(requestedCaveat)) { throw new InvalidCaveatError(requestedCaveat); } @@ -706,7 +720,10 @@ export class PermissionController< throw new InvalidCaveatTypeError(requestedCaveat.type); } - const specification = this.caveatSpecifications[requestedCaveat.type]; + const specification = + this.caveatSpecifications[ + requestedCaveat.type as keyof CaveatSpecs + ]; if (!specification) { throw new CaveatTypeDoesNotExistError( requestedCaveat.type, @@ -720,20 +737,25 @@ export class PermissionController< } const caveat = constructCaveat(requestedCaveat.type, requestedCaveat.value); - specification.validator?.(origin, target, caveat); - return caveat; + return caveat as Caveat; } - private constructEnforcer(): PermissionEnforcer { - return new PermissionEnforcer({ + private constructEnforcer(): PermissionEnforcer< + TargetKey, + Caveat, + Permission + > { + return new PermissionEnforcer({ caveatSpecifications: this.caveatSpecifications, getPermission: this.getPermission.bind(this), getRestrictedMethodImplementation: this.getRestrictedMethodImplementation.bind(this), grantPermissions: this.grantPermissions.bind(this), isRestrictedMethod: (method: string) => { - return Boolean(this.getTargetKey(method)); + return Boolean( + this.getTargetKey(method as Permission['parentCapability']), + ); }, isUnrestrictedMethod: (method: string) => { return this.unrestrictedMethods.has(method); @@ -765,10 +787,12 @@ export class PermissionController< ), hasApprovalRequest: (opts: { id?: string; - origin?: string; + origin?: OriginString; type?: string; }) => - // Typecast: It can't handle the overloads + // Typecast: There are only some valid parameter combinations, but we + // just won't worry about that here. It's unclear if the messaging + // even handles them. this.messagingSystem.call('ApprovalController:hasRequest', opts as any), }); } diff --git a/packages/controllers/src/permissions/PermissionEnforcer.ts b/packages/controllers/src/permissions/PermissionEnforcer.ts index aaf7d3b4d1..4da79ce441 100644 --- a/packages/controllers/src/permissions/PermissionEnforcer.ts +++ b/packages/controllers/src/permissions/PermissionEnforcer.ts @@ -10,7 +10,7 @@ import { import { nanoid } from 'nanoid'; import { isPlainObject, hasProperty } from '../utils'; -import { CaveatSpecifications, decorateWithCaveats } from './Caveat'; +import { CaveatSpecs, decorateWithCaveats, GenericCaveat } from './Caveat'; import { internalError, invalidParams, @@ -19,16 +19,17 @@ import { userRejectedRequest, } from './errors'; import type { - Permission, + ExtractPermissionTargetNames, + PermConstraint, RequestedPermissions, RestrictedMethodImplementation, } from './Permission'; import type { PermissionController, - PermissionsSubjectMetadata, + PermissionSubjectMetadata, } from './PermissionController'; -export type PermissionsRequestMetadata = PermissionsSubjectMetadata & { +export type PermissionsRequestMetadata = PermissionSubjectMetadata & { id: string; }; @@ -48,29 +49,63 @@ type RequestUserApproval = ( permissionsRequest: PermissionsRequest, ) => Promise; -type PermissionEnforcerArgs = { - caveatSpecifications: CaveatSpecifications; +type PermissionEnforcerArgs< + TargetKey extends string, + Caveat extends GenericCaveat, + Permission extends PermConstraint< + ExtractPermissionTargetNames, + Caveat + >, +> = { + caveatSpecifications: CaveatSpecs; isRestrictedMethod: IsRestrictedMethod; isUnrestrictedMethod: IsUnrestrictedMethod; - getPermission: PermissionController['getPermission']; - getRestrictedMethodImplementation: PermissionController['getRestrictedMethodImplementation']; - grantPermissions: PermissionController['grantPermissions']; + getPermission: PermissionController< + TargetKey, + Caveat, + Permission + >['getPermission']; + getRestrictedMethodImplementation: PermissionController< + TargetKey, + Caveat, + Permission + >['getRestrictedMethodImplementation']; + grantPermissions: PermissionController< + TargetKey, + Caveat, + Permission + >['grantPermissions']; requestUserApproval: RequestUserApproval; acceptPermissionsRequest: ApprovalController['accept']; rejectPermissionsRequest: ApprovalController['reject']; hasApprovalRequest: ApprovalController['has']; }; -export class PermissionEnforcer { - private caveatSpecifications: CaveatSpecifications; +export class PermissionEnforcer< + TargetKey extends string, + Caveat extends GenericCaveat, + Permission extends PermConstraint< + ExtractPermissionTargetNames, + Caveat + >, +> { + private caveatSpecifications: CaveatSpecs; private isRestrictedMethod: IsRestrictedMethod; private isUnrestrictedMethod: IsUnrestrictedMethod; - private getPermission: PermissionController['getPermission']; + private getPermission: PermissionController< + TargetKey, + Caveat, + Permission + >['getPermission']; - private grantPermissions: PermissionController['grantPermissions']; + private grantPermissions: PermissionController< + TargetKey, + Caveat, + Permission + >['grantPermissions']; private requestUserApproval: RequestUserApproval; @@ -80,7 +115,11 @@ export class PermissionEnforcer { private _hasApprovalRequest: ApprovalController['has']; - private _getRestrictedMethodImplementation: PermissionController['getRestrictedMethodImplementation']; + private _getRestrictedMethodImplementation: PermissionController< + TargetKey, + Caveat, + Permission + >['getRestrictedMethodImplementation']; constructor({ caveatSpecifications, @@ -93,7 +132,7 @@ export class PermissionEnforcer { acceptPermissionsRequest, rejectPermissionsRequest, hasApprovalRequest, - }: PermissionEnforcerArgs) { + }: PermissionEnforcerArgs) { this.caveatSpecifications = caveatSpecifications; this.isRestrictedMethod = isRestrictedMethod; this.isUnrestrictedMethod = isUnrestrictedMethod; @@ -221,7 +260,7 @@ export class PermissionEnforcer { } } catch (err) { // if finalization fails, reject the request - this._rejectPermissionsRequest(id, err); + this._rejectPermissionsRequest(id, err as Error); } } @@ -288,8 +327,9 @@ export class PermissionEnforcer { method: string, request: JsonRpcRequest, ): RestrictedMethodImplementation { - const methodImplementation = - this._getRestrictedMethodImplementation(method); + const methodImplementation = this._getRestrictedMethodImplementation( + method as Permission['parentCapability'], + ); if (!methodImplementation) { throw methodNotFound({ method, data: { request } }); } @@ -308,13 +348,16 @@ export class PermissionEnforcer { */ private _executeRestrictedMethod( methodImplementation: RestrictedMethodImplementation, - subject: PermissionsSubjectMetadata, + subject: PermissionSubjectMetadata, req: JsonRpcRequest, ): ReturnType> { const { origin } = subject; const { method, params } = req; - const permission = this.getPermission(origin, method); + const permission = this.getPermission( + origin, + method as Permission['parentCapability'], + ); if (!permission) { return unauthorized({ data: { origin, method } }); } @@ -327,7 +370,7 @@ export class PermissionEnforcer { } createPermissionMiddleware( - subject: PermissionsSubjectMetadata, + subject: PermissionSubjectMetadata, ): JsonRpcMiddleware { const permissionsMiddleware = async ( req: JsonRpcRequest, @@ -341,7 +384,7 @@ export class PermissionEnforcer { return next(); } - // if the method also is not a restricted method, the method does not exist + // This will throw if no restricted method implementation is found. const methodImplementation = this.getRestrictedMethodImplementation( method, req, diff --git a/packages/controllers/src/permissions/errors.ts b/packages/controllers/src/permissions/errors.ts index ee6dd54ab3..71247fbf3c 100644 --- a/packages/controllers/src/permissions/errors.ts +++ b/packages/controllers/src/permissions/errors.ts @@ -115,7 +115,7 @@ export class InvalidCaveatError extends EthereumRpcError { } export class InvalidCaveatTypeError extends Error { - constructor(caveatType: string) { + constructor(caveatType: unknown) { super(`Caveat types must be strings. Received: ${typeof caveatType}`); } } @@ -130,18 +130,26 @@ export class CaveatTypeDoesNotExistError extends Error { } export class CaveatMissingValueError extends Error { - public data: { caveat: Caveat; origin: string; target: string }; + public data: { + caveat: Record; + origin: string; + target: string; + }; - constructor(caveat: Caveat, origin: string, target: string) { + constructor(caveat: Record, origin: string, target: string) { super(`Caveat is missing "value" field.`); this.data = { caveat, origin, target }; } } export class InvalidCaveatFieldsError extends Error { - public data: { caveat: Caveat; origin: string; target: string }; + public data: { + caveat: Record; + origin: string; + target: string; + }; - constructor(caveat: Caveat, origin: string, target: string) { + constructor(caveat: Record, origin: string, target: string) { super( `Caveat has unexpected number of fields: ${Object.keys(caveat).length}`, ); diff --git a/packages/controllers/src/permissions/rpc-methods/getPermissions.ts b/packages/controllers/src/permissions/rpc-methods/getPermissions.ts index 2dadb56a91..a69ec4a7b9 100644 --- a/packages/controllers/src/permissions/rpc-methods/getPermissions.ts +++ b/packages/controllers/src/permissions/rpc-methods/getPermissions.ts @@ -3,6 +3,7 @@ import type { JsonRpcEngineEndCallback, PendingJsonRpcResponse, } from 'json-rpc-engine'; +import { GenericPerm } from '../Permission'; import { MethodNames } from '../enums'; import type { Permission } from '../Permission'; @@ -22,7 +23,7 @@ export const getPermissionsHandler: PermittedHandlerExport< }; export type GetPermissionsHooks = { - getPermissions: () => SubjectPermissions; + getPermissions: () => SubjectPermissions; }; async function getPermissionsImplementation( From d8cbb0907496e00f0d7384c70f26bb71e8543457 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 22 Sep 2021 22:25:35 -0700 Subject: [PATCH 044/147] WIP: Improved type safety 3 (mostly cleanup left) --- .../controllers/src/permissions/Caveat.ts | 74 +++++--- .../controllers/src/permissions/Permission.ts | 22 +-- .../permissions/PermissionController.test.ts | 177 ++++++++++++++---- .../src/permissions/PermissionController.ts | 30 ++- .../controllers/src/permissions/errors.ts | 20 +- .../permissions/rpc-methods/getPermissions.ts | 4 +- 6 files changed, 219 insertions(+), 108 deletions(-) diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index e52715dcca..909cfeabe6 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -21,7 +21,15 @@ export type ZcapLdCaveat = { * Identical to instances of the Caveat class, useful for when TypeScript * has a meltdown over assigning classes to the Json type. */ -export type Caveat = ZcapLdCaveat & { +export type Caveat = { + /** + * The type of the caveat. The type is presumed to be meaningful in the + * context of the capability it is associated with. + * + * In MetaMask, every permission can only have one caveat of each type. + */ + readonly type: Type; + /** * Any additional data necessary to enforce the caveat. * @@ -50,31 +58,28 @@ export function constructCaveat( // Accomplished by decorating the restricted method implementation with the // the corresponding caveat functions. -export type CaveatDecorator = ( +export type CaveatDecorator = ( decorated: AsyncRestrictedMethodImplementation, - caveat: Caveat, + caveat: ConcreteCaveat, ) => AsyncRestrictedMethodImplementation; -export type CaveatValidator = ( +export type CaveatValidator = ( origin: string, target: string, caveat: { type: string; value: unknown }, -) => caveat is Caveats; +) => caveat is ConcreteCaveat; -export type CaveatSpecification< - CaveatType extends string, - CaveatValue extends Json, -> = { +export type CaveatSpecification = { /** * The string type of the caveat. */ - type: CaveatType; + type: ConcreteCaveat['type']; /** * The decorator function used to apply the caveat to restricted method * requests. */ - decorator: CaveatDecorator; + decorator: CaveatDecorator; /** * The validator function used to validate caveats of the associated type @@ -87,31 +92,34 @@ export type CaveatSpecification< * performed. Although caveats can also be validated by permission validators, * validating caveat values separately is strongly recommended. */ - validator?: CaveatValidator>; + validator?: CaveatValidator; }; export type CaveatSpecifications = Readonly< - Record> + Record> >; -export type GenericCaveat = Caveat; +export type GenericCaveat = Caveat; -export type CaveatSpec> = CaveatSpecification< - Caveats['type'], - ExtractCaveatValue ->; +export type CaveatSpec = + CaveatSpecification; -export type CaveatSpecs> = Record< - Caveats['type'], - CaveatSpec ->; +/** + * An object mapping the type of each caveat to its corresponding specification. + */ +export type CaveatSpecs = { + [Type in Caveats['type']]: GetCaveatSpec; +}; -export type ExtractCaveatValue = Caveats extends { - type: CaveatType; - value: infer CaveatValue; -} - ? CaveatValue - : never; +export type GetCaveatSpec< + Caveats extends GenericCaveat, + CaveatType extends string, +> = Caveats extends Caveat ? CaveatSpec : never; + +export type ExtractCaveatValue< + Caveats extends GenericCaveat, + CaveatType extends string, +> = Caveats extends Caveat ? CaveatValue : never; /** * Decorate a restricted method implementation with its caveats. @@ -125,21 +133,25 @@ export function decorateWithCaveats( permission: Readonly, // bound to the requesting origin caveatSpecifications: CaveatSpecs, // all caveat implementations ): RestrictedMethodImplementation { - const caveats = permission.caveats ?? []; + const { caveats } = permission; + if (!caveats) { + return methodImplementation; + } let decorated = methodImplementation as AsyncRestrictedMethodImplementation< Json, Json >; - // TODO:types fix the base caveat types + for (const caveat of caveats) { const specification = caveatSpecifications[caveat.type as Caveats['type']]; if (!specification) { throw new UnrecognizedCaveatTypeError(caveat.type); } - decorated = specification.decorator(decorated, caveat as any); + decorated = specification.decorator(decorated, caveat); } + return decorated; } diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 3ef349a4f0..4051eb88f5 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -93,7 +93,7 @@ export type PermissionOptions = { * The caveats of the permission. * See {@link Caveat}. */ - caveats?: Caveat[]; + caveats?: Caveat[]; }; /** @@ -119,7 +119,7 @@ export type Permission = Omit< * * TODO: Make optional in typescript@4.4.x */ - readonly caveats: Caveat[] | null; + readonly caveats: Caveat[] | null; /** * The origin string of the subject that has the permission. @@ -147,18 +147,18 @@ export function constructPermission(options: PermissionOptions) { } /** - * Gets the value of the caveat of the specified type belonging to the - * specified permission. + * Gets the the caveat of the specified type belonging to the specified + * permission. * - * @param permission The permission whose caveat value to retrieve. + * @param permission The permission whose caveat to retrieve. * @param caveatType The type of the caveat to retrieve. - * @returns The caveat value, or undefined if no such caveat exists. + * @returns The caveat, or undefined if no such caveat exists. */ -export function findCaveat>( +export function findCaveat>( permission: Permission, caveatType: TargetCaveat['type'], ): TargetCaveat | undefined { - // TODO:types + // TODO:types create a type for a permission that can have particular caveats? return permission.caveats?.find( (caveat) => caveat.type === caveatType, ) as any; @@ -166,7 +166,7 @@ export function findCaveat>( type RequestedPermission = { target?: MethodName; - caveats?: Caveat[]; + caveats?: Caveat[]; }; export type RequestedPermissions = Record; @@ -251,7 +251,7 @@ export type PermissionSpecification< // MethodName, // PermissionSpecification< // string, -// GenericPerm, +// GenericPermission, // PermissionOptions, // Record, // RestrictedMethodImplementation @@ -300,7 +300,7 @@ export type PermConstraint< : TargetName; }; -export type GenericPerm = PermConstraint; +export type GenericPermission = PermConstraint; export type PermSpec = TargetKeyConstraint extends never diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 35394a9088..2f0cbc3d3a 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -1,8 +1,9 @@ import { ControllerMessenger, Json } from '@metamask/controllers'; +import { isPlainObject } from '../utils'; +import * as errors from './errors'; import { AsyncRestrictedMethodImplementation, Caveat, - CaveatDecorator, CaveatSpecs, PermConstraint, PermissionController, @@ -13,51 +14,79 @@ import { RestrictedMethodArgs, } from '.'; -const controllerName = 'PermissionController' as const; - -function getRestrictedMessenger() { - const controllerMessenger = new ControllerMessenger< - PermissionControllerActions, - PermissionControllerEvents - >(); - return controllerMessenger.getRestricted( - { - name: controllerName, - }, - ); -} +// Caveat types and specifications enum CaveatTypes { filterArrayResponse = 'filterArrayResponse', + filterObjectResponse = 'filterObjectResponse', } -type DefaultTargetNames = 'wallet_getSecretArray'; - -type DefaultCaveats = Caveat; +type FilterArrayCaveat = Caveat; +type FilterObjectCaveat = Caveat; -type DefaultPermissions = PermConstraint; +type DefaultCaveats = FilterArrayCaveat | FilterObjectCaveat; -function getCaveatSpecifications(): CaveatSpecs { +function getDefaultCaveatSpecifications(): CaveatSpecs { return { filterArrayResponse: { type: CaveatTypes.filterArrayResponse, - decorator: (( - method: AsyncRestrictedMethodImplementation, - caveat: { type: CaveatTypes.filterArrayResponse; value: string[] }, + decorator: + ( + method: AsyncRestrictedMethodImplementation, + caveat: FilterArrayCaveat, ) => async (args: RestrictedMethodArgs) => { - const result = await method(args); + const result: string[] | unknown = await method(args); if (!Array.isArray(result)) { throw Error('not an array'); } return result.filter(caveat.value.includes); - }) as CaveatDecorator, + }, + }, + filterObjectResponse: { + type: CaveatTypes.filterObjectResponse, + decorator: + ( + method: AsyncRestrictedMethodImplementation, + caveat: FilterObjectCaveat, + ) => + async (args: RestrictedMethodArgs) => { + const result = await method(args); + if (!isPlainObject(result)) { + throw Error('not a plain object'); + } + + caveat.value.forEach((key) => { + delete result[key]; + }); + return result; + }, }, }; } -function getPermissionSpecifications(): PermSpecs< +// Permission types and specifications + +type SecretArrayName = 'wallet_getSecretArray'; + +type GetSecretArrayPermission = PermConstraint< + SecretArrayName, + FilterArrayCaveat +>; + +type SecretObjectName = 'wallet_getSecretObject'; + +type GetSecretObjectPermission = PermConstraint< + SecretObjectName, + FilterObjectCaveat +>; + +// type DefaultTargetNames = SecretArrayName | SecretObjectName; + +type DefaultPermissions = GetSecretArrayPermission | GetSecretObjectPermission; + +function getDefaultPermissionSpecifications(): PermSpecs< DefaultPermissions['parentCapability'] > { return { @@ -67,26 +96,54 @@ function getPermissionSpecifications(): PermSpecs< return ['secret1', 'secret2', 'secret3']; }, }, + wallet_getSecretObject: { + target: 'wallet_getSecretArray', // TODO:types Same issue as caveat specs + methodImplementation: (_args: RestrictedMethodArgs) => { + return ['secret1', 'secret2', 'secret3']; + }, + }, }; } -function getPermissionController(): PermissionController< - DefaultTargetNames, - DefaultCaveats, - DefaultPermissions -> { - return new PermissionController({ - caveatSpecifications: getCaveatSpecifications(), - messenger: getRestrictedMessenger(), - permissionSpecifications: getPermissionSpecifications(), - unrestrictedMethods: ['wallet_unrestrictedMethod'], - }); +// The permissions controller + +const controllerName = 'PermissionController' as const; + +function getDefaultRestrictedMessenger() { + const controllerMessenger = new ControllerMessenger< + PermissionControllerActions, + PermissionControllerEvents + >(); + return controllerMessenger.getRestricted( + { + name: controllerName, + }, + ); +} + +function getDefaultUnrestrictedMethods() { + return ['wallet_unrestrictedMethod']; +} + +function getControllerOpts(opts?: Record) { + return { + caveatSpecifications: getDefaultCaveatSpecifications(), + messenger: getDefaultRestrictedMessenger(), + permissionSpecifications: getDefaultPermissionSpecifications(), + unrestrictedMethods: getDefaultUnrestrictedMethods(), + state: undefined, + ...opts, + }; +} + +function getDefaultPermissionController(opts = getControllerOpts()) { + return new PermissionController(opts); } describe('PermissionController', () => { describe('constructor', () => { it('initializes a new PermissionController', () => { - const controller = getPermissionController(); + const controller = getDefaultPermissionController(); expect(controller.state).toStrictEqual({ subjects: {} }); expect(controller.enforcer instanceof PermissionEnforcer).toStrictEqual( true, @@ -96,7 +153,7 @@ describe('PermissionController', () => { describe('grantPermissions', () => { it('grants new permissions', () => { - const controller = getPermissionController(); + const controller = getDefaultPermissionController(); const origin = 'metamask.io'; controller.grantPermissions({ @@ -106,8 +163,6 @@ describe('PermissionController', () => { }, }); - // controller.addCaveat('foo', 'wallet_getSecretArray', 'filterArrayResponse', ['foo']) - expect(controller.state).toStrictEqual({ subjects: { [origin]: { @@ -125,5 +180,47 @@ describe('PermissionController', () => { }, }); }); + + it('throws if the origin is invalid', () => { + const controller = getDefaultPermissionController(); + + expect(() => + controller.grantPermissions({ + subject: { origin: '' }, + approvedPermissions: { + wallet_getSecretArray: {}, + }, + }), + ).toThrow(new errors.InvalidSubjectIdentifierError('')); + + expect(() => + controller.grantPermissions({ + subject: { origin: 2 as any }, + approvedPermissions: { + wallet_getSecretArray: {}, + }, + }), + ).toThrow(new errors.InvalidSubjectIdentifierError(2)); + }); + + it('throws if the target does not exist', () => { + const controller = getDefaultPermissionController(); + + expect(() => + controller.grantPermissions({ + subject: { origin: 'metamask.io' }, + approvedPermissions: { + wallet_getSecretFalafel: {}, + }, + }), + ).toThrow(errors.methodNotFound({ method: 'wallet_getSecretFalafel' })); + }); }); }); + +// controller.addCaveat( +// 'foo', +// 'wallet_getSecretArray' as const, +// CaveatTypes.filterArrayResponse, +// ['foo'], +// ); diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 8ca8214087..b5c72ac052 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -23,7 +23,7 @@ import { RequestedPermissions, RestrictedMethodImplementation, constructPermission, - GenericPerm, + GenericPermission, PermSpecs, PermConstraint, ExtractPermissionTargetNames, @@ -51,20 +51,20 @@ export type PermissionSubjectMetadata = { const controllerName = 'PermissionController'; -export type SubjectPermissions = Record< +export type SubjectPermissions = Record< Permission['parentCapability'], Permission >; -export type PermissionSubjectEntry = { +export type PermissionSubjectEntry = { permissions: SubjectPermissions; } & PermissionSubjectMetadata; -export type PermissionControllerSubjects = +export type PermissionControllerSubjects = Record>; // TODO:TS4.4 Enable compiler flags to forbid unchecked member access -export type PermissionControllerState = { +export type PermissionControllerState = { subjects: PermissionControllerSubjects; }; @@ -79,12 +79,12 @@ const defaultState: PermissionControllerState = { export type GetPermissionsState = { type: `${typeof controllerName}:getState`; - handler: () => PermissionControllerState; + handler: () => PermissionControllerState; }; export type GetSubjects = { type: `${typeof controllerName}:getSubjects`; - handler: () => (keyof PermissionControllerSubjects)[]; + handler: () => (keyof PermissionControllerSubjects)[]; }; export type ClearPermissions = { @@ -100,7 +100,7 @@ export type PermissionControllerActions = export type PermissionsStateChange = { type: `${typeof controllerName}:stateChange`; - payload: [PermissionControllerState, Patch[]]; + payload: [PermissionControllerState, Patch[]]; }; /** @@ -125,7 +125,7 @@ export type PermissionControllerMessenger = RestrictedControllerMessenger< // type ExtractPermissionOptions< // PSpec, -// Perm extends GenericPerm, +// Perm extends GenericPermission, // > = PSpec extends PermissionSpecificationConstraint< // Perm['parentCapability'], // PermConstraint<>, @@ -138,7 +138,7 @@ export type PermissionControllerMessenger = RestrictedControllerMessenger< // type ExtractRequestData< // PSpec, -// Perm extends GenericPerm, +// Perm extends GenericPermission, // > = PSpec extends PermissionSpecificationConstraint< // Perm['parentCapability'], // PermissionOptions, @@ -150,7 +150,7 @@ export type PermissionControllerMessenger = RestrictedControllerMessenger< // type ExtractMethodImplementation< // PSpec, -// Perm extends GenericPerm, +// Perm extends GenericPermission, // > = PSpec extends PermissionSpecificationConstraint< // Perm['parentCapability'], // PermissionOptions, @@ -161,17 +161,14 @@ export type PermissionControllerMessenger = RestrictedControllerMessenger< // : never; type ExtractValidCaveatTypes< - Perm extends GenericPerm, + Perm extends GenericPermission, TargetName extends string, > = Perm extends { parentCapability: TargetName; caveats: null; } ? never - : Perm extends { - parentCapability: TargetName; - caveats: (infer ValidCaveats)[]; - } + : Perm extends PermConstraint ? ValidCaveats extends GenericCaveat ? ValidCaveats['type'] : never @@ -724,6 +721,7 @@ export class PermissionController< this.caveatSpecifications[ requestedCaveat.type as keyof CaveatSpecs ]; + if (!specification) { throw new CaveatTypeDoesNotExistError( requestedCaveat.type, diff --git a/packages/controllers/src/permissions/errors.ts b/packages/controllers/src/permissions/errors.ts index 71247fbf3c..d1f82295bf 100644 --- a/packages/controllers/src/permissions/errors.ts +++ b/packages/controllers/src/permissions/errors.ts @@ -23,7 +23,7 @@ export function unauthorized(arg?: ErrorArg) { export function methodNotFound(opts: MethodNotFoundArg) { const message = opts.method - ? `The method '${opts.method}' does not exist / is not available.` + ? `The method "${opts.method}" does not exist / is not available.` : undefined; return ethErrors.rpc.methodNotFound({ data: opts.data, message }); @@ -50,8 +50,12 @@ export function internalError>( } export class InvalidSubjectIdentifierError extends Error { - constructor(origin: string) { - super(`Invalid subject identifier: ${origin}`); + constructor(origin: unknown) { + super( + `Invalid subject identifier: "${ + typeof origin === 'string' ? origin : typeof origin + }"`, + ); } } @@ -75,7 +79,7 @@ export class PermissionHasNoCaveatsError extends Error { export class UnrecognizedCaveatTypeError extends Error { constructor(caveatType: string) { - super(`Unrecognized caveat type: ${caveatType}`); + super(`Unrecognized caveat type: "${caveatType}"`); } } @@ -116,7 +120,7 @@ export class InvalidCaveatError extends EthereumRpcError { export class InvalidCaveatTypeError extends Error { constructor(caveatType: unknown) { - super(`Caveat types must be strings. Received: ${typeof caveatType}`); + super(`Caveat types must be strings. Received: "${typeof caveatType}"`); } } @@ -151,16 +155,16 @@ export class InvalidCaveatFieldsError extends Error { constructor(caveat: Record, origin: string, target: string) { super( - `Caveat has unexpected number of fields: ${Object.keys(caveat).length}`, + `Caveat has unexpected number of fields: "${Object.keys(caveat).length}"`, ); this.data = { caveat, origin, target }; } } export class InvalidCaveatJsonError extends Error { - public data: { caveat: Caveat; origin: string; target: string }; + public data: { caveat: Caveat; origin: string; target: string }; - constructor(caveat: Caveat, origin: string, target: string) { + constructor(caveat: Caveat, origin: string, target: string) { super(`Caveat object is not valid JSON.`); this.data = { caveat, origin, target }; } diff --git a/packages/controllers/src/permissions/rpc-methods/getPermissions.ts b/packages/controllers/src/permissions/rpc-methods/getPermissions.ts index a69ec4a7b9..ec531ed471 100644 --- a/packages/controllers/src/permissions/rpc-methods/getPermissions.ts +++ b/packages/controllers/src/permissions/rpc-methods/getPermissions.ts @@ -3,7 +3,7 @@ import type { JsonRpcEngineEndCallback, PendingJsonRpcResponse, } from 'json-rpc-engine'; -import { GenericPerm } from '../Permission'; +import { GenericPermission } from '../Permission'; import { MethodNames } from '../enums'; import type { Permission } from '../Permission'; @@ -23,7 +23,7 @@ export const getPermissionsHandler: PermittedHandlerExport< }; export type GetPermissionsHooks = { - getPermissions: () => SubjectPermissions; + getPermissions: () => SubjectPermissions; }; async function getPermissionsImplementation( From 27a7a36e5cfd2854479ea084e63d1431c94f77b1 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Fri, 24 Sep 2021 12:18:58 -0700 Subject: [PATCH 045/147] Post-rebase fixup --- yarn.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index 79040945cc..6fc5d4fb37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2623,6 +2623,11 @@ "@types/insert-module-globals" "*" "@types/node" "*" +"@types/deep-freeze-strict@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@types/deep-freeze-strict/-/deep-freeze-strict-1.1.0.tgz#447a6a2576191344aa42310131dd3df5c41492c4" + integrity sha1-RHpqJXYZE0SqQjEBMd099cQUksQ= + "@types/glob@*", "@types/glob@^7.1.1": version "7.1.4" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.4.tgz#ea59e21d2ee5c517914cb4bc8e4153b99e566672" @@ -2631,11 +2636,6 @@ "@types/minimatch" "*" "@types/node" "*" -"@types/deep-freeze-strict@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@types/deep-freeze-strict/-/deep-freeze-strict-1.1.0.tgz#447a6a2576191344aa42310131dd3df5c41492c4" - integrity sha1-RHpqJXYZE0SqQjEBMd099cQUksQ= - "@types/graceful-fs@^4.1.2": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" From 95e45809d9f08e362168264c6fea52a61837d4da Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Fri, 24 Sep 2021 14:56:22 -0700 Subject: [PATCH 046/147] WIP: Improved type safety 4 (fix permission specifications) --- .../controllers/src/permissions/Permission.ts | 88 +++++++++---------- .../permissions/PermissionController.test.ts | 26 +++--- .../src/permissions/PermissionController.ts | 27 +++--- .../src/permissions/PermissionEnforcer.ts | 11 +-- 4 files changed, 71 insertions(+), 81 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 4051eb88f5..884ed6a812 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -154,11 +154,13 @@ export function constructPermission(options: PermissionOptions) { * @param caveatType The type of the caveat to retrieve. * @returns The caveat, or undefined if no such caveat exists. */ -export function findCaveat>( - permission: Permission, +export function findCaveat< + TargetCaveat extends GenericCaveat, + TargetPermission extends PermConstraint, +>( + permission: TargetPermission, caveatType: TargetCaveat['type'], ): TargetCaveat | undefined { - // TODO:types create a type for a permission that can have particular caveats? return permission.caveats?.find( (caveat) => caveat.type === caveatType, ) as any; @@ -247,17 +249,6 @@ export type PermissionSpecification< validator?: (permission: Permission) => void; }; -// export type PermissionSpecifications = Record< -// MethodName, -// PermissionSpecification< -// string, -// GenericPermission, -// PermissionOptions, -// Record, -// RestrictedMethodImplementation -// > -// >; - export type TargetKeyConstraint = Key extends `${string}_*` ? Key : Key extends `${string}_` @@ -269,26 +260,15 @@ export type TargetKeyConstraint = Key extends `${string}_*` export type TargetNameConstraint = Name extends `${string}*` ? never : Name extends `${string}_` ? never : Name; -// type NotWildCard = Name extends `${string}*` -// ? never -// : Name; - export type ExtractPermissionTargetNames = TargetKey extends `${infer Base}_*` ? `${Base}_${string}` : TargetKey; -// type MyKeys = 'foo' | 'foo2' | 'bar_*' | 'excluded1_' | 'excluded2*'; - -// type MyConstrainedKeys = TargetKeyConstraint - -// type MyNames = TargetNameConstraint>; - -// let foo: NotWildCard<'foo*'>; -// foo. - -// let bar: PermConstraint<'foo*', any>; -// bar. - export type PermConstraint< + TargetKey extends string, + AllowedCaveat extends GenericCaveat | never, +> = _PermConstraint, AllowedCaveat>; + +export type _PermConstraint< TargetName extends string, AllowedCaveat extends GenericCaveat | never, > = TargetNameConstraint extends never @@ -302,18 +282,36 @@ export type PermConstraint< export type GenericPermission = PermConstraint; -export type PermSpec = - TargetKeyConstraint extends never - ? never - : PermissionSpecification< - TargetKey, - PermConstraint, - PermissionOptions, - Record, - RestrictedMethodImplementation - >; - -export type PermSpecs = - TargetKeyConstraint extends never - ? never - : Record>; +export type PermSpec< + TargetKey extends string, + Perm extends PermConstraint, +> = TargetKeyConstraint extends never + ? never + : PermissionSpecification< + TargetKey, + Perm, + PermissionOptions, + Record, + RestrictedMethodImplementation + >; + +export type PermSpecs< + TargetKey extends string, + Permissions extends PermConstraint, +> = TargetKeyConstraint extends never + ? never + : { + [Key in TargetKey]: Permissions extends PermConstraint< + Key, + GenericCaveat | never + > + ? GetPermissionSpecification + : never; + }; + +export type GetPermissionSpecification< + TargetKey extends string, + Permissions extends PermConstraint, +> = Permissions extends PermConstraint + ? PermSpec + : never; diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 2f0cbc3d3a..9261ecf43e 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -70,24 +70,22 @@ function getDefaultCaveatSpecifications(): CaveatSpecs { type SecretArrayName = 'wallet_getSecretArray'; -type GetSecretArrayPermission = PermConstraint< - SecretArrayName, - FilterArrayCaveat ->; +type SecretArrayPermission = PermConstraint; type SecretObjectName = 'wallet_getSecretObject'; -type GetSecretObjectPermission = PermConstraint< +type SecretObjectPermission = PermConstraint< SecretObjectName, FilterObjectCaveat >; -// type DefaultTargetNames = SecretArrayName | SecretObjectName; +type DefaultTargetNames = SecretArrayName | SecretObjectName; -type DefaultPermissions = GetSecretArrayPermission | GetSecretObjectPermission; +type DefaultPermissions = SecretArrayPermission | SecretObjectPermission; function getDefaultPermissionSpecifications(): PermSpecs< - DefaultPermissions['parentCapability'] + DefaultTargetNames, + DefaultPermissions > { return { wallet_getSecretArray: { @@ -97,9 +95,9 @@ function getDefaultPermissionSpecifications(): PermSpecs< }, }, wallet_getSecretObject: { - target: 'wallet_getSecretArray', // TODO:types Same issue as caveat specs + target: 'wallet_getSecretObject', methodImplementation: (_args: RestrictedMethodArgs) => { - return ['secret1', 'secret2', 'secret3']; + return { secret1: 'a', secret2: 'b', secret3: 'c' }; }, }, }; @@ -136,8 +134,12 @@ function getControllerOpts(opts?: Record) { }; } -function getDefaultPermissionController(opts = getControllerOpts()) { - return new PermissionController(opts); +function getDefaultPermissionController() { + return new PermissionController< + DefaultTargetNames, + DefaultCaveats, + DefaultPermissions + >(getControllerOpts()); } describe('PermissionController', () => { diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index b5c72ac052..077244986c 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -26,7 +26,6 @@ import { GenericPermission, PermSpecs, PermConstraint, - ExtractPermissionTargetNames, } from './Permission'; import { PermissionDoesNotExistError, @@ -176,15 +175,12 @@ type ExtractValidCaveatTypes< type PermissionControllerOptions< TargetKey extends string, - Permission extends PermConstraint< - ExtractPermissionTargetNames, - GenericCaveat - >, - Caveat extends GenericCaveat, + Caveat extends GenericCaveat | never, + Permission extends PermConstraint, > = { messenger: PermissionControllerMessenger; caveatSpecifications: CaveatSpecs; - permissionSpecifications: PermSpecs; + permissionSpecifications: PermSpecs; unrestrictedMethods: string[]; state?: Partial>; }; @@ -198,11 +194,8 @@ type GrantPermissionsOptions = { export class PermissionController< TargetKey extends string, - Caveat extends GenericCaveat, - Permission extends PermConstraint< - ExtractPermissionTargetNames, - Caveat - >, + Caveat extends GenericCaveat | never, + Permission extends PermConstraint, > extends BaseController< typeof controllerName, PermissionControllerState, @@ -224,9 +217,13 @@ export class PermissionController< return this._enforcer; } - private readonly _permissionSpecifications: Readonly>; + private readonly _permissionSpecifications: Readonly< + PermSpecs + >; - public get permissionSpecifications(): Readonly> { + public get permissionSpecifications(): Readonly< + PermSpecs + > { return this._permissionSpecifications; } @@ -242,7 +239,7 @@ export class PermissionController< caveatSpecifications, permissionSpecifications, unrestrictedMethods, - }: PermissionControllerOptions) { + }: PermissionControllerOptions) { super({ name: controllerName, metadata: stateMetadata, diff --git a/packages/controllers/src/permissions/PermissionEnforcer.ts b/packages/controllers/src/permissions/PermissionEnforcer.ts index 4da79ce441..6c9ebddd89 100644 --- a/packages/controllers/src/permissions/PermissionEnforcer.ts +++ b/packages/controllers/src/permissions/PermissionEnforcer.ts @@ -19,7 +19,6 @@ import { userRejectedRequest, } from './errors'; import type { - ExtractPermissionTargetNames, PermConstraint, RequestedPermissions, RestrictedMethodImplementation, @@ -52,10 +51,7 @@ type RequestUserApproval = ( type PermissionEnforcerArgs< TargetKey extends string, Caveat extends GenericCaveat, - Permission extends PermConstraint< - ExtractPermissionTargetNames, - Caveat - >, + Permission extends PermConstraint, > = { caveatSpecifications: CaveatSpecs; isRestrictedMethod: IsRestrictedMethod; @@ -84,10 +80,7 @@ type PermissionEnforcerArgs< export class PermissionEnforcer< TargetKey extends string, Caveat extends GenericCaveat, - Permission extends PermConstraint< - ExtractPermissionTargetNames, - Caveat - >, + Permission extends PermConstraint, > { private caveatSpecifications: CaveatSpecs; From 5ab713fc38b159d014b04e59191d64f0de1e9439 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sat, 25 Sep 2021 15:54:52 -0700 Subject: [PATCH 047/147] Complete: Improved type safety --- .../controllers/src/permissions/Caveat.ts | 134 +++--- .../controllers/src/permissions/Permission.ts | 397 ++++++++++-------- .../permissions/PermissionController.test.ts | 37 +- .../src/permissions/PermissionController.ts | 271 +++++++----- .../src/permissions/PermissionEnforcer.ts | 55 ++- .../controllers/src/permissions/README.md | 37 +- .../controllers/src/permissions/errors.ts | 20 +- .../permissions/rpc-methods/getPermissions.ts | 7 +- .../rpc-methods/requestPermissions.ts | 10 +- 9 files changed, 558 insertions(+), 410 deletions(-) diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index 909cfeabe6..3d4e8b8d12 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -2,26 +2,22 @@ import { Json } from 'json-rpc-engine'; import { UnrecognizedCaveatTypeError } from './errors'; import { - Permission, - AsyncRestrictedMethodImplementation, - RestrictedMethodImplementation, + AsyncRestrictedMethod, + RestrictedMethod, + GenericPermission, } from './Permission'; -export type ZcapLdCaveat = { - /** - * The type of the caveat. The type is presumed to be meaningful in the - * context of the capability it is associated with. - * - * In MetaMask, every permission can only have one caveat of each type. - */ - readonly type: string; -}; - /** * Identical to instances of the Caveat class, useful for when TypeScript * has a meltdown over assigning classes to the Json type. + * + * @template Type - The type of the caveat. + * @template Value - The value associated with the caveat. */ -export type Caveat = { +export type CaveatConstraint< + Type extends string, + Value extends Json | never, +> = { /** * The type of the caveat. The type is presumed to be meaningful in the * context of the capability it is associated with. @@ -35,11 +31,11 @@ export type Caveat = { * * TODO: Make optional in typescript@4.4.x */ - readonly value: Value; + readonly value: Value extends never ? null : Value; }; /** - * The {@link Caveat} factory function. Naively constructs a new caveat from the + * The {@link CaveatConstraint} factory function. Naively constructs a new caveat from the * inputs. Sets `value` to `null` if no value is provided. * * @param type - The type of the caveat. @@ -58,32 +54,44 @@ export function constructCaveat( // Accomplished by decorating the restricted method implementation with the // the corresponding caveat functions. -export type CaveatDecorator = ( - decorated: AsyncRestrictedMethodImplementation, - caveat: ConcreteCaveat, -) => AsyncRestrictedMethodImplementation; +/** + * A function for applying caveats to a restricted method request. + * + * @template Caveat - The caveat type associated with this decorator. + * @param decorated - The restricted method implementation to be decorated. + * The method may have already been decorated with other caveats. + * @param caveat - The caveat object. + * @returns The decorate restricted method implementation. + */ +export type CaveatDecorator = ( + decorated: AsyncRestrictedMethod, + caveat: Caveat, +) => AsyncRestrictedMethod; -export type CaveatValidator = ( +/** + * + */ +export type CaveatValidator = ( origin: string, target: string, caveat: { type: string; value: unknown }, -) => caveat is ConcreteCaveat; +) => caveat is Caveat; -export type CaveatSpecification = { +export type CaveatSpecification = { /** * The string type of the caveat. */ - type: ConcreteCaveat['type']; + type: Caveat['type']; /** * The decorator function used to apply the caveat to restricted method * requests. */ - decorator: CaveatDecorator; + decorator: CaveatDecorator; /** * The validator function used to validate caveats of the associated type - * whenever they are instantiated. Caveats are instantiated whenever they are + * whenever they are instantiated. Caveat are instantiated whenever they are * created or mutated. * * The validator should throw an appropriate JSON-RPC error if validation fails. @@ -92,59 +100,48 @@ export type CaveatSpecification = { * performed. Although caveats can also be validated by permission validators, * validating caveat values separately is strongly recommended. */ - validator?: CaveatValidator; + validator?: CaveatValidator; }; -export type CaveatSpecifications = Readonly< - Record> ->; - -export type GenericCaveat = Caveat; - -export type CaveatSpec = - CaveatSpecification; +/** + * A generic caveat. + */ +export type GenericCaveat = CaveatConstraint; /** * An object mapping the type of each caveat to its corresponding specification. */ -export type CaveatSpecs = { - [Type in Caveats['type']]: GetCaveatSpec; +export type CaveatSpecifications = { + [Type in Caveat['type']]: GetCaveatSpecification; }; -export type GetCaveatSpec< - Caveats extends GenericCaveat, +type GetCaveatSpecification< + Caveat extends GenericCaveat, CaveatType extends string, -> = Caveats extends Caveat ? CaveatSpec : never; - -export type ExtractCaveatValue< - Caveats extends GenericCaveat, - CaveatType extends string, -> = Caveats extends Caveat ? CaveatValue : never; +> = Caveat extends CaveatConstraint + ? CaveatSpecification + : never; /** * Decorate a restricted method implementation with its caveats. * - * TODO: Document how to create caveats using below examples. - * Note that all caveat functions (i.e. the argument and return value of the decorator) - * must be awaited. + * Note that all caveat functions (i.e. the argument and return value of the + * decorator) must be awaited. */ -export function decorateWithCaveats( - methodImplementation: RestrictedMethodImplementation, - permission: Readonly, // bound to the requesting origin - caveatSpecifications: CaveatSpecs, // all caveat implementations -): RestrictedMethodImplementation { +export function decorateWithCaveats( + methodImplementation: RestrictedMethod, + permission: Readonly, // bound to the requesting origin + caveatSpecifications: CaveatSpecifications, // all caveat implementations +): RestrictedMethod { const { caveats } = permission; if (!caveats) { return methodImplementation; } - let decorated = methodImplementation as AsyncRestrictedMethodImplementation< - Json, - Json - >; + let decorated = methodImplementation as AsyncRestrictedMethod; for (const caveat of caveats) { - const specification = caveatSpecifications[caveat.type as Caveats['type']]; + const specification = caveatSpecifications[caveat.type as Caveat['type']]; if (!specification) { throw new UnrecognizedCaveatTypeError(caveat.type); } @@ -154,24 +151,3 @@ export function decorateWithCaveats( return decorated; } - -// Return next example. -// -// export function ifMetaMask(next: MethodImplementation) { -// return async (req: JsonRpcRequest, context: Record) => { -// if (context.origin !== 'metamask.io') { -// return new EthereumJsonRpcError(); -// } -// -// return next(req, context); -// }; -// } - -// "Return handler" example -// -// export function eth_accounts(next: MethodImplementation, caveat: Caveat) { -// return async (req: JsonRpcRequest, context: Record) => { -// const accounts = await next(req, context); -// return accounts.filter((account: string) => caveat.value.includes(account)) -// }; -// } diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 884ed6a812..92a4b77a28 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -1,7 +1,10 @@ import type { EthereumRpcError } from 'eth-rpc-errors'; import { Json } from 'json-rpc-engine'; import { nanoid } from 'nanoid'; -import { Caveat, GenericCaveat, ZcapLdCaveat } from './Caveat'; +import { CaveatConstraint, GenericCaveat } from './Caveat'; +// This is in fact used in a docstring, but ESLint doesn't notice this. +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import type { PermissionController } from './PermissionController'; /** * The origin of a subject. @@ -10,73 +13,126 @@ import { Caveat, GenericCaveat, ZcapLdCaveat } from './Caveat'; export type OriginString = string; /** - * The name of a restricted method. + * The name of a permission target. */ -export type MethodName = string; +export type TargetName = string; /** - * An interface + * Like {@link Array}, but always non-empty. */ -type ZcapLdCapability = { +type NonEmptyArray = [T, ...T[]]; + +/** + * The base permission interface. Lacks important constraints on its generics; + * consumers should use {@link PermissionConstraint} instead. + */ +export type PermissionBase< + Target extends TargetName, + AllowedCaveat extends GenericCaveat | never, +> = { /** * The context(s) in which this capability is meaningful. * - * It is required by the standard, but we omit it because there is only one - * context (the user's MetaMask instance). + * It is required by the standard, but we make it optional since there is only + * one context in our usage (i.e. the user's MetaMask instance). */ - readonly '@context'?: string[]; + readonly '@context'?: NonEmptyArray; + // TODO: Make optional in typescript@4.4.x /** - * The cryptograhically strong GUID of the capability. - */ - readonly id: string; - - /** - * A pointer to the resource that possession of the capability grants - * access to. - * - * In the context of MetaMask, this is always the name of an RPC method. + * The caveats of the permission. + * @see {@link CaveatConstraint} For more information. */ - readonly parentCapability: string; + readonly caveats: AllowedCaveat extends never + ? null + : NonEmptyArray | null; /** - * A pointer to the the entity that may invoke this capability. - * - * By the standard, this a link – usually some kind of URI – to a cryptographic - * key that the proof of the `proof` field "must validate against". - * - * In the context of MetaMask, this is simply the origin of subject. + * The creation date of the permission, in UNIX epoch time. */ - readonly invoker: string; + readonly date: number; /** - * The issuing date, in UNIX epoch time. + * The GUID of the permission object. */ - readonly date?: number; + readonly id: string; /** - * An array of caveat objects. See {@link ZcapLdCaveat}. - * - * TODO: Make optional in typescript@4.4.x + * The origin string of the subject that has the permission. */ - readonly caveats: ZcapLdCaveat[] | null; + readonly invoker: OriginString; /** - * The proof that this capability was delegated to the specified invoker. - * By the standard, usually just a cryptographic signature of the capability - * object, excluding this field. + * A pointer to the resource that possession of the capability grants + * access to. * - * In MetaMask, the "proof" of validity is the existence of a valid capability - * object in the designated part of our state tree, so this field is omitted. + * In the context of MetaMask, this is always the name of an RPC method. */ - readonly proof?: string; + readonly parentCapability: Target extends `${string}_` + ? `${Target}${string}` + : Target; }; +/** + * Makes the {@link PermissionBase.caveats} property of the given permission mutable. + * Useful for mutating the caveats of a permission in state updates. + * + * @template Permission - The permission type to modify. + */ +export type MutableCaveats = { + -readonly [K in keyof Pick]: Permission[K]; +} & + { + [K in keyof Omit]: Permission[K]; + }; + +/** + * A utility type for ensuring that the given permission target name conforms to + * our naming conventions. + * + * See the documentation for the distinction between target names and keys. + */ +type TargetNameConstraint = Name extends `${string}*` + ? never + : Name extends `${string}_` + ? never + : Name; + +/** + * A utility type for extracting permission target names from a union of target + * keys. + * + * See the documentation for the distinction between target names and keys. + */ +type ExtractPermissionTargetNames = + TargetKey extends `${infer Base}_*` ? `${Base}_${string}` : TargetKey; + +/** + * The consumer-facing base permission type. + */ +export type PermissionConstraint< + TargetKey extends string, + AllowedCaveat extends GenericCaveat | never, +> = TargetNameConstraint> extends never + ? never + : PermissionBase, AllowedCaveat>; + +/** + * A generic permission. + */ +export type GenericPermission = PermissionConstraint< + TargetName, + GenericCaveat | never +>; + +/** + * The options object of {@link constructPermission}. + */ export type PermissionOptions = { /** * The method that the permission corresponds to. */ - target: MethodName; + target: TargetName; /** * The origin string of the subject that has the permission. @@ -91,40 +147,9 @@ export type PermissionOptions = { /** * The caveats of the permission. - * See {@link Caveat}. - */ - caveats?: Caveat[]; -}; - -/** - * TODO: Document - */ -export type Permission = Omit< - ZcapLdCapability, - '@context' | 'caveats' | 'proof' -> & { - /** - * The GUID of the permission object. - */ - readonly id: string; - - /** - * The creation date of the permission, in UNIX epoch time. - */ - readonly date: number; - - /** - * The caveats of the permission. - * @see Caveat - * - * TODO: Make optional in typescript@4.4.x - */ - readonly caveats: Caveat[] | null; - - /** - * The origin string of the subject that has the permission. + * See {@link CaveatConstraint}. */ - readonly invoker: OriginString; + caveats?: CaveatConstraint[]; }; /** @@ -156,7 +181,7 @@ export function constructPermission(options: PermissionOptions) { */ export function findCaveat< TargetCaveat extends GenericCaveat, - TargetPermission extends PermConstraint, + TargetPermission extends PermissionConstraint, >( permission: TargetPermission, caveatType: TargetCaveat['type'], @@ -166,90 +191,123 @@ export function findCaveat< ) as any; } -type RequestedPermission = { - target?: MethodName; - caveats?: Caveat[]; -}; +/** + * A requested permission object. Just an object with any of the properties + * of a {@link GenericPermission} object. + */ +type RequestedPermission = Partial; -export type RequestedPermissions = Record; +/** + * A record of target names and their {@link RequestedPermission} objects. + */ +export type RequestedPermissions = Record; -export type RestrictedMethodContext = Readonly<{ +/** + * The restricted method context object. Essentially a way to pass internal + * arguments to restricted methods and caveat functions, most importantly the + * requesting origin. + */ +type RestrictedMethodContext = Readonly<{ origin: OriginString; [key: string]: any; }>; -// TODO: Should restricted methods just take (method, params, context) instead? +/** + * The arguments passed to a restricted method implementation. + * + * @template Params - The JSON-RPC parameters of the restricted method. + */ export type RestrictedMethodArgs = { - method: string; + method: TargetName; params?: Params; context: RestrictedMethodContext; }; -export type SyncRestrictedMethodImplementation< - Params extends Json, - Result extends Json, -> = ( +/** + * A synchronous restricted method implementation. + * + * @template Params - The JSON-RPC parameters of the restricted method. + * @template Result - The JSON-RPC result of the restricted method. + */ +export type SyncRestrictedMethod = ( args: RestrictedMethodArgs, ) => Result | Error | EthereumRpcError; -export type AsyncRestrictedMethodImplementation< - Params extends Json, - Result extends Json, -> = ( +/** + * An asynchronous restricted method implementation. + * + * @template Params - The JSON-RPC parameters of the restricted method. + * @template Result - The JSON-RPC result of the restricted method. + */ +export type AsyncRestrictedMethod = ( args: RestrictedMethodArgs, ) => Promise>; -export type RestrictedMethodImplementation< - Params extends Json, - Result extends Json, -> = - | SyncRestrictedMethodImplementation - | AsyncRestrictedMethodImplementation; +/** + * A synchronous or asynchronous restricted method implementation. + * + * @template Params - The JSON-RPC parameters of the restricted method. + * @template Result - The JSON-RPC result of the restricted method. + */ +export type RestrictedMethod = + | SyncRestrictedMethod + | AsyncRestrictedMethod; -export type PermissionSpecification< +/** + * The base permission specification interface. Lacks important constraints on + * its generics; consumers should use {@link PermissionSpecificationConstraint} + * instead. + */ +type PermissionSpecificationBase< TargetKey extends string, - Perm extends PermConstraint, + Permission extends PermissionConstraint, FactoryOptions extends PermissionOptions, RequestData extends Record, - MethodImplementation extends RestrictedMethodImplementation, -> = TargetKeyConstraint extends never - ? never - : { - /** - * The target resource of the permission. In other words, at the time of - * writing, the RPC method name. - */ - target: TargetKey; - - /** - * The factory function used to get permission objects. Permissions returned - * by this function are presumed to valid, and they will not be passed to the - * validator function associated with this specification (if any). In other - * words, the factory function should validate the permissions it creates. - * - * If no factory is specified, the {@link Permission} constructor will be - * used, and the validator function (if specified) will be called on newly - * constructed permissions. - */ - factory?: (options: FactoryOptions, requestData?: RequestData) => Perm; - - /** - * The implementation of the restricted method that the permission - * corresponds to. - */ - methodImplementation: MethodImplementation; - - /** - * The validator function used to validate permissions of the associated type - * whenever they are mutated. The only way a permission can be legally mutated - * is when its caveats are modified by the permission controller. - * - * The validator should throw an appropriate JSON-RPC error if validation fails. - */ - validator?: (permission: Permission) => void; - }; + MethodImplementation extends RestrictedMethod, +> = { + /** + * The target resource of the permission. In other words, at the time of + * writing, the RPC method name. + */ + target: TargetKey; -export type TargetKeyConstraint = Key extends `${string}_*` + /** + * The factory function used to get permission objects. Permissions returned + * by this function are presumed to valid, and they will not be passed to the + * validator function associated with this specification (if any). In other + * words, the factory function should validate the permissions it creates. + * + * If no factory is specified, the {@link Permission} constructor will be + * used, and the validator function (if specified) will be called on newly + * constructed permissions. + */ + factory?: (options: FactoryOptions, requestData?: RequestData) => Permission; + + /** + * The implementation of the restricted method that the permission + * corresponds to. + */ + methodImplementation: MethodImplementation; + + /** + * The validator function used to validate permissions of the associated type + * whenever they are mutated. The only way a permission can be legally mutated + * is when its caveats are modified by the permission controller. + * + * The validator should throw an appropriate JSON-RPC error if validation fails. + */ + validator?: (permission: GenericPermission) => void; +}; + +/** + * A utility type for ensuring that the given permission target key conforms to + * our naming conventions. + * + * See the documentation for the distinction between target names and keys. + * + * @template Key - The target key string to apply the constraint to. + */ +type TargetKeyConstraint = Key extends `${string}_*` ? Key : Key extends `${string}_` ? never @@ -257,61 +315,52 @@ export type TargetKeyConstraint = Key extends `${string}_*` ? never : Key; -export type TargetNameConstraint = - Name extends `${string}*` ? never : Name extends `${string}_` ? never : Name; - -export type ExtractPermissionTargetNames = - TargetKey extends `${infer Base}_*` ? `${Base}_${string}` : TargetKey; - -export type PermConstraint< - TargetKey extends string, - AllowedCaveat extends GenericCaveat | never, -> = _PermConstraint, AllowedCaveat>; - -export type _PermConstraint< - TargetName extends string, - AllowedCaveat extends GenericCaveat | never, -> = TargetNameConstraint extends never - ? never - : Omit & { - caveats: AllowedCaveat extends never ? null : AllowedCaveat[] | null; - parentCapability: TargetName extends `${string}_` - ? `${TargetName}${string}` - : TargetName; - }; - -export type GenericPermission = PermConstraint; - -export type PermSpec< +/** + * An individual permission specification object. Constrained such that the + * given target key must be valid. + * + * @template TargetKey - The key of the permission target. + * @template Permission - The type of the permission object. + */ +export type PermissionSpecificationConstraint< TargetKey extends string, - Perm extends PermConstraint, + Permission extends PermissionConstraint, > = TargetKeyConstraint extends never ? never - : PermissionSpecification< + : PermissionSpecificationBase< TargetKey, - Perm, + Permission, PermissionOptions, Record, - RestrictedMethodImplementation + RestrictedMethod >; -export type PermSpecs< +/** + * Utility type used in {@link PermissionSpecifications} to map permission + * target keys to their individual {@link PermissionSpecificationConstraint} + * objects. + */ +type GetPermissionSpecification< TargetKey extends string, - Permissions extends PermConstraint, + Permissions extends PermissionConstraint, +> = Permissions extends PermissionConstraint + ? PermissionSpecificationConstraint + : never; + +/** + * The specifications for all permissions and restricted methods supported by + * a particular {@link PermissionController}. + */ +export type PermissionSpecifications< + TargetKey extends string, + Permissions extends PermissionConstraint, > = TargetKeyConstraint extends never ? never : { - [Key in TargetKey]: Permissions extends PermConstraint< + [Key in TargetKey]: Permissions extends PermissionConstraint< Key, GenericCaveat | never > ? GetPermissionSpecification : never; }; - -export type GetPermissionSpecification< - TargetKey extends string, - Permissions extends PermConstraint, -> = Permissions extends PermConstraint - ? PermSpec - : never; diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 9261ecf43e..6a3d624dc8 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -2,37 +2,43 @@ import { ControllerMessenger, Json } from '@metamask/controllers'; import { isPlainObject } from '../utils'; import * as errors from './errors'; import { - AsyncRestrictedMethodImplementation, - Caveat, - CaveatSpecs, - PermConstraint, + AsyncRestrictedMethod, + CaveatConstraint, + CaveatSpecifications, + PermissionConstraint, PermissionController, PermissionControllerActions, PermissionControllerEvents, PermissionEnforcer, - PermSpecs, + PermissionSpecifications, RestrictedMethodArgs, } from '.'; -// Caveat types and specifications +// CaveatConstraint types and specifications enum CaveatTypes { filterArrayResponse = 'filterArrayResponse', filterObjectResponse = 'filterObjectResponse', } -type FilterArrayCaveat = Caveat; -type FilterObjectCaveat = Caveat; +type FilterArrayCaveat = CaveatConstraint< + CaveatTypes.filterArrayResponse, + string[] +>; +type FilterObjectCaveat = CaveatConstraint< + CaveatTypes.filterObjectResponse, + string[] +>; type DefaultCaveats = FilterArrayCaveat | FilterObjectCaveat; -function getDefaultCaveatSpecifications(): CaveatSpecs { +function getDefaultCaveatSpecifications(): CaveatSpecifications { return { filterArrayResponse: { type: CaveatTypes.filterArrayResponse, decorator: ( - method: AsyncRestrictedMethodImplementation, + method: AsyncRestrictedMethod, caveat: FilterArrayCaveat, ) => async (args: RestrictedMethodArgs) => { @@ -48,7 +54,7 @@ function getDefaultCaveatSpecifications(): CaveatSpecs { type: CaveatTypes.filterObjectResponse, decorator: ( - method: AsyncRestrictedMethodImplementation, + method: AsyncRestrictedMethod, caveat: FilterObjectCaveat, ) => async (args: RestrictedMethodArgs) => { @@ -70,11 +76,14 @@ function getDefaultCaveatSpecifications(): CaveatSpecs { type SecretArrayName = 'wallet_getSecretArray'; -type SecretArrayPermission = PermConstraint; +type SecretArrayPermission = PermissionConstraint< + SecretArrayName, + FilterArrayCaveat +>; type SecretObjectName = 'wallet_getSecretObject'; -type SecretObjectPermission = PermConstraint< +type SecretObjectPermission = PermissionConstraint< SecretObjectName, FilterObjectCaveat >; @@ -83,7 +92,7 @@ type DefaultTargetNames = SecretArrayName | SecretObjectName; type DefaultPermissions = SecretArrayPermission | SecretObjectPermission; -function getDefaultPermissionSpecifications(): PermSpecs< +function getDefaultPermissionSpecifications(): PermissionSpecifications< DefaultTargetNames, DefaultPermissions > { diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 077244986c..a8f020f11a 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -12,20 +12,21 @@ import { Json } from 'json-rpc-engine'; import { isPlainObject, hasProperty } from '../utils'; import { - CaveatSpecs, + CaveatConstraint, + CaveatSpecifications, constructCaveat, - ExtractCaveatValue, GenericCaveat, } from './Caveat'; import { - OriginString, - MethodName, - RequestedPermissions, - RestrictedMethodImplementation, constructPermission, GenericPermission, - PermSpecs, - PermConstraint, + MutableCaveats, + OriginString, + PermissionConstraint, + PermissionSpecifications, + RequestedPermissions, + RestrictedMethod, + TargetName, } from './Permission'; import { PermissionDoesNotExistError, @@ -44,76 +45,126 @@ import { import { PermissionEnforcer, PermissionsRequest } from './PermissionEnforcer'; import { MethodNames } from './enums'; +/** + * Metadata associated with {@link PermissionController} subjects. + */ export type PermissionSubjectMetadata = { origin: OriginString; }; +/** + * The name of the {@link PermissionController}. + */ const controllerName = 'PermissionController'; +/** + * Permissions associated with a {@link PermissionController} subject. + */ export type SubjectPermissions = Record< Permission['parentCapability'], Permission >; +/** + * Permissions and metadata associated with a {@link PermissionController} + * subject. + */ export type PermissionSubjectEntry = { permissions: SubjectPermissions; } & PermissionSubjectMetadata; +/** + * All subjects of a {@link PermissionController}. + */ export type PermissionControllerSubjects = Record>; // TODO:TS4.4 Enable compiler flags to forbid unchecked member access +/** + * The state of a {@link PermissionController}. + */ export type PermissionControllerState = { subjects: PermissionControllerSubjects; }; +/** + * The state metadata of the {@link PermissionController}. + */ const stateMetadata = { subjects: { persist: true, anonymous: true }, }; // Typecast: We can't reference the class generics, so we must resort to "any" +/** + * The default state of the {@link PermissionController}. + */ const defaultState: PermissionControllerState = { subjects: {}, }; -export type GetPermissionsState = { +/** + * Gets the state of the {@link PermissionController}. + */ +export type GetPermissionControllerState = { type: `${typeof controllerName}:getState`; handler: () => PermissionControllerState; }; +/** + * Gets the names of all subjects from the {@link PermissionController}. + */ export type GetSubjects = { type: `${typeof controllerName}:getSubjects`; handler: () => (keyof PermissionControllerSubjects)[]; }; +/** + * Clears all permissions from the {@link PermissionController}. + */ export type ClearPermissions = { type: `${typeof controllerName}:clearPermissions`; handler: () => void; }; // TODO: Implement all desired actions +/** + * The {@link ControllerMessenger} actions of the {@link PermissionController}. + */ export type PermissionControllerActions = - | GetPermissionsState + | GetPermissionControllerState | GetSubjects | ClearPermissions; -export type PermissionsStateChange = { +/** + * The generic state change event of the {@link PermissionController}. + */ +export type PermissionControllerStateChange = { type: `${typeof controllerName}:stateChange`; payload: [PermissionControllerState, Patch[]]; }; /** - * The controller only emits its generic state change events. Consumers should - * use selector subscriptions to subscribe to relevant substate. + * The {@link ControllerMessenger} events of the {@link PermissionController}. + * + * The permission controller only emits its generic state change events. + * Consumers should use selector subscriptions to subscribe to relevant + * substate. */ -export type PermissionControllerEvents = PermissionsStateChange; +export type PermissionControllerEvents = PermissionControllerStateChange; +/** + * The external {@link ControllerMessenger} actions available to the + * {@link PermissionController}. + */ type AllowedActions = | AddApprovalRequest | HasApprovalRequest | AcceptApprovalRequest | RejectApprovalRequest; +/** + * The messenger of the {@link PermissionController}. + */ export type PermissionControllerMessenger = RestrictedControllerMessenger< typeof controllerName, PermissionControllerActions | AllowedActions, @@ -122,69 +173,66 @@ export type PermissionControllerMessenger = RestrictedControllerMessenger< never >; -// type ExtractPermissionOptions< -// PSpec, -// Perm extends GenericPermission, -// > = PSpec extends PermissionSpecificationConstraint< -// Perm['parentCapability'], -// PermConstraint<>, -// infer PermOpts, -// Record, -// RestrictedMethodImplementation -// > -// ? PermOpts -// : never; - -// type ExtractRequestData< -// PSpec, -// Perm extends GenericPermission, -// > = PSpec extends PermissionSpecificationConstraint< -// Perm['parentCapability'], -// PermissionOptions, -// infer RequestData, -// RestrictedMethodImplementation -// > -// ? RequestData -// : never; - -// type ExtractMethodImplementation< -// PSpec, -// Perm extends GenericPermission, -// > = PSpec extends PermissionSpecificationConstraint< -// Perm['parentCapability'], -// PermissionOptions, -// Record, -// infer Impl -// > -// ? Impl -// : never; +/** + * A utility type for extracting the {@link CaveatConstraint.value} type from + * a union of caveat types. + * + * @template CaveatUnion - The caveat type union to extract a value type from. + * @template CaveatType - The type of the caveat whose value to extract. + */ +export type ExtractCaveatValue< + CaveatUnion extends GenericCaveat, + CaveatType extends string, +> = CaveatUnion extends CaveatConstraint + ? CaveatValue + : never; +/** + * A utility type for extracting the valid caveat types for a particular + * permission from a union of permission types. + * + * @template PermissionUnion - The permission type union to extract valid caveat + * types from. + * @template TargetName - The target name of the permission. + */ type ExtractValidCaveatTypes< - Perm extends GenericPermission, + PermissionUnion extends GenericPermission, TargetName extends string, -> = Perm extends { - parentCapability: TargetName; - caveats: null; -} +> = PermissionUnion extends PermissionConstraint ? never - : Perm extends PermConstraint + : PermissionUnion extends PermissionConstraint ? ValidCaveats extends GenericCaveat ? ValidCaveats['type'] : never : never; +/** + * Options for the {@link PermissionController} constructor. + * + * @template TargetKey - A union of string literal types corresponding to the + * keys of all permission target. Must match the given permission + * specifications object. + * @template Caveat - A union of the types of all caveats available to the new + * controller. Must match the given caveat specifications object. + * @template Permission - A union of the types of all permissions available to + * the new controller. Must match the given permission and caveat specification + * objects. + */ type PermissionControllerOptions< TargetKey extends string, Caveat extends GenericCaveat | never, - Permission extends PermConstraint, + Permission extends PermissionConstraint, > = { messenger: PermissionControllerMessenger; - caveatSpecifications: CaveatSpecs; - permissionSpecifications: PermSpecs; + caveatSpecifications: CaveatSpecifications; + permissionSpecifications: PermissionSpecifications; unrestrictedMethods: string[]; state?: Partial>; }; +/** + * Options for {@link PermissionController.grantPermissions}. + */ type GrantPermissionsOptions = { approvedPermissions: RequestedPermissions; subject: PermissionSubjectMetadata; @@ -192,18 +240,33 @@ type GrantPermissionsOptions = { requestData?: Record; }; +/** + * @template TargetKey - A union of string literal types corresponding to the + * keys of all permission target. Must match the permission specifications of + * the controller. + * @template Caveat - A union of the types of all caveats available to the new + * controller. Must match the caveat specifications of the controller. + * @template Permission - A union of the types of all permissions available to + * the new controller. Must match the the permission and caveat specifications + * of the controller. + */ export class PermissionController< TargetKey extends string, Caveat extends GenericCaveat | never, - Permission extends PermConstraint, + Permission extends PermissionConstraint, > extends BaseController< typeof controllerName, PermissionControllerState, PermissionControllerMessenger > { - private readonly _caveatSpecifications: Readonly>; + private readonly _caveatSpecifications: Readonly< + CaveatSpecifications + >; - public get caveatSpecifications(): Readonly> { + /** + * The {@link CaveatSpecifications} of this controller. + */ + public get caveatSpecifications(): Readonly> { return this._caveatSpecifications; } @@ -211,6 +274,9 @@ export class PermissionController< PermissionEnforcer >; + /** + * The {@link PermissionEnforcer} of this controller. + */ public get enforcer(): Readonly< PermissionEnforcer > { @@ -218,28 +284,52 @@ export class PermissionController< } private readonly _permissionSpecifications: Readonly< - PermSpecs + PermissionSpecifications >; + /** + * The {@link PermissionSpecifications} of this controller. + */ public get permissionSpecifications(): Readonly< - PermSpecs + PermissionSpecifications > { return this._permissionSpecifications; } private readonly _unrestrictedMethods: ReadonlySet; + /** + * The names of all JSON-RPC methods that will be ignored by this controller. + */ public get unrestrictedMethods(): ReadonlySet { return this._unrestrictedMethods; } - constructor({ - messenger, - state = {}, - caveatSpecifications, - permissionSpecifications, - unrestrictedMethods, - }: PermissionControllerOptions) { + /** + * @param options - Permission controller options. + * @param options.caveatSpecifications - The specifications of all caveats + * available to this controller. See {@link CaveatSpecifications} and the + * documentation for more details. + * @param options.permissionSpecifications - The specifications of all + * permissions available to this controller. See + * {@link PermissionSpecifications} and the documentation for more details. + * @param options.unrestrictedMethods - The callable names of all JSON-RPC + * methods ignored by the new controller. + * @param options.messenger - The controller messenger. See + * {@link BaseController} for more information. + * @param options.state - Existing state to hydrate the controller with at + * initialization. + */ + constructor( + options: PermissionControllerOptions, + ) { + const { + caveatSpecifications, + permissionSpecifications, + unrestrictedMethods, + messenger, + state = {}, + } = options; super({ name: controllerName, metadata: stateMetadata, @@ -343,7 +433,7 @@ export class PermissionController< */ private setValidatedPermissions( origin: OriginString, - permissions: Record, + permissions: Record, shouldPreserveExistingPermissions = false, ): void { this.update((draftState) => { @@ -472,22 +562,6 @@ export class PermissionController< this.setCaveat(origin, target, caveatType, caveatValue); } - // private getCaveat< - // TargetName extends Permission['parentCapability'], - // CaveatType extends ExtractValidCaveatTypes, - // >( - // origin: OriginString, - // target: TargetName, - // caveatType: CaveatType, - // ): CaveatType | undefined { - // const permission = this.getPermission(origin, target); - // if (!permission) { - // throw new PermissionDoesNotExistError(origin, target); - // } - - // return findCaveat(permission, caveatType); - // } - private setCaveat< TargetName extends Permission['parentCapability'], CaveatType extends ExtractValidCaveatTypes, @@ -499,9 +573,9 @@ export class PermissionController< ): void { this.update((draftState) => { // Typecast: immer's WritableDraft is incompatible with our generics - const permission = ( + const permission: MutableCaveats = ( draftState.subjects as PermissionControllerSubjects - )[origin]?.permissions[target] as Permission; + )[origin]?.permissions[target]; /* istanbul ignore if: This should be impossible, but TypeScript wants it */ if (!permission) { throw new PermissionDoesNotExistError(origin, target); @@ -531,7 +605,7 @@ export class PermissionController< permission.caveats = [caveat] as any; } - this.validateModifiedPermission(permission); + this.validateModifiedPermission(permission as Permission); }); } @@ -541,7 +615,7 @@ export class PermissionController< >(origin: OriginString, target: TargetName, caveatType: CaveatType): void { this.update((draftState) => { // Typecast: immer's WritableDraft is incompatible with our generics - const permission: Permission = ( + const permission: MutableCaveats = ( draftState.subjects as PermissionControllerSubjects )[origin]?.permissions[target]; if (!permission) { @@ -565,7 +639,7 @@ export class PermissionController< permission.caveats.splice(caveatIndex, 1); } - this.validateModifiedPermission(permission); + this.validateModifiedPermission(permission as Permission); }); } @@ -615,9 +689,9 @@ export class PermissionController< return undefined; } - getRestrictedMethodImplementation( + getRestrictedMethod( method: Permission['parentCapability'], - ): RestrictedMethodImplementation | undefined { + ): RestrictedMethod | undefined { const targetKey = this.getTargetKey(method); if (!targetKey) { return undefined; @@ -689,7 +763,7 @@ export class PermissionController< private computeCaveats( origin: OriginString, target: Permission['parentCapability'], - caveats?: unknown[], + caveats?: unknown[] | null, ): Caveat[] | undefined { const caveatArray = caveats?.map((requestedCaveat) => this.computeCaveat(origin, target, requestedCaveat), @@ -716,7 +790,7 @@ export class PermissionController< const specification = this.caveatSpecifications[ - requestedCaveat.type as keyof CaveatSpecs + requestedCaveat.type as keyof CaveatSpecifications ]; if (!specification) { @@ -744,8 +818,7 @@ export class PermissionController< return new PermissionEnforcer({ caveatSpecifications: this.caveatSpecifications, getPermission: this.getPermission.bind(this), - getRestrictedMethodImplementation: - this.getRestrictedMethodImplementation.bind(this), + getRestrictedMethod: this.getRestrictedMethod.bind(this), grantPermissions: this.grantPermissions.bind(this), isRestrictedMethod: (method: string) => { return Boolean( diff --git a/packages/controllers/src/permissions/PermissionEnforcer.ts b/packages/controllers/src/permissions/PermissionEnforcer.ts index 6c9ebddd89..ed5c723d1c 100644 --- a/packages/controllers/src/permissions/PermissionEnforcer.ts +++ b/packages/controllers/src/permissions/PermissionEnforcer.ts @@ -10,7 +10,11 @@ import { import { nanoid } from 'nanoid'; import { isPlainObject, hasProperty } from '../utils'; -import { CaveatSpecs, decorateWithCaveats, GenericCaveat } from './Caveat'; +import { + CaveatSpecifications, + decorateWithCaveats, + GenericCaveat, +} from './Caveat'; import { internalError, invalidParams, @@ -19,9 +23,9 @@ import { userRejectedRequest, } from './errors'; import type { - PermConstraint, + PermissionConstraint, RequestedPermissions, - RestrictedMethodImplementation, + RestrictedMethod, } from './Permission'; import type { PermissionController, @@ -51,9 +55,9 @@ type RequestUserApproval = ( type PermissionEnforcerArgs< TargetKey extends string, Caveat extends GenericCaveat, - Permission extends PermConstraint, + Permission extends PermissionConstraint, > = { - caveatSpecifications: CaveatSpecs; + caveatSpecifications: CaveatSpecifications; isRestrictedMethod: IsRestrictedMethod; isUnrestrictedMethod: IsUnrestrictedMethod; getPermission: PermissionController< @@ -61,11 +65,11 @@ type PermissionEnforcerArgs< Caveat, Permission >['getPermission']; - getRestrictedMethodImplementation: PermissionController< + getRestrictedMethod: PermissionController< TargetKey, Caveat, Permission - >['getRestrictedMethodImplementation']; + >['getRestrictedMethod']; grantPermissions: PermissionController< TargetKey, Caveat, @@ -80,9 +84,9 @@ type PermissionEnforcerArgs< export class PermissionEnforcer< TargetKey extends string, Caveat extends GenericCaveat, - Permission extends PermConstraint, + Permission extends PermissionConstraint, > { - private caveatSpecifications: CaveatSpecs; + private caveatSpecifications: CaveatSpecifications; private isRestrictedMethod: IsRestrictedMethod; @@ -108,18 +112,18 @@ export class PermissionEnforcer< private _hasApprovalRequest: ApprovalController['has']; - private _getRestrictedMethodImplementation: PermissionController< + private _getRestrictedMethod: PermissionController< TargetKey, Caveat, Permission - >['getRestrictedMethodImplementation']; + >['getRestrictedMethod']; constructor({ caveatSpecifications, isRestrictedMethod, isUnrestrictedMethod, getPermission, - getRestrictedMethodImplementation, + getRestrictedMethod, grantPermissions, requestUserApproval, acceptPermissionsRequest, @@ -135,7 +139,7 @@ export class PermissionEnforcer< this._acceptPermissionsRequest = acceptPermissionsRequest; this._rejectPermissionsRequest = rejectPermissionsRequest; this._hasApprovalRequest = hasApprovalRequest; - this._getRestrictedMethodImplementation = getRestrictedMethodImplementation; + this._getRestrictedMethod = getRestrictedMethod; } public async requestPermissions( @@ -207,9 +211,8 @@ export class PermissionEnforcer< const perms: RequestedPermissions = requestedPermissions; for (const methodName of Object.keys(perms)) { - const target = - perms[methodName].target || - (perms[methodName] as unknown as Permission).parentCapability; + const target = (perms[methodName] as unknown as Permission) + .parentCapability; if (target !== undefined && methodName !== target) { throw invalidParams({ data: { origin, requestedPermissions } }); } @@ -284,10 +287,7 @@ export class PermissionEnforcer< req.params = params; } - const methodImplementation = this.getRestrictedMethodImplementation( - method, - req, - ); + const methodImplementation = this.getRestrictedMethod(method, req); const result = await this._executeRestrictedMethod( methodImplementation, @@ -316,11 +316,11 @@ export class PermissionEnforcer< * @returns The restricted method implementation, or throws an error if no * method exists. */ - private getRestrictedMethodImplementation( + private getRestrictedMethod( method: string, request: JsonRpcRequest, - ): RestrictedMethodImplementation { - const methodImplementation = this._getRestrictedMethodImplementation( + ): RestrictedMethod { + const methodImplementation = this._getRestrictedMethod( method as Permission['parentCapability'], ); if (!methodImplementation) { @@ -340,10 +340,10 @@ export class PermissionEnforcer< * @returns */ private _executeRestrictedMethod( - methodImplementation: RestrictedMethodImplementation, + methodImplementation: RestrictedMethod, subject: PermissionSubjectMetadata, req: JsonRpcRequest, - ): ReturnType> { + ): ReturnType> { const { origin } = subject; const { method, params } = req; @@ -378,10 +378,7 @@ export class PermissionEnforcer< } // This will throw if no restricted method implementation is found. - const methodImplementation = this.getRestrictedMethodImplementation( - method, - req, - ); + const methodImplementation = this.getRestrictedMethod(method, req); const result = await this._executeRestrictedMethod( methodImplementation, diff --git a/packages/controllers/src/permissions/README.md b/packages/controllers/src/permissions/README.md index c1185366a1..7c27a504b1 100644 --- a/packages/controllers/src/permissions/README.md +++ b/packages/controllers/src/permissions/README.md @@ -75,7 +75,7 @@ const caveatSpecifications = { decorator: (( // Restricted methods and other caveats can be async, so we have to // assume that the method is async. - method: AsyncRestrictedMethodImplementation, + method: AsyncRestrictedMethod, caveat: { type: 'filterArrayResponse'; value: string[] }, ) => async (args: RestrictedMethodArgs) => { @@ -151,3 +151,38 @@ const existingPermissions = await ethereum.request({ method: 'wallet_getPermissions', ) ``` + +### Caveat Decorators + +```typescript +// Validation / passthrough +export function onlyArrayParams( + next: MethodImplementation, + _caveat: Caveat, +) { + return async ( + req: JsonRpcRequest, + context: Record, + ) => { + if (!Array.isArray(req.params)) { + return new EthereumJsonRpcError(); + } + + return next(req, context); + }; +} + +// "Return handler" example +export function eth_accounts( + next: MethodImplementation, + caveat: Caveat, +) { + return async ( + req: JsonRpcRequest, + context: Record, + ) => { + const accounts = await next(req, context); + return accounts.filter((account: string) => caveat.value.includes(account)); + }; +} +``` diff --git a/packages/controllers/src/permissions/errors.ts b/packages/controllers/src/permissions/errors.ts index d1f82295bf..d1242a5f93 100644 --- a/packages/controllers/src/permissions/errors.ts +++ b/packages/controllers/src/permissions/errors.ts @@ -1,7 +1,7 @@ import { JsonRpcRequest } from 'json-rpc-engine'; import { errorCodes, ethErrors, EthereumRpcError } from 'eth-rpc-errors'; -import { Permission } from './Permission'; -import { Caveat } from './Caveat'; +import { GenericPermission } from './Permission'; +import { CaveatConstraint } from './Caveat'; type ErrorArg = { message?: string; @@ -100,9 +100,9 @@ export class CaveatAlreadyExistsError extends Error { } export class InvalidPermissionJsonError extends Error { - public data: { origin: string; permission: Permission }; + public data: { origin: string; permission: GenericPermission }; - constructor(origin: string, permission: Permission) { + constructor(origin: string, permission: GenericPermission) { super(`Permission object of subject "${origin}" is not valid JSON.`); this.data = { origin, permission }; } @@ -162,9 +162,17 @@ export class InvalidCaveatFieldsError extends Error { } export class InvalidCaveatJsonError extends Error { - public data: { caveat: Caveat; origin: string; target: string }; + public data: { + caveat: CaveatConstraint; + origin: string; + target: string; + }; - constructor(caveat: Caveat, origin: string, target: string) { + constructor( + caveat: CaveatConstraint, + origin: string, + target: string, + ) { super(`Caveat object is not valid JSON.`); this.data = { caveat, origin, target }; } diff --git a/packages/controllers/src/permissions/rpc-methods/getPermissions.ts b/packages/controllers/src/permissions/rpc-methods/getPermissions.ts index ec531ed471..416d6d9a39 100644 --- a/packages/controllers/src/permissions/rpc-methods/getPermissions.ts +++ b/packages/controllers/src/permissions/rpc-methods/getPermissions.ts @@ -3,16 +3,15 @@ import type { JsonRpcEngineEndCallback, PendingJsonRpcResponse, } from 'json-rpc-engine'; -import { GenericPermission } from '../Permission'; import { MethodNames } from '../enums'; -import type { Permission } from '../Permission'; +import type { GenericPermission } from '../Permission'; import type { SubjectPermissions } from '../PermissionController'; export const getPermissionsHandler: PermittedHandlerExport< GetPermissionsHooks, void, - Permission[] + GenericPermission[] > = { methodNames: [MethodNames.getPermissions], implementation: getPermissionsImplementation, @@ -28,7 +27,7 @@ export type GetPermissionsHooks = { async function getPermissionsImplementation( _req: unknown, - res: PendingJsonRpcResponse, + res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, { getPermissions }: GetPermissionsHooks, diff --git a/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts b/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts index 927c67571e..f1af941467 100644 --- a/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts +++ b/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts @@ -8,12 +8,12 @@ import { nanoid } from 'nanoid'; import { MethodNames } from '../enums'; import { invalidParams } from '../errors'; -import type { Permission, RequestedPermissions } from '../Permission'; +import type { GenericPermission, RequestedPermissions } from '../Permission'; export const requestPermissionsHandler: PermittedHandlerExport< RequestPermissionsHooks, [RequestedPermissions], - Permission[] + GenericPermission[] > = { methodNames: [MethodNames.requestPermissions], implementation: requestPermissionsImplementation, @@ -27,7 +27,9 @@ export const requestPermissionsHandler: PermittedHandlerExport< type RequestPermissions = ( requestedPermissions: RequestedPermissions, id: string, -) => Promise<[Record, { id: string; origin: string }]>; +) => Promise< + [Record, { id: string; origin: string }] +>; export type RequestPermissionsHooks = { requestPermissions: RequestPermissions; @@ -35,7 +37,7 @@ export type RequestPermissionsHooks = { async function requestPermissionsImplementation( req: JsonRpcRequest<[RequestedPermissions]>, - res: PendingJsonRpcResponse, + res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, { requestPermissions }: RequestPermissionsHooks, From 4c15b7ad3478a73f3f16a81b3f1c492b2569212d Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sat, 25 Sep 2021 19:18:27 -0700 Subject: [PATCH 048/147] Unit tests: PermissionController.grantPermissions --- .../controllers/src/permissions/Caveat.ts | 17 +- .../permissions/PermissionController.test.ts | 282 +++++++++++++++++- .../src/permissions/PermissionController.ts | 6 +- 3 files changed, 282 insertions(+), 23 deletions(-) diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index 3d4e8b8d12..03b19ff200 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -14,10 +14,7 @@ import { * @template Type - The type of the caveat. * @template Value - The value associated with the caveat. */ -export type CaveatConstraint< - Type extends string, - Value extends Json | never, -> = { +export type CaveatConstraint = { /** * The type of the caveat. The type is presumed to be meaningful in the * context of the capability it is associated with. @@ -29,9 +26,9 @@ export type CaveatConstraint< /** * Any additional data necessary to enforce the caveat. * - * TODO: Make optional in typescript@4.4.x + * TODO:TS4.4 Make optional */ - readonly value: Value extends never ? null : Value; + readonly value: Value; }; /** @@ -42,10 +39,10 @@ export type CaveatConstraint< * @param value - The value associated with the caveat, if any. * @returns The new caveat object. */ -export function constructCaveat( - type: string, - value: unknown, -): { type: string; value: unknown } { +export function constructCaveat( + type: Type, + value: Value, +): CaveatConstraint { return { type, value }; } diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 6a3d624dc8..fb0d7f6ffc 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -1,15 +1,21 @@ +import assert from 'assert'; import { ControllerMessenger, Json } from '@metamask/controllers'; import { isPlainObject } from '../utils'; import * as errors from './errors'; +import { constructCaveat } from './Caveat'; import { AsyncRestrictedMethod, CaveatConstraint, CaveatSpecifications, + constructPermission, + GenericCaveat, + GenericPermission, PermissionConstraint, PermissionController, PermissionControllerActions, PermissionControllerEvents, PermissionEnforcer, + PermissionOptions, PermissionSpecifications, RestrictedMethodArgs, } from '.'; @@ -19,18 +25,22 @@ import { enum CaveatTypes { filterArrayResponse = 'filterArrayResponse', filterObjectResponse = 'filterObjectResponse', + noopCaveat = 'noopCaveat', } type FilterArrayCaveat = CaveatConstraint< CaveatTypes.filterArrayResponse, string[] >; + type FilterObjectCaveat = CaveatConstraint< CaveatTypes.filterObjectResponse, string[] >; -type DefaultCaveats = FilterArrayCaveat | FilterObjectCaveat; +type NoopCaveat = CaveatConstraint; + +type DefaultCaveats = FilterArrayCaveat | FilterObjectCaveat | NoopCaveat; function getDefaultCaveatSpecifications(): CaveatSpecifications { return { @@ -69,6 +79,14 @@ function getDefaultCaveatSpecifications(): CaveatSpecifications return result; }, }, + noopCaveat: { + type: CaveatTypes.noopCaveat, + decorator: + (method: AsyncRestrictedMethod, _caveat: NoopCaveat) => + async (args: RestrictedMethodArgs) => { + return method(args); + }, + }, }; } @@ -88,12 +106,25 @@ type SecretObjectPermission = PermissionConstraint< FilterObjectCaveat >; -type DefaultTargetNames = SecretArrayName | SecretObjectName; +type SecretNamespacedKey = 'wallet_getSecret_*'; + +type SecretNamespacedPermission = PermissionConstraint< + SecretNamespacedKey, + NoopCaveat +>; + +type DefaultTargetKeys = + | SecretArrayName + | SecretObjectName + | SecretNamespacedKey; -type DefaultPermissions = SecretArrayPermission | SecretObjectPermission; +type DefaultPermissions = + | SecretArrayPermission + | SecretObjectPermission + | SecretNamespacedPermission; function getDefaultPermissionSpecifications(): PermissionSpecifications< - DefaultTargetNames, + DefaultTargetKeys, DefaultPermissions > { return { @@ -109,6 +140,31 @@ function getDefaultPermissionSpecifications(): PermissionSpecifications< return { secret1: 'a', secret2: 'b', secret3: 'c' }; }, }, + 'wallet_getSecret_*': { + target: 'wallet_getSecret_*', + methodImplementation: (args: RestrictedMethodArgs) => { + return `Hello, secret friend "${args.method.replace( + 'wallet_getSecret_', + '', + )}"!`; + }, + factory: (options: PermissionOptions) => + constructPermission({ + ...options, + caveats: [constructCaveat(CaveatTypes.noopCaveat, null)], + }) as SecretNamespacedPermission, + validator: (permission: GenericPermission) => { + if (!permission.parentCapability.startsWith('wallet_getSecret_')) { + throw new Error('invalid parentCapability'); + } + + assert.deepStrictEqual( + permission.caveats, + [constructCaveat(CaveatTypes.noopCaveat, null)], + 'invalid caveats', + ); + }, + }, }; } @@ -132,6 +188,28 @@ function getDefaultUnrestrictedMethods() { return ['wallet_unrestrictedMethod']; } +/** + * Useful for populating a controller with some existing permissions. + */ +function getExistingPermissionState() { + return { + subjects: { + 'metamask.io': { + origin: 'metamask.io', + permissions: { + wallet_getSecretArray: { + id: 'escwEx9JrOxGZKZk3RkL4', + parentCapability: 'wallet_getSecretArray', + invoker: 'metamask.io', + caveats: null, + date: 1632618373085, + }, + }, + }, + }, + }; +} + function getControllerOpts(opts?: Record) { return { caveatSpecifications: getDefaultCaveatSpecifications(), @@ -145,7 +223,7 @@ function getControllerOpts(opts?: Record) { function getDefaultPermissionController() { return new PermissionController< - DefaultTargetNames, + DefaultTargetKeys, DefaultCaveats, DefaultPermissions >(getControllerOpts()); @@ -160,6 +238,15 @@ describe('PermissionController', () => { true, ); }); + + it('rehydrates state', () => { + const controller = new PermissionController< + DefaultTargetKeys, + DefaultCaveats, + DefaultPermissions + >(getControllerOpts({ state: getExistingPermissionState() })); + expect(controller.state).toStrictEqual(getExistingPermissionState()); + }); }); describe('grantPermissions', () => { @@ -179,17 +266,172 @@ describe('PermissionController', () => { [origin]: { origin, permissions: { - wallet_getSecretArray: { - id: expect.any(String), - parentCapability: 'wallet_getSecretArray', - invoker: origin, - caveats: null, - date: expect.any(Number), - }, + wallet_getSecretArray: getPermissionMatcher( + 'wallet_getSecretArray', + ), + }, + }, + }, + }); + }); + + it('grants new permissions (namespaced, with factory and validator)', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + wallet_getSecret_kabob: {}, + }, + }); + + expect(controller.state).toStrictEqual({ + subjects: { + [origin]: { + origin, + permissions: { + wallet_getSecret_kabob: getPermissionMatcher( + 'wallet_getSecret_kabob', + [constructCaveat(CaveatTypes.noopCaveat, null)], + ), + }, + }, + }, + }); + }); + + it('grants new permissions (multiple origins)', () => { + const controller = getDefaultPermissionController(); + const origin1 = 'metamask.io'; + const origin2 = 'infura.io'; + + controller.grantPermissions({ + subject: { origin: origin1 }, + approvedPermissions: { + wallet_getSecret_kabob: {}, + }, + }); + + controller.grantPermissions({ + subject: { origin: origin2 }, + approvedPermissions: { + wallet_getSecretArray: {}, + }, + }); + + expect(controller.state).toStrictEqual({ + subjects: { + [origin1]: { + origin: origin1, + permissions: { + wallet_getSecret_kabob: getPermissionMatcher( + 'wallet_getSecret_kabob', + [constructCaveat(CaveatTypes.noopCaveat, null)], + origin1, + ), + }, + }, + [origin2]: { + origin: origin2, + permissions: { + wallet_getSecretArray: getPermissionMatcher( + 'wallet_getSecretArray', + null, + origin2, + ), + }, + }, + }, + }); + }); + + it('overwrites existing permissions if shouldPreserveExistingPermissions is false', () => { + const controller = new PermissionController< + DefaultTargetKeys, + DefaultCaveats, + DefaultPermissions + >(getControllerOpts({ state: getExistingPermissionState() })); + const origin = 'metamask.io'; + + expect(controller.state).toStrictEqual({ + subjects: { + [origin]: { + origin, + permissions: { + wallet_getSecretArray: getPermissionMatcher( + 'wallet_getSecretArray', + ), }, }, }, }); + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + wallet_getSecretObject: {}, + }, + }); + + expect(controller.state).toStrictEqual({ + subjects: { + [origin]: { + origin, + permissions: expect.objectContaining({ + wallet_getSecretObject: getPermissionMatcher( + 'wallet_getSecretObject', + ), + }), + }, + }, + }); + }); + + it('preserves existing permissions if shouldPreserveExistingPermissions is true', () => { + const controller = new PermissionController< + DefaultTargetKeys, + DefaultCaveats, + DefaultPermissions + >(getControllerOpts({ state: getExistingPermissionState() })); + const origin = 'metamask.io'; + + expect(controller.state).toStrictEqual({ + subjects: { + [origin]: { + origin, + permissions: expect.objectContaining({ + wallet_getSecretArray: getPermissionMatcher( + 'wallet_getSecretArray', + ), + }), + }, + }, + }); + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + wallet_getSecretObject: {}, + }, + shouldPreserveExistingPermissions: true, + }); + + expect(controller.state).toStrictEqual({ + subjects: { + [origin]: { + origin, + permissions: expect.objectContaining({ + wallet_getSecretArray: getPermissionMatcher( + 'wallet_getSecretArray', + ), + wallet_getSecretObject: getPermissionMatcher( + 'wallet_getSecretObject', + ), + }), + }, + }, + }); }); it('throws if the origin is invalid', () => { @@ -235,3 +477,19 @@ describe('PermissionController', () => { // CaveatTypes.filterArrayResponse, // ['foo'], // ); + +// Testing utilities + +function getPermissionMatcher( + parentCapability: string, + caveats: GenericCaveat[] | null | typeof expect.objectContaining = null, + invoker = 'metamask.io', +) { + return expect.objectContaining({ + id: expect.any(String), + parentCapability, + invoker, + caveats, + date: expect.any(Number), + }); +} diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index a8f020f11a..a272333133 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -805,7 +805,11 @@ export class PermissionController< throw new CaveatMissingValueError(requestedCaveat, origin, target); } - const caveat = constructCaveat(requestedCaveat.type, requestedCaveat.value); + // TODO: Consider validating that this is Json? + const caveat = constructCaveat( + requestedCaveat.type, + (requestedCaveat.value as Json) ?? null, + ); specification.validator?.(origin, target, caveat); return caveat as Caveat; } From c912a4a5d3b42b6efc93b7e7b1e69832dd06b5d7 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sun, 26 Sep 2021 15:33:45 -0700 Subject: [PATCH 049/147] Fix caveat function type safety for namespaced permissions --- .../controllers/src/permissions/Permission.ts | 57 ++++++++++++++++++- .../permissions/PermissionController.test.ts | 7 --- .../src/permissions/PermissionController.ts | 50 ++++++++-------- 3 files changed, 80 insertions(+), 34 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 92a4b77a28..c378157d04 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -103,9 +103,11 @@ type TargetNameConstraint = Name extends `${string}*` * keys. * * See the documentation for the distinction between target names and keys. + * + * @template Key - The target key type to extract target names from. */ -type ExtractPermissionTargetNames = - TargetKey extends `${infer Base}_*` ? `${Base}_${string}` : TargetKey; +type ExtractPermissionTargetNames = + Key extends `${infer Base}_*` ? `${Base}_${string}` : Key; /** * The consumer-facing base permission type. @@ -125,6 +127,53 @@ export type GenericPermission = PermissionConstraint< GenericCaveat | never >; +/** + * An internal utility type used in {@link ExtractPermissionTargetKey}. + * + * @template Key - The target key type to extract from. + * @template Name - The name whose key to extract. + */ +type KeyOfTargetName< + Key extends string, + Name extends string, +> = Name extends ExtractPermissionTargetNames ? Key : never; + +/** + * A utility type for finding the permission target key corresponding to a + * target name. In a way, the inverse of {@link ExtractPermissionTargetNames}. + * + * See the documentation for the distinction between target names and keys. + * + * @template Key - The target key type to extract from. + * @template Name - The name whose key to extract. + */ +type ExtractPermissionTargetKey< + Key extends string, + Name extends string, +> = Name extends Key ? Name : Extract>; + +/** + * A utility type for extracting the valid caveat types for a particular + * permission from a union of permission types. + * + * @template PermissionUnion - The permission type union to extract valid caveat + * types from. + * @template Key - The target key type to extract from. + * @template Name - The target name of the permission. + */ +export type ExtractValidCaveatTypes< + PermissionUnion extends GenericPermission, + Key extends string, + Name extends string, +> = PermissionUnion extends PermissionConstraint< + ExtractPermissionTargetKey, + infer ValidCaveats +> + ? ValidCaveats extends GenericCaveat + ? ValidCaveats['type'] + : never + : never; + /** * The options object of {@link constructPermission}. */ @@ -153,9 +202,11 @@ export type PermissionOptions = { }; /** - * The default {@link Permission} factory function. Naively constructs a permission from + * The default permission factory function. Naively constructs a permission from * the inputs. Sets a default, random `id` if none is provided. * + * @see {@link PermissionBase} For more details. + * * @param options - The options for the permission. * @returns The new permission object. */ diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index fb0d7f6ffc..9df15544a6 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -471,13 +471,6 @@ describe('PermissionController', () => { }); }); -// controller.addCaveat( -// 'foo', -// 'wallet_getSecretArray' as const, -// CaveatTypes.filterArrayResponse, -// ['foo'], -// ); - // Testing utilities function getPermissionMatcher( diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index a272333133..85831b1527 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -19,6 +19,7 @@ import { } from './Caveat'; import { constructPermission, + ExtractValidCaveatTypes, GenericPermission, MutableCaveats, OriginString, @@ -187,25 +188,6 @@ export type ExtractCaveatValue< ? CaveatValue : never; -/** - * A utility type for extracting the valid caveat types for a particular - * permission from a union of permission types. - * - * @template PermissionUnion - The permission type union to extract valid caveat - * types from. - * @template TargetName - The target name of the permission. - */ -type ExtractValidCaveatTypes< - PermissionUnion extends GenericPermission, - TargetName extends string, -> = PermissionUnion extends PermissionConstraint - ? never - : PermissionUnion extends PermissionConstraint - ? ValidCaveats extends GenericCaveat - ? ValidCaveats['type'] - : never - : never; - /** * Options for the {@link PermissionController} constructor. * @@ -521,7 +503,11 @@ export class PermissionController< hasCaveat< TargetName extends Permission['parentCapability'], - CaveatType extends ExtractValidCaveatTypes, + CaveatType extends ExtractValidCaveatTypes< + Permission, + TargetKey, + TargetName + >, >(origin: OriginString, target: TargetName, caveatType: CaveatType): boolean { return ( this.getPermission(origin, target)?.caveats?.some( @@ -532,7 +518,11 @@ export class PermissionController< addCaveat< TargetName extends Permission['parentCapability'], - CaveatType extends ExtractValidCaveatTypes, + CaveatType extends ExtractValidCaveatTypes< + Permission, + TargetKey, + TargetName + >, >( origin: OriginString, target: TargetName, @@ -548,7 +538,11 @@ export class PermissionController< updateCaveat< TargetName extends Permission['parentCapability'], - CaveatType extends ExtractValidCaveatTypes, + CaveatType extends ExtractValidCaveatTypes< + Permission, + TargetKey, + TargetName + >, >( origin: OriginString, target: TargetName, @@ -564,7 +558,11 @@ export class PermissionController< private setCaveat< TargetName extends Permission['parentCapability'], - CaveatType extends ExtractValidCaveatTypes, + CaveatType extends ExtractValidCaveatTypes< + Permission, + TargetKey, + TargetName + >, >( origin: OriginString, target: TargetName, @@ -611,7 +609,11 @@ export class PermissionController< removeCaveat< TargetName extends Permission['parentCapability'], - CaveatType extends ExtractValidCaveatTypes, + CaveatType extends ExtractValidCaveatTypes< + Permission, + TargetKey, + TargetName + >, >(origin: OriginString, target: TargetName, caveatType: CaveatType): void { this.update((draftState) => { // Typecast: immer's WritableDraft is incompatible with our generics From 7cfd26bce7aad531d49641f5daa1bf4aaf4eb573 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sun, 26 Sep 2021 15:40:27 -0700 Subject: [PATCH 050/147] Some type fixup --- .../controllers/src/permissions/Permission.ts | 16 +++++++--------- .../src/permissions/PermissionController.ts | 8 +++++--- packages/controllers/src/utils.ts | 5 +++++ 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index c378157d04..5edb406c7b 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -1,6 +1,7 @@ import type { EthereumRpcError } from 'eth-rpc-errors'; import { Json } from 'json-rpc-engine'; import { nanoid } from 'nanoid'; +import { NonEmptyArray } from '../utils'; import { CaveatConstraint, GenericCaveat } from './Caveat'; // This is in fact used in a docstring, but ESLint doesn't notice this. // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -17,11 +18,6 @@ export type OriginString = string; */ export type TargetName = string; -/** - * Like {@link Array}, but always non-empty. - */ -type NonEmptyArray = [T, ...T[]]; - /** * The base permission interface. Lacks important constraints on its generics; * consumers should use {@link PermissionConstraint} instead. @@ -198,7 +194,7 @@ export type PermissionOptions = { * The caveats of the permission. * See {@link CaveatConstraint}. */ - caveats?: CaveatConstraint[]; + caveats?: NonEmptyArray>; }; /** @@ -210,14 +206,16 @@ export type PermissionOptions = { * @param options - The options for the permission. * @returns The new permission object. */ -export function constructPermission(options: PermissionOptions) { - const { caveats, id, invoker, target } = options; +export function constructPermission( + options: PermissionOptions, +): GenericPermission { + const { caveats = null, id, invoker, target } = options; return { id: id ?? nanoid(), parentCapability: target, invoker, - caveats: caveats ?? null, + caveats, date: new Date().getTime(), }; } diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 85831b1527..a3ae131d1f 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -10,7 +10,7 @@ import type { Patch } from 'immer'; import deepFreeze from 'deep-freeze-strict'; import { Json } from 'json-rpc-engine'; -import { isPlainObject, hasProperty } from '../utils'; +import { isPlainObject, hasProperty, NonEmptyArray } from '../utils'; import { CaveatConstraint, CaveatSpecifications, @@ -766,11 +766,13 @@ export class PermissionController< origin: OriginString, target: Permission['parentCapability'], caveats?: unknown[] | null, - ): Caveat[] | undefined { + ): NonEmptyArray | undefined { const caveatArray = caveats?.map((requestedCaveat) => this.computeCaveat(origin, target, requestedCaveat), ); - return caveatArray && caveatArray.length > 0 ? caveatArray : undefined; + return caveatArray && caveatArray.length > 0 + ? (caveatArray as NonEmptyArray) + : undefined; } private computeCaveat( diff --git a/packages/controllers/src/utils.ts b/packages/controllers/src/utils.ts index 53b5a4f5ab..c38535aa42 100644 --- a/packages/controllers/src/utils.ts +++ b/packages/controllers/src/utils.ts @@ -8,3 +8,8 @@ export const hasProperty = ( object: PlainObject, key: string | number | symbol, ) => Reflect.hasOwnProperty.call(object, key); + +/** + * Like {@link Array}, but always non-empty. + */ +export type NonEmptyArray = [T, ...T[]]; From c51f6d9ac84ccd45941f47f2666b887c4d1c880d Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sun, 26 Sep 2021 21:18:12 -0700 Subject: [PATCH 051/147] Add HasPermissions action --- .../src/permissions/PermissionController.ts | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index a3ae131d1f..04ce665ac0 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -119,6 +119,14 @@ export type GetSubjects = { handler: () => (keyof PermissionControllerSubjects)[]; }; +/** + * Checks whether the specified subject has any permissions. + */ +export type HasPermissions = { + type: `${typeof controllerName}:hasPermissions`; + handler: GenericPermissionController['hasPermissions']; +}; + /** * Clears all permissions from the {@link PermissionController}. */ @@ -132,9 +140,10 @@ export type ClearPermissions = { * The {@link ControllerMessenger} actions of the {@link PermissionController}. */ export type PermissionControllerActions = + | ClearPermissions | GetPermissionControllerState | GetSubjects - | ClearPermissions; + | HasPermissions; /** * The generic state change event of the {@link PermissionController}. @@ -222,6 +231,12 @@ type GrantPermissionsOptions = { requestData?: Record; }; +export type GenericPermissionController = PermissionController< + string, + GenericCaveat, + GenericPermission +>; + /** * @template TargetKey - A union of string literal types corresponding to the * keys of all permission target. Must match the permission specifications of @@ -337,14 +352,19 @@ export class PermissionController< * actions. */ private registerMessageHandlers(): void { + this.messagingSystem.registerActionHandler( + `${controllerName}:clearPermissions`, + () => this.clearState(), + ); + this.messagingSystem.registerActionHandler( `${controllerName}:getSubjects`, () => this.getSubjects(), ); this.messagingSystem.registerActionHandler( - `${controllerName}:clearPermissions`, - () => this.clearState(), + `${controllerName}:hasPermissions`, + (origin: string) => this.hasPermissions(origin), ); } From d3b1984c2d2263f347e60e89211b2ae62cde6c6e Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 28 Sep 2021 11:32:53 -0700 Subject: [PATCH 052/147] Fix some parameters, rename an export --- packages/controllers/src/permissions/Caveat.ts | 4 ++-- packages/controllers/src/permissions/PermissionController.ts | 2 +- packages/controllers/src/permissions/index.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index 03b19ff200..443690114b 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -69,9 +69,9 @@ export type CaveatDecorator = ( * */ export type CaveatValidator = ( - origin: string, - target: string, caveat: { type: string; value: unknown }, + origin?: string, + target?: string, ) => caveat is Caveat; export type CaveatSpecification = { diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 04ce665ac0..e08942d5df 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -834,7 +834,7 @@ export class PermissionController< requestedCaveat.type, (requestedCaveat.value as Json) ?? null, ); - specification.validator?.(origin, target, caveat); + specification.validator?.(caveat, origin, target); return caveat as Caveat; } diff --git a/packages/controllers/src/permissions/index.ts b/packages/controllers/src/permissions/index.ts index 75700e9fee..8d5fc431b5 100644 --- a/packages/controllers/src/permissions/index.ts +++ b/packages/controllers/src/permissions/index.ts @@ -3,4 +3,4 @@ export * from './enums'; export * from './Permission'; export * from './PermissionController'; export * from './PermissionEnforcer'; -export * as rpcMethods from './rpc-methods'; +export * as permissionRpcMethods from './rpc-methods'; From e1a62420f1f175f70f3bd7c50385ead3d36f15cc Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 29 Sep 2021 01:31:10 -0700 Subject: [PATCH 053/147] Add back getCaveat, refactor permission revocation --- .../controllers/src/permissions/Caveat.ts | 5 ++ .../controllers/src/permissions/Permission.ts | 21 +++---- .../src/permissions/PermissionController.ts | 62 +++++++++++++++---- 3 files changed, 65 insertions(+), 23 deletions(-) diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index 443690114b..1026c8ef13 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -119,6 +119,11 @@ type GetCaveatSpecification< ? CaveatSpecification : never; +export type GetCaveatFromType< + Caveat extends GenericCaveat, + CaveatType extends string, +> = Caveat extends CaveatConstraint ? Caveat : never; + /** * Decorate a restricted method implementation with its caveats. * diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 5edb406c7b..e9d08d4f2e 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -228,16 +228,11 @@ export function constructPermission( * @param caveatType The type of the caveat to retrieve. * @returns The caveat, or undefined if no such caveat exists. */ -export function findCaveat< - TargetCaveat extends GenericCaveat, - TargetPermission extends PermissionConstraint, ->( - permission: TargetPermission, - caveatType: TargetCaveat['type'], -): TargetCaveat | undefined { - return permission.caveats?.find( - (caveat) => caveat.type === caveatType, - ) as any; +export function findCaveat( + permission: GenericPermission, + caveatType: string, +): GenericCaveat | undefined { + return permission.caveats?.find((caveat) => caveat.type === caveatType); } /** @@ -345,7 +340,11 @@ type PermissionSpecificationBase< * * The validator should throw an appropriate JSON-RPC error if validation fails. */ - validator?: (permission: GenericPermission) => void; + validator?: ( + permission: GenericPermission, + origin?: string, + target?: string, + ) => void; }; /** diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index e08942d5df..95b5509051 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -16,10 +16,12 @@ import { CaveatSpecifications, constructCaveat, GenericCaveat, + GetCaveatFromType, } from './Caveat'; import { constructPermission, ExtractValidCaveatTypes, + findCaveat, GenericPermission, MutableCaveats, OriginString, @@ -456,24 +458,37 @@ export class PermissionController< revokePermission( origin: OriginString, target: Permission['parentCapability'], + ): void { + this.revokePermissions({ [origin]: [target] }); + } + + revokePermissions( + subjectsAndPermissions: Record< + OriginString, + Permission['parentCapability'][] + >, ): void { this.update((draftState) => { if (!draftState.subjects[origin]) { throw new UnrecognizedSubjectError(origin); } - const { permissions } = draftState.subjects[origin]; - if (!hasProperty(permissions as Record, target)) { - throw new PermissionDoesNotExistError(origin, target); - } + Object.keys(subjectsAndPermissions).forEach((origin) => { + subjectsAndPermissions[origin].forEach((target) => { + const { permissions } = draftState.subjects[origin]; + if (!hasProperty(permissions as Record, target)) { + throw new PermissionDoesNotExistError(origin, target); + } - // Typecast: ts(2589) - this.deletePermission( - draftState as any, - permissions as any, - origin, - target, - ); + // Typecast: ts(2589) + this.deletePermission( + draftState as any, + permissions as any, + origin, + target, + ); + }); + }); }); } @@ -556,6 +571,29 @@ export class PermissionController< this.setCaveat(origin, target, caveatType, caveatValue); } + getCaveat< + TargetName extends Permission['parentCapability'], + CaveatType extends ExtractValidCaveatTypes< + Permission, + TargetKey, + TargetName + >, + >( + origin: OriginString, + target: TargetName, + caveatType: CaveatType, + ): GetCaveatFromType | undefined { + const permission = this.getPermission(origin, target); + if (!permission) { + throw new PermissionDoesNotExistError(origin, target); + } + + return findCaveat(permission, caveatType) as GetCaveatFromType< + Caveat, + CaveatType + >; + } + updateCaveat< TargetName extends Permission['parentCapability'], CaveatType extends ExtractValidCaveatTypes< @@ -770,7 +808,7 @@ export class PermissionController< ? specification.factory(permissionOptions, requestData) : constructPermission(permissionOptions) ) as Permission; - specification.validator?.(permission); + specification.validator?.(permission, origin, targetName); permissions[targetName] = permission; } From 2deff4f1818399fe28b4b009bc8fc27d2b0d7a8c Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 29 Sep 2021 21:19:00 -0700 Subject: [PATCH 054/147] Unit tests: PermissionController, continued --- .../permissions/PermissionController.test.ts | 277 ++++++++++++++++-- .../src/permissions/PermissionController.ts | 22 +- 2 files changed, 271 insertions(+), 28 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 9df15544a6..e85d44c273 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -123,25 +123,37 @@ type DefaultPermissions = | SecretObjectPermission | SecretNamespacedPermission; +enum PermissionKeys { + 'wallet_getSecretArray' = 'wallet_getSecretArray', + 'wallet_getSecretObject' = 'wallet_getSecretObject', + 'wallet_getSecret_*' = 'wallet_getSecret_*', +} + +const PermissionNames = { + wallet_getSecretArray: 'wallet_getSecretArray' as const, + wallet_getSecretObject: 'wallet_getSecretObject' as const, + wallet_getSecret_: (str: string) => `wallet_getSecret_${str}` as const, +}; + function getDefaultPermissionSpecifications(): PermissionSpecifications< DefaultTargetKeys, DefaultPermissions > { return { wallet_getSecretArray: { - target: 'wallet_getSecretArray', + target: PermissionKeys.wallet_getSecretArray, methodImplementation: (_args: RestrictedMethodArgs) => { return ['secret1', 'secret2', 'secret3']; }, }, wallet_getSecretObject: { - target: 'wallet_getSecretObject', + target: PermissionKeys.wallet_getSecretObject, methodImplementation: (_args: RestrictedMethodArgs) => { return { secret1: 'a', secret2: 'b', secret3: 'c' }; }, }, 'wallet_getSecret_*': { - target: 'wallet_getSecret_*', + target: PermissionKeys['wallet_getSecret_*'], methodImplementation: (args: RestrictedMethodArgs) => { return `Hello, secret friend "${args.method.replace( 'wallet_getSecret_', @@ -229,6 +241,14 @@ function getDefaultPermissionController() { >(getControllerOpts()); } +function getDefaultPermissionControllerWithState() { + return new PermissionController< + DefaultTargetKeys, + DefaultCaveats, + DefaultPermissions + >(getControllerOpts({ state: getExistingPermissionState() })); +} + describe('PermissionController', () => { describe('constructor', () => { it('initializes a new PermissionController', () => { @@ -237,16 +257,241 @@ describe('PermissionController', () => { expect(controller.enforcer instanceof PermissionEnforcer).toStrictEqual( true, ); + + expect(controller.unrestrictedMethods).toStrictEqual( + new Set(getDefaultUnrestrictedMethods()), + ); }); it('rehydrates state', () => { - const controller = new PermissionController< - DefaultTargetKeys, - DefaultCaveats, - DefaultPermissions - >(getControllerOpts({ state: getExistingPermissionState() })); + const controller = getDefaultPermissionControllerWithState(); + expect(controller.state).toStrictEqual(getExistingPermissionState()); + }); + }); + + describe('clearState', () => { + it("clears the controller's state", () => { + const controller = getDefaultPermissionControllerWithState(); + expect(controller.state).toStrictEqual(getExistingPermissionState()); + + controller.clearState(); + expect(controller.state).toStrictEqual({ subjects: {} }); + }); + }); + + describe('getSubjectNames', () => { + it('gets all subject names', () => { + const controller = getDefaultPermissionController(); + expect(controller.getSubjectNames()).toStrictEqual([]); + + controller.grantPermissions({ + subject: { origin: 'foo' }, + approvedPermissions: { + wallet_getSecretArray: {}, + }, + }); + + expect(controller.getSubjectNames()).toStrictEqual(['foo']); + + controller.grantPermissions({ + subject: { origin: 'bar' }, + approvedPermissions: { + wallet_getSecretArray: {}, + }, + }); + + expect(controller.getSubjectNames()).toStrictEqual(['foo', 'bar']); + }); + }); + + describe('getPermission', () => { + it('gets existing permissions', () => { + const controller = getDefaultPermissionControllerWithState(); + + expect( + controller.getPermission( + 'metamask.io', + PermissionNames.wallet_getSecretArray, + ), + ).toStrictEqual( + getPermissionMatcher( + PermissionNames.wallet_getSecretArray, + null, + 'metamask.io', + ), + ); + }); + + it('returns undefined for non-existing permissions', () => { + const controller = getDefaultPermissionController(); + expect( + controller.getPermission( + 'metamask.io', + PermissionNames.wallet_getSecretArray, + ), + ).toBeUndefined(); + }); + }); + + describe('hasPermission', () => { + it('correctly indicates whether an origin has a permission', () => { + const controller = getDefaultPermissionControllerWithState(); + + expect( + controller.hasPermission( + 'metamask.io', + PermissionNames.wallet_getSecretArray, + ), + ).toStrictEqual(true); + + expect( + controller.hasPermission( + 'metamask.io', + PermissionNames.wallet_getSecretObject, + ), + ).toStrictEqual(false); + }); + + it('correctly indicates whether an origin has a namespaced permission', () => { + const controller = getDefaultPermissionController(); + + controller.grantPermissions({ + subject: { origin: 'metamask.io' }, + approvedPermissions: { + wallet_getSecret_kabob: {}, + }, + }); + + expect( + controller.hasPermission( + 'metamask.io', + PermissionNames.wallet_getSecret_('kabob'), + ), + ).toStrictEqual(true); + + expect( + controller.hasPermission( + 'metamask.io', + PermissionNames.wallet_getSecret_('falafel'), + ), + ).toStrictEqual(false); + }); + }); + + describe('hasPermissions', () => { + it('correctly indicates whether an origin has any permissions', () => { + const controller = getDefaultPermissionControllerWithState(); + + expect(controller.hasPermissions('metamask.io')).toStrictEqual(true); + expect(controller.hasPermissions('foo.bar')).toStrictEqual(false); + }); + }); + + describe('revokePermission', () => { + it('revokes a permission from an origin with a single permission', () => { + const controller = getDefaultPermissionControllerWithState(); expect(controller.state).toStrictEqual(getExistingPermissionState()); + + controller.revokePermission( + 'metamask.io', + PermissionNames.wallet_getSecretArray, + ); + expect(controller.state).toStrictEqual({ subjects: {} }); }); + + it('revokes a permission from an origin with multiple permissions', () => { + const controller = getDefaultPermissionControllerWithState(); + expect(controller.state).toStrictEqual(getExistingPermissionState()); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + wallet_getSecretObject: {}, + }, + shouldPreserveExistingPermissions: true, + }); + + controller.revokePermission( + origin, + PermissionNames.wallet_getSecretArray, + ); + + expect(controller.state).toStrictEqual({ + subjects: { + [origin]: { + origin, + permissions: { + [PermissionNames.wallet_getSecretObject]: getPermissionMatcher( + PermissionNames.wallet_getSecretObject, + null, + origin, + ), + }, + }, + }, + }); + }); + + it('throws an error if the specified subject has no permissions', () => { + const controller = getDefaultPermissionController(); + expect(() => + controller.revokePermission( + 'metamask.io', + PermissionNames.wallet_getSecretArray, + ), + ).toThrow(new errors.UnrecognizedSubjectError('metamask.io')); + }); + + it('throws an error if the requested subject does not have the specified permission', () => { + const controller = getDefaultPermissionControllerWithState(); + expect(() => + controller.revokePermission( + 'metamask.io', + PermissionNames.wallet_getSecretObject, + ), + ).toThrow( + new errors.PermissionDoesNotExistError( + 'metamask.io', + PermissionNames.wallet_getSecretObject, + ), + ); + }); + }); + + describe('revokePermissions', () => { + it.todo('revokes different permissions from multiple origins at once'); + }); + + describe('revokePermissionForAllSubjects', () => { + it.todo('revokePermissionForAllSubjects'); + }); + + describe('hasCaveat', () => { + it.todo('hasCaveat'); + }); + + describe('addCaveat', () => { + it.todo('addCaveat'); + }); + + describe('getCaveat', () => { + it.todo('getCaveat'); + }); + + describe('updateCaveat', () => { + it.todo('updateCaveat'); + }); + + describe('removeCaveat', () => { + it.todo('removeCaveat'); + }); + + describe('getRestrictedMethod', () => { + it.todo('getRestrictedMethod'); + // This is currently disallowed by the types, but handled by getTargetKey + // Should we add runtime validation forbidding it? Maybe in the constructor? + it.todo('handles methods ending in "_"'); }); describe('grantPermissions', () => { @@ -347,11 +592,7 @@ describe('PermissionController', () => { }); it('overwrites existing permissions if shouldPreserveExistingPermissions is false', () => { - const controller = new PermissionController< - DefaultTargetKeys, - DefaultCaveats, - DefaultPermissions - >(getControllerOpts({ state: getExistingPermissionState() })); + const controller = getDefaultPermissionControllerWithState(); const origin = 'metamask.io'; expect(controller.state).toStrictEqual({ @@ -389,11 +630,7 @@ describe('PermissionController', () => { }); it('preserves existing permissions if shouldPreserveExistingPermissions is true', () => { - const controller = new PermissionController< - DefaultTargetKeys, - DefaultCaveats, - DefaultPermissions - >(getControllerOpts({ state: getExistingPermissionState() })); + const controller = getDefaultPermissionControllerWithState(); const origin = 'metamask.io'; expect(controller.state).toStrictEqual({ @@ -469,6 +706,10 @@ describe('PermissionController', () => { ).toThrow(errors.methodNotFound({ method: 'wallet_getSecretFalafel' })); }); }); + + describe('actions', () => { + it.todo('controller actions'); + }); }); // Testing utilities diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 95b5509051..b368200f4f 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -117,7 +117,7 @@ export type GetPermissionControllerState = { * Gets the names of all subjects from the {@link PermissionController}. */ export type GetSubjects = { - type: `${typeof controllerName}:getSubjects`; + type: `${typeof controllerName}:getSubjectNames`; handler: () => (keyof PermissionControllerSubjects)[]; }; @@ -360,8 +360,8 @@ export class PermissionController< ); this.messagingSystem.registerActionHandler( - `${controllerName}:getSubjects`, - () => this.getSubjects(), + `${controllerName}:getSubjectNames`, + () => this.getSubjectNames(), ); this.messagingSystem.registerActionHandler( @@ -376,7 +376,7 @@ export class PermissionController< }); } - getSubjects(): OriginString[] { + getSubjectNames(): OriginString[] { return Object.keys(this.state.subjects); } @@ -469,11 +469,11 @@ export class PermissionController< >, ): void { this.update((draftState) => { - if (!draftState.subjects[origin]) { - throw new UnrecognizedSubjectError(origin); - } - Object.keys(subjectsAndPermissions).forEach((origin) => { + if (!hasProperty(draftState.subjects, origin)) { + throw new UnrecognizedSubjectError(origin); + } + subjectsAndPermissions[origin].forEach((target) => { const { permissions } = draftState.subjects[origin]; if (!hasProperty(permissions as Record, target)) { @@ -493,7 +493,7 @@ export class PermissionController< } revokePermissionForAllSubjects(target: Permission['parentCapability']): void { - if (this.getSubjects().length === 0) { + if (this.getSubjectNames().length === 0) { return; } @@ -713,7 +713,9 @@ export class PermissionController< * @param method - The requested RPC method. * @returns The internal key of the method. */ - getTargetKey(method: Permission['parentCapability']): TargetKey | undefined { + private getTargetKey( + method: Permission['parentCapability'], + ): TargetKey | undefined { if (hasProperty(this._permissionSpecifications, method)) { return method as TargetKey; } From 6d2e92bd6cf84798f3772df1292c85f47a96d130 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Thu, 30 Sep 2021 15:14:43 -0700 Subject: [PATCH 055/147] Update permission requestData handling --- packages/controllers/src/permissions/PermissionEnforcer.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionEnforcer.ts b/packages/controllers/src/permissions/PermissionEnforcer.ts index ed5c723d1c..c9fc9d2b3c 100644 --- a/packages/controllers/src/permissions/PermissionEnforcer.ts +++ b/packages/controllers/src/permissions/PermissionEnforcer.ts @@ -44,6 +44,7 @@ export type PermissionsRequestMetadata = PermissionSubjectMetadata & { export type PermissionsRequest = { metadata: PermissionsRequestMetadata; permissions: RequestedPermissions; + [key: string]: Json; }; type IsRestrictedMethod = (method: string) => boolean; @@ -175,7 +176,7 @@ export class PermissionEnforcer< permissions: requestedPermissions, }; - const { permissions: approvedPermissions, metadata: approvedMetadata } = + const { permissions: approvedPermissions, ...requestData } = await this.requestUserApproval(permissionsRequest); if (Object.keys(approvedPermissions).length === 0) { // The request should already have been rejected if this is the case. @@ -190,7 +191,7 @@ export class PermissionEnforcer< subject: { origin }, approvedPermissions, shouldPreserveExistingPermissions, - requestData: approvedMetadata, + requestData, }), metadata, ]; From 8860625c28616a5cc3dfdc4647a509b0ee116f0e Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Thu, 30 Sep 2021 17:42:10 -0700 Subject: [PATCH 056/147] Unit tests: PermissionController, revocation complete --- .../permissions/PermissionController.test.ts | 334 +++++++++++++++++- .../src/permissions/PermissionController.ts | 95 ++++- 2 files changed, 396 insertions(+), 33 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index e85d44c273..00512615b5 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -387,6 +387,45 @@ describe('PermissionController', () => { }); }); + describe('revokeAllPermissions', () => { + it('revokes all permissions for the specified subject', () => { + const controller = getDefaultPermissionControllerWithState(); + expect(controller.state).toStrictEqual(getExistingPermissionState()); + + controller.revokeAllPermissions('metamask.io'); + expect(controller.state).toStrictEqual({ subjects: {} }); + + controller.grantPermissions({ + subject: { origin: 'foo' }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: {}, + [PermissionNames.wallet_getSecret_('bar')]: {}, + }, + }); + + controller.revokeAllPermissions('foo'); + expect(controller.state).toStrictEqual({ subjects: {} }); + }); + + it('throws an error if the specified subject has no permissions', () => { + const controller = getDefaultPermissionController(); + expect(() => controller.revokeAllPermissions('metamask.io')).toThrow( + new errors.UnrecognizedSubjectError('metamask.io'), + ); + + controller.grantPermissions({ + subject: { origin: 'metamask.io' }, + approvedPermissions: { + [PermissionNames.wallet_getSecretObject]: {}, + }, + }); + + expect(() => controller.revokeAllPermissions('foo')).toThrow( + new errors.UnrecognizedSubjectError('foo'), + ); + }); + }); + describe('revokePermission', () => { it('revokes a permission from an origin with a single permission', () => { const controller = getDefaultPermissionControllerWithState(); @@ -407,9 +446,8 @@ describe('PermissionController', () => { controller.grantPermissions({ subject: { origin }, approvedPermissions: { - wallet_getSecretObject: {}, + [PermissionNames.wallet_getSecretObject]: {}, }, - shouldPreserveExistingPermissions: true, }); controller.revokePermission( @@ -433,6 +471,39 @@ describe('PermissionController', () => { }); }); + it('revokes a namespaced permission', () => { + const controller = getDefaultPermissionControllerWithState(); + expect(controller.state).toStrictEqual(getExistingPermissionState()); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecret_('foo')]: {}, + }, + }); + + controller.revokePermission( + origin, + PermissionNames.wallet_getSecret_('foo'), + ); + + expect(controller.state).toStrictEqual({ + subjects: { + [origin]: { + origin, + permissions: { + [PermissionNames.wallet_getSecretArray]: getPermissionMatcher( + PermissionNames.wallet_getSecretArray, + null, + origin, + ), + }, + }, + }, + }); + }); + it('throws an error if the specified subject has no permissions', () => { const controller = getDefaultPermissionController(); expect(() => @@ -460,11 +531,244 @@ describe('PermissionController', () => { }); describe('revokePermissions', () => { - it.todo('revokes different permissions from multiple origins at once'); + it('revokes different permissions for multiple subjects', () => { + const controller = getDefaultPermissionController(); + const origin0 = 'origin0'; + const origin1 = 'origin1'; + const origin2 = 'origin2'; + const origin3 = 'origin3'; + const origin4 = 'origin4'; + + controller.grantPermissions({ + subject: { origin: origin0 }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: {}, + }, + }); + + controller.grantPermissions({ + subject: { origin: origin1 }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: {}, + }, + }); + + controller.grantPermissions({ + subject: { origin: origin2 }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: {}, + [PermissionNames.wallet_getSecretObject]: {}, + [PermissionNames.wallet_getSecret_('foo')]: {}, + }, + }); + + controller.grantPermissions({ + subject: { origin: origin3 }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: {}, + [PermissionNames.wallet_getSecret_('foo')]: {}, + }, + }); + + controller.grantPermissions({ + subject: { origin: origin4 }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: {}, + [PermissionNames.wallet_getSecretObject]: {}, + [PermissionNames.wallet_getSecret_('bar')]: {}, + }, + }); + + controller.revokePermissions({ + [origin0]: [PermissionNames.wallet_getSecretArray], + [origin2]: [ + PermissionNames.wallet_getSecretArray, + PermissionNames.wallet_getSecret_('foo'), + ], + [origin3]: [PermissionNames.wallet_getSecretArray], + [origin4]: [ + PermissionNames.wallet_getSecretArray, + PermissionNames.wallet_getSecretObject, + PermissionNames.wallet_getSecret_('bar'), + ], + }); + + expect(controller.state).toStrictEqual({ + subjects: { + [origin1]: { + origin: origin1, + permissions: { + [PermissionNames.wallet_getSecretArray]: getPermissionMatcher( + PermissionNames.wallet_getSecretArray, + null, + origin1, + ), + }, + }, + [origin2]: { + origin: origin2, + permissions: { + [PermissionNames.wallet_getSecretObject]: getPermissionMatcher( + PermissionNames.wallet_getSecretObject, + null, + origin2, + ), + }, + }, + [origin3]: { + origin: origin3, + permissions: { + [PermissionNames.wallet_getSecret_('foo')]: getPermissionMatcher( + PermissionNames.wallet_getSecret_('foo'), + [constructCaveat(CaveatTypes.noopCaveat, null)], + origin3, + ), + }, + }, + }, + }); + }); + + it('throws an error if a specified subject has no permissions', () => { + const controller = getDefaultPermissionControllerWithState(); + expect(() => + controller.revokePermissions({ + foo: [PermissionNames.wallet_getSecretArray], + }), + ).toThrow(new errors.UnrecognizedSubjectError('foo')); + }); + + it('throws an error if the requested subject does not have the specified permission', () => { + const controller = getDefaultPermissionControllerWithState(); + expect(() => + controller.revokePermissions({ + 'metamask.io': [PermissionNames.wallet_getSecretObject], + }), + ).toThrow( + new errors.PermissionDoesNotExistError( + 'metamask.io', + PermissionNames.wallet_getSecretObject, + ), + ); + }); }); describe('revokePermissionForAllSubjects', () => { - it.todo('revokePermissionForAllSubjects'); + it('does nothing if there are no subjects', () => { + const controller = getDefaultPermissionController(); + controller.revokePermissionForAllSubjects( + PermissionNames.wallet_getSecretArray, + ); + expect(controller.state).toStrictEqual({ subjects: {} }); + }); + + it('revokes single permission from all subjects', () => { + const controller = getDefaultPermissionController(); + const origin0 = 'origin0'; + const origin1 = 'origin1'; + const origin2 = 'origin2'; + const origin3 = 'origin3'; + const origin4 = 'origin4'; + + controller.grantPermissions({ + subject: { origin: origin0 }, + approvedPermissions: { + [PermissionNames.wallet_getSecretObject]: {}, + }, + }); + + controller.grantPermissions({ + subject: { origin: origin1 }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: {}, + }, + }); + + controller.grantPermissions({ + subject: { origin: origin2 }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: {}, + [PermissionNames.wallet_getSecretObject]: {}, + [PermissionNames.wallet_getSecret_('foo')]: {}, + }, + }); + + controller.grantPermissions({ + subject: { origin: origin3 }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: {}, + [PermissionNames.wallet_getSecret_('foo')]: {}, + }, + }); + + controller.grantPermissions({ + subject: { origin: origin4 }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: {}, + [PermissionNames.wallet_getSecretObject]: {}, + [PermissionNames.wallet_getSecret_('bar')]: {}, + }, + }); + + controller.revokePermissionForAllSubjects( + PermissionNames.wallet_getSecretArray, + ); + + expect(controller.state).toStrictEqual({ + subjects: { + [origin0]: { + origin: origin0, + permissions: { + [PermissionNames.wallet_getSecretObject]: getPermissionMatcher( + PermissionNames.wallet_getSecretObject, + null, + origin0, + ), + }, + }, + [origin2]: { + origin: origin2, + permissions: { + [PermissionNames.wallet_getSecretObject]: getPermissionMatcher( + PermissionNames.wallet_getSecretObject, + null, + origin2, + ), + [PermissionNames.wallet_getSecret_('foo')]: getPermissionMatcher( + PermissionNames.wallet_getSecret_('foo'), + [constructCaveat(CaveatTypes.noopCaveat, null)], + origin2, + ), + }, + }, + [origin3]: { + origin: origin3, + permissions: { + [PermissionNames.wallet_getSecret_('foo')]: getPermissionMatcher( + PermissionNames.wallet_getSecret_('foo'), + [constructCaveat(CaveatTypes.noopCaveat, null)], + origin3, + ), + }, + }, + [origin4]: { + origin: origin4, + permissions: { + [PermissionNames.wallet_getSecretObject]: getPermissionMatcher( + PermissionNames.wallet_getSecretObject, + null, + origin4, + ), + [PermissionNames.wallet_getSecret_('bar')]: getPermissionMatcher( + PermissionNames.wallet_getSecret_('bar'), + [constructCaveat(CaveatTypes.noopCaveat, null)], + origin4, + ), + }, + }, + }, + }); + }); }); describe('hasCaveat', () => { @@ -591,7 +895,7 @@ describe('PermissionController', () => { }); }); - it('overwrites existing permissions if shouldPreserveExistingPermissions is false', () => { + it('preserves existing permissions if shouldPreserveExistingPermissions is true', () => { const controller = getDefaultPermissionControllerWithState(); const origin = 'metamask.io'; @@ -599,11 +903,11 @@ describe('PermissionController', () => { subjects: { [origin]: { origin, - permissions: { + permissions: expect.objectContaining({ wallet_getSecretArray: getPermissionMatcher( 'wallet_getSecretArray', ), - }, + }), }, }, }); @@ -613,6 +917,8 @@ describe('PermissionController', () => { approvedPermissions: { wallet_getSecretObject: {}, }, + // This is the default + // shouldPreserveExistingPermissions: true, }); expect(controller.state).toStrictEqual({ @@ -620,6 +926,9 @@ describe('PermissionController', () => { [origin]: { origin, permissions: expect.objectContaining({ + wallet_getSecretArray: getPermissionMatcher( + 'wallet_getSecretArray', + ), wallet_getSecretObject: getPermissionMatcher( 'wallet_getSecretObject', ), @@ -629,7 +938,7 @@ describe('PermissionController', () => { }); }); - it('preserves existing permissions if shouldPreserveExistingPermissions is true', () => { + it('overwrites existing permissions if shouldPreserveExistingPermissions is false', () => { const controller = getDefaultPermissionControllerWithState(); const origin = 'metamask.io'; @@ -637,11 +946,11 @@ describe('PermissionController', () => { subjects: { [origin]: { origin, - permissions: expect.objectContaining({ + permissions: { wallet_getSecretArray: getPermissionMatcher( 'wallet_getSecretArray', ), - }), + }, }, }, }); @@ -651,7 +960,7 @@ describe('PermissionController', () => { approvedPermissions: { wallet_getSecretObject: {}, }, - shouldPreserveExistingPermissions: true, + shouldPreserveExistingPermissions: false, }); expect(controller.state).toStrictEqual({ @@ -659,9 +968,6 @@ describe('PermissionController', () => { [origin]: { origin, permissions: expect.objectContaining({ - wallet_getSecretArray: getPermissionMatcher( - 'wallet_getSecretArray', - ), wallet_getSecretObject: getPermissionMatcher( 'wallet_getSecretObject', ), diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index b368200f4f..057651bea4 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -263,7 +263,7 @@ export class PermissionController< >; /** - * The {@link CaveatSpecifications} of this controller. + * The {@link CaveatSpecifications} of the controller. */ public get caveatSpecifications(): Readonly> { return this._caveatSpecifications; @@ -274,7 +274,7 @@ export class PermissionController< >; /** - * The {@link PermissionEnforcer} of this controller. + * The {@link PermissionEnforcer} of the controller. */ public get enforcer(): Readonly< PermissionEnforcer @@ -287,7 +287,7 @@ export class PermissionController< >; /** - * The {@link PermissionSpecifications} of this controller. + * The {@link PermissionSpecifications} of the controller. */ public get permissionSpecifications(): Readonly< PermissionSpecifications @@ -298,7 +298,7 @@ export class PermissionController< private readonly _unrestrictedMethods: ReadonlySet; /** - * The names of all JSON-RPC methods that will be ignored by this controller. + * The names of all JSON-RPC methods that will be ignored by the controller. */ public get unrestrictedMethods(): ReadonlySet { return this._unrestrictedMethods; @@ -307,10 +307,10 @@ export class PermissionController< /** * @param options - Permission controller options. * @param options.caveatSpecifications - The specifications of all caveats - * available to this controller. See {@link CaveatSpecifications} and the + * available to the controller. See {@link CaveatSpecifications} and the * documentation for more details. * @param options.permissionSpecifications - The specifications of all - * permissions available to this controller. See + * permissions available to the controller. See * {@link PermissionSpecifications} and the documentation for more details. * @param options.unrestrictedMethods - The callable names of all JSON-RPC * methods ignored by the new controller. @@ -350,7 +350,7 @@ export class PermissionController< } /** - * Constructor helper for registering this controller's messaging system + * Constructor helper for registering the controller's messaging system * actions. */ private registerMessageHandlers(): void { @@ -370,12 +370,18 @@ export class PermissionController< ); } + /** + * Clears the state of the controller. + */ clearState(): void { this.update((_draftState) => { return { ...defaultState }; }); } + /** + * @returns The origins (i.e. IDs) of all subjects. + */ getSubjectNames(): OriginString[] { return Object.keys(this.state.subjects); } @@ -397,12 +403,26 @@ export class PermissionController< ] as SubjectPermission; } + /** + * Gets all permissions for the specified subject, if any. + * + * @param origin - The origin of the subject. + * @returns The permissions of the subject, if any. + */ getPermissions( origin: OriginString, ): SubjectPermissions | undefined { return this.state.subjects[origin]?.permissions; } + /** + * Checks whether the subject with the specified origin has the specified + * permission. + * + * @param origin - The origin of the subject. + * @param target - The target name of the permission. + * @returns Whether the subject has the permission. + */ hasPermission( origin: OriginString, target: Permission['parentCapability'], @@ -410,6 +430,13 @@ export class PermissionController< return Boolean(this.getPermission(origin, target)); } + /** + * Checks whether the subject with the specified origin has any permissions. + * Use this if you want to know if a subject "exists". + * + * @param origin - The origin of the subject to check. + * @returns Whether the subject has any permissions. + */ hasPermissions(origin: OriginString): boolean { return Boolean(this.state.subjects[origin]); } @@ -438,7 +465,7 @@ export class PermissionController< private setValidatedPermissions( origin: OriginString, permissions: Record, - shouldPreserveExistingPermissions = false, + shouldPreserveExistingPermissions = true, ): void { this.update((draftState) => { if (!draftState.subjects[origin]) { @@ -455,6 +482,31 @@ export class PermissionController< }); } + /** + * Revokes all permissions from the specified origin. + * + * Throws an error of the origin has no permissions. + * + * @param origin - The origin whose permissions to revoke. + */ + revokeAllPermissions(origin: OriginString): void { + this.update((draftState) => { + if (!draftState.subjects[origin]) { + throw new UnrecognizedSubjectError(origin); + } + delete draftState.subjects[origin]; + }); + } + + /** + * Revokes the specified permission from the subject with the specified + * origin. + * + * Throws an error if the subject or the permission does not exist. + * + * @param origin - The origin of the subject whose permission to revoke. + * @param target - The target name of the permission to revoke. + */ revokePermission( origin: OriginString, target: Permission['parentCapability'], @@ -462,10 +514,18 @@ export class PermissionController< this.revokePermissions({ [origin]: [target] }); } + /** + * Revokes the specified permissions from the specified subjects. + * + * Throws an error if any of the subjects or permissions do not exist. + * + * @param subjectsAndPermissions - An object mapping subject origins + * to arrays of permission target names to revoke. + */ revokePermissions( subjectsAndPermissions: Record< OriginString, - Permission['parentCapability'][] + NonEmptyArray >, ): void { this.update((draftState) => { @@ -492,13 +552,19 @@ export class PermissionController< }); } + /** + * Revokes all permissions corresponding to the specified target for all subjects. + * Does nothing if no subjects or no such permission exists. + * + * @param target - The name of the target to revoke all permissions for. + */ revokePermissionForAllSubjects(target: Permission['parentCapability']): void { if (this.getSubjectNames().length === 0) { return; } this.update((draftState) => { - Object.values(draftState.subjects).forEach((subject) => { + Object.entries(draftState.subjects).forEach(([origin, subject]) => { const { permissions } = subject; if (hasProperty(permissions as Record, target)) { @@ -527,15 +593,6 @@ export class PermissionController< } } - revokeAllPermissions(origin: OriginString): void { - this.update((draftState) => { - if (!draftState.subjects[origin]) { - throw new UnrecognizedSubjectError(origin); - } - delete draftState.subjects[origin]; - }); - } - hasCaveat< TargetName extends Permission['parentCapability'], CaveatType extends ExtractValidCaveatTypes< From 0d4f5b4d1505046df8a1d41f20e506f947c25b83 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Fri, 1 Oct 2021 11:02:53 -0700 Subject: [PATCH 057/147] Unit tests: PermissionController, caveats complete --- .../permissions/PermissionController.test.ts | 602 +++++++++++++++++- .../src/permissions/PermissionController.ts | 216 ++++--- .../controllers/src/permissions/errors.ts | 6 - 3 files changed, 728 insertions(+), 96 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 00512615b5..a3d9fae7ae 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -103,7 +103,7 @@ type SecretObjectName = 'wallet_getSecretObject'; type SecretObjectPermission = PermissionConstraint< SecretObjectName, - FilterObjectCaveat + FilterObjectCaveat | NoopCaveat >; type SecretNamespacedKey = 'wallet_getSecret_*'; @@ -151,6 +151,15 @@ function getDefaultPermissionSpecifications(): PermissionSpecifications< methodImplementation: (_args: RestrictedMethodArgs) => { return { secret1: 'a', secret2: 'b', secret3: 'c' }; }, + validator: (permission: GenericPermission) => { + // A dummy validator for a caveat type that should be impossible to add + assert.ok( + !permission.caveats?.some( + (caveat) => caveat.type === CaveatTypes.filterArrayResponse, + ), + 'invalid caveats', + ); + }, }, 'wallet_getSecret_*': { target: PermissionKeys['wallet_getSecret_*'], @@ -279,6 +288,41 @@ describe('PermissionController', () => { }); }); + describe('getRestrictedMethod', () => { + it('gets the implementation of a restricted method', () => { + const controller = getDefaultPermissionController(); + expect( + controller.getRestrictedMethod(PermissionNames.wallet_getSecretArray), + ).toStrictEqual( + controller.permissionSpecifications[ + PermissionKeys.wallet_getSecretArray + ].methodImplementation, + ); + }); + + it('gets the implementation of a namespaced restricted method', () => { + const controller = getDefaultPermissionController(); + expect( + controller.getRestrictedMethod( + PermissionNames.wallet_getSecret_('foo'), + ), + ).toStrictEqual( + controller.permissionSpecifications[ + PermissionKeys['wallet_getSecret_*'] + ].methodImplementation, + ); + }); + + it('returns undefined if the method does not exist', () => { + const controller = getDefaultPermissionController(); + expect(controller.getRestrictedMethod('foo' as any)).toBeUndefined(); + }); + + // This is currently disallowed by the types, but handled by getTargetKey + // Should we add runtime validation forbidding it? Maybe in the constructor? + it.todo('handles methods ending in "_"'); + }); + describe('getSubjectNames', () => { it('gets all subject names', () => { const controller = getDefaultPermissionController(); @@ -333,6 +377,25 @@ describe('PermissionController', () => { }); }); + describe('getPermissions', () => { + it('gets existing permissions', () => { + const controller = getDefaultPermissionControllerWithState(); + + expect(controller.getPermissions('metamask.io')).toStrictEqual({ + [PermissionNames.wallet_getSecretArray]: getPermissionMatcher( + PermissionNames.wallet_getSecretArray, + null, + 'metamask.io', + ), + }); + }); + + it('returns undefined for subjects without permissions', () => { + const controller = getDefaultPermissionController(); + expect(controller.getPermissions('metamask.io')).toBeUndefined(); + }); + }); + describe('hasPermission', () => { it('correctly indicates whether an origin has a permission', () => { const controller = getDefaultPermissionControllerWithState(); @@ -772,30 +835,541 @@ describe('PermissionController', () => { }); describe('hasCaveat', () => { - it.todo('hasCaveat'); - }); + it('indicates whether a permission has a particular caveat', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; - describe('addCaveat', () => { - it.todo('addCaveat'); + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: {}, + [PermissionNames.wallet_getSecretObject]: { + caveats: [ + constructCaveat(CaveatTypes.filterObjectResponse, ['kaplar']), + ], + }, + [PermissionNames.wallet_getSecret_('foo')]: {}, + }, + }); + + expect( + controller.hasCaveat( + origin, + PermissionNames.wallet_getSecretArray, + CaveatTypes.filterArrayResponse, + ), + ).toStrictEqual(false); + + expect( + controller.hasCaveat( + origin, + PermissionNames.wallet_getSecretObject, + CaveatTypes.filterObjectResponse, + ), + ).toStrictEqual(true); + + expect( + controller.hasCaveat( + origin, + PermissionNames.wallet_getSecret_('foo'), + CaveatTypes.noopCaveat, + ), + ).toStrictEqual(true); + }); + + it('throws an error if no corresponding permission exists', () => { + const controller = getDefaultPermissionController(); + expect(() => + controller.hasCaveat( + 'metamask.io', + PermissionNames.wallet_getSecretArray, + CaveatTypes.filterArrayResponse, + ), + ).toThrow( + new errors.PermissionDoesNotExistError( + 'metamask.io', + PermissionNames.wallet_getSecretArray, + ), + ); + }); }); describe('getCaveat', () => { - it.todo('getCaveat'); + it('gets existing caveats', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: {}, + [PermissionNames.wallet_getSecretObject]: { + caveats: [ + constructCaveat(CaveatTypes.filterObjectResponse, ['kaplar']), + ], + }, + [PermissionNames.wallet_getSecret_('foo')]: {}, + }, + }); + + expect( + controller.getCaveat( + origin, + PermissionNames.wallet_getSecretArray, + CaveatTypes.filterArrayResponse, + ), + ).toBeUndefined(); + + expect( + controller.getCaveat( + origin, + PermissionNames.wallet_getSecretObject, + CaveatTypes.filterObjectResponse, + ), + ).toStrictEqual( + constructCaveat(CaveatTypes.filterObjectResponse, ['kaplar']), + ); + + expect( + controller.getCaveat( + origin, + PermissionNames.wallet_getSecret_('foo'), + CaveatTypes.noopCaveat, + ), + ).toStrictEqual(constructCaveat(CaveatTypes.noopCaveat, null)); + }); + + it('throws an error if no corresponding permission exists', () => { + const controller = getDefaultPermissionController(); + expect(() => + controller.getCaveat( + 'metamask.io', + PermissionNames.wallet_getSecretArray, + CaveatTypes.filterArrayResponse, + ), + ).toThrow( + new errors.PermissionDoesNotExistError( + 'metamask.io', + PermissionNames.wallet_getSecretArray, + ), + ); + }); + }); + + describe('addCaveat', () => { + it('adds a caveat to the specified permission', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: {}, + }, + }); + + controller.addCaveat( + origin, + PermissionNames.wallet_getSecretArray, + CaveatTypes.filterArrayResponse, + ['foo'], + ); + + expect(controller.state).toStrictEqual({ + subjects: { + [origin]: { + origin, + permissions: { + wallet_getSecretArray: getPermissionMatcher( + 'wallet_getSecretArray', + [constructCaveat(CaveatTypes.filterArrayResponse, ['foo'])], + origin, + ), + }, + }, + }, + }); + }); + + it("appends a caveat to the specified permission's existing caveats", () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecretObject]: { + caveats: [ + constructCaveat(CaveatTypes.filterObjectResponse, ['foo']), + ], + }, + }, + }); + + expect(controller.state).toStrictEqual({ + subjects: { + [origin]: { + origin, + permissions: { + wallet_getSecretObject: getPermissionMatcher( + 'wallet_getSecretObject', + [constructCaveat(CaveatTypes.filterObjectResponse, ['foo'])], + origin, + ), + }, + }, + }, + }); + + controller.addCaveat( + origin, + PermissionNames.wallet_getSecretObject, + CaveatTypes.noopCaveat, + null, + ); + + expect(controller.state).toStrictEqual({ + subjects: { + [origin]: { + origin, + permissions: { + wallet_getSecretObject: getPermissionMatcher( + 'wallet_getSecretObject', + [ + constructCaveat(CaveatTypes.filterObjectResponse, ['foo']), + constructCaveat(CaveatTypes.noopCaveat, null), + ], + origin, + ), + }, + }, + }, + }); + }); + + it('throws an error if a corresponding caveat already exists', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecretObject]: { + caveats: [ + constructCaveat(CaveatTypes.filterObjectResponse, ['kaplar']), + ], + }, + }, + }); + + expect(() => + controller.addCaveat( + origin, + PermissionNames.wallet_getSecretObject, + CaveatTypes.filterObjectResponse, + ['foo'], + ), + ).toThrow( + new errors.CaveatAlreadyExistsError( + origin, + PermissionNames.wallet_getSecretObject, + CaveatTypes.filterObjectResponse, + ), + ); + }); + + it('throws an error if no corresponding permission exists', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + expect(() => + controller.addCaveat( + origin, + PermissionNames.wallet_getSecretArray, + CaveatTypes.filterArrayResponse, + ['foo'], + ), + ).toThrow( + new errors.PermissionDoesNotExistError( + origin, + PermissionNames.wallet_getSecretArray, + ), + ); + }); }); describe('updateCaveat', () => { - it.todo('updateCaveat'); + it('updates an existing caveat', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: { + caveats: [ + constructCaveat(CaveatTypes.filterArrayResponse, ['foo']), + ], + }, + }, + }); + + expect(controller.state).toStrictEqual({ + subjects: { + [origin]: { + origin, + permissions: { + wallet_getSecretArray: getPermissionMatcher( + 'wallet_getSecretArray', + [constructCaveat(CaveatTypes.filterArrayResponse, ['foo'])], + origin, + ), + }, + }, + }, + }); + + controller.updateCaveat( + origin, + PermissionNames.wallet_getSecretArray, + CaveatTypes.filterArrayResponse, + ['bar'], + ); + + expect(controller.state).toStrictEqual({ + subjects: { + [origin]: { + origin, + permissions: { + wallet_getSecretArray: getPermissionMatcher( + 'wallet_getSecretArray', + [constructCaveat(CaveatTypes.filterArrayResponse, ['bar'])], + origin, + ), + }, + }, + }, + }); + }); + + it('throws an error if no corresponding permission exists', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + expect(() => + controller.updateCaveat( + origin, + PermissionNames.wallet_getSecretArray, + CaveatTypes.filterArrayResponse, + ['foo'], + ), + ).toThrow( + new errors.PermissionDoesNotExistError( + origin, + PermissionNames.wallet_getSecretArray, + ), + ); + }); + + it('throws an error if no corresponding caveat exists', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: {}, + }, + }); + + expect(() => + controller.updateCaveat( + origin, + PermissionNames.wallet_getSecretArray, + CaveatTypes.filterArrayResponse, + ['foo'], + ), + ).toThrow( + new errors.CaveatDoesNotExistError( + origin, + PermissionNames.wallet_getSecretArray, + CaveatTypes.filterArrayResponse, + ), + ); + }); }); describe('removeCaveat', () => { - it.todo('removeCaveat'); - }); + it('removes an existing caveat', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; - describe('getRestrictedMethod', () => { - it.todo('getRestrictedMethod'); - // This is currently disallowed by the types, but handled by getTargetKey - // Should we add runtime validation forbidding it? Maybe in the constructor? - it.todo('handles methods ending in "_"'); + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: { + caveats: [ + constructCaveat(CaveatTypes.filterArrayResponse, ['foo']), + ], + }, + }, + }); + + expect(controller.state).toStrictEqual({ + subjects: { + [origin]: { + origin, + permissions: { + wallet_getSecretArray: getPermissionMatcher( + 'wallet_getSecretArray', + [constructCaveat(CaveatTypes.filterArrayResponse, ['foo'])], + origin, + ), + }, + }, + }, + }); + + controller.removeCaveat( + origin, + PermissionNames.wallet_getSecretArray, + CaveatTypes.filterArrayResponse, + ); + + expect(controller.state).toStrictEqual({ + subjects: { + [origin]: { + origin, + permissions: { + wallet_getSecretArray: getPermissionMatcher( + 'wallet_getSecretArray', + null, + origin, + ), + }, + }, + }, + }); + }); + + it('removes an existing caveat, without modifying other caveats of the same permission', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecretObject]: { + caveats: [ + constructCaveat(CaveatTypes.noopCaveat, null), + constructCaveat(CaveatTypes.filterObjectResponse, ['foo']), + ], + }, + }, + }); + + expect(controller.state).toStrictEqual({ + subjects: { + [origin]: { + origin, + permissions: { + wallet_getSecretObject: getPermissionMatcher( + 'wallet_getSecretObject', + [ + constructCaveat(CaveatTypes.noopCaveat, null), + constructCaveat(CaveatTypes.filterObjectResponse, ['foo']), + ], + origin, + ), + }, + }, + }, + }); + + controller.removeCaveat( + origin, + PermissionNames.wallet_getSecretObject, + CaveatTypes.noopCaveat, + ); + + expect(controller.state).toStrictEqual({ + subjects: { + [origin]: { + origin, + permissions: { + wallet_getSecretObject: getPermissionMatcher( + 'wallet_getSecretObject', + [constructCaveat(CaveatTypes.filterObjectResponse, ['foo'])], + origin, + ), + }, + }, + }, + }); + }); + + it('throws an error if no corresponding permission exists', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + expect(() => + controller.removeCaveat( + origin, + PermissionNames.wallet_getSecretArray, + CaveatTypes.filterArrayResponse, + ), + ).toThrow( + new errors.PermissionDoesNotExistError( + origin, + PermissionNames.wallet_getSecretArray, + ), + ); + }); + + it('throws an error if no corresponding caveat exists', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: {}, + [PermissionNames.wallet_getSecretObject]: { + caveats: [ + constructCaveat(CaveatTypes.filterObjectResponse, ['foo']), + ], + }, + }, + }); + + expect(() => + controller.removeCaveat( + origin, + PermissionNames.wallet_getSecretArray, + CaveatTypes.filterArrayResponse, + ), + ).toThrow( + new errors.CaveatDoesNotExistError( + origin, + PermissionNames.wallet_getSecretArray, + CaveatTypes.filterArrayResponse, + ), + ); + + expect(() => + controller.removeCaveat( + origin, + PermissionNames.wallet_getSecretObject, + CaveatTypes.noopCaveat, + ), + ).toThrow( + new errors.CaveatDoesNotExistError( + origin, + PermissionNames.wallet_getSecretObject, + CaveatTypes.noopCaveat, + ), + ); + }); }); describe('grantPermissions', () => { diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 057651bea4..c8f663069b 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -35,7 +35,6 @@ import { PermissionDoesNotExistError, methodNotFound, UnrecognizedSubjectError, - PermissionHasNoCaveatsError, CaveatDoesNotExistError, CaveatTypeDoesNotExistError, CaveatMissingValueError, @@ -379,6 +378,23 @@ export class PermissionController< }); } + /** + * Gets the implementation of the specified restricted method. + * + * @param method - The name of the restricted method. + * @returns The restricted method implementation. + */ + getRestrictedMethod( + method: Permission['parentCapability'], + ): RestrictedMethod | undefined { + const targetKey = this.getTargetKey(method); + if (!targetKey) { + return undefined; + } + + return this.permissionSpecifications[targetKey].methodImplementation; + } + /** * @returns The origins (i.e. IDs) of all subjects. */ @@ -441,47 +457,6 @@ export class PermissionController< return Boolean(this.state.subjects[origin]); } - private validateModifiedPermission(permission: Permission): void { - const targetKey = this.getTargetKey(permission.parentCapability); - /* istanbul ignore next: this should be impossible */ - if (!targetKey) { - throw new Error( - `Fatal: Existing permission target key "${targetKey}" has no specification.`, - ); - } - this.permissionSpecifications[targetKey].validator?.(permission); - } - - /** - * Adds permissions to the given subject. Overwrites existing identical - * permissions (same subject and method). Other existing permissions - * remain unaffected. - * - * **ATTN: Assumes that the new permissions have been validated.** - * - * @param origin - The origin of the grantee subject. - * @param permissions - The new permissions for the grantee subject. - */ - private setValidatedPermissions( - origin: OriginString, - permissions: Record, - shouldPreserveExistingPermissions = true, - ): void { - this.update((draftState) => { - if (!draftState.subjects[origin]) { - // Typecast: immer's WritableDraft is incompatible with our generics - draftState.subjects[origin] = { origin, permissions: {} as any }; - } - - if (shouldPreserveExistingPermissions) { - Object.assign(draftState.subjects[origin].permissions, permissions); - } else { - // Typecast: ts(2589) - draftState.subjects[origin].permissions = permissions as any; - } - }); - } - /** * Revokes all permissions from the specified origin. * @@ -593,6 +568,21 @@ export class PermissionController< } } + /** + * Checks whether the permission of the subject corresponding to the given + * origin has a caveat of the specified type. + * + * Throws an error if the subject does not have a permission with the + * specified target name. + * + * @template TargetName - The permission target name. Should be inferred. + * @template CaveatType - The valid caveat types for the permission. Should + * be inferred. + * @param origin - The origin of the subject. + * @param target - The target name of the permission. + * @param caveatType - The type of the caveat to check for. + * @returns Whether the permission has the specified caveat. + */ hasCaveat< TargetName extends Permission['parentCapability'], CaveatType extends ExtractValidCaveatTypes< @@ -601,14 +591,25 @@ export class PermissionController< TargetName >, >(origin: OriginString, target: TargetName, caveatType: CaveatType): boolean { - return ( - this.getPermission(origin, target)?.caveats?.some( - (caveat) => caveat.type === caveatType, - ) ?? false - ); + return Boolean(this.getCaveat(origin, target, caveatType)); } - addCaveat< + /** + * Gets the caveat of the specified type, if any, for the the permission of + * the subject corresponding to the given origin. + * + * Throws an error if the subject does not have a permission with the + * specified target name. + * + * @template TargetName - The permission target name. Should be inferred. + * @template CaveatType - The valid caveat types for the permission. Should + * be inferred. + * @param origin - The origin of the subject. + * @param target - The target name of the permission. + * @param caveatType - The type of the caveat to get. + * @returns The caveat, or `undefined` if no such caveat exists. + */ + getCaveat< TargetName extends Permission['parentCapability'], CaveatType extends ExtractValidCaveatTypes< Permission, @@ -619,16 +620,30 @@ export class PermissionController< origin: OriginString, target: TargetName, caveatType: CaveatType, - caveatValue: ExtractCaveatValue, - ): void { - if (this.hasCaveat(origin, target, caveatType)) { - throw new CaveatAlreadyExistsError(origin, target, caveatType); + ): GetCaveatFromType | undefined { + const permission = this.getPermission(origin, target); + if (!permission) { + throw new PermissionDoesNotExistError(origin, target); } - this.setCaveat(origin, target, caveatType, caveatValue); + return findCaveat(permission, caveatType) as GetCaveatFromType< + Caveat, + CaveatType + >; } - getCaveat< + /** + * TODO + * + * @template TargetName - The permission target name. Should be inferred. + * @template CaveatType - The valid caveat types for the permission. Should + * be inferred. + * @param origin - The origin of the subject. + * @param target - The target name of the permission. + * @param caveatType - The type of the caveat to add. + * @param caveatValue - The value of the caveat to add. + */ + addCaveat< TargetName extends Permission['parentCapability'], CaveatType extends ExtractValidCaveatTypes< Permission, @@ -639,18 +654,26 @@ export class PermissionController< origin: OriginString, target: TargetName, caveatType: CaveatType, - ): GetCaveatFromType | undefined { - const permission = this.getPermission(origin, target); - if (!permission) { - throw new PermissionDoesNotExistError(origin, target); + caveatValue: ExtractCaveatValue, + ): void { + if (this.hasCaveat(origin, target, caveatType)) { + throw new CaveatAlreadyExistsError(origin, target, caveatType); } - return findCaveat(permission, caveatType) as GetCaveatFromType< - Caveat, - CaveatType - >; + this.setCaveat(origin, target, caveatType, caveatValue); } + /** + * TODO + * + * @template TargetName - The permission target name. Should be inferred. + * @template CaveatType - The valid caveat types for the permission. Should + * be inferred. + * @param origin - The origin of the subject. + * @param target - The target name of the permission. + * @param caveatType - The type of the caveat to update. + * @param caveatValue - The new value of the caveat. + */ updateCaveat< TargetName extends Permission['parentCapability'], CaveatType extends ExtractValidCaveatTypes< @@ -722,6 +745,16 @@ export class PermissionController< }); } + /** + * TODO + * + * @template TargetName - The permission target name. Should be inferred. + * @template CaveatType - The valid caveat types for the permission. Should + * be inferred. + * @param origin - The origin of the subject. + * @param target - The target name of the permission. + * @param caveatType - The type of the caveat to remove. + */ removeCaveat< TargetName extends Permission['parentCapability'], CaveatType extends ExtractValidCaveatTypes< @@ -740,13 +773,13 @@ export class PermissionController< } if (!permission.caveats) { - throw new PermissionHasNoCaveatsError(origin, target); + throw new CaveatDoesNotExistError(origin, target, caveatType); } const caveatIndex = permission.caveats.findIndex( (existingCaveat) => existingCaveat.type === caveatType, ); - if (caveatIndex === -1) { + if (!permission.caveats || caveatIndex === -1) { throw new CaveatDoesNotExistError(origin, target, caveatType); } @@ -760,6 +793,24 @@ export class PermissionController< }); } + /** + * Validates the specified modified permission. Effectively, calls the + * validator function of the specified permission, if any. Should always + * be invoked on a permission after its caveats have been modified. + * + * @param permission - The modified permission to validate. + */ + private validateModifiedPermission(permission: Permission): void { + const targetKey = this.getTargetKey(permission.parentCapability); + /* istanbul ignore next: this should be impossible */ + if (!targetKey) { + throw new Error( + `Fatal: Existing permission target key "${targetKey}" has no specification.`, + ); + } + this.permissionSpecifications[targetKey].validator?.(permission); + } + /** * Used for retrieving the key that manages the restricted method * associated with the current RPC `method` key. @@ -808,17 +859,6 @@ export class PermissionController< return undefined; } - getRestrictedMethod( - method: Permission['parentCapability'], - ): RestrictedMethod | undefined { - const targetKey = this.getTargetKey(method); - if (!targetKey) { - return undefined; - } - - return this.permissionSpecifications[targetKey].methodImplementation; - } - grantPermissions({ requestData, approvedPermissions, @@ -875,6 +915,30 @@ export class PermissionController< return permissions; } + /** + * Adds permissions to the given subject. Overwrites existing identical + * permissions (same subject and method). Other existing permissions + * remain unaffected. + * + * **ATTN: Assumes that the new permissions have been validated.** + * + * @param origin - The origin of the grantee subject. + * @param permissions - The new permissions for the grantee subject. + */ + private setValidatedPermissions( + origin: OriginString, + permissions: Record, + ): void { + this.update((draftState) => { + if (!draftState.subjects[origin]) { + // Typecast: immer's WritableDraft is incompatible with our generics + draftState.subjects[origin] = { origin, permissions: {} as any }; + } + + Object.assign(draftState.subjects[origin].permissions, permissions); + }); + } + /** * @param caveats The caveats to validate. * @returns Whether the given caveats are valid. @@ -905,7 +969,7 @@ export class PermissionController< throw new InvalidCaveatFieldsError(requestedCaveat, origin, target); } - if (requestedCaveat.type !== 'string') { + if (typeof requestedCaveat.type !== 'string') { throw new InvalidCaveatTypeError(requestedCaveat.type); } diff --git a/packages/controllers/src/permissions/errors.ts b/packages/controllers/src/permissions/errors.ts index d1242a5f93..dbf90a0f15 100644 --- a/packages/controllers/src/permissions/errors.ts +++ b/packages/controllers/src/permissions/errors.ts @@ -71,12 +71,6 @@ export class PermissionDoesNotExistError extends Error { } } -export class PermissionHasNoCaveatsError extends Error { - constructor(origin: string, target: string) { - super(`Permission for "${target}" of subject "${origin}" has no caveats.`); - } -} - export class UnrecognizedCaveatTypeError extends Error { constructor(caveatType: string) { super(`Unrecognized caveat type: "${caveatType}"`); From 9fd6d67b3de2ecd0bd7a45b43cdf828ff26ff37a Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Fri, 1 Oct 2021 11:04:02 -0700 Subject: [PATCH 058/147] Unit tests: PermissionController, caveats complete --- packages/controllers/src/permissions/PermissionController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index c8f663069b..c1779d883a 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -862,7 +862,7 @@ export class PermissionController< grantPermissions({ requestData, approvedPermissions, - shouldPreserveExistingPermissions = false, + shouldPreserveExistingPermissions = true, subject, }: GrantPermissionsOptions): SubjectPermissions { const { origin } = subject; From f7b2798d7e88e1a059cd9f8c39edf2026d42ed36 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Fri, 1 Oct 2021 13:40:15 -0700 Subject: [PATCH 059/147] Unit tests: PermissionController, caveat errors --- .../controllers/src/permissions/Caveat.ts | 4 +- .../permissions/PermissionController.test.ts | 196 +++++++++++++++++- .../src/permissions/PermissionController.ts | 153 ++++++++++---- .../src/permissions/PermissionEnforcer.ts | 4 +- .../controllers/src/permissions/errors.ts | 28 ++- 5 files changed, 331 insertions(+), 54 deletions(-) diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index 1026c8ef13..877ce603f1 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -69,10 +69,10 @@ export type CaveatDecorator = ( * */ export type CaveatValidator = ( - caveat: { type: string; value: unknown }, + caveat: { type: Caveat['type']; value: unknown }, origin?: string, target?: string, -) => caveat is Caveat; +) => void; export type CaveatSpecification = { /** diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index a3d9fae7ae..bbefc0f510 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -86,6 +86,11 @@ function getDefaultCaveatSpecifications(): CaveatSpecifications async (args: RestrictedMethodArgs) => { return method(args); }, + validator: (caveat: { type: CaveatTypes.noopCaveat; value: unknown }) => { + if (caveat.value !== null) { + throw new Error('Caveat value must be null'); + } + }, }, }; } @@ -157,7 +162,7 @@ function getDefaultPermissionSpecifications(): PermissionSpecifications< !permission.caveats?.some( (caveat) => caveat.type === CaveatTypes.filterArrayResponse, ), - 'invalid caveats', + 'permission validation failed', ); }, }, @@ -182,7 +187,7 @@ function getDefaultPermissionSpecifications(): PermissionSpecifications< assert.deepStrictEqual( permission.caveats, [constructCaveat(CaveatTypes.noopCaveat, null)], - 'invalid caveats', + 'permission validation failed', ); }, }, @@ -991,7 +996,7 @@ describe('PermissionController', () => { }); }); - it("appends a caveat to the specified permission's existing caveats", () => { + it(`appends a caveat to the specified permission's existing caveats`, () => { const controller = getDefaultPermissionController(); const origin = 'metamask.io'; @@ -1047,6 +1052,31 @@ describe('PermissionController', () => { }); }); + it('throws an error if the permission fails to validate with the new caveat', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecretObject]: { + caveats: [ + constructCaveat(CaveatTypes.filterObjectResponse, ['foo']), + ], + }, + }, + }); + + expect(() => + controller.addCaveat( + origin, + PermissionNames.wallet_getSecretObject, + CaveatTypes.filterArrayResponse as any, + ['foo'], + ), + ).toThrow(new Error('permission validation failed')); + }); + it('throws an error if a corresponding caveat already exists', () => { const controller = getDefaultPermissionController(); const origin = 'metamask.io'; @@ -1469,7 +1499,7 @@ describe('PermissionController', () => { }); }); - it('preserves existing permissions if shouldPreserveExistingPermissions is true', () => { + it('preserves existing permissions if preserveExistingPermissions is true', () => { const controller = getDefaultPermissionControllerWithState(); const origin = 'metamask.io'; @@ -1491,8 +1521,7 @@ describe('PermissionController', () => { approvedPermissions: { wallet_getSecretObject: {}, }, - // This is the default - // shouldPreserveExistingPermissions: true, + // preserveExistingPermissions is true by default }); expect(controller.state).toStrictEqual({ @@ -1512,7 +1541,7 @@ describe('PermissionController', () => { }); }); - it('overwrites existing permissions if shouldPreserveExistingPermissions is false', () => { + it('overwrites existing permissions if preserveExistingPermissions is false', () => { const controller = getDefaultPermissionControllerWithState(); const origin = 'metamask.io'; @@ -1534,7 +1563,7 @@ describe('PermissionController', () => { approvedPermissions: { wallet_getSecretObject: {}, }, - shouldPreserveExistingPermissions: false, + preserveExistingPermissions: false, }); expect(controller.state).toStrictEqual({ @@ -1585,6 +1614,157 @@ describe('PermissionController', () => { }), ).toThrow(errors.methodNotFound({ method: 'wallet_getSecretFalafel' })); }); + + it('throws if a requested caveat is not a plain object', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + expect(() => + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + wallet_getSecretArray: { + caveats: [[]] as any, + }, + }, + }), + ).toThrow( + new errors.InvalidCaveatError( + [], + origin, + PermissionNames.wallet_getSecretArray, + ), + ); + + expect(() => + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + wallet_getSecretArray: { + caveats: ['foo'] as any, + }, + }, + }), + ).toThrow( + new errors.InvalidCaveatError( + [], + origin, + PermissionNames.wallet_getSecretArray, + ), + ); + }); + + it('throws if a requested caveat has more than two keys', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + expect(() => + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + wallet_getSecretArray: { + caveats: [ + { + ...constructCaveat(CaveatTypes.filterArrayResponse, ['foo']), + bar: 'bar', + }, + ] as any, + }, + }, + }), + ).toThrow( + new errors.InvalidCaveatFieldsError( + { + ...constructCaveat(CaveatTypes.filterArrayResponse, ['foo']), + bar: 'bar', + }, + origin, + PermissionNames.wallet_getSecretArray, + ), + ); + }); + + it('throws if a requested caveat type is not a string', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + expect(() => + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + wallet_getSecretArray: { + caveats: [ + { + type: 2, + value: ['foo'], + }, + ] as any, + }, + }, + }), + ).toThrow( + new errors.InvalidCaveatTypeError( + { + type: 2, + value: ['foo'], + }, + origin, + PermissionNames.wallet_getSecretArray, + ), + ); + }); + + it('throws if a requested caveat type does not exist', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + expect(() => + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + wallet_getSecretArray: { + caveats: [constructCaveat('fooType', 'bar')] as any, + }, + }, + }), + ).toThrow( + new errors.CaveatTypeDoesNotExistError( + constructCaveat('fooType', 'bar'), + origin, + PermissionNames.wallet_getSecretArray, + ), + ); + }); + + it('throws if a requested caveat has no value field', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + expect(() => + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + wallet_getSecretArray: { + caveats: [ + { + type: CaveatTypes.filterArrayResponse, + foo: 'bar', + }, + ] as any, + }, + }, + }), + ).toThrow( + new errors.CaveatMissingValueError( + { + type: CaveatTypes.filterArrayResponse, + foo: 'bar', + }, + origin, + PermissionNames.wallet_getSecretArray, + ), + ); + }); }); describe('actions', () => { diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index c1779d883a..87ec7e6579 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -14,7 +14,7 @@ import { isPlainObject, hasProperty, NonEmptyArray } from '../utils'; import { CaveatConstraint, CaveatSpecifications, - constructCaveat, + constructCaveat as _constructCaveat, GenericCaveat, GetCaveatFromType, } from './Caveat'; @@ -228,7 +228,7 @@ type PermissionControllerOptions< type GrantPermissionsOptions = { approvedPermissions: RequestedPermissions; subject: PermissionSubjectMetadata; - shouldPreserveExistingPermissions?: boolean; + preserveExistingPermissions?: boolean; requestData?: Record; }; @@ -633,7 +633,15 @@ export class PermissionController< } /** - * TODO + * Adds a caveat of the specified type, with the specified caveat value, to + * the permission corresponding to the given subject origin and permission + * target. + * + * For modifying existing caveats, use + * {@link PermissionController.updateCaveat}. + * + * Throws an error if no such permission exists, or if the caveat already + * exists. * * @template TargetName - The permission target name. Should be inferred. * @template CaveatType - The valid caveat types for the permission. Should @@ -664,7 +672,14 @@ export class PermissionController< } /** - * TODO + * Updates the value of the caveat of the specified type belonging to the + * permission corresponding to the given subject origin and permission + * target. + * + * For adding new caveats, use + * {@link PermissionController.addCaveat}. + * + * Throws an error if no such permission or caveat exists. * * @template TargetName - The permission target name. Should be inferred. * @template CaveatType - The valid caveat types for the permission. Should @@ -694,6 +709,22 @@ export class PermissionController< this.setCaveat(origin, target, caveatType, caveatValue); } + /** + * Internal method for setting the caveat of a permission. + * + * Throws an error if the permission does not exist or fails to validate after + * its caveats have been modified. + * + * @see {@link PermissionController.addCaveat} + * @see {@link PermissionController.updateCaveat} + * @template TargetName - The permission target name. Should be inferred. + * @template CaveatType - The valid caveat types for the permission. Should + * be inferred. + * @param origin - The origin of the subject. + * @param target - The target name of the permission. + * @param caveatType - The type of the caveat to set. + * @param caveatValue - The value of the caveat to set. + */ private setCaveat< TargetName extends Permission['parentCapability'], CaveatType extends ExtractValidCaveatTypes< @@ -708,16 +739,27 @@ export class PermissionController< caveatValue: ExtractCaveatValue, ): void { this.update((draftState) => { + const subject = draftState.subjects[origin] as + | PermissionSubjectEntry + | undefined; + + // This should be impossible in our usage, but TypeScript wants it + /* istanbul ignore if */ + if (!subject) { + throw new UnrecognizedSubjectError(origin); + } + // Typecast: immer's WritableDraft is incompatible with our generics - const permission: MutableCaveats = ( - draftState.subjects as PermissionControllerSubjects - )[origin]?.permissions[target]; - /* istanbul ignore if: This should be impossible, but TypeScript wants it */ + const permission: MutableCaveats = + subject.permissions[target]; + + // This should also be impossible in our usage, but TypeScript wants it + /* istanbul ignore if */ if (!permission) { throw new PermissionDoesNotExistError(origin, target); } - const caveat = this.computeCaveat(origin, target, { + const caveat = this.constructCaveat(origin, target, { type: caveatType, value: caveatValue, }); @@ -746,7 +788,10 @@ export class PermissionController< } /** - * TODO + * Removes the caveat of the specified type from the permission corresponding + * to the given subject origin and target name. + * + * Throws an error if no such permission or caveat exists. * * @template TargetName - The permission target name. Should be inferred. * @template CaveatType - The valid caveat types for the permission. Should @@ -859,10 +904,25 @@ export class PermissionController< return undefined; } + /** + * Grants approved permissions to the specified subject. Every permission and + * caveat is stringently validated – including by calling every specification + * validator – and an error is thrown if any validation fails. + * + * @param options - Options bag. + * @param options.approvedPermissions - The requested permissions approved by + * the user. + * @param options.requestData - Permission request data. Passed to permission + * factory functions. + * @param options.preserveExistingPermissions - Whether to preserve the + * subject's existing permissions. + * @param options.subject - The subject to grant permissions to. + * @returns The granted permissions. + */ grantPermissions({ - requestData, approvedPermissions, - shouldPreserveExistingPermissions = true, + requestData, + preserveExistingPermissions = true, subject, }: GrantPermissionsOptions): SubjectPermissions { const { origin } = subject; @@ -872,7 +932,7 @@ export class PermissionController< } const permissions: SubjectPermissions = - ((shouldPreserveExistingPermissions && { + ((preserveExistingPermissions && { ...this.getPermissions(origin), }) || {}) as SubjectPermissions; @@ -890,7 +950,7 @@ export class PermissionController< const targetName = requestedTarget as Permission['parentCapability']; const specification = this.permissionSpecifications[targetKey]; - const caveats = this.computeCaveats( + const caveats = this.constructCaveats( origin, targetName, approvedPermissions[targetName].caveats, @@ -916,9 +976,9 @@ export class PermissionController< } /** - * Adds permissions to the given subject. Overwrites existing identical - * permissions (same subject and method). Other existing permissions - * remain unaffected. + * Assigns the specified permissions to the subject with the given origin. + * Overwrites all existing permissions, and creates a subject entry if it + * doesn't already exist. * * **ATTN: Assumes that the new permissions have been validated.** * @@ -935,34 +995,57 @@ export class PermissionController< draftState.subjects[origin] = { origin, permissions: {} as any }; } - Object.assign(draftState.subjects[origin].permissions, permissions); + draftState.subjects[origin].permissions = permissions as any; }); } /** - * @param caveats The caveats to validate. - * @returns Whether the given caveats are valid. + * Constructs the requested caveats for the permission of the specified + * subject origin and target name. + * + * Throws an error if validation fails. + * + * @see {@link PermissionController.constructCaveat} for details. + * @param origin - The origin of the permission subject. + * @param target - The permission target name. + * @param requestedCaveats - The requested caveats to construct. + * @returns The constructed caveats. */ - private computeCaveats( + private constructCaveats( origin: OriginString, target: Permission['parentCapability'], - caveats?: unknown[] | null, + requestedCaveats?: unknown[] | null, ): NonEmptyArray | undefined { - const caveatArray = caveats?.map((requestedCaveat) => - this.computeCaveat(origin, target, requestedCaveat), + const caveatArray = requestedCaveats?.map((requestedCaveat) => + this.constructCaveat(origin, target, requestedCaveat), ); return caveatArray && caveatArray.length > 0 ? (caveatArray as NonEmptyArray) : undefined; } - private computeCaveat( + /** + * Constructs a caveat for the permission of the specified subject origin and + * target name, per the requested caveat argument. This methods validates + * everything about the requested caveat, except that its `value` property + * is JSON-compatible. It also ensures that a caveat specification exist for + * the requested type, and calls the specification validator, if it exists, on + * the constructed caveat. + * + * Throws an error if validation fails. + * + * @param origin - The origin of the permission subject. + * @param target - The permission target name. + * @param requestedCaveat - The requested caveat to construct. + * @returns The constructed caveat. + */ + private constructCaveat( origin: OriginString, target: Permission['parentCapability'], requestedCaveat: unknown, ): Caveat { if (!isPlainObject(requestedCaveat)) { - throw new InvalidCaveatError(requestedCaveat); + throw new InvalidCaveatError(requestedCaveat, origin, target); } if (Object.keys(requestedCaveat).length !== 2) { @@ -970,7 +1053,7 @@ export class PermissionController< } if (typeof requestedCaveat.type !== 'string') { - throw new InvalidCaveatTypeError(requestedCaveat.type); + throw new InvalidCaveatTypeError(requestedCaveat, origin, target); } const specification = @@ -979,11 +1062,7 @@ export class PermissionController< ]; if (!specification) { - throw new CaveatTypeDoesNotExistError( - requestedCaveat.type, - origin, - target, - ); + throw new CaveatTypeDoesNotExistError(requestedCaveat, origin, target); } if (!hasProperty(requestedCaveat, 'value')) { @@ -991,7 +1070,7 @@ export class PermissionController< } // TODO: Consider validating that this is Json? - const caveat = constructCaveat( + const caveat = _constructCaveat( requestedCaveat.type, (requestedCaveat.value as Json) ?? null, ); @@ -999,6 +1078,11 @@ export class PermissionController< return caveat as Caveat; } + /** + * Constructor helper for getting the enforcer of this controller. + * + * @returns The {@link PermissionEnforcer}. + */ private constructEnforcer(): PermissionEnforcer< TargetKey, Caveat, @@ -1048,8 +1132,7 @@ export class PermissionController< type?: string; }) => // Typecast: There are only some valid parameter combinations, but we - // just won't worry about that here. It's unclear if the messaging - // even handles them. + // just won't worry about that here. this.messagingSystem.call('ApprovalController:hasRequest', opts as any), }); } diff --git a/packages/controllers/src/permissions/PermissionEnforcer.ts b/packages/controllers/src/permissions/PermissionEnforcer.ts index c9fc9d2b3c..17a5b8dc35 100644 --- a/packages/controllers/src/permissions/PermissionEnforcer.ts +++ b/packages/controllers/src/permissions/PermissionEnforcer.ts @@ -163,7 +163,7 @@ export class PermissionEnforcer< origin: string, requestedPermissions: RequestedPermissions, id: string = nanoid(), - shouldPreserveExistingPermissions = false, + preserveExistingPermissions = false, ): Promise<[Record, { id: string; origin: string }]> { this.validateRequestedPermissions(origin, requestedPermissions); const metadata: PermissionsRequest['metadata'] = { @@ -190,7 +190,7 @@ export class PermissionEnforcer< this.grantPermissions({ subject: { origin }, approvedPermissions, - shouldPreserveExistingPermissions, + preserveExistingPermissions, requestData, }), metadata, diff --git a/packages/controllers/src/permissions/errors.ts b/packages/controllers/src/permissions/errors.ts index dbf90a0f15..a600d50685 100644 --- a/packages/controllers/src/permissions/errors.ts +++ b/packages/controllers/src/permissions/errors.ts @@ -103,27 +103,41 @@ export class InvalidPermissionJsonError extends Error { } export class InvalidCaveatError extends EthereumRpcError { - constructor(receivedCaveat: unknown) { + public data: { origin: string; target: string }; + + constructor(receivedCaveat: unknown, origin: string, target: string) { super( errorCodes.rpc.invalidParams, `Invalid caveat. Caveats must be plain objects.`, { receivedCaveat }, ); + this.data = { origin, target }; } } export class InvalidCaveatTypeError extends Error { - constructor(caveatType: unknown) { - super(`Caveat types must be strings. Received: "${typeof caveatType}"`); + public data: { + caveat: Record; + origin: string; + target: string; + }; + + constructor(caveat: Record, origin: string, target: string) { + super(`Caveat types must be strings. Received: "${typeof caveat.type}"`); + this.data = { caveat, origin, target }; } } export class CaveatTypeDoesNotExistError extends Error { - public data: { origin: string; target: string }; + public data: { + caveat: Record; + origin: string; + target: string; + }; - constructor(caveatType: string, origin: string, target: string) { - super(`Caveat type "${caveatType}" does not exist.`); - this.data = { origin, target }; + constructor(caveat: Record, origin: string, target: string) { + super(`Caveat type "${caveat.type}" does not exist.`); + this.data = { caveat, origin, target }; } } From ad34223a346c645d6c135497699c7e86546be838 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Fri, 1 Oct 2021 15:02:35 -0700 Subject: [PATCH 060/147] Fix target key validation --- .../permissions/PermissionController.test.ts | 72 +++++++++++++++++-- .../src/permissions/PermissionController.ts | 39 ++++++++-- 2 files changed, 103 insertions(+), 8 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index bbefc0f510..700dc32aae 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -281,6 +281,74 @@ describe('PermissionController', () => { const controller = getDefaultPermissionControllerWithState(); expect(controller.state).toStrictEqual(getExistingPermissionState()); }); + + it('throws an error for invalid permission target keys', () => { + expect( + () => + new PermissionController< + DefaultTargetKeys, + DefaultCaveats, + DefaultPermissions + >( + getControllerOpts({ + permissionSpecifications: { + ...getDefaultPermissionSpecifications(), + '': { target: '' }, + } as any, + }), + ), + ).toThrow(`Invalid permission target key: ""`); + + expect( + () => + new PermissionController< + DefaultTargetKeys, + DefaultCaveats, + DefaultPermissions + >( + getControllerOpts({ + permissionSpecifications: { + ...getDefaultPermissionSpecifications(), + foo_: { target: 'foo_' }, + } as any, + }), + ), + ).toThrow(`Invalid permission target key: "foo_"`); + + expect( + () => + new PermissionController< + DefaultTargetKeys, + DefaultCaveats, + DefaultPermissions + >( + getControllerOpts({ + permissionSpecifications: { + ...getDefaultPermissionSpecifications(), + 'foo*': { target: 'foo*' }, + } as any, + }), + ), + ).toThrow(`Invalid permission target key: "foo*"`); + + expect( + () => + new PermissionController< + DefaultTargetKeys, + DefaultCaveats, + DefaultPermissions + >( + getControllerOpts({ + permissionSpecifications: { + ...getDefaultPermissionSpecifications(), + foo: { target: 'bar' }, + } as any, + }), + ), + ).toThrow( + `Invalid permission specification: key "foo" must match specification.target value "bar".`, + ); + }); }); describe('clearState', () => { @@ -322,10 +390,6 @@ describe('PermissionController', () => { const controller = getDefaultPermissionController(); expect(controller.getRestrictedMethod('foo' as any)).toBeUndefined(); }); - - // This is currently disallowed by the types, but handled by getTargetKey - // Should we add runtime validation forbidding it? Maybe in the constructor? - it.todo('handles methods ending in "_"'); }); describe('getSubjectNames', () => { diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 87ec7e6579..875f642492 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -26,6 +26,7 @@ import { MutableCaveats, OriginString, PermissionConstraint, + PermissionSpecificationConstraint, PermissionSpecifications, RequestedPermissions, RestrictedMethod, @@ -338,16 +339,48 @@ export class PermissionController< }, }); + this._unrestrictedMethods = new Set(unrestrictedMethods); this._caveatSpecifications = deepFreeze({ ...caveatSpecifications }); + + this.validatePermissionSpecifications(permissionSpecifications); this._permissionSpecifications = deepFreeze({ ...permissionSpecifications, }); - this._unrestrictedMethods = new Set(unrestrictedMethods); this.registerMessageHandlers(); this._enforcer = this.constructEnforcer(); } + /** + * Constructor helper for validating permission specifications. This is + * intended to prevent the use of invalid target keys which, while impossible + * to add in TypeScript, could rather easily occur in plain JavaScript. + * + * Throws an error if validation fails. + * + * @param specifications - The permission specifications passed to this + * controller's constructor. + */ + private validatePermissionSpecifications( + specifications: PermissionSpecifications, + ) { + Object.entries>( + specifications, + ).forEach(([targetKey, { target: innerTargetKey }]) => { + // Check if the target key is the empty string, ends with "_", or ends + // with "*" but not "_*" + if (!targetKey || /_$/u.test(targetKey) || /[^_]\*$/u.test(targetKey)) { + throw new Error(`Invalid permission target key: "${targetKey}"`); + } + + if (targetKey !== innerTargetKey) { + throw new Error( + `Invalid permission specification: key "${targetKey}" must match specification.target value "${innerTargetKey}".`, + ); + } + }); + } + /** * Constructor helper for registering the controller's messaging system * actions. @@ -895,9 +928,7 @@ export class PermissionController< targetKey += `${segments.shift()}_`; } - if (hasProperty(this._permissionSpecifications, targetKey)) { - return targetKey as TargetKey; - } else if (wildCardMethodsWithoutWildCard[targetKey]) { + if (wildCardMethodsWithoutWildCard[targetKey]) { return `${targetKey}*` as TargetKey; } From 9aeb9f230849a6fcb8e7abfd69c62eccfc123596 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Fri, 1 Oct 2021 15:44:38 -0700 Subject: [PATCH 061/147] Unit tests: PermissionController, actions --- .../permissions/PermissionController.test.ts | 113 +++++++++++++++--- 1 file changed, 99 insertions(+), 14 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 700dc32aae..38e235aa11 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -1,6 +1,6 @@ import assert from 'assert'; import { ControllerMessenger, Json } from '@metamask/controllers'; -import { isPlainObject } from '../utils'; +import { hasProperty, isPlainObject } from '../utils'; import * as errors from './errors'; import { constructCaveat } from './Caveat'; import { @@ -203,11 +203,18 @@ function getDefaultRestrictedMessenger() { PermissionControllerActions, PermissionControllerEvents >(); - return controllerMessenger.getRestricted( - { - name: controllerName, - }, - ); + return controllerMessenger.getRestricted< + typeof controllerName, + PermissionControllerActions['type'], + PermissionControllerEvents['type'] + >({ + name: controllerName, + allowedActions: [ + 'PermissionController:clearPermissions', + 'PermissionController:getSubjectNames', + 'PermissionController:hasPermissions', + ], + }); } function getDefaultUnrestrictedMethods() { @@ -236,7 +243,7 @@ function getExistingPermissionState() { }; } -function getControllerOpts(opts?: Record) { +function getControllerOptions(opts?: Record) { return { caveatSpecifications: getDefaultCaveatSpecifications(), messenger: getDefaultRestrictedMessenger(), @@ -252,7 +259,7 @@ function getDefaultPermissionController() { DefaultTargetKeys, DefaultCaveats, DefaultPermissions - >(getControllerOpts()); + >(getControllerOptions()); } function getDefaultPermissionControllerWithState() { @@ -260,7 +267,7 @@ function getDefaultPermissionControllerWithState() { DefaultTargetKeys, DefaultCaveats, DefaultPermissions - >(getControllerOpts({ state: getExistingPermissionState() })); + >(getControllerOptions({ state: getExistingPermissionState() })); } describe('PermissionController', () => { @@ -290,7 +297,7 @@ describe('PermissionController', () => { DefaultCaveats, DefaultPermissions >( - getControllerOpts({ + getControllerOptions({ permissionSpecifications: { ...getDefaultPermissionSpecifications(), '': { target: '' }, @@ -306,7 +313,7 @@ describe('PermissionController', () => { DefaultCaveats, DefaultPermissions >( - getControllerOpts({ + getControllerOptions({ permissionSpecifications: { ...getDefaultPermissionSpecifications(), foo_: { target: 'foo_' }, @@ -322,7 +329,7 @@ describe('PermissionController', () => { DefaultCaveats, DefaultPermissions >( - getControllerOpts({ + getControllerOptions({ permissionSpecifications: { ...getDefaultPermissionSpecifications(), 'foo*': { target: 'foo*' }, @@ -338,7 +345,7 @@ describe('PermissionController', () => { DefaultCaveats, DefaultPermissions >( - getControllerOpts({ + getControllerOptions({ permissionSpecifications: { ...getDefaultPermissionSpecifications(), foo: { target: 'bar' }, @@ -1832,7 +1839,85 @@ describe('PermissionController', () => { }); describe('actions', () => { - it.todo('controller actions'); + it('permissionController:clearPermissions', () => { + const options = getControllerOptions(); + const { messenger } = options; + const controller = new PermissionController< + DefaultTargetKeys, + DefaultCaveats, + DefaultPermissions + >(options); + const clearStateSpy = jest.spyOn(controller, 'clearState'); + + controller.grantPermissions({ + subject: { origin: 'foo' }, + approvedPermissions: { + wallet_getSecretArray: {}, + }, + }); + + expect(hasProperty(controller.state.subjects, 'foo')).toStrictEqual(true); + + messenger.call('PermissionController:clearPermissions'); + expect(clearStateSpy).toHaveBeenCalledTimes(1); + expect(controller.state).toStrictEqual({ subjects: {} }); + }); + + it('permissionController:getSubjectNames', () => { + const options = getControllerOptions(); + const { messenger } = options; + const controller = new PermissionController< + DefaultTargetKeys, + DefaultCaveats, + DefaultPermissions + >(options); + const getSubjectNamesSpy = jest.spyOn(controller, 'getSubjectNames'); + + expect( + messenger.call('PermissionController:getSubjectNames'), + ).toStrictEqual([]); + + controller.grantPermissions({ + subject: { origin: 'foo' }, + approvedPermissions: { + wallet_getSecretArray: {}, + }, + }); + + expect( + messenger.call('PermissionController:getSubjectNames'), + ).toStrictEqual(['foo']); + expect(getSubjectNamesSpy).toHaveBeenCalledTimes(2); + }); + + it('permissionController:hasPermissions', () => { + const options = getControllerOptions(); + const { messenger } = options; + const controller = new PermissionController< + DefaultTargetKeys, + DefaultCaveats, + DefaultPermissions + >(options); + const hasPermissionsSpy = jest.spyOn(controller, 'hasPermissions'); + + expect( + messenger.call('PermissionController:hasPermissions', 'foo'), + ).toStrictEqual(false); + + controller.grantPermissions({ + subject: { origin: 'foo' }, + approvedPermissions: { + wallet_getSecretArray: {}, + }, + }); + + expect( + messenger.call('PermissionController:hasPermissions', 'foo'), + ).toStrictEqual(true); + expect(hasPermissionsSpy).toHaveBeenCalledTimes(2); + expect(hasPermissionsSpy).toHaveBeenNthCalledWith(1, 'foo'); + expect(hasPermissionsSpy).toHaveBeenNthCalledWith(2, 'foo'); + }); }); }); From fdda9279a48d9c903bdf6f4498dc4693d6fd4685 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Fri, 1 Oct 2021 15:46:23 -0700 Subject: [PATCH 062/147] Remove some unused errors --- .../controllers/src/permissions/errors.ts | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/packages/controllers/src/permissions/errors.ts b/packages/controllers/src/permissions/errors.ts index a600d50685..8d31371575 100644 --- a/packages/controllers/src/permissions/errors.ts +++ b/packages/controllers/src/permissions/errors.ts @@ -93,15 +93,6 @@ export class CaveatAlreadyExistsError extends Error { } } -export class InvalidPermissionJsonError extends Error { - public data: { origin: string; permission: GenericPermission }; - - constructor(origin: string, permission: GenericPermission) { - super(`Permission object of subject "${origin}" is not valid JSON.`); - this.data = { origin, permission }; - } -} - export class InvalidCaveatError extends EthereumRpcError { public data: { origin: string; target: string }; @@ -168,20 +159,3 @@ export class InvalidCaveatFieldsError extends Error { this.data = { caveat, origin, target }; } } - -export class InvalidCaveatJsonError extends Error { - public data: { - caveat: CaveatConstraint; - origin: string; - target: string; - }; - - constructor( - caveat: CaveatConstraint, - origin: string, - target: string, - ) { - super(`Caveat object is not valid JSON.`); - this.data = { caveat, origin, target }; - } -} From a22697e315ef09ef33b758cca2e099f406d9029d Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Fri, 1 Oct 2021 16:08:45 -0700 Subject: [PATCH 063/147] fixup! Remove some unused errors --- .../permissions/PermissionController.test.ts | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 38e235aa11..b5d974f675 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -270,6 +270,20 @@ function getDefaultPermissionControllerWithState() { >(getControllerOptions({ state: getExistingPermissionState() })); } +function getPermissionMatcher( + parentCapability: string, + caveats: GenericCaveat[] | null | typeof expect.objectContaining = null, + invoker = 'metamask.io', +) { + return expect.objectContaining({ + id: expect.any(String), + parentCapability, + invoker, + caveats, + date: expect.any(Number), + }); +} + describe('PermissionController', () => { describe('constructor', () => { it('initializes a new PermissionController', () => { @@ -1920,19 +1934,3 @@ describe('PermissionController', () => { }); }); }); - -// Testing utilities - -function getPermissionMatcher( - parentCapability: string, - caveats: GenericCaveat[] | null | typeof expect.objectContaining = null, - invoker = 'metamask.io', -) { - return expect.objectContaining({ - id: expect.any(String), - parentCapability, - invoker, - caveats, - date: expect.any(Number), - }); -} From a4fa33ddc6e3b9c5291b0b3c11e64e739a9a9451 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sat, 2 Oct 2021 15:25:53 -0700 Subject: [PATCH 064/147] Add test helper docstrings, more test cases --- .../permissions/PermissionController.test.ts | 232 +++++++++++++++--- .../controllers/src/permissions/errors.ts | 2 - 2 files changed, 198 insertions(+), 36 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index b5d974f675..98071c5133 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -42,6 +42,16 @@ type NoopCaveat = CaveatConstraint; type DefaultCaveats = FilterArrayCaveat | FilterObjectCaveat | NoopCaveat; +/** + * Gets caveat specifications for: + * - {@link FilterArrayCaveat} + * - {@link FilterObjectCaveat} + * - {@link NoopCaveat} + * + * Used as a default in {@link getControllerOptions}. + * + * @returns The caveat specifications. + */ function getDefaultCaveatSpecifications(): CaveatSpecifications { return { filterArrayResponse: { @@ -88,7 +98,7 @@ function getDefaultCaveatSpecifications(): CaveatSpecifications }, validator: (caveat: { type: CaveatTypes.noopCaveat; value: unknown }) => { if (caveat.value !== null) { - throw new Error('Caveat value must be null'); + throw new Error('NoopCaveat value must be null'); } }, }, @@ -134,12 +144,30 @@ enum PermissionKeys { 'wallet_getSecret_*' = 'wallet_getSecret_*', } +/** + * Permission name (as opposed to keys) enum. Since one of the permissions are + * namespaced, it's a getter function. + */ const PermissionNames = { wallet_getSecretArray: 'wallet_getSecretArray' as const, wallet_getSecretObject: 'wallet_getSecretObject' as const, wallet_getSecret_: (str: string) => `wallet_getSecret_${str}` as const, }; +/** + * Gets permission specifications for: + * - {@link SecretArrayPermission} + * - Has neither validator nor factory. + * - {@link SecretObjectPermission} + * - Has validator, but no factory. + * - {@link SecretNamespacedPermission} + * - Has both validator and factory. + * + * + * Used as a default in {@link getControllerOptions}. + * + * @returns The permission specifications. + */ function getDefaultPermissionSpecifications(): PermissionSpecifications< DefaultTargetKeys, DefaultPermissions @@ -162,7 +190,7 @@ function getDefaultPermissionSpecifications(): PermissionSpecifications< !permission.caveats?.some( (caveat) => caveat.type === CaveatTypes.filterArrayResponse, ), - 'permission validation failed', + 'getSecretObject permission validation failed', ); }, }, @@ -180,14 +208,10 @@ function getDefaultPermissionSpecifications(): PermissionSpecifications< caveats: [constructCaveat(CaveatTypes.noopCaveat, null)], }) as SecretNamespacedPermission, validator: (permission: GenericPermission) => { - if (!permission.parentCapability.startsWith('wallet_getSecret_')) { - throw new Error('invalid parentCapability'); - } - assert.deepStrictEqual( permission.caveats, [constructCaveat(CaveatTypes.noopCaveat, null)], - 'permission validation failed', + 'getSecret_* permission validation failed', ); }, }, @@ -198,6 +222,13 @@ function getDefaultPermissionSpecifications(): PermissionSpecifications< const controllerName = 'PermissionController' as const; +/** * + * Gets a restricted controller messenger. + * + * Used as a default in {@link getControllerOptions}. + * + * @returns The restricted messenger. + */ function getDefaultRestrictedMessenger() { const controllerMessenger = new ControllerMessenger< PermissionControllerActions, @@ -217,12 +248,19 @@ function getDefaultRestrictedMessenger() { }); } +/** + * Gets the default unrestricted methods array. + * + * Used as a default in {@link getControllerOptions}. + */ function getDefaultUnrestrictedMethods() { return ['wallet_unrestrictedMethod']; } /** - * Useful for populating a controller with some existing permissions. + * Gets some existing state to populate the permission controller with. + * There is one subject, "metamask.io", with one permission, + * "wallet_getSecretArray", with no caveats. */ function getExistingPermissionState() { return { @@ -243,6 +281,20 @@ function getExistingPermissionState() { }; } +/** + * Gets constructor options for the permission controller. Returns defaults + * that can be overwritten by passing in replacement options. + * + * The following defaults are used: + * - `caveatSpecifications`: {@link getDefaultCaveatSpecifications} + * - `messenger`: {@link getDefaultRestrictedMessenger} + * - `permissionSpecifications`: {@link getDefaultPermissionSpecifications} + * - `unrestrictedMethods`: {@link getDefaultUnrestrictedMethods} + * - `state`: `undefined` + * + * @param opts - Permission controller options. + * @returns - The permission controller constructor options. + */ function getControllerOptions(opts?: Record) { return { caveatSpecifications: getDefaultCaveatSpecifications(), @@ -254,6 +306,14 @@ function getControllerOptions(opts?: Record) { }; } +/** + * Gets a "default" permission controller. This simply means a controller using + * the default caveat and permissions created in this test file. + * + * For the options used, see {@link getControllerOptions}. + * + * @returns The default permission controller for testing. + */ function getDefaultPermissionController() { return new PermissionController< DefaultTargetKeys, @@ -262,6 +322,14 @@ function getDefaultPermissionController() { >(getControllerOptions()); } +/** + * Gets an equivalent controller to the one returned by + * {@link getDefaultPermissionController}, except it's initialized with the + * state returned by {@link getExistingPermissionState}. + * + * @returns The default permission controller for testing, with some initial + * state. + */ function getDefaultPermissionControllerWithState() { return new PermissionController< DefaultTargetKeys, @@ -270,6 +338,15 @@ function getDefaultPermissionControllerWithState() { >(getControllerOptions({ state: getExistingPermissionState() })); } +/** + * Gets a Jest matcher for a permission as they are stored in controller state. + * + * @param parentCapability - The `parentCapability` of the permission. + * @param caveats - The caveat array of the permission, or `null`. + * @param invoker - The subject identifier (i.e. origin) of the subject. + * @returns A Jest matcher that matches permissions whose corresponding fields + * correspond to the parameters of this function. + */ function getPermissionMatcher( parentCapability: string, caveats: GenericCaveat[] | null | typeof expect.objectContaining = null, @@ -1137,31 +1214,6 @@ describe('PermissionController', () => { }); }); - it('throws an error if the permission fails to validate with the new caveat', () => { - const controller = getDefaultPermissionController(); - const origin = 'metamask.io'; - - controller.grantPermissions({ - subject: { origin }, - approvedPermissions: { - [PermissionNames.wallet_getSecretObject]: { - caveats: [ - constructCaveat(CaveatTypes.filterObjectResponse, ['foo']), - ], - }, - }, - }); - - expect(() => - controller.addCaveat( - origin, - PermissionNames.wallet_getSecretObject, - CaveatTypes.filterArrayResponse as any, - ['foo'], - ), - ).toThrow(new Error('permission validation failed')); - }); - it('throws an error if a corresponding caveat already exists', () => { const controller = getDefaultPermissionController(); const origin = 'metamask.io'; @@ -1211,6 +1263,31 @@ describe('PermissionController', () => { ), ); }); + + it('throws an error if the permission fails to validate with the added caveat', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecretObject]: { + caveats: [ + constructCaveat(CaveatTypes.filterObjectResponse, ['foo']), + ], + }, + }, + }); + + expect(() => + controller.addCaveat( + origin, + PermissionNames.wallet_getSecretObject, + CaveatTypes.filterArrayResponse as any, + ['foo'], + ), + ).toThrow(new Error('getSecretObject permission validation failed')); + }); }); describe('updateCaveat', () => { @@ -1312,6 +1389,29 @@ describe('PermissionController', () => { ), ); }); + + it('throws an error if the updated caveat fails to validate', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecret_('foo')]: { + caveats: [constructCaveat(CaveatTypes.noopCaveat, null)], + }, + }, + }); + + expect(() => + controller.updateCaveat( + origin, + PermissionNames.wallet_getSecret_('foo'), + CaveatTypes.noopCaveat, + 'bar' as any, + ), + ).toThrow(new Error('NoopCaveat value must be null')); + }); }); describe('removeCaveat', () => { @@ -1485,6 +1585,28 @@ describe('PermissionController', () => { ), ); }); + + it('throws an error if the permission fails to validate after caveat removal', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecret_('foo')]: { + caveats: [constructCaveat(CaveatTypes.noopCaveat, null)], + }, + }, + }); + + expect(() => + controller.removeCaveat( + origin, + PermissionNames.wallet_getSecret_('foo'), + CaveatTypes.noopCaveat, + ), + ).toThrow(new Error('getSecret_* permission validation failed')); + }); }); describe('grantPermissions', () => { @@ -1850,9 +1972,51 @@ describe('PermissionController', () => { ), ); }); + + it('throws if caveat validation fails', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + expect(() => + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecret_('foo')]: { + caveats: [ + { + type: CaveatTypes.noopCaveat, + value: 'bar', + }, + ] as any, + }, + }, + }), + ).toThrow(new Error('NoopCaveat value must be null')); + }); + + it('throws if permission validation fails', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + expect(() => + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + wallet_getSecretObject: { + caveats: [ + { + type: CaveatTypes.filterArrayResponse, + value: ['bar'], + }, + ] as any, + }, + }, + }), + ).toThrow(new Error('getSecretObject permission validation failed')); + }); }); - describe('actions', () => { + describe('controller actions', () => { it('permissionController:clearPermissions', () => { const options = getControllerOptions(); const { messenger } = options; diff --git a/packages/controllers/src/permissions/errors.ts b/packages/controllers/src/permissions/errors.ts index 8d31371575..e87cb47366 100644 --- a/packages/controllers/src/permissions/errors.ts +++ b/packages/controllers/src/permissions/errors.ts @@ -1,7 +1,5 @@ import { JsonRpcRequest } from 'json-rpc-engine'; import { errorCodes, ethErrors, EthereumRpcError } from 'eth-rpc-errors'; -import { GenericPermission } from './Permission'; -import { CaveatConstraint } from './Caveat'; type ErrorArg = { message?: string; From 832695335941903e74527a3f26b1e74bd0d3bec0 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 4 Oct 2021 10:52:36 -0700 Subject: [PATCH 065/147] Add additional runtime validation to grantPermissions --- .../permissions/PermissionController.test.ts | 77 ++++++++++++++++++- .../src/permissions/PermissionController.ts | 18 ++++- .../controllers/src/permissions/errors.ts | 19 +++++ 3 files changed, 111 insertions(+), 3 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 98071c5133..8201965f53 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -1610,7 +1610,7 @@ describe('PermissionController', () => { }); describe('grantPermissions', () => { - it('grants new permissions', () => { + it('grants new permission', () => { const controller = getDefaultPermissionController(); const origin = 'metamask.io'; @@ -1635,6 +1635,37 @@ describe('PermissionController', () => { }); }); + it('grants new permissions (multiple at once)', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + wallet_getSecretArray: {}, + wallet_getSecretObject: { + parentCapability: 'wallet_getSecretObject', + }, + }, + }); + + expect(controller.state).toStrictEqual({ + subjects: { + [origin]: { + origin, + permissions: { + wallet_getSecretArray: getPermissionMatcher( + 'wallet_getSecretArray', + ), + wallet_getSecretObject: getPermissionMatcher( + 'wallet_getSecretObject', + ), + }, + }, + }, + }); + }); + it('grants new permissions (namespaced, with factory and validator)', () => { const controller = getDefaultPermissionController(); const origin = 'metamask.io'; @@ -1822,6 +1853,50 @@ describe('PermissionController', () => { ).toThrow(errors.methodNotFound({ method: 'wallet_getSecretFalafel' })); }); + it('throws if an approved permission is malformed', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + expect(() => + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + wallet_getSecretArray: { + // This must match the key + parentCapability: 'wallet_getSecretObject', + }, + }, + }), + ).toThrow( + new errors.InvalidApprovedPermissionError( + origin, + 'wallet_getSecretArray', + { parentCapability: 'wallet_getSecretObject' }, + ), + ); + + expect(() => + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + wallet_getSecretArray: { + parentCapability: 'wallet_getSecretArray', + }, + wallet_getSecretObject: { + // This must match the key + parentCapability: 'wallet_getSecretArray', + }, + }, + }), + ).toThrow( + new errors.InvalidApprovedPermissionError( + origin, + 'wallet_getSecretObject', + { parentCapability: 'wallet_getSecretArray' }, + ), + ); + }); + it('throws if a requested caveat is not a plain object', () => { const controller = getDefaultPermissionController(); const origin = 'metamask.io'; diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 875f642492..a98bd451f7 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -44,6 +44,7 @@ import { InvalidCaveatTypeError, CaveatAlreadyExistsError, InvalidCaveatError, + InvalidApprovedPermissionError, } from './errors'; import { PermissionEnforcer, PermissionsRequest } from './PermissionEnforcer'; import { MethodNames } from './enums'; @@ -968,7 +969,9 @@ export class PermissionController< }) || {}) as SubjectPermissions; - for (const requestedTarget of Object.keys(approvedPermissions)) { + for (const [requestedTarget, approvedPermission] of Object.entries( + approvedPermissions, + )) { const targetKey = this.getTargetKey( requestedTarget as Permission['parentCapability'], ); @@ -976,6 +979,17 @@ export class PermissionController< throw methodNotFound({ method: requestedTarget }); } + if ( + approvedPermission.parentCapability !== undefined && + requestedTarget !== approvedPermission.parentCapability + ) { + throw new InvalidApprovedPermissionError( + origin, + requestedTarget, + approvedPermission, + ); + } + // The requested target must be a valid target name if we found its key. // We reassign it to change its type. const targetName = requestedTarget as Permission['parentCapability']; @@ -984,7 +998,7 @@ export class PermissionController< const caveats = this.constructCaveats( origin, targetName, - approvedPermissions[targetName].caveats, + approvedPermission.caveats, ); const permissionOptions = { diff --git a/packages/controllers/src/permissions/errors.ts b/packages/controllers/src/permissions/errors.ts index e87cb47366..8129fe7256 100644 --- a/packages/controllers/src/permissions/errors.ts +++ b/packages/controllers/src/permissions/errors.ts @@ -63,6 +63,25 @@ export class UnrecognizedSubjectError extends Error { } } +export class InvalidApprovedPermissionError extends Error { + public data: { + origin: string; + target: string; + approvedPermission: Record; + }; + + constructor( + origin: string, + target: string, + approvedPermission: Record, + ) { + super( + `Invalid approved permission for origin "${origin}" and target "${target}".`, + ); + this.data = { origin, target, approvedPermission }; + } +} + export class PermissionDoesNotExistError extends Error { constructor(origin: string, target: string) { super(`Subject "${origin}" has no permission for "${target}".`); From 74e2fb78fbc965977c58ed496c7e041e4c62735a Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 4 Oct 2021 23:07:47 -0700 Subject: [PATCH 066/147] Consolidate enforcement in PermissionController --- .../permissions/PermissionController.test.ts | 20 +- .../src/permissions/PermissionController.ts | 406 ++++++++++++++--- .../permissions/PermissionEnforcer.test.ts | 7 - .../src/permissions/PermissionEnforcer.ts | 419 ------------------ .../controllers/src/permissions/README.md | 2 +- packages/controllers/src/permissions/index.ts | 2 +- .../permissions/permission-middleware.test.ts | 15 + .../src/permissions/permission-middleware.ts | 87 ++++ 8 files changed, 456 insertions(+), 502 deletions(-) delete mode 100644 packages/controllers/src/permissions/PermissionEnforcer.test.ts delete mode 100644 packages/controllers/src/permissions/PermissionEnforcer.ts create mode 100644 packages/controllers/src/permissions/permission-middleware.test.ts create mode 100644 packages/controllers/src/permissions/permission-middleware.ts diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 8201965f53..7a595fb006 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -14,7 +14,6 @@ import { PermissionController, PermissionControllerActions, PermissionControllerEvents, - PermissionEnforcer, PermissionOptions, PermissionSpecifications, RestrictedMethodArgs, @@ -366,9 +365,6 @@ describe('PermissionController', () => { it('initializes a new PermissionController', () => { const controller = getDefaultPermissionController(); expect(controller.state).toStrictEqual({ subjects: {} }); - expect(controller.enforcer instanceof PermissionEnforcer).toStrictEqual( - true, - ); expect(controller.unrestrictedMethods).toStrictEqual( new Set(getDefaultUnrestrictedMethods()), @@ -2091,6 +2087,22 @@ describe('PermissionController', () => { }); }); + describe('requestPermissions', () => { + it.todo('requests the permissions'); + }); + + describe('acceptPermissionsRequest', () => { + it.todo('accepts a permissions request'); + }); + + describe('rejectPermissionsRequest', () => { + it.todo('rejects a permissions request'); + }); + + describe('executeRestrictedMethod', () => { + it.todo('executes a restricted method'); + }); + describe('controller actions', () => { it('permissionController:clearPermissions', () => { const options = getControllerOptions(); diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index a98bd451f7..6bdedd8164 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -1,4 +1,5 @@ import { + Json, BaseControllerV2 as BaseController, RestrictedControllerMessenger, AddApprovalRequest, @@ -8,13 +9,13 @@ import { } from '@metamask/controllers'; import type { Patch } from 'immer'; import deepFreeze from 'deep-freeze-strict'; -import { Json } from 'json-rpc-engine'; - +import { nanoid } from 'nanoid'; import { isPlainObject, hasProperty, NonEmptyArray } from '../utils'; import { CaveatConstraint, CaveatSpecifications, constructCaveat as _constructCaveat, + decorateWithCaveats, GenericCaveat, GetCaveatFromType, } from './Caveat'; @@ -45,9 +46,13 @@ import { CaveatAlreadyExistsError, InvalidCaveatError, InvalidApprovedPermissionError, + unauthorized, + invalidParams, + internalError, + userRejectedRequest, } from './errors'; -import { PermissionEnforcer, PermissionsRequest } from './PermissionEnforcer'; import { MethodNames } from './enums'; +import { getPermissionMiddlewareFactory } from './permission-middleware'; /** * Metadata associated with {@link PermissionController} subjects. @@ -56,6 +61,27 @@ export type PermissionSubjectMetadata = { origin: OriginString; }; +/** + * Metadata associated with permission requests. + */ +export type PermissionsRequestMetadata = PermissionSubjectMetadata & { + id: string; +}; + +/** + * Used for prompting the user about a proposed new permission. + * Includes information about the grantee subject, requested permissions, and + * any additional information added by the consumer. + * + * All properties except `permissions` are passed to any factories found for + * the requested permissions. + */ +export type PermissionsRequest = { + metadata: PermissionsRequestMetadata; + permissions: RequestedPermissions; + [key: string]: Json; +}; + /** * The name of the {@link PermissionController}. */ @@ -270,19 +296,6 @@ export class PermissionController< return this._caveatSpecifications; } - private readonly _enforcer: Readonly< - PermissionEnforcer - >; - - /** - * The {@link PermissionEnforcer} of the controller. - */ - public get enforcer(): Readonly< - PermissionEnforcer - > { - return this._enforcer; - } - private readonly _permissionSpecifications: Readonly< PermissionSpecifications >; @@ -305,6 +318,18 @@ export class PermissionController< return this._unrestrictedMethods; } + /** + * Returns a `json-rpc-engine` middleware function factory, so that the rules + * described by the state of this controller can be applied to incoming + * JSON-RPC requests. + * + * The middleware **must** be added in the correct place in the middleware + * stack in order for it to work. See the documentation for more details. + */ + public createPermissionMiddleware: ReturnType< + typeof getPermissionMiddlewareFactory + >; + /** * @param options - Permission controller options. * @param options.caveatSpecifications - The specifications of all caveats @@ -349,7 +374,12 @@ export class PermissionController< }); this.registerMessageHandlers(); - this._enforcer = this.constructEnforcer(); + this.createPermissionMiddleware = getPermissionMiddlewareFactory({ + executeRestrictedMethod: this._executeRestrictedMethod.bind(this), + getRestrictedMethod: this._getRestrictedMethod.bind(this), + isUnrestrictedMethod: (methodName: string) => + this.unrestrictedMethods.has(methodName), + }); } /** @@ -419,9 +449,11 @@ export class PermissionController< * @returns The restricted method implementation. */ getRestrictedMethod( - method: Permission['parentCapability'], + method: string, ): RestrictedMethod | undefined { - const targetKey = this.getTargetKey(method); + const targetKey = this.getTargetKey( + method as Permission['parentCapability'], + ); if (!targetKey) { return undefined; } @@ -937,10 +969,14 @@ export class PermissionController< } /** - * Grants approved permissions to the specified subject. Every permission and + * Grants _approved_ permissions to the specified subject. Every permission and * caveat is stringently validated – including by calling every specification * validator – and an error is thrown if any validation fails. * + * **ATTN:** This method does **not** prompt the user for approval. + * + * @see {@link PermissionController.requestPermissions} For initiating a + * permissions request requiring user approval. * @param options - Options bag. * @param options.approvedPermissions - The requested permissions approved by * the user. @@ -1124,61 +1160,291 @@ export class PermissionController< } /** - * Constructor helper for getting the enforcer of this controller. + * TODO * - * @returns The {@link PermissionEnforcer}. + * @param origin - The origin of the grantee subject. + * @param requestedPermissions - The requested permissions. + * @param id - The id of the permissions request. + * @param preserveExistingPermissions - Whether to preserve the subject's + * existing permissions. + * @returns The granted permissions and request metadata. */ - private constructEnforcer(): PermissionEnforcer< - TargetKey, - Caveat, - Permission - > { - return new PermissionEnforcer({ - caveatSpecifications: this.caveatSpecifications, - getPermission: this.getPermission.bind(this), - getRestrictedMethod: this.getRestrictedMethod.bind(this), - grantPermissions: this.grantPermissions.bind(this), - isRestrictedMethod: (method: string) => { - return Boolean( - this.getTargetKey(method as Permission['parentCapability']), - ); - }, - isUnrestrictedMethod: (method: string) => { - return this.unrestrictedMethods.has(method); - }, - requestUserApproval: async (permissionsRequest: PermissionsRequest) => { - const { origin, id } = permissionsRequest.metadata; - return (await this.messagingSystem.call( - 'ApprovalController:addRequest', - { - id, - origin, - requestData: permissionsRequest, - type: MethodNames.requestPermissions, - }, - true, - )) as PermissionsRequest; + async requestPermissions( + origin: string, + requestedPermissions: RequestedPermissions, + id: string = nanoid(), + preserveExistingPermissions = true, + ): Promise<[Record, { id: string; origin: string }]> { + this.validateRequestedPermissions(origin, requestedPermissions); + const metadata: PermissionsRequest['metadata'] = { + id, + origin, + }; + + const permissionsRequest: PermissionsRequest = { + metadata, + permissions: requestedPermissions, + }; + + const { permissions: approvedPermissions, ...requestData } = + await this.requestUserApproval(permissionsRequest); + if (Object.keys(approvedPermissions).length === 0) { + // The request should already have been rejected if this is the case. + throw internalError( + `Approved permissions request for origin "${origin}" contains no permissions.`, + { requestedPermissions }, + ); + } + + return [ + this.grantPermissions({ + subject: { origin }, + approvedPermissions, + preserveExistingPermissions, + requestData, + }), + metadata, + ]; + } + + /** + * TODO + * + * @param permissionsRequest - The permissions request object. + * @returns The approved permissions request object. + */ + private async requestUserApproval(permissionsRequest: PermissionsRequest) { + const { origin, id } = permissionsRequest.metadata; + return (await this.messagingSystem.call( + 'ApprovalController:addRequest', + { + id, + origin, + requestData: permissionsRequest, + type: MethodNames.requestPermissions, }, - acceptPermissionsRequest: (id: string, value?: unknown) => - this.messagingSystem.call( - 'ApprovalController:acceptRequest', + true, + )) as PermissionsRequest; + } + + /** + * TODO + * + * @param origin - The origin of the grantee subject. + * @param requestedPermissions - The requested permissions. + */ + private validateRequestedPermissions( + origin: string, + requestedPermissions: RequestedPermissions, + ) { + if ( + !requestedPermissions || + typeof requestedPermissions !== 'object' || + Array.isArray(requestedPermissions) + ) { + throw invalidParams({ data: { origin, requestedPermissions } }); + } + + const perms: RequestedPermissions = requestedPermissions; + + for (const methodName of Object.keys(perms)) { + const target = (perms[methodName] as unknown as Permission) + .parentCapability; + if (target !== undefined && methodName !== target) { + throw invalidParams({ data: { origin, requestedPermissions } }); + } + + if (!this.getRestrictedMethod(methodName)) { + throw methodNotFound({ + method: methodName, + data: { origin, requestedPermissions }, + }); + } + } + } + + /** + * TODO + * + * @param request - The permissions request. + */ + async acceptPermissionsRequest(request: PermissionsRequest): Promise { + const { id } = request.metadata; + + if (!this.hasApprovalRequest({ id })) { + console.debug(`Permissions request with id "${id}" not found.`); + return; + } + + try { + if (Object.keys(request.permissions).length === 0) { + this._rejectPermissionsRequest( id, - value, - ), - rejectPermissionsRequest: (id: string, error: Error) => + invalidParams({ + message: 'Must request at least one permission.', + }), + ); + } else { this.messagingSystem.call( - 'ApprovalController:rejectRequest', + 'ApprovalController:acceptRequest', id, - error, - ), - hasApprovalRequest: (opts: { - id?: string; - origin?: OriginString; - type?: string; - }) => - // Typecast: There are only some valid parameter combinations, but we - // just won't worry about that here. - this.messagingSystem.call('ApprovalController:hasRequest', opts as any), - }); + request, + ); + } + } catch (err) { + // if finalization fails, reject the request + this._rejectPermissionsRequest(id, err as Error); + } + } + + /** + * TODO + * + * @param id - The id of the request to be rejected. + */ + async rejectPermissionsRequest(id: string): Promise { + if (!this.hasApprovalRequest({ id })) { + console.debug(`Permissions request with id '${id}' not found.`); + return; + } + + this._rejectPermissionsRequest(id, userRejectedRequest()); + } + + /** + * TODO + * + * @param options - The {@link HasApprovalRequest} options. + * @returns Whether the specified request exists. + */ + private hasApprovalRequest(options: { id: string }) { + // Typecast: There are only some valid parameter combinations, but we + // just won't worry about that here. + return this.messagingSystem.call( + 'ApprovalController:hasRequest', + // Typecast: Passing just an id is definitely valid, so I don't know + // what's going on here. + options as any, + ); + } + + /** + * TODO + * + * @param id - The id of the request to reject. + * @param error - The error associated with the rejection. + */ + private _rejectPermissionsRequest(id: string, error: Error): void { + return this.messagingSystem.call( + 'ApprovalController:rejectRequest', + id, + error, + ); + } + + /** + * TODO + * + * @param origin - The origin of the subject to execute the method on behalf + * of. + * @param methodName - The name of the method to execute. + * @param params - The parameters to pass to the method implementation. + * @returns The result of the executed method. + */ + async executeRestrictedMethod( + origin: string, + methodName: Permission['parentCapability'], + params?: Json, + ): Promise { + // Throws if the method does not exist + const methodImplementation = this._getRestrictedMethod(methodName, origin); + + const result = await this._executeRestrictedMethod( + methodImplementation, + { origin }, + methodName, + params, + ); + + if (resultIsError(result)) { + throw result; + } + + if (result === undefined) { + throw new Error( + `Internal request for method "${methodName}" as origin "${origin}" has no result.`, + ); + } + + return result; + } + + /** + * TODO + * + * @param method - The name of the method to get. + * @param request - The request associated with the request for the method. + * @returns - The restricted method implementation, if it exists. + */ + private _getRestrictedMethod( + method: string, + origin: string, + ): RestrictedMethod { + const methodImplementation = this.getRestrictedMethod( + method as Permission['parentCapability'], + ); + if (!methodImplementation) { + throw methodNotFound({ method, data: { origin } }); + } + + return methodImplementation; } + + /** + * TODO + * + * @param methodImplementation - The implementation of the method to call. + * @param subject - Metadata about the subject that made the request. + * @param req - The request object associated with the request. + * @returns + */ + private _executeRestrictedMethod( + methodImplementation: RestrictedMethod, + subject: PermissionSubjectMetadata, + method: Permission['parentCapability'], + params: Json = [], + ): ReturnType> { + const { origin } = subject; + + const permission = this.getPermission(origin, method); + if (!permission) { + return unauthorized({ data: { origin, method } }); + } + + return decorateWithCaveats( + methodImplementation, + permission, + this.caveatSpecifications, + )({ method, params, context: { origin } }); + } +} + +/** + * Type guard for checking whether the value returned from a restricted method + * invocation is a result or an error. + * + * @param resultOrError - The value returned from a restricted method + * invocation. + * @returns Whether the value is an {@link Error}. + */ +export function resultIsError( + resultOrError: Json | Error, +): resultOrError is Error { + return Boolean( + resultOrError instanceof Error || + (resultOrError && + isPlainObject(resultOrError) && + hasProperty(resultOrError, 'message') && + typeof resultOrError.message === 'string'), + ); } diff --git a/packages/controllers/src/permissions/PermissionEnforcer.test.ts b/packages/controllers/src/permissions/PermissionEnforcer.test.ts deleted file mode 100644 index 49e5b53c91..0000000000 --- a/packages/controllers/src/permissions/PermissionEnforcer.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable import/unambiguous */ - -describe('PermissionEnforcer', () => { - describe('executeRestrictedMethod', () => { - it.todo('can execute restricted methods'); - }); -}); diff --git a/packages/controllers/src/permissions/PermissionEnforcer.ts b/packages/controllers/src/permissions/PermissionEnforcer.ts deleted file mode 100644 index 17a5b8dc35..0000000000 --- a/packages/controllers/src/permissions/PermissionEnforcer.ts +++ /dev/null @@ -1,419 +0,0 @@ -import type { ApprovalController } from '@metamask/controllers'; -import { - AsyncJsonRpcEngineNextCallback, - createAsyncMiddleware, - Json, - JsonRpcRequest, - JsonRpcMiddleware, - PendingJsonRpcResponse, -} from 'json-rpc-engine'; -import { nanoid } from 'nanoid'; - -import { isPlainObject, hasProperty } from '../utils'; -import { - CaveatSpecifications, - decorateWithCaveats, - GenericCaveat, -} from './Caveat'; -import { - internalError, - invalidParams, - methodNotFound, - unauthorized, - userRejectedRequest, -} from './errors'; -import type { - PermissionConstraint, - RequestedPermissions, - RestrictedMethod, -} from './Permission'; -import type { - PermissionController, - PermissionSubjectMetadata, -} from './PermissionController'; - -export type PermissionsRequestMetadata = PermissionSubjectMetadata & { - id: string; -}; - -/** - * Used for prompting the user about a proposed new permission. - * Includes information about the subject granted, as well as the permissions - * assigned. - */ -export type PermissionsRequest = { - metadata: PermissionsRequestMetadata; - permissions: RequestedPermissions; - [key: string]: Json; -}; - -type IsRestrictedMethod = (method: string) => boolean; -type IsUnrestrictedMethod = (method: string) => boolean; -type RequestUserApproval = ( - permissionsRequest: PermissionsRequest, -) => Promise; - -type PermissionEnforcerArgs< - TargetKey extends string, - Caveat extends GenericCaveat, - Permission extends PermissionConstraint, -> = { - caveatSpecifications: CaveatSpecifications; - isRestrictedMethod: IsRestrictedMethod; - isUnrestrictedMethod: IsUnrestrictedMethod; - getPermission: PermissionController< - TargetKey, - Caveat, - Permission - >['getPermission']; - getRestrictedMethod: PermissionController< - TargetKey, - Caveat, - Permission - >['getRestrictedMethod']; - grantPermissions: PermissionController< - TargetKey, - Caveat, - Permission - >['grantPermissions']; - requestUserApproval: RequestUserApproval; - acceptPermissionsRequest: ApprovalController['accept']; - rejectPermissionsRequest: ApprovalController['reject']; - hasApprovalRequest: ApprovalController['has']; -}; - -export class PermissionEnforcer< - TargetKey extends string, - Caveat extends GenericCaveat, - Permission extends PermissionConstraint, -> { - private caveatSpecifications: CaveatSpecifications; - - private isRestrictedMethod: IsRestrictedMethod; - - private isUnrestrictedMethod: IsUnrestrictedMethod; - - private getPermission: PermissionController< - TargetKey, - Caveat, - Permission - >['getPermission']; - - private grantPermissions: PermissionController< - TargetKey, - Caveat, - Permission - >['grantPermissions']; - - private requestUserApproval: RequestUserApproval; - - private _acceptPermissionsRequest: ApprovalController['accept']; - - private _rejectPermissionsRequest: ApprovalController['reject']; - - private _hasApprovalRequest: ApprovalController['has']; - - private _getRestrictedMethod: PermissionController< - TargetKey, - Caveat, - Permission - >['getRestrictedMethod']; - - constructor({ - caveatSpecifications, - isRestrictedMethod, - isUnrestrictedMethod, - getPermission, - getRestrictedMethod, - grantPermissions, - requestUserApproval, - acceptPermissionsRequest, - rejectPermissionsRequest, - hasApprovalRequest, - }: PermissionEnforcerArgs) { - this.caveatSpecifications = caveatSpecifications; - this.isRestrictedMethod = isRestrictedMethod; - this.isUnrestrictedMethod = isUnrestrictedMethod; - this.getPermission = getPermission; - this.grantPermissions = grantPermissions; - this.requestUserApproval = requestUserApproval; - this._acceptPermissionsRequest = acceptPermissionsRequest; - this._rejectPermissionsRequest = rejectPermissionsRequest; - this._hasApprovalRequest = hasApprovalRequest; - this._getRestrictedMethod = getRestrictedMethod; - } - - public async requestPermissions( - origin: string, - requestedPermissions: RequestedPermissions, - id: string = nanoid(), - ) { - return this._requestPermissions(origin, requestedPermissions, id, false); - } - - public async requestAndPreservePermissions( - origin: string, - requestedPermissions: RequestedPermissions, - id: string = nanoid(), - ) { - return this._requestPermissions(origin, requestedPermissions, id, true); - } - - private async _requestPermissions( - origin: string, - requestedPermissions: RequestedPermissions, - id: string = nanoid(), - preserveExistingPermissions = false, - ): Promise<[Record, { id: string; origin: string }]> { - this.validateRequestedPermissions(origin, requestedPermissions); - const metadata: PermissionsRequest['metadata'] = { - id, - origin, - }; - - const permissionsRequest: PermissionsRequest = { - metadata, - permissions: requestedPermissions, - }; - - const { permissions: approvedPermissions, ...requestData } = - await this.requestUserApproval(permissionsRequest); - if (Object.keys(approvedPermissions).length === 0) { - // The request should already have been rejected if this is the case. - throw internalError( - `Approved permissions request for origin "${origin}" contains no permissions.`, - { requestedPermissions }, - ); - } - - return [ - this.grantPermissions({ - subject: { origin }, - approvedPermissions, - preserveExistingPermissions, - requestData, - }), - metadata, - ]; - } - - private validateRequestedPermissions( - origin: string, - requestedPermissions: RequestedPermissions, - ) { - if ( - !requestedPermissions || - typeof requestedPermissions !== 'object' || - Array.isArray(requestedPermissions) - ) { - throw invalidParams({ data: { origin, requestedPermissions } }); - } - - const perms: RequestedPermissions = requestedPermissions; - - for (const methodName of Object.keys(perms)) { - const target = (perms[methodName] as unknown as Permission) - .parentCapability; - if (target !== undefined && methodName !== target) { - throw invalidParams({ data: { origin, requestedPermissions } }); - } - - if (!this.isRestrictedMethod(methodName)) { - throw methodNotFound({ - method: methodName, - data: { origin, requestedPermissions }, - }); - } - } - } - - /** - * User approval callback. Resolves the Promise for the permissions request - * waited upon by rpc-cap, see requestUserApproval in _initializePermissions. - * The request will be rejected if finalizePermissionsRequest fails. - * Idempotent for a given request id. - * - * @param approved - The request object approved by the user - * @param accounts - The accounts to expose, if any - */ - async acceptPermissionsRequest(request: PermissionsRequest) { - const { id } = request.metadata; - - if (!this._hasApprovalRequest({ id })) { - console.debug(`Permissions request with id '${id}' not found.`); - return; - } - - try { - if (Object.keys(request.permissions).length === 0) { - this._rejectPermissionsRequest( - id, - invalidParams({ - message: 'Must request at least one permission.', - }), - ); - } else { - this._acceptPermissionsRequest(id, request); - } - } catch (err) { - // if finalization fails, reject the request - this._rejectPermissionsRequest(id, err as Error); - } - } - - /** - * User rejection callback. Rejects the Promise for the permissions request - * waited upon by rpc-cap, see requestUserApproval in _initializePermissions. - * Idempotent for a given id. - * - * @param id - The id of the request rejected by the user - */ - async rejectPermissionsRequest(id: string) { - if (!this._hasApprovalRequest({ id })) { - console.debug(`Permissions request with id '${id}' not found.`); - return; - } - - this._rejectPermissionsRequest(id, userRejectedRequest()); - } - - async executeRestrictedMethod( - origin: string, - method: string, - params?: Json, - ): Promise { - const id = nanoid(); - const req: JsonRpcRequest = { id, jsonrpc: '2.0', method }; - if (params !== undefined) { - req.params = params; - } - - const methodImplementation = this.getRestrictedMethod(method, req); - - const result = await this._executeRestrictedMethod( - methodImplementation, - { origin }, - req, - ); - - if (resultIsError(result)) { - throw result; - } - - if (result === undefined) { - throw new Error( - `Internal request for method "${method}" as origin "${origin}" has no result.`, - ); - } - - return result; - } - - /** - * Get the implementation of the given method. - * - * @param method - The name of the method whose implementation to retrieve. - * @param request - The JSON-RPC request object calling the restricted method. - * @returns The restricted method implementation, or throws an error if no - * method exists. - */ - private getRestrictedMethod( - method: string, - request: JsonRpcRequest, - ): RestrictedMethod { - const methodImplementation = this._getRestrictedMethod( - method as Permission['parentCapability'], - ); - if (!methodImplementation) { - throw methodNotFound({ method, data: { request } }); - } - - return methodImplementation; - } - - /** - * If called as a result of an RPC request, the existence of the method - * should already be verified. - * - * @param methodImplementation - The implementation of the method to call. - * @param subject - Metadata about the subject that made the request. - * @param req - The request object associated with the request. - * @returns - */ - private _executeRestrictedMethod( - methodImplementation: RestrictedMethod, - subject: PermissionSubjectMetadata, - req: JsonRpcRequest, - ): ReturnType> { - const { origin } = subject; - const { method, params } = req; - - const permission = this.getPermission( - origin, - method as Permission['parentCapability'], - ); - if (!permission) { - return unauthorized({ data: { origin, method } }); - } - - return decorateWithCaveats( - methodImplementation, - permission, - this.caveatSpecifications, - )({ method, params, context: { origin } }); - } - - createPermissionMiddleware( - subject: PermissionSubjectMetadata, - ): JsonRpcMiddleware { - const permissionsMiddleware = async ( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - next: AsyncJsonRpcEngineNextCallback, - ): Promise => { - const { method } = req; - - // skip registered safe/passthrough methods. - if (this.isUnrestrictedMethod(method)) { - return next(); - } - - // This will throw if no restricted method implementation is found. - const methodImplementation = this.getRestrictedMethod(method, req); - - const result = await this._executeRestrictedMethod( - methodImplementation, - subject, - req, - ); - - if (resultIsError(result)) { - res.error = result; - return undefined; - } - - if (result === undefined) { - res.error = internalError( - `Request for method "${req.method}" returned undefined result.`, - { request: req }, - ); - return undefined; - } - - res.result = result; - return undefined; - }; - - return createAsyncMiddleware(permissionsMiddleware); - } -} - -function resultIsError(resultOrError: Json | Error): resultOrError is Error { - return Boolean( - resultOrError instanceof Error || - (resultOrError && - isPlainObject(resultOrError) && - hasProperty(resultOrError, 'message') && - hasProperty(resultOrError, 'code')), - ); -} diff --git a/packages/controllers/src/permissions/README.md b/packages/controllers/src/permissions/README.md index 7c27a504b1..b015b3a993 100644 --- a/packages/controllers/src/permissions/README.md +++ b/packages/controllers/src/permissions/README.md @@ -165,7 +165,7 @@ export function onlyArrayParams( context: Record, ) => { if (!Array.isArray(req.params)) { - return new EthereumJsonRpcError(); + throw new EthereumJsonRpcError(); } return next(req, context); diff --git a/packages/controllers/src/permissions/index.ts b/packages/controllers/src/permissions/index.ts index 8d5fc431b5..a4d3df5b24 100644 --- a/packages/controllers/src/permissions/index.ts +++ b/packages/controllers/src/permissions/index.ts @@ -2,5 +2,5 @@ export * from './Caveat'; export * from './enums'; export * from './Permission'; export * from './PermissionController'; -export * from './PermissionEnforcer'; +// TODO: Move these to the appropriate package export * as permissionRpcMethods from './rpc-methods'; diff --git a/packages/controllers/src/permissions/permission-middleware.test.ts b/packages/controllers/src/permissions/permission-middleware.test.ts new file mode 100644 index 0000000000..4715dd5e10 --- /dev/null +++ b/packages/controllers/src/permissions/permission-middleware.test.ts @@ -0,0 +1,15 @@ +import { getPermissionMiddlewareFactory } from './permission-middleware'; + +describe('permission middleware', () => { + describe('getPermissionMiddlewareFactory', () => { + it('returns a permission middleware function', () => { + expect( + typeof getPermissionMiddlewareFactory({ + executeRestrictedMethod: jest.fn(), + getRestrictedMethod: jest.fn(), + isUnrestrictedMethod: jest.fn(), + }), + ).toStrictEqual('function'); + }); + }); +}); diff --git a/packages/controllers/src/permissions/permission-middleware.ts b/packages/controllers/src/permissions/permission-middleware.ts new file mode 100644 index 0000000000..d48d156c69 --- /dev/null +++ b/packages/controllers/src/permissions/permission-middleware.ts @@ -0,0 +1,87 @@ +import { + AsyncJsonRpcEngineNextCallback, + createAsyncMiddleware, + Json, + JsonRpcRequest, + JsonRpcMiddleware, + PendingJsonRpcResponse, +} from 'json-rpc-engine'; +import { internalError } from './errors'; +import { + GenericPermission, + PermissionSubjectMetadata, + RestrictedMethod, + resultIsError, +} from '.'; + +type ExecuteRestrictedMethod = ( + methodImplementation: RestrictedMethod, + subject: PermissionSubjectMetadata, + method: Permission['parentCapability'], + params?: Json, +) => ReturnType>; +type GetRestrictedMethod = ( + method: string, + origin: string, +) => RestrictedMethod; +type IsUnrestrictedMethod = (method: string) => boolean; + +type PermissionMiddlewareFactoryOptions = + { + executeRestrictedMethod: ExecuteRestrictedMethod; + getRestrictedMethod: GetRestrictedMethod; + isUnrestrictedMethod: IsUnrestrictedMethod; + }; + +export function getPermissionMiddlewareFactory< + Permission extends GenericPermission, +>({ + executeRestrictedMethod, + getRestrictedMethod, + isUnrestrictedMethod, +}: PermissionMiddlewareFactoryOptions) { + return function createPermissionMiddleware( + subject: PermissionSubjectMetadata, + ): JsonRpcMiddleware { + const permissionsMiddleware = async ( + req: JsonRpcRequest, + res: PendingJsonRpcResponse, + next: AsyncJsonRpcEngineNextCallback, + ): Promise => { + const { method, params } = req; + + // skip registered safe/passthrough methods. + if (isUnrestrictedMethod(method)) { + return next(); + } + + // This will throw if no restricted method implementation is found. + const methodImplementation = getRestrictedMethod(method, subject.origin); + + const result = await executeRestrictedMethod( + methodImplementation, + subject, + method as Permission['parentCapability'], + params, + ); + + if (resultIsError(result)) { + res.error = result; + return undefined; + } + + if (result === undefined) { + res.error = internalError( + `Request for method "${req.method}" returned undefined result.`, + { request: req }, + ); + return undefined; + } + + res.result = result; + return undefined; + }; + + return createAsyncMiddleware(permissionsMiddleware); + }; +} From a8acab41ed4a26da0eef768800d880bf1885607e Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 4 Oct 2021 23:22:24 -0700 Subject: [PATCH 067/147] Fix restricted method params type --- .../controllers/src/permissions/Caveat.ts | 14 ++++++---- .../controllers/src/permissions/Permission.ts | 23 +++++++++++---- .../permissions/PermissionController.test.ts | 28 +++++++++++++------ .../src/permissions/PermissionController.ts | 13 +++++---- .../controllers/src/permissions/README.md | 6 ++-- .../src/permissions/permission-middleware.ts | 13 +++++---- 6 files changed, 63 insertions(+), 34 deletions(-) diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index 877ce603f1..c5e418113d 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -5,6 +5,7 @@ import { AsyncRestrictedMethod, RestrictedMethod, GenericPermission, + RestrictedMethodParams, } from './Permission'; /** @@ -61,9 +62,9 @@ export function constructCaveat( * @returns The decorate restricted method implementation. */ export type CaveatDecorator = ( - decorated: AsyncRestrictedMethod, + decorated: AsyncRestrictedMethod, caveat: Caveat, -) => AsyncRestrictedMethod; +) => AsyncRestrictedMethod; /** * @@ -131,16 +132,19 @@ export type GetCaveatFromType< * decorator) must be awaited. */ export function decorateWithCaveats( - methodImplementation: RestrictedMethod, + methodImplementation: RestrictedMethod, permission: Readonly, // bound to the requesting origin caveatSpecifications: CaveatSpecifications, // all caveat implementations -): RestrictedMethod { +): RestrictedMethod { const { caveats } = permission; if (!caveats) { return methodImplementation; } - let decorated = methodImplementation as AsyncRestrictedMethod; + let decorated = methodImplementation as AsyncRestrictedMethod< + RestrictedMethodParams, + Json + >; for (const caveat of caveats) { const specification = caveatSpecifications[caveat.type as Caveat['type']]; diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index e9d08d4f2e..b392851058 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -256,12 +256,14 @@ type RestrictedMethodContext = Readonly<{ [key: string]: any; }>; +export type RestrictedMethodParams = Json[] | Record; + /** * The arguments passed to a restricted method implementation. * * @template Params - The JSON-RPC parameters of the restricted method. */ -export type RestrictedMethodArgs = { +export type RestrictedMethodArgs = { method: TargetName; params?: Params; context: RestrictedMethodContext; @@ -273,7 +275,10 @@ export type RestrictedMethodArgs = { * @template Params - The JSON-RPC parameters of the restricted method. * @template Result - The JSON-RPC result of the restricted method. */ -export type SyncRestrictedMethod = ( +export type SyncRestrictedMethod< + Params extends RestrictedMethodParams, + Result extends Json, +> = ( args: RestrictedMethodArgs, ) => Result | Error | EthereumRpcError; @@ -283,7 +288,10 @@ export type SyncRestrictedMethod = ( * @template Params - The JSON-RPC parameters of the restricted method. * @template Result - The JSON-RPC result of the restricted method. */ -export type AsyncRestrictedMethod = ( +export type AsyncRestrictedMethod< + Params extends RestrictedMethodParams, + Result extends Json, +> = ( args: RestrictedMethodArgs, ) => Promise>; @@ -293,7 +301,10 @@ export type AsyncRestrictedMethod = ( * @template Params - The JSON-RPC parameters of the restricted method. * @template Result - The JSON-RPC result of the restricted method. */ -export type RestrictedMethod = +export type RestrictedMethod< + Params extends RestrictedMethodParams, + Result extends Json, +> = | SyncRestrictedMethod | AsyncRestrictedMethod; @@ -307,7 +318,7 @@ type PermissionSpecificationBase< Permission extends PermissionConstraint, FactoryOptions extends PermissionOptions, RequestData extends Record, - MethodImplementation extends RestrictedMethod, + MethodImplementation extends RestrictedMethod, > = { /** * The target resource of the permission. In other words, at the time of @@ -380,7 +391,7 @@ export type PermissionSpecificationConstraint< Permission, PermissionOptions, Record, - RestrictedMethod + RestrictedMethod >; /** diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 7a595fb006..cfaa824fb2 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -17,6 +17,7 @@ import { PermissionOptions, PermissionSpecifications, RestrictedMethodArgs, + RestrictedMethodParams, } from '.'; // CaveatConstraint types and specifications @@ -57,10 +58,10 @@ function getDefaultCaveatSpecifications(): CaveatSpecifications type: CaveatTypes.filterArrayResponse, decorator: ( - method: AsyncRestrictedMethod, + method: AsyncRestrictedMethod, caveat: FilterArrayCaveat, ) => - async (args: RestrictedMethodArgs) => { + async (args: RestrictedMethodArgs) => { const result: string[] | unknown = await method(args); if (!Array.isArray(result)) { throw Error('not an array'); @@ -73,10 +74,10 @@ function getDefaultCaveatSpecifications(): CaveatSpecifications type: CaveatTypes.filterObjectResponse, decorator: ( - method: AsyncRestrictedMethod, + method: AsyncRestrictedMethod, caveat: FilterObjectCaveat, ) => - async (args: RestrictedMethodArgs) => { + async (args: RestrictedMethodArgs) => { const result = await method(args); if (!isPlainObject(result)) { throw Error('not a plain object'); @@ -91,8 +92,11 @@ function getDefaultCaveatSpecifications(): CaveatSpecifications noopCaveat: { type: CaveatTypes.noopCaveat, decorator: - (method: AsyncRestrictedMethod, _caveat: NoopCaveat) => - async (args: RestrictedMethodArgs) => { + ( + method: AsyncRestrictedMethod, + _caveat: NoopCaveat, + ) => + async (args: RestrictedMethodArgs) => { return method(args); }, validator: (caveat: { type: CaveatTypes.noopCaveat; value: unknown }) => { @@ -174,13 +178,17 @@ function getDefaultPermissionSpecifications(): PermissionSpecifications< return { wallet_getSecretArray: { target: PermissionKeys.wallet_getSecretArray, - methodImplementation: (_args: RestrictedMethodArgs) => { + methodImplementation: ( + _args: RestrictedMethodArgs, + ) => { return ['secret1', 'secret2', 'secret3']; }, }, wallet_getSecretObject: { target: PermissionKeys.wallet_getSecretObject, - methodImplementation: (_args: RestrictedMethodArgs) => { + methodImplementation: ( + _args: RestrictedMethodArgs, + ) => { return { secret1: 'a', secret2: 'b', secret3: 'c' }; }, validator: (permission: GenericPermission) => { @@ -195,7 +203,9 @@ function getDefaultPermissionSpecifications(): PermissionSpecifications< }, 'wallet_getSecret_*': { target: PermissionKeys['wallet_getSecret_*'], - methodImplementation: (args: RestrictedMethodArgs) => { + methodImplementation: ( + args: RestrictedMethodArgs, + ) => { return `Hello, secret friend "${args.method.replace( 'wallet_getSecret_', '', diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 6bdedd8164..4b4dc6ae85 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -31,6 +31,7 @@ import { PermissionSpecifications, RequestedPermissions, RestrictedMethod, + RestrictedMethodParams, TargetName, } from './Permission'; import { @@ -450,7 +451,7 @@ export class PermissionController< */ getRestrictedMethod( method: string, - ): RestrictedMethod | undefined { + ): RestrictedMethod | undefined { const targetKey = this.getTargetKey( method as Permission['parentCapability'], ); @@ -1354,7 +1355,7 @@ export class PermissionController< async executeRestrictedMethod( origin: string, methodName: Permission['parentCapability'], - params?: Json, + params?: RestrictedMethodParams, ): Promise { // Throws if the method does not exist const methodImplementation = this._getRestrictedMethod(methodName, origin); @@ -1389,7 +1390,7 @@ export class PermissionController< private _getRestrictedMethod( method: string, origin: string, - ): RestrictedMethod { + ): RestrictedMethod { const methodImplementation = this.getRestrictedMethod( method as Permission['parentCapability'], ); @@ -1409,11 +1410,11 @@ export class PermissionController< * @returns */ private _executeRestrictedMethod( - methodImplementation: RestrictedMethod, + methodImplementation: RestrictedMethod, subject: PermissionSubjectMetadata, method: Permission['parentCapability'], - params: Json = [], - ): ReturnType> { + params: RestrictedMethodParams = [], + ): ReturnType> { const { origin } = subject; const permission = this.getPermission(origin, method); diff --git a/packages/controllers/src/permissions/README.md b/packages/controllers/src/permissions/README.md index b015b3a993..8458f4eaab 100644 --- a/packages/controllers/src/permissions/README.md +++ b/packages/controllers/src/permissions/README.md @@ -78,7 +78,7 @@ const caveatSpecifications = { method: AsyncRestrictedMethod, caveat: { type: 'filterArrayResponse'; value: string[] }, ) => - async (args: RestrictedMethodArgs) => { + async (args: RestrictedMethodArgs) => { const result = await method(args); return Array.isArray(result) ? result.filter(caveat.value.includes) @@ -91,7 +91,9 @@ const permissionSpecifications = { wallet_getSecretArray: { // i.e. the restricted method name target: 'wallet_getSecretArray', - methodImplementation: (_args: RestrictedMethodArgs) => { + methodImplementation: ( + _args: RestrictedMethodArgs, + ) => { return ['secret1', 'secret2', 'secret3']; }, }, diff --git a/packages/controllers/src/permissions/permission-middleware.ts b/packages/controllers/src/permissions/permission-middleware.ts index d48d156c69..a2dd47dece 100644 --- a/packages/controllers/src/permissions/permission-middleware.ts +++ b/packages/controllers/src/permissions/permission-middleware.ts @@ -11,19 +11,20 @@ import { GenericPermission, PermissionSubjectMetadata, RestrictedMethod, + RestrictedMethodParams, resultIsError, } from '.'; type ExecuteRestrictedMethod = ( - methodImplementation: RestrictedMethod, + methodImplementation: RestrictedMethod, subject: PermissionSubjectMetadata, method: Permission['parentCapability'], - params?: Json, -) => ReturnType>; + params?: RestrictedMethodParams, +) => ReturnType>; type GetRestrictedMethod = ( method: string, origin: string, -) => RestrictedMethod; +) => RestrictedMethod; type IsUnrestrictedMethod = (method: string) => boolean; type PermissionMiddlewareFactoryOptions = @@ -42,9 +43,9 @@ export function getPermissionMiddlewareFactory< }: PermissionMiddlewareFactoryOptions) { return function createPermissionMiddleware( subject: PermissionSubjectMetadata, - ): JsonRpcMiddleware { + ): JsonRpcMiddleware { const permissionsMiddleware = async ( - req: JsonRpcRequest, + req: JsonRpcRequest, res: PendingJsonRpcResponse, next: AsyncJsonRpcEngineNextCallback, ): Promise => { From 0e216de4150007cc5183704554c426f7ae078c2b Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 5 Oct 2021 10:25:45 -0700 Subject: [PATCH 068/147] Unit tests: PermissionController.requestPermissions --- .../permissions/PermissionController.test.ts | 299 +++++++++++++++++- .../src/permissions/PermissionController.ts | 3 +- 2 files changed, 284 insertions(+), 18 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index cfaa824fb2..00d68040cb 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -10,6 +10,7 @@ import { constructPermission, GenericCaveat, GenericPermission, + MethodNames, PermissionConstraint, PermissionController, PermissionControllerActions, @@ -48,7 +49,7 @@ type DefaultCaveats = FilterArrayCaveat | FilterObjectCaveat | NoopCaveat; * - {@link FilterObjectCaveat} * - {@link NoopCaveat} * - * Used as a default in {@link getControllerOptions}. + * Used as a default in {@link getPermissionControllerOptions}. * * @returns The caveat specifications. */ @@ -167,7 +168,7 @@ const PermissionNames = { * - Has both validator and factory. * * - * Used as a default in {@link getControllerOptions}. + * Used as a default in {@link getPermissionControllerOptions}. * * @returns The permission specifications. */ @@ -234,7 +235,7 @@ const controllerName = 'PermissionController' as const; /** * * Gets a restricted controller messenger. * - * Used as a default in {@link getControllerOptions}. + * Used as a default in {@link getPermissionControllerOptions}. * * @returns The restricted messenger. */ @@ -260,7 +261,7 @@ function getDefaultRestrictedMessenger() { /** * Gets the default unrestricted methods array. * - * Used as a default in {@link getControllerOptions}. + * Used as a default in {@link getPermissionControllerOptions}. */ function getDefaultUnrestrictedMethods() { return ['wallet_unrestrictedMethod']; @@ -304,7 +305,7 @@ function getExistingPermissionState() { * @param opts - Permission controller options. * @returns - The permission controller constructor options. */ -function getControllerOptions(opts?: Record) { +function getPermissionControllerOptions(opts?: Record) { return { caveatSpecifications: getDefaultCaveatSpecifications(), messenger: getDefaultRestrictedMessenger(), @@ -319,16 +320,18 @@ function getControllerOptions(opts?: Record) { * Gets a "default" permission controller. This simply means a controller using * the default caveat and permissions created in this test file. * - * For the options used, see {@link getControllerOptions}. + * For the options used, see {@link getPermissionControllerOptions}. * * @returns The default permission controller for testing. */ -function getDefaultPermissionController() { +function getDefaultPermissionController( + opts = getPermissionControllerOptions(), +) { return new PermissionController< DefaultTargetKeys, DefaultCaveats, DefaultPermissions - >(getControllerOptions()); + >(opts); } /** @@ -344,7 +347,7 @@ function getDefaultPermissionControllerWithState() { DefaultTargetKeys, DefaultCaveats, DefaultPermissions - >(getControllerOptions({ state: getExistingPermissionState() })); + >(getPermissionControllerOptions({ state: getExistingPermissionState() })); } /** @@ -394,7 +397,7 @@ describe('PermissionController', () => { DefaultCaveats, DefaultPermissions >( - getControllerOptions({ + getPermissionControllerOptions({ permissionSpecifications: { ...getDefaultPermissionSpecifications(), '': { target: '' }, @@ -410,7 +413,7 @@ describe('PermissionController', () => { DefaultCaveats, DefaultPermissions >( - getControllerOptions({ + getPermissionControllerOptions({ permissionSpecifications: { ...getDefaultPermissionSpecifications(), foo_: { target: 'foo_' }, @@ -426,7 +429,7 @@ describe('PermissionController', () => { DefaultCaveats, DefaultPermissions >( - getControllerOptions({ + getPermissionControllerOptions({ permissionSpecifications: { ...getDefaultPermissionSpecifications(), 'foo*': { target: 'foo*' }, @@ -442,7 +445,7 @@ describe('PermissionController', () => { DefaultCaveats, DefaultPermissions >( - getControllerOptions({ + getPermissionControllerOptions({ permissionSpecifications: { ...getDefaultPermissionSpecifications(), foo: { target: 'bar' }, @@ -2098,7 +2101,269 @@ describe('PermissionController', () => { }); describe('requestPermissions', () => { - it.todo('requests the permissions'); + it('requests a permission', async () => { + const options = getPermissionControllerOptions(); + const { messenger } = options; + const origin = 'metamask.io'; + + const callActionSpy = jest + .spyOn(messenger, 'call') + .mockImplementationOnce((async (...args: any) => { + const [, { requestData }] = args; + return { + metadata: { ...requestData.metadata }, + permissions: { ...requestData.permissions }, + }; + }) as any); + + const controller = getDefaultPermissionController(options); + expect( + await controller.requestPermissions(origin, { + [PermissionNames.wallet_getSecretArray]: {}, + }), + ).toMatchObject([ + { + [PermissionNames.wallet_getSecretArray]: getPermissionMatcher( + PermissionNames.wallet_getSecretArray, + null, + origin, + ), + }, + { id: expect.any(String), origin }, + ]); + + expect(callActionSpy).toHaveBeenCalledTimes(1); + expect(callActionSpy).toHaveBeenCalledWith( + 'ApprovalController:addRequest', + { + id: expect.any(String), + origin, + requestData: { + metadata: { id: expect.any(String), origin }, + permissions: { [PermissionNames.wallet_getSecretArray]: {} }, + }, + type: MethodNames.requestPermissions, + }, + true, + ); + }); + + it('requests multiple permissions', async () => { + const options = getPermissionControllerOptions(); + const { messenger } = options; + const origin = 'metamask.io'; + + const callActionSpy = jest + .spyOn(messenger, 'call') + .mockImplementationOnce((async (...args: any) => { + const [, { requestData }] = args; + return { + metadata: { ...requestData.metadata }, + permissions: { ...requestData.permissions }, + }; + }) as any); + + const controller = getDefaultPermissionController(options); + expect( + await controller.requestPermissions(origin, { + [PermissionNames.wallet_getSecretArray]: {}, + [PermissionNames.wallet_getSecretObject]: { + caveats: [ + constructCaveat(CaveatTypes.filterObjectResponse, ['baz']), + ], + }, + [PermissionNames.wallet_getSecret_('foo')]: {}, + }), + ).toMatchObject([ + { + [PermissionNames.wallet_getSecretArray]: getPermissionMatcher( + PermissionNames.wallet_getSecretArray, + null, + origin, + ), + [PermissionNames.wallet_getSecretObject]: getPermissionMatcher( + PermissionNames.wallet_getSecretObject, + [constructCaveat(CaveatTypes.filterObjectResponse, ['baz'])], + origin, + ), + [PermissionNames.wallet_getSecret_('foo')]: getPermissionMatcher( + PermissionNames.wallet_getSecret_('foo'), + [constructCaveat(CaveatTypes.noopCaveat, null)], + origin, + ), + }, + { id: expect.any(String), origin }, + ]); + + expect(callActionSpy).toHaveBeenCalledTimes(1); + expect(callActionSpy).toHaveBeenCalledWith( + 'ApprovalController:addRequest', + { + id: expect.any(String), + origin, + requestData: { + metadata: { id: expect.any(String), origin }, + permissions: { + [PermissionNames.wallet_getSecretArray]: {}, + [PermissionNames.wallet_getSecretObject]: { + caveats: [ + constructCaveat(CaveatTypes.filterObjectResponse, ['baz']), + ], + }, + [PermissionNames.wallet_getSecret_('foo')]: {}, + }, + }, + type: MethodNames.requestPermissions, + }, + true, + ); + }); + + it('throws if no permissions were approved', async () => { + const options = getPermissionControllerOptions(); + const { messenger } = options; + const origin = 'metamask.io'; + + const callActionSpy = jest + .spyOn(messenger, 'call') + .mockImplementationOnce((async (...args: any) => { + const [, { requestData }] = args; + return { + metadata: { ...requestData.metadata }, + permissions: {}, // no permissions + }; + }) as any); + + const controller = getDefaultPermissionController(options); + await expect( + async () => + await controller.requestPermissions(origin, { + [PermissionNames.wallet_getSecretArray]: {}, + }), + ).rejects.toThrow( + errors.internalError( + `Approved permissions request for origin "${origin}" contains no permissions.`, + { + [PermissionNames.wallet_getSecretArray]: {}, + }, + ), + ); + + expect(callActionSpy).toHaveBeenCalledTimes(1); + expect(callActionSpy).toHaveBeenCalledWith( + 'ApprovalController:addRequest', + { + id: expect.any(String), + origin, + requestData: { + metadata: { id: expect.any(String), origin }, + permissions: { [PermissionNames.wallet_getSecretArray]: {} }, + }, + type: MethodNames.requestPermissions, + }, + true, + ); + }); + + it('throws if requested permissions are invalid', async () => { + const options = getPermissionControllerOptions(); + const { messenger } = options; + const origin = 'metamask.io'; + + const callActionSpy = jest.spyOn(messenger, 'call'); + + const controller = getDefaultPermissionController(options); + // Iterate over some invalid inputs + [ + // not plain objects + null, + 'foo', + [{ [PermissionNames.wallet_getSecretArray]: {} }], + // no permissions + {}, + ].forEach(async (input) => { + await expect( + async () => await controller.requestPermissions(origin, input as any), + ).rejects.toThrow( + errors.invalidParams({ + data: { origin, requestedPermissions: input }, + }), + ); + }); + + expect(callActionSpy).not.toHaveBeenCalled(); + }); + + it('throws if requested permissions contain a (key : value.parentCapability) mismatch', async () => { + const options = getPermissionControllerOptions(); + const { messenger } = options; + const origin = 'metamask.io'; + + const callActionSpy = jest.spyOn(messenger, 'call'); + + const controller = getDefaultPermissionController(options); + await expect( + async () => + await controller.requestPermissions(origin, { + [PermissionNames.wallet_getSecretArray]: { + parentCapability: PermissionNames.wallet_getSecretArray, + }, + [PermissionNames.wallet_getSecretObject]: { + parentCapability: PermissionNames.wallet_getSecretArray, + }, + }), + ).rejects.toThrow( + errors.invalidParams({ + data: { + origin, + requestedPermissions: { + [PermissionNames.wallet_getSecretArray]: { + [PermissionNames.wallet_getSecretArray]: { + parentCapability: PermissionNames.wallet_getSecretArray, + }, + [PermissionNames.wallet_getSecretObject]: { + parentCapability: PermissionNames.wallet_getSecretArray, + }, + }, + }, + }, + }), + ); + + expect(callActionSpy).not.toHaveBeenCalled(); + }); + + it('throws if requesting a permission for an unre target', async () => { + const options = getPermissionControllerOptions(); + const { messenger } = options; + const origin = 'metamask.io'; + + const callActionSpy = jest.spyOn(messenger, 'call'); + + const controller = getDefaultPermissionController(options); + await expect( + async () => + await controller.requestPermissions(origin, { + [PermissionNames.wallet_getSecretArray]: {}, + wallet_getSecretKabob: {}, + }), + ).rejects.toThrow( + errors.methodNotFound({ + method: 'wallet_getSecretKabob', + data: { + origin, + requestedPermissions: { + [PermissionNames.wallet_getSecretArray]: { + [PermissionNames.wallet_getSecretArray]: {}, + wallet_getSecretKabob: {}, + }, + }, + }, + }), + ); + + expect(callActionSpy).not.toHaveBeenCalled(); + }); }); describe('acceptPermissionsRequest', () => { @@ -2115,7 +2380,7 @@ describe('PermissionController', () => { describe('controller actions', () => { it('permissionController:clearPermissions', () => { - const options = getControllerOptions(); + const options = getPermissionControllerOptions(); const { messenger } = options; const controller = new PermissionController< DefaultTargetKeys, @@ -2139,7 +2404,7 @@ describe('PermissionController', () => { }); it('permissionController:getSubjectNames', () => { - const options = getControllerOptions(); + const options = getPermissionControllerOptions(); const { messenger } = options; const controller = new PermissionController< DefaultTargetKeys, @@ -2166,7 +2431,7 @@ describe('PermissionController', () => { }); it('permissionController:hasPermissions', () => { - const options = getControllerOptions(); + const options = getPermissionControllerOptions(); const { messenger } = options; const controller = new PermissionController< DefaultTargetKeys, diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 4b4dc6ae85..def6591343 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -1241,7 +1241,8 @@ export class PermissionController< if ( !requestedPermissions || typeof requestedPermissions !== 'object' || - Array.isArray(requestedPermissions) + Array.isArray(requestedPermissions) || + Object.keys(requestedPermissions).length === 0 ) { throw invalidParams({ data: { origin, requestedPermissions } }); } From 4a7f73f952d22249943c174dc5f8009edda1a10b Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 5 Oct 2021 10:51:51 -0700 Subject: [PATCH 069/147] Document requestPermissions and related methods --- .../controllers/src/permissions/Permission.ts | 2 +- .../src/permissions/PermissionController.ts | 26 ++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index b392851058..19b2254df2 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -3,7 +3,7 @@ import { Json } from 'json-rpc-engine'; import { nanoid } from 'nanoid'; import { NonEmptyArray } from '../utils'; import { CaveatConstraint, GenericCaveat } from './Caveat'; -// This is in fact used in a docstring, but ESLint doesn't notice this. +// This is used in a docstring, but ESLint doesn't notice it. // eslint-disable-next-line @typescript-eslint/no-unused-vars import type { PermissionController } from './PermissionController'; diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index def6591343..3cf2afc6a8 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -7,6 +7,9 @@ import { RejectRequest as RejectApprovalRequest, HasApprovalRequest, } from '@metamask/controllers'; +// This is used in a docstring, but ESLint doesn't notice it. +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import type { ApprovalController } from '@metamask/controllers'; import type { Patch } from 'immer'; import deepFreeze from 'deep-freeze-strict'; import { nanoid } from 'nanoid'; @@ -1161,8 +1164,11 @@ export class PermissionController< } /** - * TODO + * Initiates a permission request that requires user approval. This should + * always be used to grant additional permissions to a subject, unless user + * approval has been obtained through some other means. * + * @see {@link ApprovalController} For the user approval logic. * @param origin - The origin of the grantee subject. * @param requestedPermissions - The requested permissions. * @param id - The id of the permissions request. @@ -1209,7 +1215,8 @@ export class PermissionController< } /** - * TODO + * Adds a request to the {@link ApprovalController} using the + * {@link AddApprovalRequest} action. * * @param permissionsRequest - The permissions request object. * @returns The approved permissions request object. @@ -1229,7 +1236,18 @@ export class PermissionController< } /** - * TODO + * Validates requested permissions. Throws if validation fails. + * + * All properties of {@link RequestedPermissions} values are ignored during + * request processing in this controller except `caveats`, which is validated + * by {@link PermissionController.computeCaveats}, caveat validators, and + * permission validators. + * + * This method still validates the `parentCapability` field, since a mismatch + * between a {@link RequestedPermissions} key and `parentCapability` makes it + * impossible to determine which permission the consumer intended to request. + * At the time of writing, our types also do not prevent this kind of + * mismatch. * * @param origin - The origin of the grantee subject. * @param requestedPermissions - The requested permissions. @@ -1237,7 +1255,7 @@ export class PermissionController< private validateRequestedPermissions( origin: string, requestedPermissions: RequestedPermissions, - ) { + ): void { if ( !requestedPermissions || typeof requestedPermissions !== 'object' || From 04a8352dae8f602d32d9e01c9cffefbfb02816b0 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 5 Oct 2021 17:49:19 -0700 Subject: [PATCH 070/147] Unit tests: accept and rejectPermissionsRequest --- .../permissions/PermissionController.test.ts | 245 +++++++++++++++++- .../src/permissions/PermissionController.ts | 124 +++++---- .../controllers/src/permissions/errors.ts | 6 + 3 files changed, 321 insertions(+), 54 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 00d68040cb..9d697c99f7 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -1,5 +1,12 @@ import assert from 'assert'; -import { ControllerMessenger, Json } from '@metamask/controllers'; +import { + ControllerMessenger, + Json, + AddApprovalRequest, + AcceptRequest as AcceptApprovalRequest, + RejectRequest as RejectApprovalRequest, + HasApprovalRequest, +} from '@metamask/controllers'; import { hasProperty, isPlainObject } from '../utils'; import * as errors from './errors'; import { constructCaveat } from './Caveat'; @@ -232,6 +239,12 @@ function getDefaultPermissionSpecifications(): PermissionSpecifications< const controllerName = 'PermissionController' as const; +type ApprovalActions = + | HasApprovalRequest + | AddApprovalRequest + | AcceptApprovalRequest + | RejectApprovalRequest; + /** * * Gets a restricted controller messenger. * @@ -241,12 +254,12 @@ const controllerName = 'PermissionController' as const; */ function getDefaultRestrictedMessenger() { const controllerMessenger = new ControllerMessenger< - PermissionControllerActions, + PermissionControllerActions | ApprovalActions, PermissionControllerEvents >(); return controllerMessenger.getRestricted< typeof controllerName, - PermissionControllerActions['type'], + PermissionControllerActions['type'] | ApprovalActions['type'], PermissionControllerEvents['type'] >({ name: controllerName, @@ -254,6 +267,10 @@ function getDefaultRestrictedMessenger() { 'PermissionController:clearPermissions', 'PermissionController:getSubjectNames', 'PermissionController:hasPermissions', + 'ApprovalController:hasRequest', + 'ApprovalController:addRequest', + 'ApprovalController:acceptRequest', + 'ApprovalController:rejectRequest', ], }); } @@ -2367,11 +2384,229 @@ describe('PermissionController', () => { }); describe('acceptPermissionsRequest', () => { - it.todo('accepts a permissions request'); + it('accepts a permissions request', async () => { + const options = getPermissionControllerOptions(); + const { messenger } = options; + const origin = 'metamask.io'; + const id = 'foobar'; + + const callActionSpy = jest + .spyOn(messenger, 'call') + .mockImplementationOnce((async (..._args: any) => true) as any) + .mockImplementationOnce((async (..._args: any) => undefined) as any); + + const controller = getDefaultPermissionController(options); + + await controller.acceptPermissionsRequest({ + metadata: { id, origin }, + permissions: { + [PermissionNames.wallet_getSecretArray]: {}, + }, + }); + + expect(callActionSpy).toHaveBeenCalledTimes(2); + expect(callActionSpy).toHaveBeenNthCalledWith( + 1, + 'ApprovalController:hasRequest', + { + id, + }, + ); + + expect(callActionSpy).toHaveBeenNthCalledWith( + 2, + 'ApprovalController:acceptRequest', + id, + { + metadata: { id, origin }, + permissions: { + [PermissionNames.wallet_getSecretArray]: {}, + }, + }, + ); + }); + + it('rejects the request if it contains no permissions', async () => { + const options = getPermissionControllerOptions(); + const { messenger } = options; + const origin = 'metamask.io'; + const id = 'foobar'; + + const callActionSpy = jest + .spyOn(messenger, 'call') + .mockImplementationOnce(((..._args: any) => true) as any) + .mockImplementationOnce(((..._args: any) => undefined) as any); + + const controller = getDefaultPermissionController(options); + + await controller.acceptPermissionsRequest({ + metadata: { id, origin }, + permissions: {}, + }); + + expect(callActionSpy).toHaveBeenCalledTimes(2); + expect(callActionSpy).toHaveBeenNthCalledWith( + 1, + 'ApprovalController:hasRequest', + { + id, + }, + ); + + expect(callActionSpy).toHaveBeenNthCalledWith( + 2, + 'ApprovalController:rejectRequest', + id, + errors.invalidParams({ + message: 'Must request at least one permission.', + }), + ); + }); + + it('throws if the request does not exist', async () => { + const options = getPermissionControllerOptions(); + const { messenger } = options; + const origin = 'metamask.io'; + const id = 'foobar'; + + const callActionSpy = jest + .spyOn(messenger, 'call') + .mockImplementationOnce(((..._args: any) => false) as any); + + const controller = getDefaultPermissionController(options); + + await expect( + async () => + await controller.acceptPermissionsRequest({ + metadata: { id, origin }, + permissions: { + [PermissionNames.wallet_getSecretArray]: {}, + }, + }), + ).rejects.toThrow(new errors.PermissionsRequestNotFoundError(id)); + + expect(callActionSpy).toHaveBeenCalledTimes(1); + expect(callActionSpy).toHaveBeenNthCalledWith( + 1, + 'ApprovalController:hasRequest', + { + id, + }, + ); + }); + + it('rejects the request and throws if accepting the request throws', async () => { + const options = getPermissionControllerOptions(); + const { messenger } = options; + const origin = 'metamask.io'; + const id = 'foobar'; + + const callActionSpy = jest + .spyOn(messenger, 'call') + .mockImplementationOnce(((..._args: any) => true) as any) + .mockImplementationOnce(((..._args: any) => { + throw new Error('unexpected failure'); + }) as any) + .mockImplementationOnce(((..._args: any) => undefined) as any); + + const controller = getDefaultPermissionController(options); + + await expect( + async () => + await controller.acceptPermissionsRequest({ + metadata: { id, origin }, + permissions: { + [PermissionNames.wallet_getSecretArray]: {}, + }, + }), + ).rejects.toThrow(new Error('unexpected failure')); + + expect(callActionSpy).toHaveBeenCalledTimes(3); + expect(callActionSpy).toHaveBeenNthCalledWith( + 1, + 'ApprovalController:hasRequest', + { + id, + }, + ); + + expect(callActionSpy).toHaveBeenNthCalledWith( + 2, + 'ApprovalController:acceptRequest', + id, + { + metadata: { id, origin }, + permissions: { + [PermissionNames.wallet_getSecretArray]: {}, + }, + }, + ); + + expect(callActionSpy).toHaveBeenNthCalledWith( + 3, + 'ApprovalController:rejectRequest', + id, + new Error('unexpected failure'), + ); + }); }); describe('rejectPermissionsRequest', () => { - it.todo('rejects a permissions request'); + it('rejects a permissions request', async () => { + const options = getPermissionControllerOptions(); + const { messenger } = options; + const id = 'foobar'; + + const callActionSpy = jest + .spyOn(messenger, 'call') + .mockImplementationOnce((async (..._args: any) => true) as any) + .mockImplementationOnce((async (..._args: any) => undefined) as any); + + const controller = getDefaultPermissionController(options); + + await controller.rejectPermissionsRequest(id); + + expect(callActionSpy).toHaveBeenCalledTimes(2); + expect(callActionSpy).toHaveBeenNthCalledWith( + 1, + 'ApprovalController:hasRequest', + { + id, + }, + ); + + expect(callActionSpy).toHaveBeenNthCalledWith( + 2, + 'ApprovalController:rejectRequest', + id, + errors.userRejectedRequest(), + ); + }); + + it('throws if the request does not exist', async () => { + const options = getPermissionControllerOptions(); + const { messenger } = options; + const id = 'foobar'; + + const callActionSpy = jest + .spyOn(messenger, 'call') + .mockImplementationOnce(((..._args: any) => false) as any); + + const controller = getDefaultPermissionController(options); + + await expect( + async () => await controller.rejectPermissionsRequest(id), + ).rejects.toThrow(new errors.PermissionsRequestNotFoundError(id)); + + expect(callActionSpy).toHaveBeenCalledTimes(1); + expect(callActionSpy).toHaveBeenNthCalledWith( + 1, + 'ApprovalController:hasRequest', + { + id, + }, + ); + }); }); describe('executeRestrictedMethod', () => { diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 3cf2afc6a8..ec10a4337b 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -7,9 +7,13 @@ import { RejectRequest as RejectApprovalRequest, HasApprovalRequest, } from '@metamask/controllers'; -// This is used in a docstring, but ESLint doesn't notice it. -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import type { ApprovalController } from '@metamask/controllers'; +// These are used in docstrings, but ESLint is ignorant of docstrings. +/* eslint-disable @typescript-eslint/no-unused-vars */ +import type { + ApprovalController, + ControllerMessenger, +} from '@metamask/controllers'; +/* eslint-enable @typescript-eslint/no-unused-vars */ import type { Patch } from 'immer'; import deepFreeze from 'deep-freeze-strict'; import { nanoid } from 'nanoid'; @@ -54,6 +58,7 @@ import { invalidParams, internalError, userRejectedRequest, + PermissionsRequestNotFoundError, } from './errors'; import { MethodNames } from './enums'; import { getPermissionMiddlewareFactory } from './permission-middleware'; @@ -271,6 +276,11 @@ export type GenericPermissionController = PermissionController< >; /** + * The permission controller. See the documentation for details. + * + * Assumes the existence of an {@link ApprovalController} reachable via the + * {@link ControllerMessenger}. + * * @template TargetKey - A union of string literal types corresponding to the * keys of all permission target. Must match the permission specifications of * the controller. @@ -1169,6 +1179,10 @@ export class PermissionController< * approval has been obtained through some other means. * * @see {@link ApprovalController} For the user approval logic. + * @see {@link PermissionController.acceptPermissionsRequest} For the method + * that _accepts_ the request and resolves the user approval promise. + * @see {@link PermissionController.rejectPermissionsRequest} For the method + * that _rejects_ the request and the user approval promise. * @param origin - The origin of the grantee subject. * @param requestedPermissions - The requested permissions. * @param id - The id of the permissions request. @@ -1284,7 +1298,8 @@ export class PermissionController< } /** - * TODO + * Accepts a permissions request created by + * {@link PermissionController.requestPermissions}. * * @param request - The permissions request. */ @@ -1292,52 +1307,57 @@ export class PermissionController< const { id } = request.metadata; if (!this.hasApprovalRequest({ id })) { - console.debug(`Permissions request with id "${id}" not found.`); + throw new PermissionsRequestNotFoundError(id); + } + + if (Object.keys(request.permissions).length === 0) { + this._rejectPermissionsRequest( + id, + invalidParams({ + message: 'Must request at least one permission.', + }), + ); return; } try { - if (Object.keys(request.permissions).length === 0) { - this._rejectPermissionsRequest( - id, - invalidParams({ - message: 'Must request at least one permission.', - }), - ); - } else { - this.messagingSystem.call( - 'ApprovalController:acceptRequest', - id, - request, - ); - } - } catch (err) { - // if finalization fails, reject the request - this._rejectPermissionsRequest(id, err as Error); + this.messagingSystem.call( + 'ApprovalController:acceptRequest', + id, + request, + ); + } catch (error) { + // If accepting unexpectedly fails, reject the request and re-throw the + // error + this._rejectPermissionsRequest(id, error); + throw error; } } /** - * TODO + * Rejects a permissions request created by + * {@link PermissionController.requestPermissions}. * * @param id - The id of the request to be rejected. */ async rejectPermissionsRequest(id: string): Promise { if (!this.hasApprovalRequest({ id })) { - console.debug(`Permissions request with id '${id}' not found.`); - return; + throw new PermissionsRequestNotFoundError(id); } this._rejectPermissionsRequest(id, userRejectedRequest()); } /** - * TODO + * Checks whether the {@link ApprovalController} has a particular permissions + * request. * + * @see {@link PermissionController.acceptPermissionsRequest} and + * {@link PermissionController.rejectPermissionsRequest} for usage. * @param options - The {@link HasApprovalRequest} options. * @returns Whether the specified request exists. */ - private hasApprovalRequest(options: { id: string }) { + private hasApprovalRequest(options: { id: string }): boolean { // Typecast: There are only some valid parameter combinations, but we // just won't worry about that here. return this.messagingSystem.call( @@ -1349,8 +1369,10 @@ export class PermissionController< } /** - * TODO + * Internal function for rejecting a permission request. * + * @see {@link PermissionController.acceptPermissionsRequest} and + * {@link PermissionController.rejectPermissionsRequest} for usage. * @param id - The id of the request to reject. * @param error - The error associated with the rejection. */ @@ -1399,27 +1421,6 @@ export class PermissionController< return result; } - /** - * TODO - * - * @param method - The name of the method to get. - * @param request - The request associated with the request for the method. - * @returns - The restricted method implementation, if it exists. - */ - private _getRestrictedMethod( - method: string, - origin: string, - ): RestrictedMethod { - const methodImplementation = this.getRestrictedMethod( - method as Permission['parentCapability'], - ); - if (!methodImplementation) { - throw methodNotFound({ method, data: { origin } }); - } - - return methodImplementation; - } - /** * TODO * @@ -1447,6 +1448,31 @@ export class PermissionController< this.caveatSpecifications, )({ method, params, context: { origin } }); } + + /** + * Like {@link PermissionController.getRestrictedMethod}, except the request + * for the restricted method is associated with a specific subject, and a + * JSON-RPC error is thrown if the method does not exist. + * + * @see {@link PermissionController.executeRestrictedMethod} and + * {@link PermissionController.createPermissionMiddleware} for usage. + * @param method - The name of the method to get. + * @param request - The request associated with the request for the method. + * @returns - The restricted method implementation, if it exists. + */ + private _getRestrictedMethod( + method: string, + origin: string, + ): RestrictedMethod { + const methodImplementation = this.getRestrictedMethod( + method as Permission['parentCapability'], + ); + if (!methodImplementation) { + throw methodNotFound({ method, data: { origin } }); + } + + return methodImplementation; + } } /** diff --git a/packages/controllers/src/permissions/errors.ts b/packages/controllers/src/permissions/errors.ts index 8129fe7256..d0d2c15eaa 100644 --- a/packages/controllers/src/permissions/errors.ts +++ b/packages/controllers/src/permissions/errors.ts @@ -176,3 +176,9 @@ export class InvalidCaveatFieldsError extends Error { this.data = { caveat, origin, target }; } } + +export class PermissionsRequestNotFoundError extends Error { + constructor(id: string) { + super(`Permissions request with id "${id}" not found.`); + } +} From b9fbe478731ae81820733b96a117f9f68747f028 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 6 Oct 2021 00:24:02 -0700 Subject: [PATCH 071/147] Unit tests: begin executeRestrictedMethod --- .../permissions/PermissionController.test.ts | 154 +++++++++++++++++- 1 file changed, 146 insertions(+), 8 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 9d697c99f7..8ebbb2b4c5 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -32,6 +32,7 @@ import { enum CaveatTypes { filterArrayResponse = 'filterArrayResponse', + reverseArrayResponse = 'reverseArrayResponse', filterObjectResponse = 'filterObjectResponse', noopCaveat = 'noopCaveat', } @@ -41,6 +42,11 @@ type FilterArrayCaveat = CaveatConstraint< string[] >; +type ReverseArrayCaveat = CaveatConstraint< + CaveatTypes.reverseArrayResponse, + null +>; + type FilterObjectCaveat = CaveatConstraint< CaveatTypes.filterObjectResponse, string[] @@ -48,7 +54,11 @@ type FilterObjectCaveat = CaveatConstraint< type NoopCaveat = CaveatConstraint; -type DefaultCaveats = FilterArrayCaveat | FilterObjectCaveat | NoopCaveat; +type DefaultCaveats = + | FilterArrayCaveat + | ReverseArrayCaveat + | FilterObjectCaveat + | NoopCaveat; /** * Gets caveat specifications for: @@ -75,7 +85,25 @@ function getDefaultCaveatSpecifications(): CaveatSpecifications throw Error('not an array'); } - return result.filter(caveat.value.includes); + return result.filter((resultValue) => + caveat.value.includes(resultValue), + ); + }, + }, + reverseArrayResponse: { + type: CaveatTypes.reverseArrayResponse, + decorator: + ( + method: AsyncRestrictedMethod, + _caveat: ReverseArrayCaveat, + ) => + async (args: RestrictedMethodArgs) => { + const result: unknown[] | unknown = await method(args); + if (!Array.isArray(result)) { + throw Error('not an array'); + } + + return result.reverse(); }, }, filterObjectResponse: { @@ -91,8 +119,10 @@ function getDefaultCaveatSpecifications(): CaveatSpecifications throw Error('not a plain object'); } - caveat.value.forEach((key) => { - delete result[key]; + Object.keys(result).forEach((key) => { + if (!caveat.value.includes(key)) { + delete result[key]; + } }); return result; }, @@ -122,7 +152,7 @@ type SecretArrayName = 'wallet_getSecretArray'; type SecretArrayPermission = PermissionConstraint< SecretArrayName, - FilterArrayCaveat + FilterArrayCaveat | ReverseArrayCaveat >; type SecretObjectName = 'wallet_getSecretObject'; @@ -139,20 +169,36 @@ type SecretNamespacedPermission = PermissionConstraint< NoopCaveat >; +// Dummy permission that returns an error object +// type GetErrorName = 'wallet_getError'; + +// type GetErrorPermission = PermissionConstraint; + +// Illegal dummy permission +// type GetUndefinedName = 'wallet_getUndefined'; + +// type GetUndefinedPermission = PermissionConstraint; + type DefaultTargetKeys = | SecretArrayName | SecretObjectName | SecretNamespacedKey; +// | GetErrorName +// | GetUndefinedName; type DefaultPermissions = | SecretArrayPermission | SecretObjectPermission | SecretNamespacedPermission; +// | GetErrorPermission +// | GetUndefinedPermission; enum PermissionKeys { 'wallet_getSecretArray' = 'wallet_getSecretArray', 'wallet_getSecretObject' = 'wallet_getSecretObject', 'wallet_getSecret_*' = 'wallet_getSecret_*', + // 'wallet_getError' = 'wallet_getError', + // 'wallet_getUndefined' = 'wallet_getUndefined', } /** @@ -163,6 +209,8 @@ const PermissionNames = { wallet_getSecretArray: 'wallet_getSecretArray' as const, wallet_getSecretObject: 'wallet_getSecretObject' as const, wallet_getSecret_: (str: string) => `wallet_getSecret_${str}` as const, + // wallet_getError: 'wallet_getError' as const, + // wallet_getUndefined: 'wallet_getUndefined' as const, }; /** @@ -189,7 +237,7 @@ function getDefaultPermissionSpecifications(): PermissionSpecifications< methodImplementation: ( _args: RestrictedMethodArgs, ) => { - return ['secret1', 'secret2', 'secret3']; + return ['a', 'b', 'c']; }, }, wallet_getSecretObject: { @@ -197,7 +245,7 @@ function getDefaultPermissionSpecifications(): PermissionSpecifications< methodImplementation: ( _args: RestrictedMethodArgs, ) => { - return { secret1: 'a', secret2: 'b', secret3: 'c' }; + return { a: 'x', b: 'y', c: 'z' }; }, validator: (permission: GenericPermission) => { // A dummy validator for a caveat type that should be impossible to add @@ -232,6 +280,15 @@ function getDefaultPermissionSpecifications(): PermissionSpecifications< ); }, }, + // wallet_getError: { + // target: PermissionKeys.wallet_getError, + // methodImplementation: ((args: RestrictedMethodArgs) => { + // return { message: `The error says: ${args.params?.[0] || 'default'}` } + // }) + // }, + // wallet_getUndefined: { + // target: PermissionKeys.wallet_getUndefined, + // } }; } @@ -2610,7 +2667,88 @@ describe('PermissionController', () => { }); describe('executeRestrictedMethod', () => { - it.todo('executes a restricted method'); + it('executes a restricted method', async () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: {}, + }, + }); + + expect( + await controller.executeRestrictedMethod( + origin, + PermissionNames.wallet_getSecretArray, + ), + ).toStrictEqual(['a', 'b', 'c']); + }); + + it('executes a namespaced restricted method', async () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecret_('foo')]: {}, + }, + }); + + expect( + await controller.executeRestrictedMethod( + origin, + PermissionNames.wallet_getSecret_('foo'), + ), + ).toStrictEqual('Hello, secret friend "foo"!'); + }); + + it('executes a restricted method with caveats', async () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: { + caveats: [constructCaveat(CaveatTypes.filterArrayResponse, ['b'])], + }, + }, + }); + + expect( + await controller.executeRestrictedMethod( + origin, + PermissionNames.wallet_getSecretArray, + ), + ).toStrictEqual(['b']); + }); + + it('executes a restricted method with multiple caveats', async () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: { + caveats: [ + constructCaveat(CaveatTypes.filterArrayResponse, ['a', 'c']), + constructCaveat(CaveatTypes.reverseArrayResponse, null), + ], + }, + }, + }); + + expect( + await controller.executeRestrictedMethod( + origin, + PermissionNames.wallet_getSecretArray, + ), + ).toStrictEqual(['c', 'a']); + }); }); describe('controller actions', () => { From 4b74e214c9c39859d888eff5fed4cde6bbd30e8d Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 6 Oct 2021 17:36:33 -0700 Subject: [PATCH 072/147] WIP: Restricted method types --- .../controllers/src/permissions/Caveat.ts | 6 +- .../controllers/src/permissions/Permission.ts | 137 +++++++++++------- .../permissions/PermissionController.test.ts | 56 ++++--- .../src/permissions/PermissionController.ts | 89 ++++++++---- .../controllers/src/permissions/README.md | 4 +- .../src/permissions/permission-middleware.ts | 5 - 6 files changed, 188 insertions(+), 109 deletions(-) diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index c5e418113d..6b870df954 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -110,17 +110,17 @@ export type GenericCaveat = CaveatConstraint; * An object mapping the type of each caveat to its corresponding specification. */ export type CaveatSpecifications = { - [Type in Caveat['type']]: GetCaveatSpecification; + [Type in Caveat['type']]: ExtractCaveatSpecification; }; -type GetCaveatSpecification< +type ExtractCaveatSpecification< Caveat extends GenericCaveat, CaveatType extends string, > = Caveat extends CaveatConstraint ? CaveatSpecification : never; -export type GetCaveatFromType< +export type ExtractCaveatFromType< Caveat extends GenericCaveat, CaveatType extends string, > = Caveat extends CaveatConstraint ? Caveat : never; diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 19b2254df2..3c43d976db 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -1,8 +1,7 @@ -import type { EthereumRpcError } from 'eth-rpc-errors'; import { Json } from 'json-rpc-engine'; import { nanoid } from 'nanoid'; import { NonEmptyArray } from '../utils'; -import { CaveatConstraint, GenericCaveat } from './Caveat'; +import { GenericCaveat } from './Caveat'; // This is used in a docstring, but ESLint doesn't notice it. // eslint-disable-next-line @typescript-eslint/no-unused-vars import type { PermissionController } from './PermissionController'; @@ -16,14 +15,14 @@ export type OriginString = string; /** * The name of a permission target. */ -export type TargetName = string; +export type GenericTargetName = string; /** * The base permission interface. Lacks important constraints on its generics; * consumers should use {@link PermissionConstraint} instead. */ export type PermissionBase< - Target extends TargetName, + Target extends GenericTargetName, AllowedCaveat extends GenericCaveat | never, > = { /** @@ -119,7 +118,7 @@ export type PermissionConstraint< * A generic permission. */ export type GenericPermission = PermissionConstraint< - TargetName, + GenericTargetName, GenericCaveat | never >; @@ -149,35 +148,46 @@ type ExtractPermissionTargetKey< > = Name extends Key ? Name : Extract>; /** - * A utility type for extracting the valid caveat types for a particular - * permission from a union of permission types. - * - * @template PermissionUnion - The permission type union to extract valid caveat - * types from. + * @template Permission - The permission type to extract valid caveats from. * @template Key - The target key type to extract from. * @template Name - The target name of the permission. */ -export type ExtractValidCaveatTypes< - PermissionUnion extends GenericPermission, +type ExtractValidCaveats< + Permission extends GenericPermission, Key extends string, Name extends string, -> = PermissionUnion extends PermissionConstraint< +> = Permission extends PermissionConstraint< ExtractPermissionTargetKey, infer ValidCaveats > ? ValidCaveats extends GenericCaveat - ? ValidCaveats['type'] + ? ValidCaveats : never : never; +/** + * A utility type for extracting the valid caveat types for a particular + * permission from a union of permission types. + * + * @template Permission - The permission type to extract valid caveat types + * types from. + * @template Key - The target key type to extract from. + * @template Name - The target name of the permission. + */ +export type ExtractValidCaveatTypes< + Permission extends GenericPermission, + Key extends string, + Name extends string, +> = ExtractValidCaveats['type']; + /** * The options object of {@link constructPermission}. */ -export type PermissionOptions = { +export type PermissionOptions = { /** * The method that the permission corresponds to. */ - target: TargetName; + target: Permission['parentCapability']; /** * The origin string of the subject that has the permission. @@ -194,7 +204,7 @@ export type PermissionOptions = { * The caveats of the permission. * See {@link CaveatConstraint}. */ - caveats?: NonEmptyArray>; + caveats?: NonEmptyArray; }; /** @@ -207,7 +217,7 @@ export type PermissionOptions = { * @returns The new permission object. */ export function constructPermission( - options: PermissionOptions, + options: PermissionOptions, ): GenericPermission { const { caveats = null, id, invoker, target } = options; @@ -244,7 +254,10 @@ type RequestedPermission = Partial; /** * A record of target names and their {@link RequestedPermission} objects. */ -export type RequestedPermissions = Record; +export type RequestedPermissions = Record< + GenericTargetName, + RequestedPermission +>; /** * The restricted method context object. Essentially a way to pass internal @@ -263,8 +276,8 @@ export type RestrictedMethodParams = Json[] | Record; * * @template Params - The JSON-RPC parameters of the restricted method. */ -export type RestrictedMethodArgs = { - method: TargetName; +export type RestrictedMethodOptions = { + method: GenericTargetName; params?: Params; context: RestrictedMethodContext; }; @@ -278,9 +291,7 @@ export type RestrictedMethodArgs = { export type SyncRestrictedMethod< Params extends RestrictedMethodParams, Result extends Json, -> = ( - args: RestrictedMethodArgs, -) => Result | Error | EthereumRpcError; +> = (args: RestrictedMethodOptions) => Result; /** * An asynchronous restricted method implementation. @@ -291,9 +302,7 @@ export type SyncRestrictedMethod< export type AsyncRestrictedMethod< Params extends RestrictedMethodParams, Result extends Json, -> = ( - args: RestrictedMethodArgs, -) => Promise>; +> = (args: RestrictedMethodOptions) => Promise; /** * A synchronous or asynchronous restricted method implementation. @@ -308,6 +317,21 @@ export type RestrictedMethod< | SyncRestrictedMethod | AsyncRestrictedMethod; +let foo: RestrictedMethod<[], 'kaplar'>; +type Restriction = T extends Json[] | Record ? T : never; +type a = Restriction<[]>; + +type Restriction2 = T extends RestrictedMethod + ? T + : never; +type b = Restriction2; + +type Restriction3< + Params extends RestrictedMethodParams, + T, +> = T extends RestrictedMethod ? T : never; +type c = Restriction3<[], typeof foo>; + /** * The base permission specification interface. Lacks important constraints on * its generics; consumers should use {@link PermissionSpecificationConstraint} @@ -316,7 +340,7 @@ export type RestrictedMethod< type PermissionSpecificationBase< TargetKey extends string, Permission extends PermissionConstraint, - FactoryOptions extends PermissionOptions, + FactoryOptions extends PermissionOptions, RequestData extends Record, MethodImplementation extends RestrictedMethod, > = { @@ -354,7 +378,7 @@ type PermissionSpecificationBase< validator?: ( permission: GenericPermission, origin?: string, - target?: string, + target?: Permission['parentCapability'], ) => void; }; @@ -384,42 +408,57 @@ type TargetKeyConstraint = Key extends `${string}_*` export type PermissionSpecificationConstraint< TargetKey extends string, Permission extends PermissionConstraint, + FactoryOptions extends PermissionOptions, + RequestData extends Record, + MethodImplementation extends RestrictedMethod, > = TargetKeyConstraint extends never ? never : PermissionSpecificationBase< TargetKey, Permission, - PermissionOptions, - Record, - RestrictedMethod + FactoryOptions, + RequestData, + MethodImplementation >; -/** - * Utility type used in {@link PermissionSpecifications} to map permission - * target keys to their individual {@link PermissionSpecificationConstraint} - * objects. - */ -type GetPermissionSpecification< - TargetKey extends string, - Permissions extends PermissionConstraint, -> = Permissions extends PermissionConstraint - ? PermissionSpecificationConstraint - : never; +export type GenericPermissionSpecification = PermissionSpecificationConstraint< + string, + GenericPermission, + PermissionOptions, + Record, + RestrictedMethod +>; /** * The specifications for all permissions and restricted methods supported by * a particular {@link PermissionController}. */ -export type PermissionSpecifications< - TargetKey extends string, - Permissions extends PermissionConstraint, -> = TargetKeyConstraint extends never +export type PermissionSpecificationsMap< + TargetKeys extends string, + Permissions extends PermissionConstraint, + Specifications extends PermissionSpecificationConstraint< + TargetKeys, + Permissions, + PermissionOptions, + Record, + RestrictedMethod + >, +> = TargetKeyConstraint extends never ? never : { - [Key in TargetKey]: Permissions extends PermissionConstraint< + [Key in TargetKeys]: Permissions extends PermissionConstraint< Key, GenericCaveat | never > - ? GetPermissionSpecification + ? // ? ExtractPermissionSpecification + Specifications extends PermissionSpecificationConstraint< + TargetKeys, + Permissions, + PermissionOptions, + Record, + RestrictedMethod + > + ? Specifications + : never : never; }; diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 8ebbb2b4c5..e0e2db6e7a 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -23,8 +23,10 @@ import { PermissionControllerActions, PermissionControllerEvents, PermissionOptions, - PermissionSpecifications, - RestrictedMethodArgs, + PermissionSpecificationConstraint, + PermissionSpecificationsMap, + RestrictedMethod, + RestrictedMethodOptions, RestrictedMethodParams, } from '.'; @@ -79,7 +81,7 @@ function getDefaultCaveatSpecifications(): CaveatSpecifications method: AsyncRestrictedMethod, caveat: FilterArrayCaveat, ) => - async (args: RestrictedMethodArgs) => { + async (args: RestrictedMethodOptions) => { const result: string[] | unknown = await method(args); if (!Array.isArray(result)) { throw Error('not an array'); @@ -97,7 +99,7 @@ function getDefaultCaveatSpecifications(): CaveatSpecifications method: AsyncRestrictedMethod, _caveat: ReverseArrayCaveat, ) => - async (args: RestrictedMethodArgs) => { + async (args: RestrictedMethodOptions) => { const result: unknown[] | unknown = await method(args); if (!Array.isArray(result)) { throw Error('not an array'); @@ -113,7 +115,7 @@ function getDefaultCaveatSpecifications(): CaveatSpecifications method: AsyncRestrictedMethod, caveat: FilterObjectCaveat, ) => - async (args: RestrictedMethodArgs) => { + async (args: RestrictedMethodOptions) => { const result = await method(args); if (!isPlainObject(result)) { throw Error('not a plain object'); @@ -134,7 +136,7 @@ function getDefaultCaveatSpecifications(): CaveatSpecifications method: AsyncRestrictedMethod, _caveat: NoopCaveat, ) => - async (args: RestrictedMethodArgs) => { + async (args: RestrictedMethodOptions) => { return method(args); }, validator: (caveat: { type: CaveatTypes.noopCaveat; value: unknown }) => { @@ -148,24 +150,38 @@ function getDefaultCaveatSpecifications(): CaveatSpecifications // Permission types and specifications -type SecretArrayName = 'wallet_getSecretArray'; +// wallet_getSecretArray + +type SecretArrayPermissionName = 'wallet_getSecretArray'; type SecretArrayPermission = PermissionConstraint< - SecretArrayName, + SecretArrayPermissionName, FilterArrayCaveat | ReverseArrayCaveat >; -type SecretObjectName = 'wallet_getSecretObject'; +type SecretArrayPermissionSpecification = PermissionSpecificationConstraint< + SecretArrayPermissionName, + SecretArrayPermission, + PermissionOptions, + Record, + RestrictedMethod<[], Json> +>; + +// wallet_getSecretObject + +type SecretObjectPermissionName = 'wallet_getSecretObject'; type SecretObjectPermission = PermissionConstraint< - SecretObjectName, + SecretObjectPermissionName, FilterObjectCaveat | NoopCaveat >; -type SecretNamespacedKey = 'wallet_getSecret_*'; +// wallet_getSecret_* + +type SecretNamespacedPermissionKey = 'wallet_getSecret_*'; type SecretNamespacedPermission = PermissionConstraint< - SecretNamespacedKey, + SecretNamespacedPermissionKey, NoopCaveat >; @@ -180,9 +196,9 @@ type SecretNamespacedPermission = PermissionConstraint< // type GetUndefinedPermission = PermissionConstraint; type DefaultTargetKeys = - | SecretArrayName - | SecretObjectName - | SecretNamespacedKey; + | SecretArrayPermissionName + | SecretObjectPermissionName + | SecretNamespacedPermissionKey; // | GetErrorName // | GetUndefinedName; @@ -227,7 +243,7 @@ const PermissionNames = { * * @returns The permission specifications. */ -function getDefaultPermissionSpecifications(): PermissionSpecifications< +function getDefaultPermissionSpecifications(): PermissionSpecificationsMap< DefaultTargetKeys, DefaultPermissions > { @@ -235,7 +251,7 @@ function getDefaultPermissionSpecifications(): PermissionSpecifications< wallet_getSecretArray: { target: PermissionKeys.wallet_getSecretArray, methodImplementation: ( - _args: RestrictedMethodArgs, + _args: RestrictedMethodOptions, ) => { return ['a', 'b', 'c']; }, @@ -243,7 +259,7 @@ function getDefaultPermissionSpecifications(): PermissionSpecifications< wallet_getSecretObject: { target: PermissionKeys.wallet_getSecretObject, methodImplementation: ( - _args: RestrictedMethodArgs, + _args: RestrictedMethodOptions, ) => { return { a: 'x', b: 'y', c: 'z' }; }, @@ -260,7 +276,7 @@ function getDefaultPermissionSpecifications(): PermissionSpecifications< 'wallet_getSecret_*': { target: PermissionKeys['wallet_getSecret_*'], methodImplementation: ( - args: RestrictedMethodArgs, + args: RestrictedMethodOptions, ) => { return `Hello, secret friend "${args.method.replace( 'wallet_getSecret_', @@ -282,7 +298,7 @@ function getDefaultPermissionSpecifications(): PermissionSpecifications< }, // wallet_getError: { // target: PermissionKeys.wallet_getError, - // methodImplementation: ((args: RestrictedMethodArgs) => { + // methodImplementation: ((args: RestrictedMethodOptions) => { // return { message: `The error says: ${args.params?.[0] || 'default'}` } // }) // }, diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index ec10a4337b..694dbb52df 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -24,22 +24,23 @@ import { constructCaveat as _constructCaveat, decorateWithCaveats, GenericCaveat, - GetCaveatFromType, + ExtractCaveatFromType, } from './Caveat'; import { constructPermission, ExtractValidCaveatTypes, findCaveat, GenericPermission, + GenericPermissionSpecification, MutableCaveats, OriginString, PermissionConstraint, + PermissionOptions, PermissionSpecificationConstraint, - PermissionSpecifications, + PermissionSpecificationsMap, RequestedPermissions, RestrictedMethod, RestrictedMethodParams, - TargetName, } from './Permission'; import { PermissionDoesNotExistError, @@ -251,10 +252,21 @@ type PermissionControllerOptions< TargetKey extends string, Caveat extends GenericCaveat | never, Permission extends PermissionConstraint, + Specification extends PermissionSpecificationConstraint< + TargetKey, + Permission, + PermissionOptions, + Record, + RestrictedMethod + >, > = { messenger: PermissionControllerMessenger; caveatSpecifications: CaveatSpecifications; - permissionSpecifications: PermissionSpecifications; + permissionSpecifications: PermissionSpecificationsMap< + TargetKey, + Permission, + Specification + >; unrestrictedMethods: string[]; state?: Partial>; }; @@ -272,7 +284,8 @@ type GrantPermissionsOptions = { export type GenericPermissionController = PermissionController< string, GenericCaveat, - GenericPermission + GenericPermission, + GenericPermissionSpecification >; /** @@ -294,6 +307,13 @@ export class PermissionController< TargetKey extends string, Caveat extends GenericCaveat | never, Permission extends PermissionConstraint, + Specification extends PermissionSpecificationConstraint< + TargetKey, + Permission, + PermissionOptions, + Record, + RestrictedMethod + >, > extends BaseController< typeof controllerName, PermissionControllerState, @@ -311,14 +331,14 @@ export class PermissionController< } private readonly _permissionSpecifications: Readonly< - PermissionSpecifications + PermissionSpecificationsMap >; /** - * The {@link PermissionSpecifications} of the controller. + * The {@link PermissionSpecificationsMap} of the controller. */ public get permissionSpecifications(): Readonly< - PermissionSpecifications + PermissionSpecificationsMap > { return this._permissionSpecifications; } @@ -351,7 +371,7 @@ export class PermissionController< * documentation for more details. * @param options.permissionSpecifications - The specifications of all * permissions available to the controller. See - * {@link PermissionSpecifications} and the documentation for more details. + * {@link PermissionSpecificationsMap} and the documentation for more details. * @param options.unrestrictedMethods - The callable names of all JSON-RPC * methods ignored by the new controller. * @param options.messenger - The controller messenger. See @@ -360,7 +380,12 @@ export class PermissionController< * initialization. */ constructor( - options: PermissionControllerOptions, + options: PermissionControllerOptions< + TargetKey, + Caveat, + Permission, + Specification + >, ) { const { caveatSpecifications, @@ -407,23 +432,27 @@ export class PermissionController< * controller's constructor. */ private validatePermissionSpecifications( - specifications: PermissionSpecifications, + specifications: PermissionSpecificationsMap< + TargetKey, + Permission, + Specification + >, ) { - Object.entries>( - specifications, - ).forEach(([targetKey, { target: innerTargetKey }]) => { - // Check if the target key is the empty string, ends with "_", or ends - // with "*" but not "_*" - if (!targetKey || /_$/u.test(targetKey) || /[^_]\*$/u.test(targetKey)) { - throw new Error(`Invalid permission target key: "${targetKey}"`); - } + Object.entries(specifications).forEach( + ([targetKey, { target: innerTargetKey }]) => { + // Check if the target key is the empty string, ends with "_", or ends + // with "*" but not "_*" + if (!targetKey || /_$/u.test(targetKey) || /[^_]\*$/u.test(targetKey)) { + throw new Error(`Invalid permission target key: "${targetKey}"`); + } - if (targetKey !== innerTargetKey) { - throw new Error( - `Invalid permission specification: key "${targetKey}" must match specification.target value "${innerTargetKey}".`, - ); - } - }); + if (targetKey !== innerTargetKey) { + throw new Error( + `Invalid permission specification: key "${targetKey}" must match specification.target value "${innerTargetKey}".`, + ); + } + }, + ); } /** @@ -700,13 +729,13 @@ export class PermissionController< origin: OriginString, target: TargetName, caveatType: CaveatType, - ): GetCaveatFromType | undefined { + ): ExtractCaveatFromType | undefined { const permission = this.getPermission(origin, target); if (!permission) { throw new PermissionDoesNotExistError(origin, target); } - return findCaveat(permission, caveatType) as GetCaveatFromType< + return findCaveat(permission, caveatType) as ExtractCaveatFromType< Caveat, CaveatType >; @@ -1051,7 +1080,7 @@ export class PermissionController< approvedPermission.caveats, ); - const permissionOptions = { + const permissionOptions: PermissionOptions = { caveats, invoker: origin, target: targetName, @@ -1082,7 +1111,7 @@ export class PermissionController< */ private setValidatedPermissions( origin: OriginString, - permissions: Record, + permissions: Record, ): void { this.update((draftState) => { if (!draftState.subjects[origin]) { @@ -1439,7 +1468,7 @@ export class PermissionController< const permission = this.getPermission(origin, method); if (!permission) { - return unauthorized({ data: { origin, method } }); + throw unauthorized({ data: { origin, method } }); } return decorateWithCaveats( diff --git a/packages/controllers/src/permissions/README.md b/packages/controllers/src/permissions/README.md index 8458f4eaab..e76c662bf1 100644 --- a/packages/controllers/src/permissions/README.md +++ b/packages/controllers/src/permissions/README.md @@ -78,7 +78,7 @@ const caveatSpecifications = { method: AsyncRestrictedMethod, caveat: { type: 'filterArrayResponse'; value: string[] }, ) => - async (args: RestrictedMethodArgs) => { + async (args: RestrictedMethodOptions) => { const result = await method(args); return Array.isArray(result) ? result.filter(caveat.value.includes) @@ -92,7 +92,7 @@ const permissionSpecifications = { // i.e. the restricted method name target: 'wallet_getSecretArray', methodImplementation: ( - _args: RestrictedMethodArgs, + _args: RestrictedMethodOptions, ) => { return ['secret1', 'secret2', 'secret3']; }, diff --git a/packages/controllers/src/permissions/permission-middleware.ts b/packages/controllers/src/permissions/permission-middleware.ts index a2dd47dece..85ea07c739 100644 --- a/packages/controllers/src/permissions/permission-middleware.ts +++ b/packages/controllers/src/permissions/permission-middleware.ts @@ -66,11 +66,6 @@ export function getPermissionMiddlewareFactory< params, ); - if (resultIsError(result)) { - res.error = result; - return undefined; - } - if (result === undefined) { res.error = internalError( `Request for method "${req.method}" returned undefined result.`, From 3acb6f32f69ef1c5a4bb46182b5008591ed8447c Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 6 Oct 2021 20:21:01 -0700 Subject: [PATCH 073/147] WIP: Restricted method types II --- .../controllers/src/permissions/Caveat.ts | 17 ++- .../controllers/src/permissions/Permission.ts | 131 ++++++++++++------ .../permissions/PermissionController.test.ts | 78 ++++++++--- .../src/permissions/PermissionController.ts | 47 +++++-- .../controllers/src/permissions/README.md | 4 +- .../src/permissions/permission-middleware.ts | 20 +-- 6 files changed, 203 insertions(+), 94 deletions(-) diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index 6b870df954..b0912ed5df 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -3,9 +3,9 @@ import { Json } from 'json-rpc-engine'; import { UnrecognizedCaveatTypeError } from './errors'; import { AsyncRestrictedMethod, - RestrictedMethod, + RestrictedMethodBase, GenericPermission, - RestrictedMethodParams, + GenericRestrictedMethodParams, } from './Permission'; /** @@ -62,9 +62,9 @@ export function constructCaveat( * @returns The decorate restricted method implementation. */ export type CaveatDecorator = ( - decorated: AsyncRestrictedMethod, + decorated: AsyncRestrictedMethod, caveat: Caveat, -) => AsyncRestrictedMethod; +) => AsyncRestrictedMethod; /** * @@ -132,17 +132,20 @@ export type ExtractCaveatFromType< * decorator) must be awaited. */ export function decorateWithCaveats( - methodImplementation: RestrictedMethod, + methodImplementation: RestrictedMethodBase< + GenericRestrictedMethodParams, + Json + >, permission: Readonly, // bound to the requesting origin caveatSpecifications: CaveatSpecifications, // all caveat implementations -): RestrictedMethod { +): RestrictedMethodBase { const { caveats } = permission; if (!caveats) { return methodImplementation; } let decorated = methodImplementation as AsyncRestrictedMethod< - RestrictedMethodParams, + GenericRestrictedMethodParams, Json >; diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 3c43d976db..153c05fd26 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -183,12 +183,8 @@ export type ExtractValidCaveatTypes< /** * The options object of {@link constructPermission}. */ -export type PermissionOptions = { - /** - * The method that the permission corresponds to. - */ - target: Permission['parentCapability']; - +export type PermissionOptions = { + target: TargetName; /** * The origin string of the subject that has the permission. */ @@ -217,7 +213,7 @@ export type PermissionOptions = { * @returns The new permission object. */ export function constructPermission( - options: PermissionOptions, + options: PermissionOptions, ): GenericPermission { const { caveats = null, id, invoker, target } = options; @@ -269,14 +265,16 @@ type RestrictedMethodContext = Readonly<{ [key: string]: any; }>; -export type RestrictedMethodParams = Json[] | Record; +export type GenericRestrictedMethodParams = Json[] | Record; /** * The arguments passed to a restricted method implementation. * * @template Params - The JSON-RPC parameters of the restricted method. */ -export type RestrictedMethodOptions = { +export type RestrictedMethodOptions< + Params extends GenericRestrictedMethodParams, +> = { method: GenericTargetName; params?: Params; context: RestrictedMethodContext; @@ -289,7 +287,7 @@ export type RestrictedMethodOptions = { * @template Result - The JSON-RPC result of the restricted method. */ export type SyncRestrictedMethod< - Params extends RestrictedMethodParams, + Params extends GenericRestrictedMethodParams, Result extends Json, > = (args: RestrictedMethodOptions) => Result; @@ -300,7 +298,7 @@ export type SyncRestrictedMethod< * @template Result - The JSON-RPC result of the restricted method. */ export type AsyncRestrictedMethod< - Params extends RestrictedMethodParams, + Params extends GenericRestrictedMethodParams, Result extends Json, > = (args: RestrictedMethodOptions) => Promise; @@ -310,27 +308,27 @@ export type AsyncRestrictedMethod< * @template Params - The JSON-RPC parameters of the restricted method. * @template Result - The JSON-RPC result of the restricted method. */ -export type RestrictedMethod< - Params extends RestrictedMethodParams, +export type RestrictedMethodBase< + Params extends GenericRestrictedMethodParams, Result extends Json, > = | SyncRestrictedMethod | AsyncRestrictedMethod; -let foo: RestrictedMethod<[], 'kaplar'>; -type Restriction = T extends Json[] | Record ? T : never; -type a = Restriction<[]>; - -type Restriction2 = T extends RestrictedMethod - ? T +export type RestrictedMethodConstraint< + MethodImplementation extends RestrictedMethodBase, +> = MethodImplementation extends (...args: infer Args) => Json + ? Args extends GenericRestrictedMethodParams + ? MethodImplementation + : never : never; -type b = Restriction2; -type Restriction3< - Params extends RestrictedMethodParams, - T, -> = T extends RestrictedMethod ? T : never; -type c = Restriction3<[], typeof foo>; +// type ExtractPermission< +// TargetKey extends string, +// Permission extends PermissionConstraint, +// > = Permission extends PermissionConstraint +// ? Permission +// : never; /** * The base permission specification interface. Lacks important constraints on @@ -340,9 +338,11 @@ type c = Restriction3<[], typeof foo>; type PermissionSpecificationBase< TargetKey extends string, Permission extends PermissionConstraint, - FactoryOptions extends PermissionOptions, RequestData extends Record, - MethodImplementation extends RestrictedMethod, + MethodImplementation extends RestrictedMethodBase< + GenericRestrictedMethodParams, + Json + >, > = { /** * The target resource of the permission. In other words, at the time of @@ -360,7 +360,11 @@ type PermissionSpecificationBase< * used, and the validator function (if specified) will be called on newly * constructed permissions. */ - factory?: (options: FactoryOptions, requestData?: RequestData) => Permission; + // factory?: (options: PermissionOptions, requestData?: RequestData) => Permission; + factory?: ( + options: PermissionOptions, + requestData?: RequestData, + ) => Permission; /** * The implementation of the restricted method that the permission @@ -378,7 +382,7 @@ type PermissionSpecificationBase< validator?: ( permission: GenericPermission, origin?: string, - target?: Permission['parentCapability'], + target?: string, ) => void; }; @@ -408,15 +412,15 @@ type TargetKeyConstraint = Key extends `${string}_*` export type PermissionSpecificationConstraint< TargetKey extends string, Permission extends PermissionConstraint, - FactoryOptions extends PermissionOptions, RequestData extends Record, - MethodImplementation extends RestrictedMethod, + MethodImplementation extends RestrictedMethodBase, > = TargetKeyConstraint extends never + ? never + : RestrictedMethodConstraint extends never ? never : PermissionSpecificationBase< TargetKey, Permission, - FactoryOptions, RequestData, MethodImplementation >; @@ -424,11 +428,37 @@ export type PermissionSpecificationConstraint< export type GenericPermissionSpecification = PermissionSpecificationConstraint< string, GenericPermission, - PermissionOptions, Record, - RestrictedMethod + RestrictedMethodBase >; +/** + * Utility type used in {@link PermissionSpecifications} to map permission + * target keys to their individual {@link PermissionSpecificationConstraint} + * objects. + */ +type ExtractPermissionSpecification< + TargetKey extends string, + Permissions extends PermissionConstraint, + RestrictedMethods extends RestrictedMethodBase, + Specifications extends PermissionSpecificationConstraint< + string, + Permissions, + Record, + RestrictedMethods + >, +> = Specifications extends PermissionSpecificationConstraint< + TargetKey, + Permissions, + Record, + RestrictedMethods +> + ? Specifications + : never; +// > = Permissions extends PermissionConstraint +// ? PermissionSpecificationConstraint +// : never; + /** * The specifications for all permissions and restricted methods supported by * a particular {@link PermissionController}. @@ -436,29 +466,42 @@ export type GenericPermissionSpecification = PermissionSpecificationConstraint< export type PermissionSpecificationsMap< TargetKeys extends string, Permissions extends PermissionConstraint, + RestrictedMethods extends RestrictedMethodBase, Specifications extends PermissionSpecificationConstraint< TargetKeys, Permissions, - PermissionOptions, Record, - RestrictedMethod + RestrictedMethods >, > = TargetKeyConstraint extends never + ? never + : RestrictedMethodConstraint extends never ? never : { [Key in TargetKeys]: Permissions extends PermissionConstraint< Key, GenericCaveat | never > - ? // ? ExtractPermissionSpecification - Specifications extends PermissionSpecificationConstraint< - TargetKeys, + ? ExtractPermissionSpecification< + Key, Permissions, - PermissionOptions, - Record, - RestrictedMethod + RestrictedMethods, + Specifications > - ? Specifications - : never : never; }; +// [Key in TargetKeys]: Permissions extends PermissionConstraint< +// Key, +// GenericCaveat | never +// > +// ? Specifications extends PermissionSpecificationConstraint< +// Key, +// // Permissions, +// ExtractPermission, +// Record, +// RestrictedMethods +// > +// ? Specifications +// : never +// : never; +// }; diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index e0e2db6e7a..324336272d 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -25,9 +25,9 @@ import { PermissionOptions, PermissionSpecificationConstraint, PermissionSpecificationsMap, - RestrictedMethod, + RestrictedMethodBase, RestrictedMethodOptions, - RestrictedMethodParams, + GenericRestrictedMethodParams, } from '.'; // CaveatConstraint types and specifications @@ -78,10 +78,12 @@ function getDefaultCaveatSpecifications(): CaveatSpecifications type: CaveatTypes.filterArrayResponse, decorator: ( - method: AsyncRestrictedMethod, + method: AsyncRestrictedMethod, caveat: FilterArrayCaveat, ) => - async (args: RestrictedMethodOptions) => { + async ( + args: RestrictedMethodOptions, + ) => { const result: string[] | unknown = await method(args); if (!Array.isArray(result)) { throw Error('not an array'); @@ -96,10 +98,12 @@ function getDefaultCaveatSpecifications(): CaveatSpecifications type: CaveatTypes.reverseArrayResponse, decorator: ( - method: AsyncRestrictedMethod, + method: AsyncRestrictedMethod, _caveat: ReverseArrayCaveat, ) => - async (args: RestrictedMethodOptions) => { + async ( + args: RestrictedMethodOptions, + ) => { const result: unknown[] | unknown = await method(args); if (!Array.isArray(result)) { throw Error('not an array'); @@ -112,10 +116,12 @@ function getDefaultCaveatSpecifications(): CaveatSpecifications type: CaveatTypes.filterObjectResponse, decorator: ( - method: AsyncRestrictedMethod, + method: AsyncRestrictedMethod, caveat: FilterObjectCaveat, ) => - async (args: RestrictedMethodOptions) => { + async ( + args: RestrictedMethodOptions, + ) => { const result = await method(args); if (!isPlainObject(result)) { throw Error('not a plain object'); @@ -133,10 +139,12 @@ function getDefaultCaveatSpecifications(): CaveatSpecifications type: CaveatTypes.noopCaveat, decorator: ( - method: AsyncRestrictedMethod, + method: AsyncRestrictedMethod, _caveat: NoopCaveat, ) => - async (args: RestrictedMethodOptions) => { + async ( + args: RestrictedMethodOptions, + ) => { return method(args); }, validator: (caveat: { type: CaveatTypes.noopCaveat; value: unknown }) => { @@ -159,12 +167,13 @@ type SecretArrayPermission = PermissionConstraint< FilterArrayCaveat | ReverseArrayCaveat >; +type SecretArrayMethod = RestrictedMethodBase<[], string[]>; + type SecretArrayPermissionSpecification = PermissionSpecificationConstraint< SecretArrayPermissionName, SecretArrayPermission, - PermissionOptions, Record, - RestrictedMethod<[], Json> + SecretArrayMethod >; // wallet_getSecretObject @@ -176,6 +185,15 @@ type SecretObjectPermission = PermissionConstraint< FilterObjectCaveat | NoopCaveat >; +type SecretObjectMethod = RestrictedMethodBase<[], Record>; + +type SecretObjectPermissionSpecification = PermissionSpecificationConstraint< + SecretObjectPermissionName, + SecretObjectPermission, + Record, + SecretObjectMethod +>; + // wallet_getSecret_* type SecretNamespacedPermissionKey = 'wallet_getSecret_*'; @@ -185,6 +203,16 @@ type SecretNamespacedPermission = PermissionConstraint< NoopCaveat >; +type SecretNamespacedMethod = RestrictedMethodBase<[], string>; + +type SecretNamespacedPermissionSpecification = + PermissionSpecificationConstraint< + SecretNamespacedPermissionKey, + SecretNamespacedPermission, + Record, + SecretNamespacedMethod + >; + // Dummy permission that returns an error object // type GetErrorName = 'wallet_getError'; @@ -209,6 +237,16 @@ type DefaultPermissions = // | GetErrorPermission // | GetUndefinedPermission; +type DefaultRestrictedMethods = + | SecretArrayMethod + | SecretObjectMethod + | SecretNamespacedMethod; + +type DefaultPermissionSpecifications = + | SecretArrayPermissionSpecification + | SecretObjectPermissionSpecification + | SecretNamespacedPermissionSpecification; + enum PermissionKeys { 'wallet_getSecretArray' = 'wallet_getSecretArray', 'wallet_getSecretObject' = 'wallet_getSecretObject', @@ -245,13 +283,15 @@ const PermissionNames = { */ function getDefaultPermissionSpecifications(): PermissionSpecificationsMap< DefaultTargetKeys, - DefaultPermissions + DefaultPermissions, + DefaultRestrictedMethods, + DefaultPermissionSpecifications > { return { wallet_getSecretArray: { target: PermissionKeys.wallet_getSecretArray, methodImplementation: ( - _args: RestrictedMethodOptions, + _args: RestrictedMethodOptions, ) => { return ['a', 'b', 'c']; }, @@ -259,7 +299,7 @@ function getDefaultPermissionSpecifications(): PermissionSpecificationsMap< wallet_getSecretObject: { target: PermissionKeys.wallet_getSecretObject, methodImplementation: ( - _args: RestrictedMethodOptions, + _args: RestrictedMethodOptions, ) => { return { a: 'x', b: 'y', c: 'z' }; }, @@ -276,14 +316,18 @@ function getDefaultPermissionSpecifications(): PermissionSpecificationsMap< 'wallet_getSecret_*': { target: PermissionKeys['wallet_getSecret_*'], methodImplementation: ( - args: RestrictedMethodOptions, + args: RestrictedMethodOptions, ) => { return `Hello, secret friend "${args.method.replace( 'wallet_getSecret_', '', )}"!`; }, - factory: (options: PermissionOptions) => + factory: ( + options: PermissionOptions< + SecretNamespacedPermission['parentCapability'] + >, + ) => constructPermission({ ...options, caveats: [constructCaveat(CaveatTypes.noopCaveat, null)], diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 694dbb52df..82a69e4b3f 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -39,8 +39,8 @@ import { PermissionSpecificationConstraint, PermissionSpecificationsMap, RequestedPermissions, - RestrictedMethod, - RestrictedMethodParams, + RestrictedMethodBase, + GenericRestrictedMethodParams, } from './Permission'; import { PermissionDoesNotExistError, @@ -252,12 +252,12 @@ type PermissionControllerOptions< TargetKey extends string, Caveat extends GenericCaveat | never, Permission extends PermissionConstraint, + InstanceRestrictedMethod extends RestrictedMethodBase, Specification extends PermissionSpecificationConstraint< TargetKey, Permission, - PermissionOptions, Record, - RestrictedMethod + InstanceRestrictedMethod >, > = { messenger: PermissionControllerMessenger; @@ -265,6 +265,7 @@ type PermissionControllerOptions< permissionSpecifications: PermissionSpecificationsMap< TargetKey, Permission, + InstanceRestrictedMethod, Specification >; unrestrictedMethods: string[]; @@ -285,6 +286,7 @@ export type GenericPermissionController = PermissionController< string, GenericCaveat, GenericPermission, + RestrictedMethodBase, GenericPermissionSpecification >; @@ -307,12 +309,12 @@ export class PermissionController< TargetKey extends string, Caveat extends GenericCaveat | never, Permission extends PermissionConstraint, + InstanceRestrictedMethod extends RestrictedMethodBase, Specification extends PermissionSpecificationConstraint< TargetKey, Permission, - PermissionOptions, Record, - RestrictedMethod + InstanceRestrictedMethod >, > extends BaseController< typeof controllerName, @@ -331,14 +333,24 @@ export class PermissionController< } private readonly _permissionSpecifications: Readonly< - PermissionSpecificationsMap + PermissionSpecificationsMap< + TargetKey, + Permission, + InstanceRestrictedMethod, + Specification + > >; /** * The {@link PermissionSpecificationsMap} of the controller. */ public get permissionSpecifications(): Readonly< - PermissionSpecificationsMap + PermissionSpecificationsMap< + TargetKey, + Permission, + InstanceRestrictedMethod, + Specification + > > { return this._permissionSpecifications; } @@ -384,6 +396,7 @@ export class PermissionController< TargetKey, Caveat, Permission, + InstanceRestrictedMethod, Specification >, ) { @@ -435,6 +448,7 @@ export class PermissionController< specifications: PermissionSpecificationsMap< TargetKey, Permission, + InstanceRestrictedMethod, Specification >, ) { @@ -493,7 +507,7 @@ export class PermissionController< */ getRestrictedMethod( method: string, - ): RestrictedMethod | undefined { + ): RestrictedMethodBase | undefined { const targetKey = this.getTargetKey( method as Permission['parentCapability'], ); @@ -1080,7 +1094,7 @@ export class PermissionController< approvedPermission.caveats, ); - const permissionOptions: PermissionOptions = { + const permissionOptions: PermissionOptions = { caveats, invoker: origin, target: targetName, @@ -1425,7 +1439,7 @@ export class PermissionController< async executeRestrictedMethod( origin: string, methodName: Permission['parentCapability'], - params?: RestrictedMethodParams, + params?: GenericRestrictedMethodParams, ): Promise { // Throws if the method does not exist const methodImplementation = this._getRestrictedMethod(methodName, origin); @@ -1459,11 +1473,14 @@ export class PermissionController< * @returns */ private _executeRestrictedMethod( - methodImplementation: RestrictedMethod, + methodImplementation: RestrictedMethodBase< + GenericRestrictedMethodParams, + Json + >, subject: PermissionSubjectMetadata, method: Permission['parentCapability'], - params: RestrictedMethodParams = [], - ): ReturnType> { + params: GenericRestrictedMethodParams = [], + ): ReturnType> { const { origin } = subject; const permission = this.getPermission(origin, method); @@ -1492,7 +1509,7 @@ export class PermissionController< private _getRestrictedMethod( method: string, origin: string, - ): RestrictedMethod { + ): RestrictedMethodBase { const methodImplementation = this.getRestrictedMethod( method as Permission['parentCapability'], ); diff --git a/packages/controllers/src/permissions/README.md b/packages/controllers/src/permissions/README.md index e76c662bf1..7d0621acec 100644 --- a/packages/controllers/src/permissions/README.md +++ b/packages/controllers/src/permissions/README.md @@ -78,7 +78,7 @@ const caveatSpecifications = { method: AsyncRestrictedMethod, caveat: { type: 'filterArrayResponse'; value: string[] }, ) => - async (args: RestrictedMethodOptions) => { + async (args: RestrictedMethodOptions) => { const result = await method(args); return Array.isArray(result) ? result.filter(caveat.value.includes) @@ -92,7 +92,7 @@ const permissionSpecifications = { // i.e. the restricted method name target: 'wallet_getSecretArray', methodImplementation: ( - _args: RestrictedMethodOptions, + _args: RestrictedMethodOptions, ) => { return ['secret1', 'secret2', 'secret3']; }, diff --git a/packages/controllers/src/permissions/permission-middleware.ts b/packages/controllers/src/permissions/permission-middleware.ts index 85ea07c739..12519951b0 100644 --- a/packages/controllers/src/permissions/permission-middleware.ts +++ b/packages/controllers/src/permissions/permission-middleware.ts @@ -10,21 +10,23 @@ import { internalError } from './errors'; import { GenericPermission, PermissionSubjectMetadata, - RestrictedMethod, - RestrictedMethodParams, - resultIsError, + RestrictedMethodBase, + GenericRestrictedMethodParams, } from '.'; type ExecuteRestrictedMethod = ( - methodImplementation: RestrictedMethod, + methodImplementation: RestrictedMethodBase< + GenericRestrictedMethodParams, + Json + >, subject: PermissionSubjectMetadata, method: Permission['parentCapability'], - params?: RestrictedMethodParams, -) => ReturnType>; + params?: GenericRestrictedMethodParams, +) => ReturnType>; type GetRestrictedMethod = ( method: string, origin: string, -) => RestrictedMethod; +) => RestrictedMethodBase; type IsUnrestrictedMethod = (method: string) => boolean; type PermissionMiddlewareFactoryOptions = @@ -43,9 +45,9 @@ export function getPermissionMiddlewareFactory< }: PermissionMiddlewareFactoryOptions) { return function createPermissionMiddleware( subject: PermissionSubjectMetadata, - ): JsonRpcMiddleware { + ): JsonRpcMiddleware { const permissionsMiddleware = async ( - req: JsonRpcRequest, + req: JsonRpcRequest, res: PendingJsonRpcResponse, next: AsyncJsonRpcEngineNextCallback, ): Promise => { From 531d5adbdbfd192e6b9d85ec84d2171cd297fc93 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Thu, 7 Oct 2021 13:03:00 -0700 Subject: [PATCH 074/147] WIP: Messy --- .../controllers/src/permissions/Permission.ts | 189 ++++++++++-------- .../permissions/PermissionController.test.ts | 87 ++++++-- .../src/permissions/PermissionController.ts | 84 +++----- 3 files changed, 214 insertions(+), 146 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 153c05fd26..790f293171 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -180,11 +180,15 @@ export type ExtractValidCaveatTypes< Name extends string, > = ExtractValidCaveats['type']; +type ExtractArrayMembers = T extends [...infer U] ? U : never; +export type ExtractCaveats = + ExtractArrayMembers[number]; + /** * The options object of {@link constructPermission}. */ -export type PermissionOptions = { - target: TargetName; +export type PermissionOptions = { + target: Permission['parentCapability']; /** * The origin string of the subject that has the permission. */ @@ -200,7 +204,7 @@ export type PermissionOptions = { * The caveats of the permission. * See {@link CaveatConstraint}. */ - caveats?: NonEmptyArray; + caveats?: NonEmptyArray>; }; /** @@ -212,8 +216,8 @@ export type PermissionOptions = { * @param options - The options for the permission. * @returns The new permission object. */ -export function constructPermission( - options: PermissionOptions, +export function constructPermission( + options: PermissionOptions, ): GenericPermission { const { caveats = null, id, invoker, target } = options; @@ -330,12 +334,26 @@ export type RestrictedMethodConstraint< // ? Permission // : never; +export type PermissionFactory< + Permission extends GenericPermission, + RequestData extends Record, +> = ( + options: PermissionOptions, + requestData?: RequestData, +) => Permission; + +export type PermissionValidator = ( + permission: GenericPermission, + origin?: string, + target?: Permission['parentCapability'], +) => void; + /** * The base permission specification interface. Lacks important constraints on * its generics; consumers should use {@link PermissionSpecificationConstraint} * instead. */ -type PermissionSpecificationBase< +export type PermissionSpecificationBase< TargetKey extends string, Permission extends PermissionConstraint, RequestData extends Record, @@ -360,11 +378,7 @@ type PermissionSpecificationBase< * used, and the validator function (if specified) will be called on newly * constructed permissions. */ - // factory?: (options: PermissionOptions, requestData?: RequestData) => Permission; - factory?: ( - options: PermissionOptions, - requestData?: RequestData, - ) => Permission; + factory?: PermissionFactory; /** * The implementation of the restricted method that the permission @@ -379,13 +393,13 @@ type PermissionSpecificationBase< * * The validator should throw an appropriate JSON-RPC error if validation fails. */ - validator?: ( - permission: GenericPermission, - origin?: string, - target?: string, - ) => void; + validator?: PermissionValidator; }; +// type AllTheThings = { +// specification: +// } + /** * A utility type for ensuring that the given permission target key conforms to * our naming conventions. @@ -432,76 +446,95 @@ export type GenericPermissionSpecification = PermissionSpecificationConstraint< RestrictedMethodBase >; -/** - * Utility type used in {@link PermissionSpecifications} to map permission - * target keys to their individual {@link PermissionSpecificationConstraint} - * objects. - */ -type ExtractPermissionSpecification< - TargetKey extends string, - Permissions extends PermissionConstraint, - RestrictedMethods extends RestrictedMethodBase, - Specifications extends PermissionSpecificationConstraint< - string, - Permissions, - Record, - RestrictedMethods - >, -> = Specifications extends PermissionSpecificationConstraint< - TargetKey, - Permissions, - Record, - RestrictedMethods -> - ? Specifications - : never; -// > = Permissions extends PermissionConstraint -// ? PermissionSpecificationConstraint +// /** +// * Utility type used in {@link PermissionSpecifications} to map permission +// * target keys to their individual {@link PermissionSpecificationConstraint} +// * objects. +// */ +// type ExtractPermissionSpecification< +// TargetKey extends string, +// Permissions extends PermissionConstraint, +// RestrictedMethods extends RestrictedMethodBase, +// Specifications extends PermissionSpecificationConstraint< +// string, +// Permissions, +// Record, +// RestrictedMethods +// >, +// > = Specifications extends PermissionSpecificationConstraint< +// TargetKey, +// Permissions, +// Record, +// RestrictedMethods +// > +// ? Specifications // : never; +// // > = Permissions extends PermissionConstraint +// // ? PermissionSpecificationConstraint +// // : never; -/** - * The specifications for all permissions and restricted methods supported by - * a particular {@link PermissionController}. - */ export type PermissionSpecificationsMap< - TargetKeys extends string, - Permissions extends PermissionConstraint, - RestrictedMethods extends RestrictedMethodBase, - Specifications extends PermissionSpecificationConstraint< - TargetKeys, - Permissions, - Record, - RestrictedMethods - >, -> = TargetKeyConstraint extends never + Specifications extends PermissionSpecificationBase, +> = TargetKeyConstraint extends never ? never - : RestrictedMethodConstraint extends never + : RestrictedMethodConstraint< + Specifications['methodImplementation'] + > extends never ? never : { - [Key in TargetKeys]: Permissions extends PermissionConstraint< - Key, - GenericCaveat | never + [TargetKey in Specifications['target']]: Specifications extends PermissionSpecificationConstraint< + TargetKey, + PermissionConstraint, + Record, + RestrictedMethodBase > - ? ExtractPermissionSpecification< - Key, - Permissions, - RestrictedMethods, - Specifications - > + ? Specifications : never; }; -// [Key in TargetKeys]: Permissions extends PermissionConstraint< -// Key, -// GenericCaveat | never -// > -// ? Specifications extends PermissionSpecificationConstraint< + +// /** +// * The specifications for all permissions and restricted methods supported by +// * a particular {@link PermissionController}. +// */ +// export type PermissionSpecificationsMap< +// TargetKeys extends string, +// Permissions extends PermissionConstraint, +// RestrictedMethods extends RestrictedMethodBase, +// Specifications extends PermissionSpecificationConstraint< +// TargetKeys, +// Permissions, +// Record, +// RestrictedMethods +// >, +// > = TargetKeyConstraint extends never +// ? never +// : RestrictedMethodConstraint extends never +// ? never +// : { +// [Key in TargetKeys]: Permissions extends PermissionConstraint< // Key, -// // Permissions, -// ExtractPermission, -// Record, -// RestrictedMethods +// GenericCaveat | never // > -// ? Specifications -// : never -// : never; -// }; +// ? ExtractPermissionSpecification< +// Key, +// Permissions, +// RestrictedMethods, +// Specifications +// > +// : never; +// }; +// // [Key in TargetKeys]: Permissions extends PermissionConstraint< +// // Key, +// // GenericCaveat | never +// // > +// // ? Specifications extends PermissionSpecificationConstraint< +// // Key, +// // // Permissions, +// // ExtractPermission, +// // Record, +// // RestrictedMethods +// // > +// // ? Specifications +// // : never +// // : never; +// // }; diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 324336272d..b209ebd17d 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -28,6 +28,8 @@ import { RestrictedMethodBase, RestrictedMethodOptions, GenericRestrictedMethodParams, + PermissionSpecificationBase, + PermissionFactory, } from '.'; // CaveatConstraint types and specifications @@ -267,6 +269,15 @@ const PermissionNames = { // wallet_getUndefined: 'wallet_getUndefined' as const, }; +// let a: SecretArrayPermissionSpecification; +// a.target + +type foobar = PermissionSpecificationsMap; +// SecretArrayPermissionSpecification + +// let baz: foobar +// baz['wallet_getSecret_*'].factory + /** * Gets permission specifications for: * - {@link SecretArrayPermission} @@ -281,17 +292,12 @@ const PermissionNames = { * * @returns The permission specifications. */ -function getDefaultPermissionSpecifications(): PermissionSpecificationsMap< - DefaultTargetKeys, - DefaultPermissions, - DefaultRestrictedMethods, - DefaultPermissionSpecifications -> { +function getDefaultPermissionSpecifications(): PermissionSpecificationsMap { return { wallet_getSecretArray: { - target: PermissionKeys.wallet_getSecretArray, + target: 'wallet_getSecretArray', methodImplementation: ( - _args: RestrictedMethodOptions, + _args: RestrictedMethodOptions<[]>, ) => { return ['a', 'b', 'c']; }, @@ -323,12 +329,8 @@ function getDefaultPermissionSpecifications(): PermissionSpecificationsMap< '', )}"!`; }, - factory: ( - options: PermissionOptions< - SecretNamespacedPermission['parentCapability'] - >, - ) => - constructPermission({ + factory: (options: PermissionOptions) => + constructPermission({ ...options, caveats: [constructCaveat(CaveatTypes.noopCaveat, null)], }) as SecretNamespacedPermission, @@ -450,6 +452,58 @@ function getPermissionControllerOptions(opts?: Record) { }; } +type DoesExtend = T extends U ? true : false; + +// True +type Targets = DoesExtend; +type Perms = DoesExtend; +type Methods = DoesExtend< + DefaultRestrictedMethods, + RestrictedMethodBase +>; + +// False +type Specs = DoesExtend< + DefaultPermissionSpecifications, + PermissionSpecificationBase< + string, + GenericPermission, + Record, + RestrictedMethodBase + > +>; +type SpecsReverse = DoesExtend< + PermissionSpecificationBase< + string, + GenericPermission, + Record, + RestrictedMethodBase + >, + DefaultPermissionSpecifications +>; + +type base = PermissionSpecificationBase< + string, + GenericPermission, + Record, + RestrictedMethodBase +>; +let fizz: DefaultPermissionSpecifications = {} as any; +const foo: base = fizz; + +type SpecificFactory = PermissionFactory> +type GenericFactory = PermissionFactory> + +let specificFactory: SpecificFactory = (() => undefined) as any +let genericFactory: GenericFactory = specificFactory +specificFactory = genericFactory + +let specificPermission: SecretArrayPermission = {} as any +let genericPermission: GenericPermission = specificPermission + +let specificOptions: PermissionOptions = {} as any +let genericOptions: PermissionOptions = specificOptions + /** * Gets a "default" permission controller. This simply means a controller using * the default caveat and permissions created in this test file. @@ -462,9 +516,10 @@ function getDefaultPermissionController( opts = getPermissionControllerOptions(), ) { return new PermissionController< + DefaultPermissionSpecifications, DefaultTargetKeys, - DefaultCaveats, - DefaultPermissions + DefaultPermissions, + DefaultCaveats >(opts); } diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 82a69e4b3f..ea643ec32b 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -34,13 +34,14 @@ import { GenericPermissionSpecification, MutableCaveats, OriginString, - PermissionConstraint, PermissionOptions, PermissionSpecificationConstraint, PermissionSpecificationsMap, RequestedPermissions, RestrictedMethodBase, GenericRestrictedMethodParams, + ExtractCaveats, + PermissionSpecificationBase, } from './Permission'; import { PermissionDoesNotExistError, @@ -249,25 +250,18 @@ export type ExtractCaveatValue< * objects. */ type PermissionControllerOptions< - TargetKey extends string, - Caveat extends GenericCaveat | never, - Permission extends PermissionConstraint, - InstanceRestrictedMethod extends RestrictedMethodBase, Specification extends PermissionSpecificationConstraint< - TargetKey, - Permission, + string, + GenericPermission, Record, - InstanceRestrictedMethod + RestrictedMethodBase >, + Permission extends ExtractPermission, + Caveat extends ExtractCaveats, > = { messenger: PermissionControllerMessenger; caveatSpecifications: CaveatSpecifications; - permissionSpecifications: PermissionSpecificationsMap< - TargetKey, - Permission, - InstanceRestrictedMethod, - Specification - >; + permissionSpecifications: PermissionSpecificationsMap; unrestrictedMethods: string[]; state?: Partial>; }; @@ -283,13 +277,21 @@ type GrantPermissionsOptions = { }; export type GenericPermissionController = PermissionController< + GenericPermissionSpecification, string, - GenericCaveat, GenericPermission, - RestrictedMethodBase, - GenericPermissionSpecification + GenericCaveat >; +type ExtractPermission = Type extends PermissionSpecificationConstraint< + string, + infer X, + Record, + RestrictedMethodBase +> + ? X + : never; + /** * The permission controller. See the documentation for details. * @@ -306,16 +308,15 @@ export type GenericPermissionController = PermissionController< * of the controller. */ export class PermissionController< - TargetKey extends string, - Caveat extends GenericCaveat | never, - Permission extends PermissionConstraint, - InstanceRestrictedMethod extends RestrictedMethodBase, - Specification extends PermissionSpecificationConstraint< - TargetKey, - Permission, - Record, - InstanceRestrictedMethod + Specification extends PermissionSpecificationBase< + string, + GenericPermission, + Record, + RestrictedMethodBase >, + TargetKey extends Specification['target'], + Permission extends ExtractPermission, + Caveat extends ExtractCaveats, > extends BaseController< typeof controllerName, PermissionControllerState, @@ -333,24 +334,14 @@ export class PermissionController< } private readonly _permissionSpecifications: Readonly< - PermissionSpecificationsMap< - TargetKey, - Permission, - InstanceRestrictedMethod, - Specification - > + PermissionSpecificationsMap >; /** * The {@link PermissionSpecificationsMap} of the controller. */ public get permissionSpecifications(): Readonly< - PermissionSpecificationsMap< - TargetKey, - Permission, - InstanceRestrictedMethod, - Specification - > + PermissionSpecificationsMap > { return this._permissionSpecifications; } @@ -392,13 +383,7 @@ export class PermissionController< * initialization. */ constructor( - options: PermissionControllerOptions< - TargetKey, - Caveat, - Permission, - InstanceRestrictedMethod, - Specification - >, + options: PermissionControllerOptions, ) { const { caveatSpecifications, @@ -445,12 +430,7 @@ export class PermissionController< * controller's constructor. */ private validatePermissionSpecifications( - specifications: PermissionSpecificationsMap< - TargetKey, - Permission, - InstanceRestrictedMethod, - Specification - >, + specifications: PermissionSpecificationsMap, ) { Object.entries(specifications).forEach( ([targetKey, { target: innerTargetKey }]) => { @@ -1094,7 +1074,7 @@ export class PermissionController< approvedPermission.caveats, ); - const permissionOptions: PermissionOptions = { + const permissionOptions: PermissionOptions = { caveats, invoker: origin, target: targetName, From 6e151ae15ec1ac7e0873549312ffa65c0615beec Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Fri, 8 Oct 2021 16:46:06 -0700 Subject: [PATCH 075/147] WIP: Refactor controller types (Builds) --- .../controllers/src/permissions/Permission.ts | 81 ++++- .../permissions/PermissionController.test.ts | 245 +++++-------- .../src/permissions/PermissionController.ts | 330 +++++++++++------- 3 files changed, 361 insertions(+), 295 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 790f293171..3a139fe4d5 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -1,7 +1,7 @@ import { Json } from 'json-rpc-engine'; import { nanoid } from 'nanoid'; import { NonEmptyArray } from '../utils'; -import { GenericCaveat } from './Caveat'; +import { CaveatConstraint, GenericCaveat } from './Caveat'; // This is used in a docstring, but ESLint doesn't notice it. // eslint-disable-next-line @typescript-eslint/no-unused-vars import type { PermissionController } from './PermissionController'; @@ -180,9 +180,11 @@ export type ExtractValidCaveatTypes< Name extends string, > = ExtractValidCaveats['type']; -type ExtractArrayMembers = T extends [...infer U] ? U : never; +type ExtractArrayMembers = T extends readonly [...infer U] | [...infer U] + ? U[number] + : never; export type ExtractCaveats = - ExtractArrayMembers[number]; + ExtractArrayMembers; /** * The options object of {@link constructPermission}. @@ -204,7 +206,8 @@ export type PermissionOptions = { * The caveats of the permission. * See {@link CaveatConstraint}. */ - caveats?: NonEmptyArray>; + // caveats?: NonEmptyArray>; + caveats?: NonEmptyArray; }; /** @@ -320,7 +323,8 @@ export type RestrictedMethodBase< | AsyncRestrictedMethod; export type RestrictedMethodConstraint< - MethodImplementation extends RestrictedMethodBase, + // MethodImplementation extends RestrictedMethodBase, + MethodImplementation, > = MethodImplementation extends (...args: infer Args) => Json ? Args extends GenericRestrictedMethodParams ? MethodImplementation @@ -416,6 +420,58 @@ type TargetKeyConstraint = Key extends `${string}_*` ? never : Key; +type GenericFunction = (...args: any[]) => any; + +export type NewSpecShape = { + targetKey: TargetKey; + methodImplementation: GenericFunction; + allowedCaveats?: Readonly>; + validator?: GenericFunction; + factory?: GenericFunction; +}; + +// const foo: NewSpecShape = { +// targetKey: 'foo', +// methodImplementation: () => undefined, +// }; +// type bar = ExtractArrayMembers['allowedCaveats']>; +// type xxx = ExtractArrayMembers<[]>; +// type yyy = ExtractArrayMembers; + +export type ExtractCaveatTypes = Specification extends Record< + string, + unknown +> + ? ExtractArrayMembers + : never; + +export type ExtractCaveatsFromTypes< + CaveatType, + Caveats extends GenericCaveat, +> = CaveatType extends string + ? Caveats extends CaveatConstraint + ? Caveats + : never + : never; + +/** + * An individual permission specification object. Constrained such that the + * given target key must be valid. + * + * @template TargetKey - The key of the permission target. + * @template Permission - The type of the permission object. + */ +export type PermissionSpecificationConstraint2 = + T extends NewSpecShape + ? T['targetKey'] extends TargetKeyConstraint + ? T['methodImplementation'] extends RestrictedMethodConstraint< + T['methodImplementation'] + > + ? T + : never + : never + : never; + /** * An individual permission specification object. Constrained such that the * given target key must be valid. @@ -473,6 +529,21 @@ export type GenericPermissionSpecification = PermissionSpecificationConstraint< // // ? PermissionSpecificationConstraint // // : never; +export type PermissionSpecificationsMap2< + Specifications extends NewSpecShape, +> = PermissionSpecificationConstraint2 extends never + ? never + : { + [TargetKey in Specifications['targetKey']]: Specifications extends NewSpecShape + ? Specifications + : never; + }; + +// TODO:types Map the names correctly. +export type PermissionSpecificationsMap3< + Specifications extends NewSpecShape, +> = Record; + export type PermissionSpecificationsMap< Specifications extends PermissionSpecificationBase, > = TargetKeyConstraint extends never diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index b209ebd17d..2a87e6b708 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -23,13 +23,10 @@ import { PermissionControllerActions, PermissionControllerEvents, PermissionOptions, - PermissionSpecificationConstraint, - PermissionSpecificationsMap, - RestrictedMethodBase, RestrictedMethodOptions, GenericRestrictedMethodParams, - PermissionSpecificationBase, - PermissionFactory, + NewSpecShape, + PermissionSpecificationsMap2, } from '.'; // CaveatConstraint types and specifications @@ -162,39 +159,32 @@ function getDefaultCaveatSpecifications(): CaveatSpecifications // wallet_getSecretArray -type SecretArrayPermissionName = 'wallet_getSecretArray'; +// type SecretArrayPermissionName = 'wallet_getSecretArray'; -type SecretArrayPermission = PermissionConstraint< - SecretArrayPermissionName, - FilterArrayCaveat | ReverseArrayCaveat ->; +// type SecretArrayPermission = PermissionConstraint< +// SecretArrayPermissionName, +// FilterArrayCaveat | ReverseArrayCaveat +// >; -type SecretArrayMethod = RestrictedMethodBase<[], string[]>; +// type SecretArrayMethod = RestrictedMethodBase<[], string[]>; -type SecretArrayPermissionSpecification = PermissionSpecificationConstraint< - SecretArrayPermissionName, - SecretArrayPermission, - Record, - SecretArrayMethod ->; +// type SecretArrayPermissionSpecification = PermissionSpecificationConstraint2< +// SecretArrayPermissionName, +// SecretArrayPermission, +// Record, +// SecretArrayMethod +// >; // wallet_getSecretObject -type SecretObjectPermissionName = 'wallet_getSecretObject'; - -type SecretObjectPermission = PermissionConstraint< - SecretObjectPermissionName, - FilterObjectCaveat | NoopCaveat ->; +// type SecretObjectPermissionName = 'wallet_getSecretObject'; -type SecretObjectMethod = RestrictedMethodBase<[], Record>; +// type SecretObjectPermission = PermissionConstraint< +// SecretObjectPermissionName, +// FilterObjectCaveat | NoopCaveat +// >; -type SecretObjectPermissionSpecification = PermissionSpecificationConstraint< - SecretObjectPermissionName, - SecretObjectPermission, - Record, - SecretObjectMethod ->; +// type SecretObjectMethod = RestrictedMethodBase<[], Record>; // wallet_getSecret_* @@ -205,15 +195,7 @@ type SecretNamespacedPermission = PermissionConstraint< NoopCaveat >; -type SecretNamespacedMethod = RestrictedMethodBase<[], string>; - -type SecretNamespacedPermissionSpecification = - PermissionSpecificationConstraint< - SecretNamespacedPermissionKey, - SecretNamespacedPermission, - Record, - SecretNamespacedMethod - >; +// type SecretNamespacedMethod = RestrictedMethodBase<[], string>; // Dummy permission that returns an error object // type GetErrorName = 'wallet_getError'; @@ -225,29 +207,29 @@ type SecretNamespacedPermissionSpecification = // type GetUndefinedPermission = PermissionConstraint; -type DefaultTargetKeys = - | SecretArrayPermissionName - | SecretObjectPermissionName - | SecretNamespacedPermissionKey; +// type DefaultTargetKeys = +// | SecretArrayPermissionName +// | SecretObjectPermissionName +// | SecretNamespacedPermissionKey; // | GetErrorName // | GetUndefinedName; -type DefaultPermissions = - | SecretArrayPermission - | SecretObjectPermission - | SecretNamespacedPermission; +// type DefaultPermissions = +// | SecretArrayPermission +// | SecretObjectPermission +// | SecretNamespacedPermission; // | GetErrorPermission // | GetUndefinedPermission; -type DefaultRestrictedMethods = - | SecretArrayMethod - | SecretObjectMethod - | SecretNamespacedMethod; +// type DefaultRestrictedMethods = +// | SecretArrayMethod +// | SecretObjectMethod +// | SecretNamespacedMethod; -type DefaultPermissionSpecifications = - | SecretArrayPermissionSpecification - | SecretObjectPermissionSpecification - | SecretNamespacedPermissionSpecification; +// type DefaultPermissionSpecifications = +// | SecretArrayPermissionSpecification +// | SecretObjectPermissionSpecification +// | SecretNamespacedPermissionSpecification; enum PermissionKeys { 'wallet_getSecretArray' = 'wallet_getSecretArray', @@ -262,22 +244,13 @@ enum PermissionKeys { * namespaced, it's a getter function. */ const PermissionNames = { - wallet_getSecretArray: 'wallet_getSecretArray' as const, - wallet_getSecretObject: 'wallet_getSecretObject' as const, + wallet_getSecretArray: PermissionKeys.wallet_getSecretArray, + wallet_getSecretObject: PermissionKeys.wallet_getSecretObject, wallet_getSecret_: (str: string) => `wallet_getSecret_${str}` as const, // wallet_getError: 'wallet_getError' as const, // wallet_getUndefined: 'wallet_getUndefined' as const, }; -// let a: SecretArrayPermissionSpecification; -// a.target - -type foobar = PermissionSpecificationsMap; -// SecretArrayPermissionSpecification - -// let baz: foobar -// baz['wallet_getSecret_*'].factory - /** * Gets permission specifications for: * - {@link SecretArrayPermission} @@ -292,18 +265,22 @@ type foobar = PermissionSpecificationsMap; * * @returns The permission specifications. */ -function getDefaultPermissionSpecifications(): PermissionSpecificationsMap { +function getDefaultPermissionSpecifications() { return { - wallet_getSecretArray: { - target: 'wallet_getSecretArray', - methodImplementation: ( - _args: RestrictedMethodOptions<[]>, - ) => { + [PermissionKeys.wallet_getSecretArray]: { + targetKey: PermissionKeys.wallet_getSecretArray, + allowedCaveats: [ + CaveatTypes.filterArrayResponse, + CaveatTypes.reverseArrayResponse, + ] as const, + // allowedCaveats: ['foo'] as const, + methodImplementation: (_args: RestrictedMethodOptions<[]>) => { return ['a', 'b', 'c']; }, }, - wallet_getSecretObject: { - target: PermissionKeys.wallet_getSecretObject, + [PermissionKeys.wallet_getSecretObject]: { + targetKey: PermissionKeys.wallet_getSecretObject, + allowedCaveats: [CaveatTypes.filterObjectResponse] as const, methodImplementation: ( _args: RestrictedMethodOptions, ) => { @@ -319,8 +296,9 @@ function getDefaultPermissionSpecifications(): PermissionSpecificationsMap, ) => { @@ -354,6 +332,19 @@ function getDefaultPermissionSpecifications(): PermissionSpecificationsMap = T extends +type GetSpecMapValues = T extends PermissionSpecificationsMap2< + NewSpecShape +> + ? T[keyof T] + : never; + +type DefaultPermissionSpecifications = GetSpecMapValues< + ReturnType +>; + // The permissions controller const controllerName = 'PermissionController' as const; @@ -452,58 +443,6 @@ function getPermissionControllerOptions(opts?: Record) { }; } -type DoesExtend = T extends U ? true : false; - -// True -type Targets = DoesExtend; -type Perms = DoesExtend; -type Methods = DoesExtend< - DefaultRestrictedMethods, - RestrictedMethodBase ->; - -// False -type Specs = DoesExtend< - DefaultPermissionSpecifications, - PermissionSpecificationBase< - string, - GenericPermission, - Record, - RestrictedMethodBase - > ->; -type SpecsReverse = DoesExtend< - PermissionSpecificationBase< - string, - GenericPermission, - Record, - RestrictedMethodBase - >, - DefaultPermissionSpecifications ->; - -type base = PermissionSpecificationBase< - string, - GenericPermission, - Record, - RestrictedMethodBase ->; -let fizz: DefaultPermissionSpecifications = {} as any; -const foo: base = fizz; - -type SpecificFactory = PermissionFactory> -type GenericFactory = PermissionFactory> - -let specificFactory: SpecificFactory = (() => undefined) as any -let genericFactory: GenericFactory = specificFactory -specificFactory = genericFactory - -let specificPermission: SecretArrayPermission = {} as any -let genericPermission: GenericPermission = specificPermission - -let specificOptions: PermissionOptions = {} as any -let genericOptions: PermissionOptions = specificOptions - /** * Gets a "default" permission controller. This simply means a controller using * the default caveat and permissions created in this test file. @@ -517,8 +456,6 @@ function getDefaultPermissionController( ) { return new PermissionController< DefaultPermissionSpecifications, - DefaultTargetKeys, - DefaultPermissions, DefaultCaveats >(opts); } @@ -533,9 +470,8 @@ function getDefaultPermissionController( */ function getDefaultPermissionControllerWithState() { return new PermissionController< - DefaultTargetKeys, - DefaultCaveats, - DefaultPermissions + DefaultPermissionSpecifications, + DefaultCaveats >(getPermissionControllerOptions({ state: getExistingPermissionState() })); } @@ -582,14 +518,13 @@ describe('PermissionController', () => { expect( () => new PermissionController< - DefaultTargetKeys, - DefaultCaveats, - DefaultPermissions + DefaultPermissionSpecifications, + DefaultCaveats >( getPermissionControllerOptions({ permissionSpecifications: { ...getDefaultPermissionSpecifications(), - '': { target: '' }, + '': { targetKey: '' }, } as any, }), ), @@ -598,14 +533,13 @@ describe('PermissionController', () => { expect( () => new PermissionController< - DefaultTargetKeys, - DefaultCaveats, - DefaultPermissions + DefaultPermissionSpecifications, + DefaultCaveats >( getPermissionControllerOptions({ permissionSpecifications: { ...getDefaultPermissionSpecifications(), - foo_: { target: 'foo_' }, + foo_: { targetKey: 'foo_' }, } as any, }), ), @@ -614,14 +548,13 @@ describe('PermissionController', () => { expect( () => new PermissionController< - DefaultTargetKeys, - DefaultCaveats, - DefaultPermissions + DefaultPermissionSpecifications, + DefaultCaveats >( getPermissionControllerOptions({ permissionSpecifications: { ...getDefaultPermissionSpecifications(), - 'foo*': { target: 'foo*' }, + 'foo*': { targetKey: 'foo*' }, } as any, }), ), @@ -630,14 +563,13 @@ describe('PermissionController', () => { expect( () => new PermissionController< - DefaultTargetKeys, - DefaultCaveats, - DefaultPermissions + DefaultPermissionSpecifications, + DefaultCaveats >( getPermissionControllerOptions({ permissionSpecifications: { ...getDefaultPermissionSpecifications(), - foo: { target: 'bar' }, + foo: { targetKey: 'bar' }, } as any, }), ), @@ -2871,9 +2803,8 @@ describe('PermissionController', () => { const options = getPermissionControllerOptions(); const { messenger } = options; const controller = new PermissionController< - DefaultTargetKeys, - DefaultCaveats, - DefaultPermissions + DefaultPermissionSpecifications, + DefaultCaveats >(options); const clearStateSpy = jest.spyOn(controller, 'clearState'); @@ -2895,9 +2826,8 @@ describe('PermissionController', () => { const options = getPermissionControllerOptions(); const { messenger } = options; const controller = new PermissionController< - DefaultTargetKeys, - DefaultCaveats, - DefaultPermissions + DefaultPermissionSpecifications, + DefaultCaveats >(options); const getSubjectNamesSpy = jest.spyOn(controller, 'getSubjectNames'); @@ -2922,9 +2852,8 @@ describe('PermissionController', () => { const options = getPermissionControllerOptions(); const { messenger } = options; const controller = new PermissionController< - DefaultTargetKeys, - DefaultCaveats, - DefaultPermissions + DefaultPermissionSpecifications, + DefaultCaveats >(options); const hasPermissionsSpy = jest.spyOn(controller, 'hasPermissions'); diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index ea643ec32b..16860b2409 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -31,17 +31,19 @@ import { ExtractValidCaveatTypes, findCaveat, GenericPermission, - GenericPermissionSpecification, MutableCaveats, OriginString, PermissionOptions, - PermissionSpecificationConstraint, - PermissionSpecificationsMap, RequestedPermissions, RestrictedMethodBase, GenericRestrictedMethodParams, + PermissionSpecificationConstraint2, + PermissionSpecificationsMap3, + NewSpecShape, + PermissionConstraint, + ExtractCaveatsFromTypes, + ExtractCaveatTypes, ExtractCaveats, - PermissionSpecificationBase, } from './Permission'; import { PermissionDoesNotExistError, @@ -124,9 +126,12 @@ export type PermissionControllerSubjects = /** * The state of a {@link PermissionController}. */ -export type PermissionControllerState = { - subjects: PermissionControllerSubjects; -}; +export type PermissionControllerState = + Permission extends GenericPermission + ? { + subjects: PermissionControllerSubjects; + } + : never; /** * The state metadata of the {@link PermissionController}. @@ -237,35 +242,6 @@ export type ExtractCaveatValue< ? CaveatValue : never; -/** - * Options for the {@link PermissionController} constructor. - * - * @template TargetKey - A union of string literal types corresponding to the - * keys of all permission target. Must match the given permission - * specifications object. - * @template Caveat - A union of the types of all caveats available to the new - * controller. Must match the given caveat specifications object. - * @template Permission - A union of the types of all permissions available to - * the new controller. Must match the given permission and caveat specification - * objects. - */ -type PermissionControllerOptions< - Specification extends PermissionSpecificationConstraint< - string, - GenericPermission, - Record, - RestrictedMethodBase - >, - Permission extends ExtractPermission, - Caveat extends ExtractCaveats, -> = { - messenger: PermissionControllerMessenger; - caveatSpecifications: CaveatSpecifications; - permissionSpecifications: PermissionSpecificationsMap; - unrestrictedMethods: string[]; - state?: Partial>; -}; - /** * Options for {@link PermissionController.grantPermissions}. */ @@ -277,20 +253,55 @@ type GrantPermissionsOptions = { }; export type GenericPermissionController = PermissionController< - GenericPermissionSpecification, - string, - GenericPermission, + NewSpecShape, GenericCaveat >; -type ExtractPermission = Type extends PermissionSpecificationConstraint< - string, - infer X, - Record, - RestrictedMethodBase -> - ? X - : never; +// type ExtractPermission = Type extends PermissionSpecificationConstraint< +// string, +// infer X, +// Record, +// RestrictedMethodBase +// > +// ? X +// : never; + +export type ExtractPermission< + PermissionSpecification extends NewSpecShape, + Caveat extends GenericCaveat, +> = PermissionConstraint< + PermissionSpecification['targetKey'], + ExtractCaveatsFromTypes, Caveat> +>; + +/** + * Options for the {@link PermissionController} constructor. + * + * @template TargetKey - A union of string literal types corresponding to the + * keys of all permission target. Must match the given permission + * specifications object. + * @template Caveat - A union of the types of all caveats available to the new + * controller. Must match the given caveat specifications object. + * @template Permission - A union of the types of all permissions available to + * the new controller. Must match the given permission and caveat specification + * objects. + */ +export type PermissionControllerOptions< + PermissionSpecification extends NewSpecShape, + Caveat extends GenericCaveat, +> = PermissionSpecificationConstraint2 extends never + ? never + : { + messenger: PermissionControllerMessenger; + caveatSpecifications: CaveatSpecifications; + permissionSpecifications: PermissionSpecificationsMap3; + unrestrictedMethods: string[]; + state?: Partial< + PermissionControllerState< + ExtractPermission + > + >; + }; /** * The permission controller. See the documentation for details. @@ -308,18 +319,11 @@ type ExtractPermission = Type extends PermissionSpecificationConstraint< * of the controller. */ export class PermissionController< - Specification extends PermissionSpecificationBase< - string, - GenericPermission, - Record, - RestrictedMethodBase - >, - TargetKey extends Specification['target'], - Permission extends ExtractPermission, - Caveat extends ExtractCaveats, + Specification extends NewSpecShape, + Caveat extends GenericCaveat, > extends BaseController< typeof controllerName, - PermissionControllerState, + PermissionControllerState>, PermissionControllerMessenger > { private readonly _caveatSpecifications: Readonly< @@ -334,14 +338,14 @@ export class PermissionController< } private readonly _permissionSpecifications: Readonly< - PermissionSpecificationsMap + PermissionSpecificationsMap3 >; /** - * The {@link PermissionSpecificationsMap} of the controller. + * The {@link PermissionSpecificationsMap3} of the controller. */ public get permissionSpecifications(): Readonly< - PermissionSpecificationsMap + PermissionSpecificationsMap3 > { return this._permissionSpecifications; } @@ -374,7 +378,7 @@ export class PermissionController< * documentation for more details. * @param options.permissionSpecifications - The specifications of all * permissions available to the controller. See - * {@link PermissionSpecificationsMap} and the documentation for more details. + * {@link PermissionSpecificationsMap3} and the documentation for more details. * @param options.unrestrictedMethods - The callable names of all JSON-RPC * methods ignored by the new controller. * @param options.messenger - The controller messenger. See @@ -382,9 +386,7 @@ export class PermissionController< * @param options.state - Existing state to hydrate the controller with at * initialization. */ - constructor( - options: PermissionControllerOptions, - ) { + constructor(options: PermissionControllerOptions) { const { caveatSpecifications, permissionSpecifications, @@ -394,10 +396,13 @@ export class PermissionController< } = options; super({ name: controllerName, - metadata: stateMetadata, + // TODO:types why? + metadata: stateMetadata as any, messenger, state: { - ...(defaultState as PermissionControllerState), + ...(defaultState as PermissionControllerState< + ExtractPermission + >), ...state, }, }); @@ -430,10 +435,10 @@ export class PermissionController< * controller's constructor. */ private validatePermissionSpecifications( - specifications: PermissionSpecificationsMap, + specifications: PermissionSpecificationsMap3, ) { Object.entries(specifications).forEach( - ([targetKey, { target: innerTargetKey }]) => { + ([targetKey, { targetKey: innerTargetKey }]) => { // Check if the target key is the empty string, ends with "_", or ends // with "*" but not "_*" if (!targetKey || /_$/u.test(targetKey) || /[^_]\*$/u.test(targetKey)) { @@ -475,7 +480,8 @@ export class PermissionController< */ clearState(): void { this.update((_draftState) => { - return { ...defaultState }; + // TODO:types why? + return { ...defaultState } as any; }); } @@ -489,7 +495,7 @@ export class PermissionController< method: string, ): RestrictedMethodBase | undefined { const targetKey = this.getTargetKey( - method as Permission['parentCapability'], + method as ExtractPermission['parentCapability'], ); if (!targetKey) { return undefined; @@ -513,7 +519,9 @@ export class PermissionController< * @param target - The method name as invoked by a third party (i.e., not a method key). * @returns The permission if it exists, or undefined otherwise. */ - getPermission( + getPermission< + SubjectPermission extends ExtractPermission, + >( origin: OriginString, target: SubjectPermission['parentCapability'], ): SubjectPermission | undefined { @@ -530,8 +538,11 @@ export class PermissionController< */ getPermissions( origin: OriginString, - ): SubjectPermissions | undefined { - return this.state.subjects[origin]?.permissions; + ): SubjectPermissions> | undefined { + // TODO:types why? + return this.state.subjects[origin]?.permissions as + | SubjectPermissions> + | undefined; } /** @@ -544,7 +555,7 @@ export class PermissionController< */ hasPermission( origin: OriginString, - target: Permission['parentCapability'], + target: ExtractPermission['parentCapability'], ): boolean { return Boolean(this.getPermission(origin, target)); } @@ -587,7 +598,7 @@ export class PermissionController< */ revokePermission( origin: OriginString, - target: Permission['parentCapability'], + target: ExtractPermission['parentCapability'], ): void { this.revokePermissions({ [origin]: [target] }); } @@ -603,7 +614,9 @@ export class PermissionController< revokePermissions( subjectsAndPermissions: Record< OriginString, - NonEmptyArray + NonEmptyArray< + ExtractPermission['parentCapability'] + > >, ): void { this.update((draftState) => { @@ -636,7 +649,9 @@ export class PermissionController< * * @param target - The name of the target to revoke all permissions for. */ - revokePermissionForAllSubjects(target: Permission['parentCapability']): void { + revokePermissionForAllSubjects( + target: ExtractPermission['parentCapability'], + ): void { if (this.getSubjectNames().length === 0) { return; } @@ -659,10 +674,12 @@ export class PermissionController< } private deletePermission( - draftState: PermissionControllerState, - permissions: SubjectPermissions, + draftState: PermissionControllerState< + ExtractPermission + >, + permissions: SubjectPermissions>, origin: OriginString, - target: Permission['parentCapability'], + target: ExtractPermission['parentCapability'], ): void { if (Object.keys(permissions).length > 1) { delete permissions[target]; @@ -687,12 +704,18 @@ export class PermissionController< * @returns Whether the permission has the specified caveat. */ hasCaveat< - TargetName extends Permission['parentCapability'], - CaveatType extends ExtractValidCaveatTypes< - Permission, - TargetKey, - TargetName - >, + TargetName extends ExtractPermission< + Specification, + Caveat + >['parentCapability'], + // CaveatType extends ExtractValidCaveatTypes< + // ExtractPermission, + // Specification['targetKey'], + // TargetName + // >, + CaveatType extends ExtractCaveats< + ExtractPermission + >['type'], >(origin: OriginString, target: TargetName, caveatType: CaveatType): boolean { return Boolean(this.getCaveat(origin, target, caveatType)); } @@ -713,10 +736,13 @@ export class PermissionController< * @returns The caveat, or `undefined` if no such caveat exists. */ getCaveat< - TargetName extends Permission['parentCapability'], + TargetName extends ExtractPermission< + Specification, + Caveat + >['parentCapability'], CaveatType extends ExtractValidCaveatTypes< - Permission, - TargetKey, + ExtractPermission, + Specification['targetKey'], TargetName >, >( @@ -755,10 +781,13 @@ export class PermissionController< * @param caveatValue - The value of the caveat to add. */ addCaveat< - TargetName extends Permission['parentCapability'], + TargetName extends ExtractPermission< + Specification, + Caveat + >['parentCapability'], CaveatType extends ExtractValidCaveatTypes< - Permission, - TargetKey, + ExtractPermission, + Specification['targetKey'], TargetName >, >( @@ -793,10 +822,13 @@ export class PermissionController< * @param caveatValue - The new value of the caveat. */ updateCaveat< - TargetName extends Permission['parentCapability'], + TargetName extends ExtractPermission< + Specification, + Caveat + >['parentCapability'], CaveatType extends ExtractValidCaveatTypes< - Permission, - TargetKey, + ExtractPermission, + Specification['targetKey'], TargetName >, >( @@ -829,10 +861,13 @@ export class PermissionController< * @param caveatValue - The value of the caveat to set. */ private setCaveat< - TargetName extends Permission['parentCapability'], + TargetName extends ExtractPermission< + Specification, + Caveat + >['parentCapability'], CaveatType extends ExtractValidCaveatTypes< - Permission, - TargetKey, + ExtractPermission, + Specification['targetKey'], TargetName >, >( @@ -843,7 +878,7 @@ export class PermissionController< ): void { this.update((draftState) => { const subject = draftState.subjects[origin] as - | PermissionSubjectEntry + | PermissionSubjectEntry> | undefined; // This should be impossible in our usage, but TypeScript wants it @@ -853,8 +888,9 @@ export class PermissionController< } // Typecast: immer's WritableDraft is incompatible with our generics - const permission: MutableCaveats = - subject.permissions[target]; + const permission: MutableCaveats< + ExtractPermission + > = subject.permissions[target]; // This should also be impossible in our usage, but TypeScript wants it /* istanbul ignore if */ @@ -886,7 +922,9 @@ export class PermissionController< permission.caveats = [caveat] as any; } - this.validateModifiedPermission(permission as Permission); + this.validateModifiedPermission( + permission as ExtractPermission, + ); }); } @@ -904,17 +942,24 @@ export class PermissionController< * @param caveatType - The type of the caveat to remove. */ removeCaveat< - TargetName extends Permission['parentCapability'], + TargetName extends ExtractPermission< + Specification, + Caveat + >['parentCapability'], CaveatType extends ExtractValidCaveatTypes< - Permission, - TargetKey, + ExtractPermission, + Specification['targetKey'], TargetName >, >(origin: OriginString, target: TargetName, caveatType: CaveatType): void { this.update((draftState) => { // Typecast: immer's WritableDraft is incompatible with our generics - const permission: MutableCaveats = ( - draftState.subjects as PermissionControllerSubjects + const permission: MutableCaveats< + ExtractPermission + > = ( + draftState.subjects as PermissionControllerSubjects< + ExtractPermission + > )[origin]?.permissions[target]; if (!permission) { throw new PermissionDoesNotExistError(origin, target); @@ -937,7 +982,9 @@ export class PermissionController< permission.caveats.splice(caveatIndex, 1); } - this.validateModifiedPermission(permission as Permission); + this.validateModifiedPermission( + permission as ExtractPermission, + ); }); } @@ -948,7 +995,9 @@ export class PermissionController< * * @param permission - The modified permission to validate. */ - private validateModifiedPermission(permission: Permission): void { + private validateModifiedPermission( + permission: ExtractPermission, + ): void { const targetKey = this.getTargetKey(permission.parentCapability); /* istanbul ignore next: this should be impossible */ if (!targetKey) { @@ -970,10 +1019,10 @@ export class PermissionController< * @returns The internal key of the method. */ private getTargetKey( - method: Permission['parentCapability'], - ): TargetKey | undefined { + method: ExtractPermission['parentCapability'], + ): Specification['targetKey'] | undefined { if (hasProperty(this._permissionSpecifications, method)) { - return method as TargetKey; + return method as Specification['targetKey']; } const wildCardMethodsWithoutWildCard: Record = {}; @@ -999,7 +1048,7 @@ export class PermissionController< } if (wildCardMethodsWithoutWildCard[targetKey]) { - return `${targetKey}*` as TargetKey; + return `${targetKey}*` as Specification['targetKey']; } return undefined; @@ -1029,24 +1078,30 @@ export class PermissionController< requestData, preserveExistingPermissions = true, subject, - }: GrantPermissionsOptions): SubjectPermissions { + }: GrantPermissionsOptions): SubjectPermissions< + ExtractPermission + > { const { origin } = subject; if (!origin || typeof origin !== 'string') { throw new InvalidSubjectIdentifierError(origin); } - const permissions: SubjectPermissions = - ((preserveExistingPermissions && { - ...this.getPermissions(origin), - }) || - {}) as SubjectPermissions; + const permissions: SubjectPermissions< + ExtractPermission + > = ((preserveExistingPermissions && { + ...this.getPermissions(origin), + }) || + {}) as SubjectPermissions>; for (const [requestedTarget, approvedPermission] of Object.entries( approvedPermissions, )) { const targetKey = this.getTargetKey( - requestedTarget as Permission['parentCapability'], + requestedTarget as ExtractPermission< + Specification, + Caveat + >['parentCapability'], ); if (!targetKey) { throw methodNotFound({ method: requestedTarget }); @@ -1065,7 +1120,10 @@ export class PermissionController< // The requested target must be a valid target name if we found its key. // We reassign it to change its type. - const targetName = requestedTarget as Permission['parentCapability']; + const targetName = requestedTarget as ExtractPermission< + Specification, + Caveat + >['parentCapability']; const specification = this.permissionSpecifications[targetKey]; const caveats = this.constructCaveats( @@ -1074,7 +1132,9 @@ export class PermissionController< approvedPermission.caveats, ); - const permissionOptions: PermissionOptions = { + const permissionOptions: PermissionOptions< + ExtractPermission + > = { caveats, invoker: origin, target: targetName, @@ -1084,7 +1144,7 @@ export class PermissionController< specification.factory ? specification.factory(permissionOptions, requestData) : constructPermission(permissionOptions) - ) as Permission; + ) as ExtractPermission; specification.validator?.(permission, origin, targetName); permissions[targetName] = permission; } @@ -1105,7 +1165,7 @@ export class PermissionController< */ private setValidatedPermissions( origin: OriginString, - permissions: Record, + permissions: Record>, ): void { this.update((draftState) => { if (!draftState.subjects[origin]) { @@ -1131,7 +1191,7 @@ export class PermissionController< */ private constructCaveats( origin: OriginString, - target: Permission['parentCapability'], + target: ExtractPermission['parentCapability'], requestedCaveats?: unknown[] | null, ): NonEmptyArray | undefined { const caveatArray = requestedCaveats?.map((requestedCaveat) => @@ -1159,7 +1219,7 @@ export class PermissionController< */ private constructCaveat( origin: OriginString, - target: Permission['parentCapability'], + target: ExtractPermission['parentCapability'], requestedCaveat: unknown, ): Caveat { if (!isPlainObject(requestedCaveat)) { @@ -1218,7 +1278,12 @@ export class PermissionController< requestedPermissions: RequestedPermissions, id: string = nanoid(), preserveExistingPermissions = true, - ): Promise<[Record, { id: string; origin: string }]> { + ): Promise< + [ + Record>, + { id: string; origin: string }, + ] + > { this.validateRequestedPermissions(origin, requestedPermissions); const metadata: PermissionsRequest['metadata'] = { id, @@ -1305,8 +1370,9 @@ export class PermissionController< const perms: RequestedPermissions = requestedPermissions; for (const methodName of Object.keys(perms)) { - const target = (perms[methodName] as unknown as Permission) - .parentCapability; + const target = ( + perms[methodName] as unknown as ExtractPermission + ).parentCapability; if (target !== undefined && methodName !== target) { throw invalidParams({ data: { origin, requestedPermissions } }); } @@ -1418,7 +1484,7 @@ export class PermissionController< */ async executeRestrictedMethod( origin: string, - methodName: Permission['parentCapability'], + methodName: ExtractPermission['parentCapability'], params?: GenericRestrictedMethodParams, ): Promise { // Throws if the method does not exist @@ -1458,7 +1524,7 @@ export class PermissionController< Json >, subject: PermissionSubjectMetadata, - method: Permission['parentCapability'], + method: ExtractPermission['parentCapability'], params: GenericRestrictedMethodParams = [], ): ReturnType> { const { origin } = subject; @@ -1491,7 +1557,7 @@ export class PermissionController< origin: string, ): RestrictedMethodBase { const methodImplementation = this.getRestrictedMethod( - method as Permission['parentCapability'], + method as ExtractPermission['parentCapability'], ); if (!methodImplementation) { throw methodNotFound({ method, data: { origin } }); From f5cd9d097d565919781bca75bde7b6abb4747832 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sat, 9 Oct 2021 11:25:36 -0700 Subject: [PATCH 076/147] WIP: Refactor controller types II (Builds) --- .../controllers/src/permissions/Permission.ts | 17 +- .../permissions/PermissionController.test.ts | 169 ++++++++++-------- .../src/permissions/PermissionController.ts | 68 ++++--- 3 files changed, 141 insertions(+), 113 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 3a139fe4d5..87960656d6 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -272,7 +272,15 @@ type RestrictedMethodContext = Readonly<{ [key: string]: any; }>; -export type GenericRestrictedMethodParams = Json[] | Record; +export type GenericRestrictedMethodParams = + | Json[] + | Record + | never; + +export type GenericRestrictedMethod = RestrictedMethodBase< + GenericRestrictedMethodParams, + Json +>; /** * The arguments passed to a restricted method implementation. @@ -325,8 +333,8 @@ export type RestrictedMethodBase< export type RestrictedMethodConstraint< // MethodImplementation extends RestrictedMethodBase, MethodImplementation, -> = MethodImplementation extends (...args: infer Args) => Json - ? Args extends GenericRestrictedMethodParams +> = MethodImplementation extends (args: infer Args) => Json | Promise + ? Args extends RestrictedMethodOptions ? MethodImplementation : never : never; @@ -425,6 +433,8 @@ type GenericFunction = (...args: any[]) => any; export type NewSpecShape = { targetKey: TargetKey; methodImplementation: GenericFunction; + // methodImplementation: GenericRestrictedMethod; + // methodImplementation: (args: RestrictedMethodOptions) => Json | Promise, allowedCaveats?: Readonly>; validator?: GenericFunction; factory?: GenericFunction; @@ -543,6 +553,7 @@ export type PermissionSpecificationsMap2< export type PermissionSpecificationsMap3< Specifications extends NewSpecShape, > = Record; +// > = PermissionSpecificationConstraint2 extends never ? never : Record; export type PermissionSpecificationsMap< Specifications extends PermissionSpecificationBase, diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 2a87e6b708..25a4fed0b9 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -157,7 +157,7 @@ function getDefaultCaveatSpecifications(): CaveatSpecifications // Permission types and specifications -// wallet_getSecretArray +// // wallet_getSecretArray // type SecretArrayPermissionName = 'wallet_getSecretArray'; @@ -166,16 +166,7 @@ function getDefaultCaveatSpecifications(): CaveatSpecifications // FilterArrayCaveat | ReverseArrayCaveat // >; -// type SecretArrayMethod = RestrictedMethodBase<[], string[]>; - -// type SecretArrayPermissionSpecification = PermissionSpecificationConstraint2< -// SecretArrayPermissionName, -// SecretArrayPermission, -// Record, -// SecretArrayMethod -// >; - -// wallet_getSecretObject +// // wallet_getSecretObject // type SecretObjectPermissionName = 'wallet_getSecretObject'; @@ -184,8 +175,6 @@ function getDefaultCaveatSpecifications(): CaveatSpecifications // FilterObjectCaveat | NoopCaveat // >; -// type SecretObjectMethod = RestrictedMethodBase<[], Record>; - // wallet_getSecret_* type SecretNamespacedPermissionKey = 'wallet_getSecret_*'; @@ -195,53 +184,20 @@ type SecretNamespacedPermission = PermissionConstraint< NoopCaveat >; -// type SecretNamespacedMethod = RestrictedMethodBase<[], string>; - -// Dummy permission that returns an error object -// type GetErrorName = 'wallet_getError'; - -// type GetErrorPermission = PermissionConstraint; - -// Illegal dummy permission -// type GetUndefinedName = 'wallet_getUndefined'; - -// type GetUndefinedPermission = PermissionConstraint; - -// type DefaultTargetKeys = -// | SecretArrayPermissionName -// | SecretObjectPermissionName -// | SecretNamespacedPermissionKey; -// | GetErrorName -// | GetUndefinedName; - -// type DefaultPermissions = -// | SecretArrayPermission -// | SecretObjectPermission -// | SecretNamespacedPermission; -// | GetErrorPermission -// | GetUndefinedPermission; - -// type DefaultRestrictedMethods = -// | SecretArrayMethod -// | SecretObjectMethod -// | SecretNamespacedMethod; - -// type DefaultPermissionSpecifications = -// | SecretArrayPermissionSpecification -// | SecretObjectPermissionSpecification -// | SecretNamespacedPermissionSpecification; - -enum PermissionKeys { - 'wallet_getSecretArray' = 'wallet_getSecretArray', - 'wallet_getSecretObject' = 'wallet_getSecretObject', - 'wallet_getSecret_*' = 'wallet_getSecret_*', +/** + * Permission key constants. + */ +const PermissionKeys = { + wallet_getSecretArray: 'wallet_getSecretArray', + wallet_getSecretObject: 'wallet_getSecretObject', + 'wallet_getSecret_*': 'wallet_getSecret_*', // 'wallet_getError' = 'wallet_getError', // 'wallet_getUndefined' = 'wallet_getUndefined', -} +} as const; /** - * Permission name (as opposed to keys) enum. Since one of the permissions are - * namespaced, it's a getter function. + * Permission name (as opposed to keys) constants and getters. Since one of the + * permissions are namespaced, it's a getter function. */ const PermissionNames = { wallet_getSecretArray: PermissionKeys.wallet_getSecretArray, @@ -249,7 +205,7 @@ const PermissionNames = { wallet_getSecret_: (str: string) => `wallet_getSecret_${str}` as const, // wallet_getError: 'wallet_getError' as const, // wallet_getUndefined: 'wallet_getUndefined' as const, -}; +} as const; /** * Gets permission specifications for: @@ -272,19 +228,21 @@ function getDefaultPermissionSpecifications() { allowedCaveats: [ CaveatTypes.filterArrayResponse, CaveatTypes.reverseArrayResponse, - ] as const, - // allowedCaveats: ['foo'] as const, - methodImplementation: (_args: RestrictedMethodOptions<[]>) => { + ], + methodImplementation: (_args: RestrictedMethodOptions) => { return ['a', 'b', 'c']; + // return () => undefined; }, }, [PermissionKeys.wallet_getSecretObject]: { targetKey: PermissionKeys.wallet_getSecretObject, - allowedCaveats: [CaveatTypes.filterObjectResponse] as const, - methodImplementation: ( - _args: RestrictedMethodOptions, - ) => { + allowedCaveats: [ + CaveatTypes.filterObjectResponse, + CaveatTypes.noopCaveat, + ], + methodImplementation: (_args: RestrictedMethodOptions) => { return { a: 'x', b: 'y', c: 'z' }; + // return () => undefined; }, validator: (permission: GenericPermission) => { // A dummy validator for a caveat type that should be impossible to add @@ -298,14 +256,13 @@ function getDefaultPermissionSpecifications() { }, [PermissionKeys['wallet_getSecret_*']]: { targetKey: PermissionKeys['wallet_getSecret_*'], - allowedCaveats: [CaveatTypes.noopCaveat] as const, - methodImplementation: ( - args: RestrictedMethodOptions, - ) => { + allowedCaveats: [CaveatTypes.noopCaveat], + methodImplementation: (args: RestrictedMethodOptions<[]>) => { return `Hello, secret friend "${args.method.replace( 'wallet_getSecret_', '', )}"!`; + // return Symbol('foo') }, factory: (options: PermissionOptions) => constructPermission({ @@ -329,12 +286,80 @@ function getDefaultPermissionSpecifications() { // wallet_getUndefined: { // target: PermissionKeys.wallet_getUndefined, // } - }; + } as const; } -// type Keys = keyof typeof MyType; -// type Values = typeof MyType[Keys]; -// type Thing = T extends +// type RestrictedMethodContext = Readonly<{ +// origin: string; +// [key: string]: any; +// }>; + +// const specs = getDefaultPermissionSpecifications() +// type DoesExtend = T extends U ? T : never; +// // type func = typeof specs.wallet_getSecretArray.methodImplementation +// type func = typeof specs['wallet_getSecret_*']['methodImplementation'] +// type x = DoesExtend +// type xx = DoesExtend) => Json | Promise>; +// type xxx = DoesExtend Json | Promise>; +// type xxxx = DoesExtend Json | Promise>; +// type y = DoesExtend +// type yy = DoesExtend, RestrictedMethodOptions>; +// type z = DoesExtend +// type zz = DoesExtend<[], Json> + +// type RestrictedMethodConstraint2< +// // MethodImplementation extends RestrictedMethodBase, +// MethodImplementation, +// > = MethodImplementation extends (args: infer Args) => Json | Promise +// ? Args extends RestrictedMethodOptions +// ? MethodImplementation +// // ? Args extends any +// // ? Args +// : never +// : never; + +// type RestrictedMethodConstraint3< +// // MethodImplementation extends RestrictedMethodBase, +// MethodImplementation, +// > = MethodImplementation extends (args: infer Args) => infer Result +// ? Args extends RestrictedMethodOptions +// ? Result extends Json | Promise +// ? MethodImplementation +// : never +// : never +// : never; + +// type funcA = (args: RestrictedMethodOptions<[]>) => Promise +// type funcB = (args: RestrictedMethodOptions<[]>) => (() => undefined) +// type funcC = (args: RestrictedMethodOptions<[]>) => Promise<() => undefined> + +// type a = RestrictedMethodConstraint3 +// type b = RestrictedMethodConstraint3 +// type c = RestrictedMethodConstraint3 + +// type j = PermissionSpecificationConstraint2 +// type k = PermissionSpecificationConstraint2 + +// type PermissionControllerOptions< +// PermissionSpecification extends NewSpecShape, +// Caveat extends GenericCaveat, +// > = PermissionSpecificationConstraint2 extends never +// ? never +// : { +// messenger: any; +// caveatSpecifications: CaveatSpecifications; +// // permissionSpecifications: PermissionSpecificationsMap2>; +// permissionSpecifications: PermissionSpecificationsMap2; +// unrestrictedMethods: string[]; +// state?: Partial<{}> +// }; + +// type q = PermissionControllerOptions + type GetSpecMapValues = T extends PermissionSpecificationsMap2< NewSpecShape > diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 16860b2409..f6746041d6 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -31,19 +31,17 @@ import { ExtractValidCaveatTypes, findCaveat, GenericPermission, - MutableCaveats, OriginString, PermissionOptions, RequestedPermissions, RestrictedMethodBase, GenericRestrictedMethodParams, PermissionSpecificationConstraint2, - PermissionSpecificationsMap3, + PermissionSpecificationsMap2, NewSpecShape, PermissionConstraint, ExtractCaveatsFromTypes, ExtractCaveatTypes, - ExtractCaveats, } from './Permission'; import { PermissionDoesNotExistError, @@ -269,10 +267,15 @@ export type GenericPermissionController = PermissionController< export type ExtractPermission< PermissionSpecification extends NewSpecShape, Caveat extends GenericCaveat, -> = PermissionConstraint< - PermissionSpecification['targetKey'], - ExtractCaveatsFromTypes, Caveat> ->; +> = PermissionSpecification extends any + ? PermissionConstraint< + PermissionSpecification['targetKey'], + ExtractCaveatsFromTypes< + ExtractCaveatTypes, + Caveat + > + > + : never; /** * Options for the {@link PermissionController} constructor. @@ -294,7 +297,7 @@ export type PermissionControllerOptions< : { messenger: PermissionControllerMessenger; caveatSpecifications: CaveatSpecifications; - permissionSpecifications: PermissionSpecificationsMap3; + permissionSpecifications: PermissionSpecificationsMap2; unrestrictedMethods: string[]; state?: Partial< PermissionControllerState< @@ -338,14 +341,14 @@ export class PermissionController< } private readonly _permissionSpecifications: Readonly< - PermissionSpecificationsMap3 + PermissionSpecificationsMap2 >; /** - * The {@link PermissionSpecificationsMap3} of the controller. + * The {@link PermissionSpecificationsMap2} of the controller. */ public get permissionSpecifications(): Readonly< - PermissionSpecificationsMap3 + PermissionSpecificationsMap2 > { return this._permissionSpecifications; } @@ -378,7 +381,7 @@ export class PermissionController< * documentation for more details. * @param options.permissionSpecifications - The specifications of all * permissions available to the controller. See - * {@link PermissionSpecificationsMap3} and the documentation for more details. + * {@link PermissionSpecificationsMap2} and the documentation for more details. * @param options.unrestrictedMethods - The callable names of all JSON-RPC * methods ignored by the new controller. * @param options.messenger - The controller messenger. See @@ -435,7 +438,7 @@ export class PermissionController< * controller's constructor. */ private validatePermissionSpecifications( - specifications: PermissionSpecificationsMap3, + specifications: PermissionSpecificationsMap2, ) { Object.entries(specifications).forEach( ([targetKey, { targetKey: innerTargetKey }]) => { @@ -708,14 +711,11 @@ export class PermissionController< Specification, Caveat >['parentCapability'], - // CaveatType extends ExtractValidCaveatTypes< - // ExtractPermission, - // Specification['targetKey'], - // TargetName - // >, - CaveatType extends ExtractCaveats< - ExtractPermission - >['type'], + CaveatType extends ExtractValidCaveatTypes< + ExtractPermission, + Specification['targetKey'], + TargetName + >, >(origin: OriginString, target: TargetName, caveatType: CaveatType): boolean { return Boolean(this.getCaveat(origin, target, caveatType)); } @@ -877,9 +877,7 @@ export class PermissionController< caveatValue: ExtractCaveatValue, ): void { this.update((draftState) => { - const subject = draftState.subjects[origin] as - | PermissionSubjectEntry> - | undefined; + const subject = draftState.subjects[origin]; // This should be impossible in our usage, but TypeScript wants it /* istanbul ignore if */ @@ -888,9 +886,7 @@ export class PermissionController< } // Typecast: immer's WritableDraft is incompatible with our generics - const permission: MutableCaveats< - ExtractPermission - > = subject.permissions[target]; + const permission = subject.permissions[target]; // This should also be impossible in our usage, but TypeScript wants it /* istanbul ignore if */ @@ -923,7 +919,8 @@ export class PermissionController< } this.validateModifiedPermission( - permission as ExtractPermission, + // TODO:types why? + permission as unknown as ExtractPermission, ); }); } @@ -953,14 +950,7 @@ export class PermissionController< >, >(origin: OriginString, target: TargetName, caveatType: CaveatType): void { this.update((draftState) => { - // Typecast: immer's WritableDraft is incompatible with our generics - const permission: MutableCaveats< - ExtractPermission - > = ( - draftState.subjects as PermissionControllerSubjects< - ExtractPermission - > - )[origin]?.permissions[target]; + const permission = draftState.subjects[origin]?.permissions[target]; if (!permission) { throw new PermissionDoesNotExistError(origin, target); } @@ -977,13 +967,15 @@ export class PermissionController< } if (permission.caveats.length === 1) { - permission.caveats = null; + // TODO:types why? + permission.caveats = null as any; } else { permission.caveats.splice(caveatIndex, 1); } this.validateModifiedPermission( - permission as ExtractPermission, + // TODO:types why? + permission as unknown as ExtractPermission, ); }); } From eeb6812891ae5b43422a88108746625821b6c7e8 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sat, 9 Oct 2021 12:36:02 -0700 Subject: [PATCH 077/147] WIP: Refactor controller types III (caveat specifications generic) --- .../controllers/src/permissions/Caveat.ts | 114 ++++--- .../permissions/PermissionController.test.ts | 70 +++-- .../src/permissions/PermissionController.ts | 280 +++++++++++------- 3 files changed, 287 insertions(+), 177 deletions(-) diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index b0912ed5df..75ec498e59 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -66,6 +66,16 @@ export type CaveatDecorator = ( caveat: Caveat, ) => AsyncRestrictedMethod; +type ExtractCaveatValueFromDecorator> = + Decorator extends ( + decorated: any, + caveat: infer Caveat, + ) => AsyncRestrictedMethod + ? Caveat extends GenericCaveat + ? Caveat['value'] + : never + : never; + /** * */ @@ -75,32 +85,48 @@ export type CaveatValidator = ( target?: string, ) => void; -export type CaveatSpecification = { - /** - * The string type of the caveat. - */ - type: Caveat['type']; - - /** - * The decorator function used to apply the caveat to restricted method - * requests. - */ - decorator: CaveatDecorator; - - /** - * The validator function used to validate caveats of the associated type - * whenever they are instantiated. Caveat are instantiated whenever they are - * created or mutated. - * - * The validator should throw an appropriate JSON-RPC error if validation fails. - * - * If no validator is specified, no validation of caveat values will be - * performed. Although caveats can also be validated by permission validators, - * validating caveat values separately is strongly recommended. - */ - validator?: CaveatValidator; +// export type CaveatSpecification = { +// /** +// * The string type of the caveat. +// */ +// type: Caveat['type']; + +// /** +// * The decorator function used to apply the caveat to restricted method +// * requests. +// */ +// decorator: CaveatDecorator; + +// /** +// * The validator function used to validate caveats of the associated type +// * whenever they are instantiated. Caveat are instantiated whenever they are +// * created or mutated. +// * +// * The validator should throw an appropriate JSON-RPC error if validation fails. +// * +// * If no validator is specified, no validation of caveat values will be +// * performed. Although caveats can also be validated by permission validators, +// * validating caveat values separately is strongly recommended. +// */ +// validator?: CaveatValidator; +// }; + +export type CaveatSpecification2 = { + type: Type; + + decorator: CaveatDecorator; + + validator?: CaveatValidator; }; +export type ExtractCaveatsFromSpec> = + Spec extends any + ? CaveatConstraint< + Spec['type'], + ExtractCaveatValueFromDecorator + > + : never; + /** * A generic caveat. */ @@ -109,21 +135,32 @@ export type GenericCaveat = CaveatConstraint; /** * An object mapping the type of each caveat to its corresponding specification. */ -export type CaveatSpecifications = { - [Type in Caveat['type']]: ExtractCaveatSpecification; +// export type CaveatSpecifications = { +// [Type in Caveat['type']]: ExtractCaveatSpecification; +// }; + +export type CaveatSpecifications2< + CaveatSpecification extends CaveatSpecification2, +> = { + [Key in CaveatSpecification['type']]: CaveatSpecification; }; -type ExtractCaveatSpecification< - Caveat extends GenericCaveat, - CaveatType extends string, -> = Caveat extends CaveatConstraint - ? CaveatSpecification - : never; +// type ExtractCaveatSpecification< +// Caveat extends GenericCaveat, +// CaveatType extends string, +// > = Caveat extends CaveatConstraint +// ? CaveatSpecification +// : never; export type ExtractCaveatFromType< - Caveat extends GenericCaveat, + CaveatSpecification extends CaveatSpecification2, CaveatType extends string, -> = Caveat extends CaveatConstraint ? Caveat : never; +> = ExtractCaveatsFromSpec extends CaveatConstraint< + CaveatType, + Json +> + ? ExtractCaveatsFromSpec + : never; /** * Decorate a restricted method implementation with its caveats. @@ -131,13 +168,15 @@ export type ExtractCaveatFromType< * Note that all caveat functions (i.e. the argument and return value of the * decorator) must be awaited. */ -export function decorateWithCaveats( +export function decorateWithCaveats< + CaveatSpecification extends CaveatSpecification2, +>( methodImplementation: RestrictedMethodBase< GenericRestrictedMethodParams, Json >, permission: Readonly, // bound to the requesting origin - caveatSpecifications: CaveatSpecifications, // all caveat implementations + caveatSpecifications: CaveatSpecifications2, // all caveat implementations ): RestrictedMethodBase { const { caveats } = permission; if (!caveats) { @@ -150,7 +189,8 @@ export function decorateWithCaveats( >; for (const caveat of caveats) { - const specification = caveatSpecifications[caveat.type as Caveat['type']]; + const specification = + caveatSpecifications[caveat.type as CaveatSpecification['type']]; if (!specification) { throw new UnrecognizedCaveatTypeError(caveat.type); } diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 25a4fed0b9..a025815eec 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -13,7 +13,6 @@ import { constructCaveat } from './Caveat'; import { AsyncRestrictedMethod, CaveatConstraint, - CaveatSpecifications, constructPermission, GenericCaveat, GenericPermission, @@ -27,39 +26,41 @@ import { GenericRestrictedMethodParams, NewSpecShape, PermissionSpecificationsMap2, + CaveatSpecifications2, + CaveatSpecification2, } from '.'; // CaveatConstraint types and specifications -enum CaveatTypes { - filterArrayResponse = 'filterArrayResponse', - reverseArrayResponse = 'reverseArrayResponse', - filterObjectResponse = 'filterObjectResponse', - noopCaveat = 'noopCaveat', -} +const CaveatTypes = { + filterArrayResponse: 'filterArrayResponse', + reverseArrayResponse: 'reverseArrayResponse', + filterObjectResponse: 'filterObjectResponse', + noopCaveat: 'noopCaveat', +} as const; type FilterArrayCaveat = CaveatConstraint< - CaveatTypes.filterArrayResponse, + typeof CaveatTypes.filterArrayResponse, string[] >; type ReverseArrayCaveat = CaveatConstraint< - CaveatTypes.reverseArrayResponse, + typeof CaveatTypes.reverseArrayResponse, null >; type FilterObjectCaveat = CaveatConstraint< - CaveatTypes.filterObjectResponse, + typeof CaveatTypes.filterObjectResponse, string[] >; -type NoopCaveat = CaveatConstraint; +type NoopCaveat = CaveatConstraint; -type DefaultCaveats = - | FilterArrayCaveat - | ReverseArrayCaveat - | FilterObjectCaveat - | NoopCaveat; +// type DefaultCaveats = +// | FilterArrayCaveat +// | ReverseArrayCaveat +// | FilterObjectCaveat +// | NoopCaveat; /** * Gets caveat specifications for: @@ -71,7 +72,7 @@ type DefaultCaveats = * * @returns The caveat specifications. */ -function getDefaultCaveatSpecifications(): CaveatSpecifications { +function getDefaultCaveatSpecifications() { return { filterArrayResponse: { type: CaveatTypes.filterArrayResponse, @@ -146,15 +147,28 @@ function getDefaultCaveatSpecifications(): CaveatSpecifications ) => { return method(args); }, - validator: (caveat: { type: CaveatTypes.noopCaveat; value: unknown }) => { + validator: (caveat: { + type: typeof CaveatTypes.noopCaveat; + value: unknown; + }) => { if (caveat.value !== null) { throw new Error('NoopCaveat value must be null'); } }, }, - }; + } as const; } +type GetCaveatSpecMapValues = T extends CaveatSpecifications2< + CaveatSpecification2 +> + ? T[keyof T] + : never; + +type DefaultCaveatSpecifications = GetCaveatSpecMapValues< + ReturnType +>; + // Permission types and specifications // // wallet_getSecretArray @@ -481,7 +495,7 @@ function getDefaultPermissionController( ) { return new PermissionController< DefaultPermissionSpecifications, - DefaultCaveats + DefaultCaveatSpecifications >(opts); } @@ -496,7 +510,7 @@ function getDefaultPermissionController( function getDefaultPermissionControllerWithState() { return new PermissionController< DefaultPermissionSpecifications, - DefaultCaveats + DefaultCaveatSpecifications >(getPermissionControllerOptions({ state: getExistingPermissionState() })); } @@ -544,7 +558,7 @@ describe('PermissionController', () => { () => new PermissionController< DefaultPermissionSpecifications, - DefaultCaveats + DefaultCaveatSpecifications >( getPermissionControllerOptions({ permissionSpecifications: { @@ -559,7 +573,7 @@ describe('PermissionController', () => { () => new PermissionController< DefaultPermissionSpecifications, - DefaultCaveats + DefaultCaveatSpecifications >( getPermissionControllerOptions({ permissionSpecifications: { @@ -574,7 +588,7 @@ describe('PermissionController', () => { () => new PermissionController< DefaultPermissionSpecifications, - DefaultCaveats + DefaultCaveatSpecifications >( getPermissionControllerOptions({ permissionSpecifications: { @@ -589,7 +603,7 @@ describe('PermissionController', () => { () => new PermissionController< DefaultPermissionSpecifications, - DefaultCaveats + DefaultCaveatSpecifications >( getPermissionControllerOptions({ permissionSpecifications: { @@ -2829,7 +2843,7 @@ describe('PermissionController', () => { const { messenger } = options; const controller = new PermissionController< DefaultPermissionSpecifications, - DefaultCaveats + DefaultCaveatSpecifications >(options); const clearStateSpy = jest.spyOn(controller, 'clearState'); @@ -2852,7 +2866,7 @@ describe('PermissionController', () => { const { messenger } = options; const controller = new PermissionController< DefaultPermissionSpecifications, - DefaultCaveats + DefaultCaveatSpecifications >(options); const getSubjectNamesSpy = jest.spyOn(controller, 'getSubjectNames'); @@ -2878,7 +2892,7 @@ describe('PermissionController', () => { const { messenger } = options; const controller = new PermissionController< DefaultPermissionSpecifications, - DefaultCaveats + DefaultCaveatSpecifications >(options); const hasPermissionsSpy = jest.spyOn(controller, 'hasPermissions'); diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index f6746041d6..e84d8a8683 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -20,15 +20,16 @@ import { nanoid } from 'nanoid'; import { isPlainObject, hasProperty, NonEmptyArray } from '../utils'; import { CaveatConstraint, - CaveatSpecifications, constructCaveat as _constructCaveat, decorateWithCaveats, GenericCaveat, ExtractCaveatFromType, + CaveatSpecification2, + ExtractCaveatsFromSpec, + CaveatSpecifications2, } from './Caveat'; import { constructPermission, - ExtractValidCaveatTypes, findCaveat, GenericPermission, OriginString, @@ -40,8 +41,6 @@ import { PermissionSpecificationsMap2, NewSpecShape, PermissionConstraint, - ExtractCaveatsFromTypes, - ExtractCaveatTypes, } from './Permission'; import { PermissionDoesNotExistError, @@ -252,7 +251,7 @@ type GrantPermissionsOptions = { export type GenericPermissionController = PermissionController< NewSpecShape, - GenericCaveat + CaveatSpecification2 >; // type ExtractPermission = Type extends PermissionSpecificationConstraint< @@ -266,14 +265,11 @@ export type GenericPermissionController = PermissionController< export type ExtractPermission< PermissionSpecification extends NewSpecShape, - Caveat extends GenericCaveat, + CaveatSpecification extends CaveatSpecification2, > = PermissionSpecification extends any ? PermissionConstraint< PermissionSpecification['targetKey'], - ExtractCaveatsFromTypes< - ExtractCaveatTypes, - Caveat - > + ExtractCaveatsFromSpec > : never; @@ -291,17 +287,17 @@ export type ExtractPermission< */ export type PermissionControllerOptions< PermissionSpecification extends NewSpecShape, - Caveat extends GenericCaveat, + CaveatSpecification extends CaveatSpecification2, > = PermissionSpecificationConstraint2 extends never ? never : { messenger: PermissionControllerMessenger; - caveatSpecifications: CaveatSpecifications; + caveatSpecifications: CaveatSpecifications2; permissionSpecifications: PermissionSpecificationsMap2; unrestrictedMethods: string[]; state?: Partial< PermissionControllerState< - ExtractPermission + ExtractPermission > >; }; @@ -322,33 +318,37 @@ export type PermissionControllerOptions< * of the controller. */ export class PermissionController< - Specification extends NewSpecShape, - Caveat extends GenericCaveat, + PermissionSpecification extends NewSpecShape, + CaveatSpecification extends CaveatSpecification2, > extends BaseController< typeof controllerName, - PermissionControllerState>, + PermissionControllerState< + ExtractPermission + >, PermissionControllerMessenger > { private readonly _caveatSpecifications: Readonly< - CaveatSpecifications + CaveatSpecifications2 >; /** * The {@link CaveatSpecifications} of the controller. */ - public get caveatSpecifications(): Readonly> { + public get caveatSpecifications(): Readonly< + CaveatSpecifications2 + > { return this._caveatSpecifications; } private readonly _permissionSpecifications: Readonly< - PermissionSpecificationsMap2 + PermissionSpecificationsMap2 >; /** * The {@link PermissionSpecificationsMap2} of the controller. */ public get permissionSpecifications(): Readonly< - PermissionSpecificationsMap2 + PermissionSpecificationsMap2 > { return this._permissionSpecifications; } @@ -389,7 +389,12 @@ export class PermissionController< * @param options.state - Existing state to hydrate the controller with at * initialization. */ - constructor(options: PermissionControllerOptions) { + constructor( + options: PermissionControllerOptions< + PermissionSpecification, + CaveatSpecification + >, + ) { const { caveatSpecifications, permissionSpecifications, @@ -404,7 +409,7 @@ export class PermissionController< messenger, state: { ...(defaultState as PermissionControllerState< - ExtractPermission + ExtractPermission >), ...state, }, @@ -438,9 +443,9 @@ export class PermissionController< * controller's constructor. */ private validatePermissionSpecifications( - specifications: PermissionSpecificationsMap2, + specifications: PermissionSpecificationsMap2, ) { - Object.entries(specifications).forEach( + Object.entries(specifications).forEach( ([targetKey, { targetKey: innerTargetKey }]) => { // Check if the target key is the empty string, ends with "_", or ends // with "*" but not "_*" @@ -498,7 +503,10 @@ export class PermissionController< method: string, ): RestrictedMethodBase | undefined { const targetKey = this.getTargetKey( - method as ExtractPermission['parentCapability'], + method as ExtractPermission< + PermissionSpecification, + CaveatSpecification + >['parentCapability'], ); if (!targetKey) { return undefined; @@ -523,7 +531,10 @@ export class PermissionController< * @returns The permission if it exists, or undefined otherwise. */ getPermission< - SubjectPermission extends ExtractPermission, + SubjectPermission extends ExtractPermission< + PermissionSpecification, + CaveatSpecification + >, >( origin: OriginString, target: SubjectPermission['parentCapability'], @@ -541,10 +552,16 @@ export class PermissionController< */ getPermissions( origin: OriginString, - ): SubjectPermissions> | undefined { + ): + | SubjectPermissions< + ExtractPermission + > + | undefined { // TODO:types why? return this.state.subjects[origin]?.permissions as - | SubjectPermissions> + | SubjectPermissions< + ExtractPermission + > | undefined; } @@ -558,7 +575,10 @@ export class PermissionController< */ hasPermission( origin: OriginString, - target: ExtractPermission['parentCapability'], + target: ExtractPermission< + PermissionSpecification, + CaveatSpecification + >['parentCapability'], ): boolean { return Boolean(this.getPermission(origin, target)); } @@ -601,7 +621,10 @@ export class PermissionController< */ revokePermission( origin: OriginString, - target: ExtractPermission['parentCapability'], + target: ExtractPermission< + PermissionSpecification, + CaveatSpecification + >['parentCapability'], ): void { this.revokePermissions({ [origin]: [target] }); } @@ -618,7 +641,10 @@ export class PermissionController< subjectsAndPermissions: Record< OriginString, NonEmptyArray< - ExtractPermission['parentCapability'] + ExtractPermission< + PermissionSpecification, + CaveatSpecification + >['parentCapability'] > >, ): void { @@ -653,7 +679,10 @@ export class PermissionController< * @param target - The name of the target to revoke all permissions for. */ revokePermissionForAllSubjects( - target: ExtractPermission['parentCapability'], + target: ExtractPermission< + PermissionSpecification, + CaveatSpecification + >['parentCapability'], ): void { if (this.getSubjectNames().length === 0) { return; @@ -678,11 +707,16 @@ export class PermissionController< private deletePermission( draftState: PermissionControllerState< - ExtractPermission + ExtractPermission + >, + permissions: SubjectPermissions< + ExtractPermission >, - permissions: SubjectPermissions>, origin: OriginString, - target: ExtractPermission['parentCapability'], + target: ExtractPermission< + PermissionSpecification, + CaveatSpecification + >['parentCapability'], ): void { if (Object.keys(permissions).length > 1) { delete permissions[target]; @@ -708,14 +742,10 @@ export class PermissionController< */ hasCaveat< TargetName extends ExtractPermission< - Specification, - Caveat + PermissionSpecification, + CaveatSpecification >['parentCapability'], - CaveatType extends ExtractValidCaveatTypes< - ExtractPermission, - Specification['targetKey'], - TargetName - >, + CaveatType extends CaveatSpecification['type'], >(origin: OriginString, target: TargetName, caveatType: CaveatType): boolean { return Boolean(this.getCaveat(origin, target, caveatType)); } @@ -737,26 +767,22 @@ export class PermissionController< */ getCaveat< TargetName extends ExtractPermission< - Specification, - Caveat + PermissionSpecification, + CaveatSpecification >['parentCapability'], - CaveatType extends ExtractValidCaveatTypes< - ExtractPermission, - Specification['targetKey'], - TargetName - >, + CaveatType extends CaveatSpecification['type'], >( origin: OriginString, target: TargetName, caveatType: CaveatType, - ): ExtractCaveatFromType | undefined { + ): ExtractCaveatFromType | undefined { const permission = this.getPermission(origin, target); if (!permission) { throw new PermissionDoesNotExistError(origin, target); } return findCaveat(permission, caveatType) as ExtractCaveatFromType< - Caveat, + CaveatSpecification, CaveatType >; } @@ -782,19 +808,18 @@ export class PermissionController< */ addCaveat< TargetName extends ExtractPermission< - Specification, - Caveat + PermissionSpecification, + CaveatSpecification >['parentCapability'], - CaveatType extends ExtractValidCaveatTypes< - ExtractPermission, - Specification['targetKey'], - TargetName - >, + CaveatType extends CaveatSpecification['type'], >( origin: OriginString, target: TargetName, caveatType: CaveatType, - caveatValue: ExtractCaveatValue, + caveatValue: ExtractCaveatValue< + ExtractCaveatsFromSpec, + CaveatType + >, ): void { if (this.hasCaveat(origin, target, caveatType)) { throw new CaveatAlreadyExistsError(origin, target, caveatType); @@ -823,19 +848,18 @@ export class PermissionController< */ updateCaveat< TargetName extends ExtractPermission< - Specification, - Caveat + PermissionSpecification, + CaveatSpecification >['parentCapability'], - CaveatType extends ExtractValidCaveatTypes< - ExtractPermission, - Specification['targetKey'], - TargetName - >, + CaveatType extends CaveatSpecification['type'], >( origin: OriginString, target: TargetName, caveatType: CaveatType, - caveatValue: ExtractCaveatValue, + caveatValue: ExtractCaveatValue< + ExtractCaveatsFromSpec, + CaveatType + >, ): void { if (!this.hasCaveat(origin, target, caveatType)) { throw new CaveatDoesNotExistError(origin, target, caveatType); @@ -862,19 +886,18 @@ export class PermissionController< */ private setCaveat< TargetName extends ExtractPermission< - Specification, - Caveat + PermissionSpecification, + CaveatSpecification >['parentCapability'], - CaveatType extends ExtractValidCaveatTypes< - ExtractPermission, - Specification['targetKey'], - TargetName - >, + CaveatType extends CaveatSpecification['type'], >( origin: OriginString, target: TargetName, caveatType: CaveatType, - caveatValue: ExtractCaveatValue, + caveatValue: ExtractCaveatValue< + ExtractCaveatsFromSpec, + CaveatType + >, ): void { this.update((draftState) => { const subject = draftState.subjects[origin]; @@ -920,7 +943,10 @@ export class PermissionController< this.validateModifiedPermission( // TODO:types why? - permission as unknown as ExtractPermission, + permission as unknown as ExtractPermission< + PermissionSpecification, + CaveatSpecification + >, ); }); } @@ -940,14 +966,10 @@ export class PermissionController< */ removeCaveat< TargetName extends ExtractPermission< - Specification, - Caveat + PermissionSpecification, + CaveatSpecification >['parentCapability'], - CaveatType extends ExtractValidCaveatTypes< - ExtractPermission, - Specification['targetKey'], - TargetName - >, + CaveatType extends CaveatSpecification['type'], >(origin: OriginString, target: TargetName, caveatType: CaveatType): void { this.update((draftState) => { const permission = draftState.subjects[origin]?.permissions[target]; @@ -975,7 +997,10 @@ export class PermissionController< this.validateModifiedPermission( // TODO:types why? - permission as unknown as ExtractPermission, + permission as unknown as ExtractPermission< + PermissionSpecification, + CaveatSpecification + >, ); }); } @@ -988,7 +1013,7 @@ export class PermissionController< * @param permission - The modified permission to validate. */ private validateModifiedPermission( - permission: ExtractPermission, + permission: ExtractPermission, ): void { const targetKey = this.getTargetKey(permission.parentCapability); /* istanbul ignore next: this should be impossible */ @@ -1011,10 +1036,13 @@ export class PermissionController< * @returns The internal key of the method. */ private getTargetKey( - method: ExtractPermission['parentCapability'], - ): Specification['targetKey'] | undefined { + method: ExtractPermission< + PermissionSpecification, + CaveatSpecification + >['parentCapability'], + ): PermissionSpecification['targetKey'] | undefined { if (hasProperty(this._permissionSpecifications, method)) { - return method as Specification['targetKey']; + return method as PermissionSpecification['targetKey']; } const wildCardMethodsWithoutWildCard: Record = {}; @@ -1040,7 +1068,7 @@ export class PermissionController< } if (wildCardMethodsWithoutWildCard[targetKey]) { - return `${targetKey}*` as Specification['targetKey']; + return `${targetKey}*` as PermissionSpecification['targetKey']; } return undefined; @@ -1071,7 +1099,7 @@ export class PermissionController< preserveExistingPermissions = true, subject, }: GrantPermissionsOptions): SubjectPermissions< - ExtractPermission + ExtractPermission > { const { origin } = subject; @@ -1080,19 +1108,21 @@ export class PermissionController< } const permissions: SubjectPermissions< - ExtractPermission + ExtractPermission > = ((preserveExistingPermissions && { ...this.getPermissions(origin), }) || - {}) as SubjectPermissions>; + {}) as SubjectPermissions< + ExtractPermission + >; for (const [requestedTarget, approvedPermission] of Object.entries( approvedPermissions, )) { const targetKey = this.getTargetKey( requestedTarget as ExtractPermission< - Specification, - Caveat + PermissionSpecification, + CaveatSpecification >['parentCapability'], ); if (!targetKey) { @@ -1113,8 +1143,8 @@ export class PermissionController< // The requested target must be a valid target name if we found its key. // We reassign it to change its type. const targetName = requestedTarget as ExtractPermission< - Specification, - Caveat + PermissionSpecification, + CaveatSpecification >['parentCapability']; const specification = this.permissionSpecifications[targetKey]; @@ -1125,7 +1155,7 @@ export class PermissionController< ); const permissionOptions: PermissionOptions< - ExtractPermission + ExtractPermission > = { caveats, invoker: origin, @@ -1136,7 +1166,7 @@ export class PermissionController< specification.factory ? specification.factory(permissionOptions, requestData) : constructPermission(permissionOptions) - ) as ExtractPermission; + ) as ExtractPermission; specification.validator?.(permission, origin, targetName); permissions[targetName] = permission; } @@ -1157,7 +1187,10 @@ export class PermissionController< */ private setValidatedPermissions( origin: OriginString, - permissions: Record>, + permissions: Record< + string, + ExtractPermission + >, ): void { this.update((draftState) => { if (!draftState.subjects[origin]) { @@ -1183,14 +1216,19 @@ export class PermissionController< */ private constructCaveats( origin: OriginString, - target: ExtractPermission['parentCapability'], + target: ExtractPermission< + PermissionSpecification, + CaveatSpecification + >['parentCapability'], requestedCaveats?: unknown[] | null, - ): NonEmptyArray | undefined { + ): NonEmptyArray> | undefined { const caveatArray = requestedCaveats?.map((requestedCaveat) => this.constructCaveat(origin, target, requestedCaveat), ); return caveatArray && caveatArray.length > 0 - ? (caveatArray as NonEmptyArray) + ? (caveatArray as NonEmptyArray< + ExtractCaveatsFromSpec + >) : undefined; } @@ -1211,9 +1249,12 @@ export class PermissionController< */ private constructCaveat( origin: OriginString, - target: ExtractPermission['parentCapability'], + target: ExtractPermission< + PermissionSpecification, + CaveatSpecification + >['parentCapability'], requestedCaveat: unknown, - ): Caveat { + ): ExtractCaveatsFromSpec { if (!isPlainObject(requestedCaveat)) { throw new InvalidCaveatError(requestedCaveat, origin, target); } @@ -1228,7 +1269,7 @@ export class PermissionController< const specification = this.caveatSpecifications[ - requestedCaveat.type as keyof CaveatSpecifications + requestedCaveat.type as CaveatSpecification['type'] ]; if (!specification) { @@ -1245,7 +1286,7 @@ export class PermissionController< (requestedCaveat.value as Json) ?? null, ); specification.validator?.(caveat, origin, target); - return caveat as Caveat; + return caveat as ExtractCaveatsFromSpec; } /** @@ -1272,7 +1313,10 @@ export class PermissionController< preserveExistingPermissions = true, ): Promise< [ - Record>, + Record< + string, + ExtractPermission + >, { id: string; origin: string }, ] > { @@ -1363,7 +1407,10 @@ export class PermissionController< for (const methodName of Object.keys(perms)) { const target = ( - perms[methodName] as unknown as ExtractPermission + perms[methodName] as unknown as ExtractPermission< + PermissionSpecification, + CaveatSpecification + > ).parentCapability; if (target !== undefined && methodName !== target) { throw invalidParams({ data: { origin, requestedPermissions } }); @@ -1476,7 +1523,10 @@ export class PermissionController< */ async executeRestrictedMethod( origin: string, - methodName: ExtractPermission['parentCapability'], + methodName: ExtractPermission< + PermissionSpecification, + CaveatSpecification + >['parentCapability'], params?: GenericRestrictedMethodParams, ): Promise { // Throws if the method does not exist @@ -1516,7 +1566,10 @@ export class PermissionController< Json >, subject: PermissionSubjectMetadata, - method: ExtractPermission['parentCapability'], + method: ExtractPermission< + PermissionSpecification, + CaveatSpecification + >['parentCapability'], params: GenericRestrictedMethodParams = [], ): ReturnType> { const { origin } = subject; @@ -1526,7 +1579,7 @@ export class PermissionController< throw unauthorized({ data: { origin, method } }); } - return decorateWithCaveats( + return decorateWithCaveats( methodImplementation, permission, this.caveatSpecifications, @@ -1549,7 +1602,10 @@ export class PermissionController< origin: string, ): RestrictedMethodBase { const methodImplementation = this.getRestrictedMethod( - method as ExtractPermission['parentCapability'], + method as ExtractPermission< + PermissionSpecification, + CaveatSpecification + >['parentCapability'], ); if (!methodImplementation) { throw methodNotFound({ method, data: { origin } }); From fa61f4f84df5509c937b1b98201840a18955b724 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sat, 9 Oct 2021 14:32:48 -0700 Subject: [PATCH 078/147] WIP: Refactor controller types IV (cleanup) --- .../controllers/src/permissions/Caveat.ts | 124 ++---- .../controllers/src/permissions/Permission.ts | 400 ++++-------------- .../permissions/PermissionController.test.ts | 153 ++----- .../src/permissions/PermissionController.ts | 153 ++++--- .../controllers/src/permissions/README.md | 4 +- .../src/permissions/permission-middleware.ts | 17 +- 6 files changed, 254 insertions(+), 597 deletions(-) diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index 75ec498e59..9e21989b7d 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -5,7 +5,7 @@ import { AsyncRestrictedMethod, RestrictedMethodBase, GenericPermission, - GenericRestrictedMethodParams, + RestrictedMethodParameters, } from './Permission'; /** @@ -15,7 +15,7 @@ import { * @template Type - The type of the caveat. * @template Value - The value associated with the caveat. */ -export type CaveatConstraint = { +export type CaveatBase = { /** * The type of the caveat. The type is presumed to be meaningful in the * context of the capability it is associated with. @@ -33,7 +33,7 @@ export type CaveatConstraint = { }; /** - * The {@link CaveatConstraint} factory function. Naively constructs a new caveat from the + * The {@link CaveatBase} factory function. Naively constructs a new caveat from the * inputs. Sets `value` to `null` if no value is provided. * * @param type - The type of the caveat. @@ -43,7 +43,7 @@ export type CaveatConstraint = { export function constructCaveat( type: Type, value: Value, -): CaveatConstraint { +): CaveatBase { return { type, value }; } @@ -62,104 +62,77 @@ export function constructCaveat( * @returns The decorate restricted method implementation. */ export type CaveatDecorator = ( - decorated: AsyncRestrictedMethod, + decorated: AsyncRestrictedMethod, caveat: Caveat, -) => AsyncRestrictedMethod; +) => AsyncRestrictedMethod; type ExtractCaveatValueFromDecorator> = Decorator extends ( decorated: any, caveat: infer Caveat, - ) => AsyncRestrictedMethod + ) => AsyncRestrictedMethod ? Caveat extends GenericCaveat ? Caveat['value'] : never : never; -/** - * - */ -export type CaveatValidator = ( +type CaveatValidator = ( caveat: { type: Caveat['type']; value: unknown }, origin?: string, target?: string, ) => void; -// export type CaveatSpecification = { -// /** -// * The string type of the caveat. -// */ -// type: Caveat['type']; - -// /** -// * The decorator function used to apply the caveat to restricted method -// * requests. -// */ -// decorator: CaveatDecorator; - -// /** -// * The validator function used to validate caveats of the associated type -// * whenever they are instantiated. Caveat are instantiated whenever they are -// * created or mutated. -// * -// * The validator should throw an appropriate JSON-RPC error if validation fails. -// * -// * If no validator is specified, no validation of caveat values will be -// * performed. Although caveats can also be validated by permission validators, -// * validating caveat values separately is strongly recommended. -// */ -// validator?: CaveatValidator; -// }; - -export type CaveatSpecification2 = { +export type CaveatSpecificationBase = { + /** + * The string type of the caveat. + */ type: Type; + /** + * The decorator function used to apply the caveat to restricted method + * requests. + */ decorator: CaveatDecorator; + /** + * The validator function used to validate caveats of the associated type + * whenever they are instantiated. Caveat are instantiated whenever they are + * created or mutated. + * + * The validator should throw an appropriate JSON-RPC error if validation fails. + * + * If no validator is specified, no validation of caveat values will be + * performed. Although caveats can also be validated by permission validators, + * validating caveat values separately is strongly recommended. + */ validator?: CaveatValidator; }; -export type ExtractCaveatsFromSpec> = - Spec extends any - ? CaveatConstraint< - Spec['type'], - ExtractCaveatValueFromDecorator - > - : never; - /** * A generic caveat. */ -export type GenericCaveat = CaveatConstraint; +export type GenericCaveat = CaveatBase; -/** - * An object mapping the type of each caveat to its corresponding specification. - */ -// export type CaveatSpecifications = { -// [Type in Caveat['type']]: ExtractCaveatSpecification; -// }; - -export type CaveatSpecifications2< - CaveatSpecification extends CaveatSpecification2, +export type CaveatSpecificationsMap< + CaveatSpecification extends CaveatSpecificationBase, > = { [Key in CaveatSpecification['type']]: CaveatSpecification; }; -// type ExtractCaveatSpecification< -// Caveat extends GenericCaveat, -// CaveatType extends string, -// > = Caveat extends CaveatConstraint -// ? CaveatSpecification -// : never; +export type ExtractCaveats< + Specification extends CaveatSpecificationBase, +> = Specification extends any + ? CaveatBase< + Specification['type'], + ExtractCaveatValueFromDecorator + > + : never; -export type ExtractCaveatFromType< - CaveatSpecification extends CaveatSpecification2, +export type ExtractCaveat< + CaveatSpecification extends CaveatSpecificationBase, CaveatType extends string, -> = ExtractCaveatsFromSpec extends CaveatConstraint< - CaveatType, - Json -> - ? ExtractCaveatsFromSpec +> = ExtractCaveats extends CaveatBase + ? ExtractCaveats : never; /** @@ -169,22 +142,19 @@ export type ExtractCaveatFromType< * decorator) must be awaited. */ export function decorateWithCaveats< - CaveatSpecification extends CaveatSpecification2, + CaveatSpecification extends CaveatSpecificationBase, >( - methodImplementation: RestrictedMethodBase< - GenericRestrictedMethodParams, - Json - >, + methodImplementation: RestrictedMethodBase, permission: Readonly, // bound to the requesting origin - caveatSpecifications: CaveatSpecifications2, // all caveat implementations -): RestrictedMethodBase { + caveatSpecifications: CaveatSpecificationsMap, // all caveat implementations +): RestrictedMethodBase { const { caveats } = permission; if (!caveats) { return methodImplementation; } let decorated = methodImplementation as AsyncRestrictedMethod< - GenericRestrictedMethodParams, + RestrictedMethodParameters, Json >; diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 87960656d6..e8f5a04458 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -1,10 +1,12 @@ import { Json } from 'json-rpc-engine'; import { nanoid } from 'nanoid'; import { NonEmptyArray } from '../utils'; -import { CaveatConstraint, GenericCaveat } from './Caveat'; +import { GenericCaveat } from './Caveat'; // This is used in a docstring, but ESLint doesn't notice it. -// eslint-disable-next-line @typescript-eslint/no-unused-vars +/* eslint-disable @typescript-eslint/no-unused-vars */ import type { PermissionController } from './PermissionController'; +import type { CaveatBase } from './Caveat'; +/* eslint-enable @typescript-eslint/no-unused-vars */ /** * The origin of a subject. @@ -36,7 +38,7 @@ export type PermissionBase< // TODO: Make optional in typescript@4.4.x /** * The caveats of the permission. - * @see {@link CaveatConstraint} For more information. + * @see {@link CaveatBase} For more information. */ readonly caveats: AllowedCaveat extends never ? null @@ -68,6 +70,7 @@ export type PermissionBase< : Target; }; +// TODO:delete? /** * Makes the {@link PermissionBase.caveats} property of the given permission mutable. * Useful for mutating the caveats of a permission in state updates. @@ -106,13 +109,18 @@ type ExtractPermissionTargetNames = /** * The consumer-facing base permission type. + * + * @template TargetKey - They key of the permission target that the permission + * corresponds to. + * @template AllowedCaveats - A union of the allowed {@link CaveatBase} types + * for the permission. */ export type PermissionConstraint< TargetKey extends string, - AllowedCaveat extends GenericCaveat | never, + AllowedCaveats extends GenericCaveat | never, > = TargetNameConstraint> extends never ? never - : PermissionBase, AllowedCaveat>; + : PermissionBase, AllowedCaveats>; /** * A generic permission. @@ -122,72 +130,15 @@ export type GenericPermission = PermissionConstraint< GenericCaveat | never >; -/** - * An internal utility type used in {@link ExtractPermissionTargetKey}. - * - * @template Key - The target key type to extract from. - * @template Name - The name whose key to extract. - */ -type KeyOfTargetName< - Key extends string, - Name extends string, -> = Name extends ExtractPermissionTargetNames ? Key : never; - -/** - * A utility type for finding the permission target key corresponding to a - * target name. In a way, the inverse of {@link ExtractPermissionTargetNames}. - * - * See the documentation for the distinction between target names and keys. - * - * @template Key - The target key type to extract from. - * @template Name - The name whose key to extract. - */ -type ExtractPermissionTargetKey< - Key extends string, - Name extends string, -> = Name extends Key ? Name : Extract>; - -/** - * @template Permission - The permission type to extract valid caveats from. - * @template Key - The target key type to extract from. - * @template Name - The target name of the permission. - */ -type ExtractValidCaveats< - Permission extends GenericPermission, - Key extends string, - Name extends string, -> = Permission extends PermissionConstraint< - ExtractPermissionTargetKey, - infer ValidCaveats -> - ? ValidCaveats extends GenericCaveat - ? ValidCaveats - : never - : never; - -/** - * A utility type for extracting the valid caveat types for a particular - * permission from a union of permission types. - * - * @template Permission - The permission type to extract valid caveat types - * types from. - * @template Key - The target key type to extract from. - * @template Name - The target name of the permission. - */ -export type ExtractValidCaveatTypes< - Permission extends GenericPermission, - Key extends string, - Name extends string, -> = ExtractValidCaveats['type']; - -type ExtractArrayMembers = T extends readonly [...infer U] | [...infer U] - ? U[number] - : never; -export type ExtractCaveats = - ExtractArrayMembers; +// type ExtractCaveats = Permission extends PermissionConstraint< +// GenericTargetName, +// infer AllowedCaveats +// > ? AllowedCaveats : never; /** * The options object of {@link constructPermission}. + * + * @template Permission - The {@link PermissionBase} that will be constructed. */ export type PermissionOptions = { target: Permission['parentCapability']; @@ -204,10 +155,11 @@ export type PermissionOptions = { /** * The caveats of the permission. - * See {@link CaveatConstraint}. + * See {@link CaveatBase}. */ - // caveats?: NonEmptyArray>; caveats?: NonEmptyArray; + // TODO:feat Add runtime check for allowed caveats to ensure that we can do this. + // caveats?: NonEmptyArray>; }; /** @@ -216,6 +168,7 @@ export type PermissionOptions = { * * @see {@link PermissionBase} For more details. * + * @template Permission - The {@link PermissionBase} that will be constructed. * @param options - The options for the permission. * @returns The new permission object. */ @@ -272,28 +225,19 @@ type RestrictedMethodContext = Readonly<{ [key: string]: any; }>; -export type GenericRestrictedMethodParams = - | Json[] - | Record - | never; - -export type GenericRestrictedMethod = RestrictedMethodBase< - GenericRestrictedMethodParams, - Json ->; +export type RestrictedMethodParameters = Json[] | Record | never; /** * The arguments passed to a restricted method implementation. * * @template Params - The JSON-RPC parameters of the restricted method. */ -export type RestrictedMethodOptions< - Params extends GenericRestrictedMethodParams, -> = { - method: GenericTargetName; - params?: Params; - context: RestrictedMethodContext; -}; +export type RestrictedMethodOptions = + { + method: GenericTargetName; + params?: Params; + context: RestrictedMethodContext; + }; /** * A synchronous restricted method implementation. @@ -302,7 +246,7 @@ export type RestrictedMethodOptions< * @template Result - The JSON-RPC result of the restricted method. */ export type SyncRestrictedMethod< - Params extends GenericRestrictedMethodParams, + Params extends RestrictedMethodParameters, Result extends Json, > = (args: RestrictedMethodOptions) => Result; @@ -313,7 +257,7 @@ export type SyncRestrictedMethod< * @template Result - The JSON-RPC result of the restricted method. */ export type AsyncRestrictedMethod< - Params extends GenericRestrictedMethodParams, + Params extends RestrictedMethodParameters, Result extends Json, > = (args: RestrictedMethodOptions) => Promise; @@ -324,27 +268,24 @@ export type AsyncRestrictedMethod< * @template Result - The JSON-RPC result of the restricted method. */ export type RestrictedMethodBase< - Params extends GenericRestrictedMethodParams, + Params extends RestrictedMethodParameters, Result extends Json, > = | SyncRestrictedMethod | AsyncRestrictedMethod; export type RestrictedMethodConstraint< - // MethodImplementation extends RestrictedMethodBase, - MethodImplementation, -> = MethodImplementation extends (args: infer Args) => Json | Promise - ? Args extends RestrictedMethodOptions + MethodImplementation extends GenericRestrictedMethod, +> = MethodImplementation extends (args: infer Options) => Json | Promise + ? Options extends RestrictedMethodOptions ? MethodImplementation : never : never; -// type ExtractPermission< -// TargetKey extends string, -// Permission extends PermissionConstraint, -// > = Permission extends PermissionConstraint -// ? Permission -// : never; +export type GenericRestrictedMethod = RestrictedMethodBase< + RestrictedMethodParameters, + Json +>; export type PermissionFactory< Permission extends GenericPermission, @@ -356,29 +297,56 @@ export type PermissionFactory< export type PermissionValidator = ( permission: GenericPermission, - origin?: string, + origin?: OriginString, target?: Permission['parentCapability'], ) => void; /** - * The base permission specification interface. Lacks important constraints on - * its generics; consumers should use {@link PermissionSpecificationConstraint} - * instead. + * A utility type for ensuring that the given permission target key conforms to + * our naming conventions. + * + * See the documentation for the distinction between target names and keys. + * + * @template Key - The target key string to apply the constraint to. */ -export type PermissionSpecificationBase< - TargetKey extends string, - Permission extends PermissionConstraint, - RequestData extends Record, - MethodImplementation extends RestrictedMethodBase< - GenericRestrictedMethodParams, - Json - >, -> = { +type TargetKeyConstraint = Key extends `${string}_*` + ? Key + : Key extends `${string}_` + ? never + : Key extends `${string}*` + ? never + : Key; + +/** + * A type extended by any function. + */ +type GenericFunction = (...args: any[]) => any; + +/** + * The base permission specification type. + * + * @template TargetKey - They key of the permission target that the permission + * corresponds to. + */ +export type PermissionSpecificationBase = { /** * The target resource of the permission. In other words, at the time of * writing, the RPC method name. */ - target: TargetKey; + targetKey: TargetKey; + + /** + * An array of the caveat types that may be added to instances of this + * permission. + */ + allowedCaveats?: Readonly>; + + /** + * The implementation of the restricted method that the permission + * corresponds to. + */ + methodImplementation: GenericFunction; + // methodImplementation: RestrictedMethodConstraint; /** * The factory function used to get permission objects. Permissions returned @@ -390,13 +358,7 @@ export type PermissionSpecificationBase< * used, and the validator function (if specified) will be called on newly * constructed permissions. */ - factory?: PermissionFactory; - - /** - * The implementation of the restricted method that the permission - * corresponds to. - */ - methodImplementation: MethodImplementation; + factory?: PermissionFactory; /** * The validator function used to validate permissions of the associated type @@ -405,65 +367,9 @@ export type PermissionSpecificationBase< * * The validator should throw an appropriate JSON-RPC error if validation fails. */ - validator?: PermissionValidator; + validator?: PermissionValidator; }; -// type AllTheThings = { -// specification: -// } - -/** - * A utility type for ensuring that the given permission target key conforms to - * our naming conventions. - * - * See the documentation for the distinction between target names and keys. - * - * @template Key - The target key string to apply the constraint to. - */ -type TargetKeyConstraint = Key extends `${string}_*` - ? Key - : Key extends `${string}_` - ? never - : Key extends `${string}*` - ? never - : Key; - -type GenericFunction = (...args: any[]) => any; - -export type NewSpecShape = { - targetKey: TargetKey; - methodImplementation: GenericFunction; - // methodImplementation: GenericRestrictedMethod; - // methodImplementation: (args: RestrictedMethodOptions) => Json | Promise, - allowedCaveats?: Readonly>; - validator?: GenericFunction; - factory?: GenericFunction; -}; - -// const foo: NewSpecShape = { -// targetKey: 'foo', -// methodImplementation: () => undefined, -// }; -// type bar = ExtractArrayMembers['allowedCaveats']>; -// type xxx = ExtractArrayMembers<[]>; -// type yyy = ExtractArrayMembers; - -export type ExtractCaveatTypes = Specification extends Record< - string, - unknown -> - ? ExtractArrayMembers - : never; - -export type ExtractCaveatsFromTypes< - CaveatType, - Caveats extends GenericCaveat, -> = CaveatType extends string - ? Caveats extends CaveatConstraint - ? Caveats - : never - : never; - /** * An individual permission specification object. Constrained such that the * given target key must be valid. @@ -471,8 +377,8 @@ export type ExtractCaveatsFromTypes< * @template TargetKey - The key of the permission target. * @template Permission - The type of the permission object. */ -export type PermissionSpecificationConstraint2 = - T extends NewSpecShape +export type PermissionSpecificationConstraint = + T extends PermissionSpecificationBase ? T['targetKey'] extends TargetKeyConstraint ? T['methodImplementation'] extends RestrictedMethodConstraint< T['methodImplementation'] @@ -483,140 +389,18 @@ export type PermissionSpecificationConstraint2 = : never; /** - * An individual permission specification object. Constrained such that the - * given target key must be valid. + * The specifications for all permissions and restricted methods supported by + * a particular {@link PermissionController}. * - * @template TargetKey - The key of the permission target. - * @template Permission - The type of the permission object. + * @template Specifications - A union of all {@link PermissionSpecificationBase} + * types. */ -export type PermissionSpecificationConstraint< - TargetKey extends string, - Permission extends PermissionConstraint, - RequestData extends Record, - MethodImplementation extends RestrictedMethodBase, -> = TargetKeyConstraint extends never - ? never - : RestrictedMethodConstraint extends never - ? never - : PermissionSpecificationBase< - TargetKey, - Permission, - RequestData, - MethodImplementation - >; - -export type GenericPermissionSpecification = PermissionSpecificationConstraint< - string, - GenericPermission, - Record, - RestrictedMethodBase ->; - -// /** -// * Utility type used in {@link PermissionSpecifications} to map permission -// * target keys to their individual {@link PermissionSpecificationConstraint} -// * objects. -// */ -// type ExtractPermissionSpecification< -// TargetKey extends string, -// Permissions extends PermissionConstraint, -// RestrictedMethods extends RestrictedMethodBase, -// Specifications extends PermissionSpecificationConstraint< -// string, -// Permissions, -// Record, -// RestrictedMethods -// >, -// > = Specifications extends PermissionSpecificationConstraint< -// TargetKey, -// Permissions, -// Record, -// RestrictedMethods -// > -// ? Specifications -// : never; -// // > = Permissions extends PermissionConstraint -// // ? PermissionSpecificationConstraint -// // : never; - -export type PermissionSpecificationsMap2< - Specifications extends NewSpecShape, -> = PermissionSpecificationConstraint2 extends never - ? never - : { - [TargetKey in Specifications['targetKey']]: Specifications extends NewSpecShape - ? Specifications - : never; - }; - -// TODO:types Map the names correctly. -export type PermissionSpecificationsMap3< - Specifications extends NewSpecShape, -> = Record; -// > = PermissionSpecificationConstraint2 extends never ? never : Record; - export type PermissionSpecificationsMap< - Specifications extends PermissionSpecificationBase, -> = TargetKeyConstraint extends never - ? never - : RestrictedMethodConstraint< - Specifications['methodImplementation'] - > extends never + Specifications extends PermissionSpecificationBase, +> = PermissionSpecificationConstraint extends never ? never : { - [TargetKey in Specifications['target']]: Specifications extends PermissionSpecificationConstraint< - TargetKey, - PermissionConstraint, - Record, - RestrictedMethodBase - > + [TargetKey in Specifications['targetKey']]: Specifications extends PermissionSpecificationBase ? Specifications : never; }; - -// /** -// * The specifications for all permissions and restricted methods supported by -// * a particular {@link PermissionController}. -// */ -// export type PermissionSpecificationsMap< -// TargetKeys extends string, -// Permissions extends PermissionConstraint, -// RestrictedMethods extends RestrictedMethodBase, -// Specifications extends PermissionSpecificationConstraint< -// TargetKeys, -// Permissions, -// Record, -// RestrictedMethods -// >, -// > = TargetKeyConstraint extends never -// ? never -// : RestrictedMethodConstraint extends never -// ? never -// : { -// [Key in TargetKeys]: Permissions extends PermissionConstraint< -// Key, -// GenericCaveat | never -// > -// ? ExtractPermissionSpecification< -// Key, -// Permissions, -// RestrictedMethods, -// Specifications -// > -// : never; -// }; -// // [Key in TargetKeys]: Permissions extends PermissionConstraint< -// // Key, -// // GenericCaveat | never -// // > -// // ? Specifications extends PermissionSpecificationConstraint< -// // Key, -// // // Permissions, -// // ExtractPermission, -// // Record, -// // RestrictedMethods -// // > -// // ? Specifications -// // : never -// // : never; -// // }; diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index a025815eec..6f682dfaa9 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -12,7 +12,7 @@ import * as errors from './errors'; import { constructCaveat } from './Caveat'; import { AsyncRestrictedMethod, - CaveatConstraint, + CaveatBase, constructPermission, GenericCaveat, GenericPermission, @@ -23,14 +23,14 @@ import { PermissionControllerEvents, PermissionOptions, RestrictedMethodOptions, - GenericRestrictedMethodParams, - NewSpecShape, - PermissionSpecificationsMap2, - CaveatSpecifications2, - CaveatSpecification2, + RestrictedMethodParameters, + PermissionSpecificationBase, + PermissionSpecificationsMap, + CaveatSpecificationsMap, + CaveatSpecificationBase, } from '.'; -// CaveatConstraint types and specifications +// CaveatBase types and specifications const CaveatTypes = { filterArrayResponse: 'filterArrayResponse', @@ -39,22 +39,22 @@ const CaveatTypes = { noopCaveat: 'noopCaveat', } as const; -type FilterArrayCaveat = CaveatConstraint< +type FilterArrayCaveat = CaveatBase< typeof CaveatTypes.filterArrayResponse, string[] >; -type ReverseArrayCaveat = CaveatConstraint< +type ReverseArrayCaveat = CaveatBase< typeof CaveatTypes.reverseArrayResponse, null >; -type FilterObjectCaveat = CaveatConstraint< +type FilterObjectCaveat = CaveatBase< typeof CaveatTypes.filterObjectResponse, string[] >; -type NoopCaveat = CaveatConstraint; +type NoopCaveat = CaveatBase; // type DefaultCaveats = // | FilterArrayCaveat @@ -78,12 +78,10 @@ function getDefaultCaveatSpecifications() { type: CaveatTypes.filterArrayResponse, decorator: ( - method: AsyncRestrictedMethod, + method: AsyncRestrictedMethod, caveat: FilterArrayCaveat, ) => - async ( - args: RestrictedMethodOptions, - ) => { + async (args: RestrictedMethodOptions) => { const result: string[] | unknown = await method(args); if (!Array.isArray(result)) { throw Error('not an array'); @@ -98,12 +96,10 @@ function getDefaultCaveatSpecifications() { type: CaveatTypes.reverseArrayResponse, decorator: ( - method: AsyncRestrictedMethod, + method: AsyncRestrictedMethod, _caveat: ReverseArrayCaveat, ) => - async ( - args: RestrictedMethodOptions, - ) => { + async (args: RestrictedMethodOptions) => { const result: unknown[] | unknown = await method(args); if (!Array.isArray(result)) { throw Error('not an array'); @@ -116,12 +112,10 @@ function getDefaultCaveatSpecifications() { type: CaveatTypes.filterObjectResponse, decorator: ( - method: AsyncRestrictedMethod, + method: AsyncRestrictedMethod, caveat: FilterObjectCaveat, ) => - async ( - args: RestrictedMethodOptions, - ) => { + async (args: RestrictedMethodOptions) => { const result = await method(args); if (!isPlainObject(result)) { throw Error('not a plain object'); @@ -139,12 +133,10 @@ function getDefaultCaveatSpecifications() { type: CaveatTypes.noopCaveat, decorator: ( - method: AsyncRestrictedMethod, + method: AsyncRestrictedMethod, _caveat: NoopCaveat, ) => - async ( - args: RestrictedMethodOptions, - ) => { + async (args: RestrictedMethodOptions) => { return method(args); }, validator: (caveat: { @@ -159,8 +151,8 @@ function getDefaultCaveatSpecifications() { } as const; } -type GetCaveatSpecMapValues = T extends CaveatSpecifications2< - CaveatSpecification2 +type GetCaveatSpecMapValues = T extends CaveatSpecificationsMap< + CaveatSpecificationBase > ? T[keyof T] : never; @@ -171,25 +163,10 @@ type DefaultCaveatSpecifications = GetCaveatSpecMapValues< // Permission types and specifications -// // wallet_getSecretArray - -// type SecretArrayPermissionName = 'wallet_getSecretArray'; - -// type SecretArrayPermission = PermissionConstraint< -// SecretArrayPermissionName, -// FilterArrayCaveat | ReverseArrayCaveat -// >; - -// // wallet_getSecretObject - -// type SecretObjectPermissionName = 'wallet_getSecretObject'; - -// type SecretObjectPermission = PermissionConstraint< -// SecretObjectPermissionName, -// FilterObjectCaveat | NoopCaveat -// >; - // wallet_getSecret_* +// We only define types for this permission because it's the only one with a +// factory. +// Other permission types are extracted from the permission specifications. type SecretNamespacedPermissionKey = 'wallet_getSecret_*'; @@ -205,8 +182,6 @@ const PermissionKeys = { wallet_getSecretArray: 'wallet_getSecretArray', wallet_getSecretObject: 'wallet_getSecretObject', 'wallet_getSecret_*': 'wallet_getSecret_*', - // 'wallet_getError' = 'wallet_getError', - // 'wallet_getUndefined' = 'wallet_getUndefined', } as const; /** @@ -217,8 +192,6 @@ const PermissionNames = { wallet_getSecretArray: PermissionKeys.wallet_getSecretArray, wallet_getSecretObject: PermissionKeys.wallet_getSecretObject, wallet_getSecret_: (str: string) => `wallet_getSecret_${str}` as const, - // wallet_getError: 'wallet_getError' as const, - // wallet_getUndefined: 'wallet_getUndefined' as const, } as const; /** @@ -291,91 +264,25 @@ function getDefaultPermissionSpecifications() { ); }, }, - // wallet_getError: { - // target: PermissionKeys.wallet_getError, - // methodImplementation: ((args: RestrictedMethodOptions) => { - // return { message: `The error says: ${args.params?.[0] || 'default'}` } - // }) - // }, - // wallet_getUndefined: { - // target: PermissionKeys.wallet_getUndefined, - // } } as const; } -// type RestrictedMethodContext = Readonly<{ -// origin: string; -// [key: string]: any; -// }>; +// TODO:delete +// type DoesExtend = T extends U ? T : never; // const specs = getDefaultPermissionSpecifications() -// type DoesExtend = T extends U ? T : never; -// // type func = typeof specs.wallet_getSecretArray.methodImplementation // type func = typeof specs['wallet_getSecret_*']['methodImplementation'] // type x = DoesExtend -// type xx = DoesExtend) => Json | Promise>; +// type xx = DoesExtend) => Json | Promise>; // type xxx = DoesExtend Json | Promise>; // type xxxx = DoesExtend Json | Promise>; -// type y = DoesExtend -// type yy = DoesExtend, RestrictedMethodOptions>; -// type z = DoesExtend -// type zz = DoesExtend<[], Json> - -// type RestrictedMethodConstraint2< -// // MethodImplementation extends RestrictedMethodBase, -// MethodImplementation, -// > = MethodImplementation extends (args: infer Args) => Json | Promise -// ? Args extends RestrictedMethodOptions -// ? MethodImplementation -// // ? Args extends any -// // ? Args -// : never -// : never; - -// type RestrictedMethodConstraint3< -// // MethodImplementation extends RestrictedMethodBase, -// MethodImplementation, -// > = MethodImplementation extends (args: infer Args) => infer Result -// ? Args extends RestrictedMethodOptions -// ? Result extends Json | Promise -// ? MethodImplementation -// : never -// : never -// : never; - -// type funcA = (args: RestrictedMethodOptions<[]>) => Promise -// type funcB = (args: RestrictedMethodOptions<[]>) => (() => undefined) -// type funcC = (args: RestrictedMethodOptions<[]>) => Promise<() => undefined> - -// type a = RestrictedMethodConstraint3 -// type b = RestrictedMethodConstraint3 -// type c = RestrictedMethodConstraint3 - -// type j = PermissionSpecificationConstraint2 -// type k = PermissionSpecificationConstraint2 - -// type PermissionControllerOptions< -// PermissionSpecification extends NewSpecShape, -// Caveat extends GenericCaveat, -// > = PermissionSpecificationConstraint2 extends never -// ? never -// : { -// messenger: any; -// caveatSpecifications: CaveatSpecifications; -// // permissionSpecifications: PermissionSpecificationsMap2>; -// permissionSpecifications: PermissionSpecificationsMap2; -// unrestrictedMethods: string[]; -// state?: Partial<{}> -// }; - -// type q = PermissionControllerOptions - -type GetSpecMapValues = T extends PermissionSpecificationsMap2< - NewSpecShape + +type GetSpecMapValues = T extends PermissionSpecificationsMap< + PermissionSpecificationBase > ? T[keyof T] : never; diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index e84d8a8683..aa58369414 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -19,14 +19,14 @@ import deepFreeze from 'deep-freeze-strict'; import { nanoid } from 'nanoid'; import { isPlainObject, hasProperty, NonEmptyArray } from '../utils'; import { - CaveatConstraint, + CaveatBase, constructCaveat as _constructCaveat, decorateWithCaveats, GenericCaveat, - ExtractCaveatFromType, - CaveatSpecification2, - ExtractCaveatsFromSpec, - CaveatSpecifications2, + ExtractCaveat, + CaveatSpecificationBase, + ExtractCaveats, + CaveatSpecificationsMap, } from './Caveat'; import { constructPermission, @@ -36,10 +36,10 @@ import { PermissionOptions, RequestedPermissions, RestrictedMethodBase, - GenericRestrictedMethodParams, - PermissionSpecificationConstraint2, - PermissionSpecificationsMap2, - NewSpecShape, + RestrictedMethodParameters, + PermissionSpecificationConstraint, + PermissionSpecificationsMap, + PermissionSpecificationBase, PermissionConstraint, } from './Permission'; import { @@ -177,7 +177,7 @@ export type ClearPermissions = { handler: () => void; }; -// TODO: Implement all desired actions +// TODO:feat Implement all desired actions /** * The {@link ControllerMessenger} actions of the {@link PermissionController}. */ @@ -226,7 +226,7 @@ export type PermissionControllerMessenger = RestrictedControllerMessenger< >; /** - * A utility type for extracting the {@link CaveatConstraint.value} type from + * A utility type for extracting the {@link CaveatBase.value} type from * a union of caveat types. * * @template CaveatUnion - The caveat type union to extract a value type from. @@ -235,7 +235,7 @@ export type PermissionControllerMessenger = RestrictedControllerMessenger< export type ExtractCaveatValue< CaveatUnion extends GenericCaveat, CaveatType extends string, -> = CaveatUnion extends CaveatConstraint +> = CaveatUnion extends CaveatBase ? CaveatValue : never; @@ -250,27 +250,30 @@ type GrantPermissionsOptions = { }; export type GenericPermissionController = PermissionController< - NewSpecShape, - CaveatSpecification2 + PermissionSpecificationBase, + CaveatSpecificationBase >; -// type ExtractPermission = Type extends PermissionSpecificationConstraint< -// string, -// infer X, -// Record, -// RestrictedMethodBase -// > -// ? X -// : never; - +/** + * Extracts the permission type or types from the specified permission and + * caveat specifications. + * + * @template PermissionSpecification - The permission specification(s) to + * extract a permission type union from. + * @template CaveatSpecification - The caveat specification(s) to extract a + * permission type union from. Necessary because {@link PermissionConstraint} + * has a generic that describes the allowed caveats for the permission. + */ export type ExtractPermission< - PermissionSpecification extends NewSpecShape, - CaveatSpecification extends CaveatSpecification2, + PermissionSpecification extends PermissionSpecificationBase, + CaveatSpecification extends CaveatSpecificationBase, > = PermissionSpecification extends any - ? PermissionConstraint< - PermissionSpecification['targetKey'], - ExtractCaveatsFromSpec - > + ? CaveatSpecification extends any + ? PermissionConstraint< + PermissionSpecification['targetKey'], + ExtractCaveats + > + : never : never; /** @@ -286,14 +289,14 @@ export type ExtractPermission< * objects. */ export type PermissionControllerOptions< - PermissionSpecification extends NewSpecShape, - CaveatSpecification extends CaveatSpecification2, -> = PermissionSpecificationConstraint2 extends never + PermissionSpecification extends PermissionSpecificationBase, + CaveatSpecification extends CaveatSpecificationBase, +> = PermissionSpecificationConstraint extends never ? never : { messenger: PermissionControllerMessenger; - caveatSpecifications: CaveatSpecifications2; - permissionSpecifications: PermissionSpecificationsMap2; + caveatSpecifications: CaveatSpecificationsMap; + permissionSpecifications: PermissionSpecificationsMap; unrestrictedMethods: string[]; state?: Partial< PermissionControllerState< @@ -318,8 +321,8 @@ export type PermissionControllerOptions< * of the controller. */ export class PermissionController< - PermissionSpecification extends NewSpecShape, - CaveatSpecification extends CaveatSpecification2, + PermissionSpecification extends PermissionSpecificationBase, + CaveatSpecification extends CaveatSpecificationBase, > extends BaseController< typeof controllerName, PermissionControllerState< @@ -328,27 +331,27 @@ export class PermissionController< PermissionControllerMessenger > { private readonly _caveatSpecifications: Readonly< - CaveatSpecifications2 + CaveatSpecificationsMap >; /** * The {@link CaveatSpecifications} of the controller. */ public get caveatSpecifications(): Readonly< - CaveatSpecifications2 + CaveatSpecificationsMap > { return this._caveatSpecifications; } private readonly _permissionSpecifications: Readonly< - PermissionSpecificationsMap2 + PermissionSpecificationsMap >; /** - * The {@link PermissionSpecificationsMap2} of the controller. + * The {@link PermissionSpecificationsMap} of the controller. */ public get permissionSpecifications(): Readonly< - PermissionSpecificationsMap2 + PermissionSpecificationsMap > { return this._permissionSpecifications; } @@ -381,7 +384,7 @@ export class PermissionController< * documentation for more details. * @param options.permissionSpecifications - The specifications of all * permissions available to the controller. See - * {@link PermissionSpecificationsMap2} and the documentation for more details. + * {@link PermissionSpecificationsMap} and the documentation for more details. * @param options.unrestrictedMethods - The callable names of all JSON-RPC * methods ignored by the new controller. * @param options.messenger - The controller messenger. See @@ -443,7 +446,7 @@ export class PermissionController< * controller's constructor. */ private validatePermissionSpecifications( - specifications: PermissionSpecificationsMap2, + specifications: PermissionSpecificationsMap, ) { Object.entries(specifications).forEach( ([targetKey, { targetKey: innerTargetKey }]) => { @@ -479,7 +482,7 @@ export class PermissionController< this.messagingSystem.registerActionHandler( `${controllerName}:hasPermissions`, - (origin: string) => this.hasPermissions(origin), + (origin: OriginString) => this.hasPermissions(origin), ); } @@ -488,8 +491,11 @@ export class PermissionController< */ clearState(): void { this.update((_draftState) => { - // TODO:types why? - return { ...defaultState } as any; + // Typecast: The default state is declared outside of the controller + // instance and therefore incorrectly genericized. + return { ...defaultState } as PermissionControllerState< + ExtractPermission + >; }); } @@ -501,7 +507,7 @@ export class PermissionController< */ getRestrictedMethod( method: string, - ): RestrictedMethodBase | undefined { + ): RestrictedMethodBase | undefined { const targetKey = this.getTargetKey( method as ExtractPermission< PermissionSpecification, @@ -551,12 +557,8 @@ export class PermissionController< * @returns The permissions of the subject, if any. */ getPermissions( - origin: OriginString, - ): - | SubjectPermissions< - ExtractPermission - > - | undefined { + origin: OriginString, // | SubjectPermissions< // ExtractPermission // > // | undefined { + ) { // TODO:types why? return this.state.subjects[origin]?.permissions as | SubjectPermissions< @@ -775,16 +777,15 @@ export class PermissionController< origin: OriginString, target: TargetName, caveatType: CaveatType, - ): ExtractCaveatFromType | undefined { + ): ExtractCaveat | undefined { const permission = this.getPermission(origin, target); if (!permission) { throw new PermissionDoesNotExistError(origin, target); } - return findCaveat(permission, caveatType) as ExtractCaveatFromType< - CaveatSpecification, - CaveatType - >; + return findCaveat(permission, caveatType) as + | ExtractCaveat + | undefined; } /** @@ -817,7 +818,7 @@ export class PermissionController< target: TargetName, caveatType: CaveatType, caveatValue: ExtractCaveatValue< - ExtractCaveatsFromSpec, + ExtractCaveats, CaveatType >, ): void { @@ -857,7 +858,7 @@ export class PermissionController< target: TargetName, caveatType: CaveatType, caveatValue: ExtractCaveatValue< - ExtractCaveatsFromSpec, + ExtractCaveats, CaveatType >, ): void { @@ -895,7 +896,7 @@ export class PermissionController< target: TargetName, caveatType: CaveatType, caveatValue: ExtractCaveatValue< - ExtractCaveatsFromSpec, + ExtractCaveats, CaveatType >, ): void { @@ -1221,14 +1222,12 @@ export class PermissionController< CaveatSpecification >['parentCapability'], requestedCaveats?: unknown[] | null, - ): NonEmptyArray> | undefined { + ): NonEmptyArray> | undefined { const caveatArray = requestedCaveats?.map((requestedCaveat) => this.constructCaveat(origin, target, requestedCaveat), ); return caveatArray && caveatArray.length > 0 - ? (caveatArray as NonEmptyArray< - ExtractCaveatsFromSpec - >) + ? (caveatArray as NonEmptyArray>) : undefined; } @@ -1254,7 +1253,7 @@ export class PermissionController< CaveatSpecification >['parentCapability'], requestedCaveat: unknown, - ): ExtractCaveatsFromSpec { + ): ExtractCaveats { if (!isPlainObject(requestedCaveat)) { throw new InvalidCaveatError(requestedCaveat, origin, target); } @@ -1286,7 +1285,7 @@ export class PermissionController< (requestedCaveat.value as Json) ?? null, ); specification.validator?.(caveat, origin, target); - return caveat as ExtractCaveatsFromSpec; + return caveat as ExtractCaveats; } /** @@ -1307,7 +1306,7 @@ export class PermissionController< * @returns The granted permissions and request metadata. */ async requestPermissions( - origin: string, + origin: OriginString, requestedPermissions: RequestedPermissions, id: string = nanoid(), preserveExistingPermissions = true, @@ -1317,7 +1316,7 @@ export class PermissionController< string, ExtractPermission >, - { id: string; origin: string }, + { id: string; origin: OriginString }, ] > { this.validateRequestedPermissions(origin, requestedPermissions); @@ -1391,7 +1390,7 @@ export class PermissionController< * @param requestedPermissions - The requested permissions. */ private validateRequestedPermissions( - origin: string, + origin: OriginString, requestedPermissions: RequestedPermissions, ): void { if ( @@ -1522,12 +1521,12 @@ export class PermissionController< * @returns The result of the executed method. */ async executeRestrictedMethod( - origin: string, + origin: OriginString, methodName: ExtractPermission< PermissionSpecification, CaveatSpecification >['parentCapability'], - params?: GenericRestrictedMethodParams, + params?: RestrictedMethodParameters, ): Promise { // Throws if the method does not exist const methodImplementation = this._getRestrictedMethod(methodName, origin); @@ -1562,7 +1561,7 @@ export class PermissionController< */ private _executeRestrictedMethod( methodImplementation: RestrictedMethodBase< - GenericRestrictedMethodParams, + RestrictedMethodParameters, Json >, subject: PermissionSubjectMetadata, @@ -1570,8 +1569,8 @@ export class PermissionController< PermissionSpecification, CaveatSpecification >['parentCapability'], - params: GenericRestrictedMethodParams = [], - ): ReturnType> { + params: RestrictedMethodParameters = [], + ): ReturnType> { const { origin } = subject; const permission = this.getPermission(origin, method); @@ -1599,8 +1598,8 @@ export class PermissionController< */ private _getRestrictedMethod( method: string, - origin: string, - ): RestrictedMethodBase { + origin: OriginString, + ): RestrictedMethodBase { const methodImplementation = this.getRestrictedMethod( method as ExtractPermission< PermissionSpecification, diff --git a/packages/controllers/src/permissions/README.md b/packages/controllers/src/permissions/README.md index 7d0621acec..02510418ab 100644 --- a/packages/controllers/src/permissions/README.md +++ b/packages/controllers/src/permissions/README.md @@ -78,7 +78,7 @@ const caveatSpecifications = { method: AsyncRestrictedMethod, caveat: { type: 'filterArrayResponse'; value: string[] }, ) => - async (args: RestrictedMethodOptions) => { + async (args: RestrictedMethodOptions) => { const result = await method(args); return Array.isArray(result) ? result.filter(caveat.value.includes) @@ -92,7 +92,7 @@ const permissionSpecifications = { // i.e. the restricted method name target: 'wallet_getSecretArray', methodImplementation: ( - _args: RestrictedMethodOptions, + _args: RestrictedMethodOptions, ) => { return ['secret1', 'secret2', 'secret3']; }, diff --git a/packages/controllers/src/permissions/permission-middleware.ts b/packages/controllers/src/permissions/permission-middleware.ts index 12519951b0..5882fe463f 100644 --- a/packages/controllers/src/permissions/permission-middleware.ts +++ b/packages/controllers/src/permissions/permission-middleware.ts @@ -11,22 +11,19 @@ import { GenericPermission, PermissionSubjectMetadata, RestrictedMethodBase, - GenericRestrictedMethodParams, + RestrictedMethodParameters, } from '.'; type ExecuteRestrictedMethod = ( - methodImplementation: RestrictedMethodBase< - GenericRestrictedMethodParams, - Json - >, + methodImplementation: RestrictedMethodBase, subject: PermissionSubjectMetadata, method: Permission['parentCapability'], - params?: GenericRestrictedMethodParams, -) => ReturnType>; + params?: RestrictedMethodParameters, +) => ReturnType>; type GetRestrictedMethod = ( method: string, origin: string, -) => RestrictedMethodBase; +) => RestrictedMethodBase; type IsUnrestrictedMethod = (method: string) => boolean; type PermissionMiddlewareFactoryOptions = @@ -45,9 +42,9 @@ export function getPermissionMiddlewareFactory< }: PermissionMiddlewareFactoryOptions) { return function createPermissionMiddleware( subject: PermissionSubjectMetadata, - ): JsonRpcMiddleware { + ): JsonRpcMiddleware { const permissionsMiddleware = async ( - req: JsonRpcRequest, + req: JsonRpcRequest, res: PendingJsonRpcResponse, next: AsyncJsonRpcEngineNextCallback, ): Promise => { From cf0f54792f1b6973a0352b8524a880331faedc2e Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sat, 9 Oct 2021 15:27:04 -0700 Subject: [PATCH 079/147] WIP: Refactor controller types V (more cleanup) --- .../controllers/src/permissions/Caveat.ts | 59 ++++++++++++++----- .../controllers/src/permissions/Permission.ts | 20 +++---- .../permissions/PermissionController.test.ts | 1 + .../src/permissions/PermissionController.ts | 39 ++++-------- 4 files changed, 66 insertions(+), 53 deletions(-) diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index 9e21989b7d..23cc0e281a 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -66,15 +66,16 @@ export type CaveatDecorator = ( caveat: Caveat, ) => AsyncRestrictedMethod; -type ExtractCaveatValueFromDecorator> = - Decorator extends ( - decorated: any, - caveat: infer Caveat, - ) => AsyncRestrictedMethod - ? Caveat extends GenericCaveat - ? Caveat['value'] - : never - : never; +export type ExtractCaveatValueFromDecorator< + Decorator extends CaveatDecorator, +> = Decorator extends ( + decorated: any, + caveat: infer Caveat, +) => AsyncRestrictedMethod + ? Caveat extends GenericCaveat + ? Caveat['value'] + : never + : never; type CaveatValidator = ( caveat: { type: Caveat['type']; value: unknown }, @@ -114,9 +115,11 @@ export type CaveatSpecificationBase = { export type GenericCaveat = CaveatBase; export type CaveatSpecificationsMap< - CaveatSpecification extends CaveatSpecificationBase, + Specification extends CaveatSpecificationBase, > = { - [Key in CaveatSpecification['type']]: CaveatSpecification; + [Key in Specification['type']]: Specification extends CaveatSpecificationBase + ? Specification + : never; }; export type ExtractCaveats< @@ -128,12 +131,40 @@ export type ExtractCaveats< > : never; +/** + * Internal utility type, because using parameterized types in conditional types + * causes weird things to happen. + */ +type _ExtractCaveat< + Caveat extends GenericCaveat, + CaveatType extends string, +> = Caveat extends CaveatBase ? Caveat : never; + export type ExtractCaveat< CaveatSpecification extends CaveatSpecificationBase, CaveatType extends string, -> = ExtractCaveats extends CaveatBase - ? ExtractCaveats - : never; +> = _ExtractCaveat, CaveatType>; +// extends CaveatBase +// ? ExtractCaveats +// : never; + +/** + * A utility type for extracting the {@link CaveatBase.value} type from + * a union of caveat types. + * + * @template CaveatUnion - The caveat type union to extract a value type from. + * @template CaveatType - The type of the caveat whose value to extract. + */ +export type ExtractCaveatValue< + CaveatSpecification extends CaveatSpecificationBase, + CaveatType extends string, +> = ExtractCaveat['value']; +// export type ExtractCaveatValue< +// CaveatUnion extends GenericCaveat, +// CaveatType extends string, +// > = CaveatUnion extends CaveatBase +// ? CaveatValue +// : never; /** * Decorate a restricted method implementation with its caveats. diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index e8f5a04458..06ac6cdf5d 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -274,6 +274,11 @@ export type RestrictedMethodBase< | SyncRestrictedMethod | AsyncRestrictedMethod; +export type GenericRestrictedMethod = RestrictedMethodBase< + RestrictedMethodParameters, + Json +>; + export type RestrictedMethodConstraint< MethodImplementation extends GenericRestrictedMethod, > = MethodImplementation extends (args: infer Options) => Json | Promise @@ -282,11 +287,6 @@ export type RestrictedMethodConstraint< : never : never; -export type GenericRestrictedMethod = RestrictedMethodBase< - RestrictedMethodParameters, - Json ->; - export type PermissionFactory< Permission extends GenericPermission, RequestData extends Record, @@ -346,7 +346,7 @@ export type PermissionSpecificationBase = { * corresponds to. */ methodImplementation: GenericFunction; - // methodImplementation: RestrictedMethodConstraint; + // methodImplementation: RestrictedMethodConstraint; /** * The factory function used to get permission objects. Permissions returned @@ -396,11 +396,11 @@ export type PermissionSpecificationConstraint = * types. */ export type PermissionSpecificationsMap< - Specifications extends PermissionSpecificationBase, -> = PermissionSpecificationConstraint extends never + Specification extends PermissionSpecificationBase, +> = PermissionSpecificationConstraint extends never ? never : { - [TargetKey in Specifications['targetKey']]: Specifications extends PermissionSpecificationBase - ? Specifications + [TargetKey in Specification['targetKey']]: Specification extends PermissionSpecificationBase + ? Specification : never; }; diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 6f682dfaa9..5c54af3165 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -404,6 +404,7 @@ function getDefaultPermissionController( DefaultPermissionSpecifications, DefaultCaveatSpecifications >(opts); + // return new PermissionController(opts); } /** diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index aa58369414..a5330c32e9 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -19,14 +19,13 @@ import deepFreeze from 'deep-freeze-strict'; import { nanoid } from 'nanoid'; import { isPlainObject, hasProperty, NonEmptyArray } from '../utils'; import { - CaveatBase, constructCaveat as _constructCaveat, decorateWithCaveats, - GenericCaveat, ExtractCaveat, CaveatSpecificationBase, ExtractCaveats, CaveatSpecificationsMap, + ExtractCaveatValue, } from './Caveat'; import { constructPermission, @@ -225,20 +224,6 @@ export type PermissionControllerMessenger = RestrictedControllerMessenger< never >; -/** - * A utility type for extracting the {@link CaveatBase.value} type from - * a union of caveat types. - * - * @template CaveatUnion - The caveat type union to extract a value type from. - * @template CaveatType - The type of the caveat whose value to extract. - */ -export type ExtractCaveatValue< - CaveatUnion extends GenericCaveat, - CaveatType extends string, -> = CaveatUnion extends CaveatBase - ? CaveatValue - : never; - /** * Options for {@link PermissionController.grantPermissions}. */ @@ -380,7 +365,7 @@ export class PermissionController< /** * @param options - Permission controller options. * @param options.caveatSpecifications - The specifications of all caveats - * available to the controller. See {@link CaveatSpecifications} and the + * available to the controller. See {@link CaveatSpecificationsMap} and the * documentation for more details. * @param options.permissionSpecifications - The specifications of all * permissions available to the controller. See @@ -817,10 +802,7 @@ export class PermissionController< origin: OriginString, target: TargetName, caveatType: CaveatType, - caveatValue: ExtractCaveatValue< - ExtractCaveats, - CaveatType - >, + caveatValue: ExtractCaveatValue, ): void { if (this.hasCaveat(origin, target, caveatType)) { throw new CaveatAlreadyExistsError(origin, target, caveatType); @@ -853,14 +835,16 @@ export class PermissionController< CaveatSpecification >['parentCapability'], CaveatType extends CaveatSpecification['type'], + CaveatValue extends ExtractCaveatValue, >( origin: OriginString, target: TargetName, caveatType: CaveatType, - caveatValue: ExtractCaveatValue< - ExtractCaveats, - CaveatType - >, + caveatValue: CaveatValue, + // caveatValue: ExtractCaveatValue< + // CaveatSpecification, + // CaveatType + // >, ): void { if (!this.hasCaveat(origin, target, caveatType)) { throw new CaveatDoesNotExistError(origin, target, caveatType); @@ -895,10 +879,7 @@ export class PermissionController< origin: OriginString, target: TargetName, caveatType: CaveatType, - caveatValue: ExtractCaveatValue< - ExtractCaveats, - CaveatType - >, + caveatValue: ExtractCaveatValue, ): void { this.update((draftState) => { const subject = draftState.subjects[origin]; From 83835250e0550f6e3ffe3e3d1ebb068e021aa6e3 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sat, 9 Oct 2021 16:02:28 -0700 Subject: [PATCH 080/147] =?UTF-8?q?Refactor=20controller=20types=20?= =?UTF-8?q?=F0=9F=98=AD=F0=9F=99=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/src/permissions/Permission.ts | 26 +++--------- .../permissions/PermissionController.test.ts | 27 +++--------- .../src/permissions/PermissionController.ts | 41 ++++++++----------- 3 files changed, 29 insertions(+), 65 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 06ac6cdf5d..9ea6cb78e6 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -70,20 +70,6 @@ export type PermissionBase< : Target; }; -// TODO:delete? -/** - * Makes the {@link PermissionBase.caveats} property of the given permission mutable. - * Useful for mutating the caveats of a permission in state updates. - * - * @template Permission - The permission type to modify. - */ -export type MutableCaveats = { - -readonly [K in keyof Pick]: Permission[K]; -} & - { - [K in keyof Omit]: Permission[K]; - }; - /** * A utility type for ensuring that the given permission target name conforms to * our naming conventions. @@ -225,7 +211,7 @@ type RestrictedMethodContext = Readonly<{ [key: string]: any; }>; -export type RestrictedMethodParameters = Json[] | Record | never; +export type RestrictedMethodParameters = Json[] | Record | void; /** * The arguments passed to a restricted method implementation. @@ -280,7 +266,7 @@ export type GenericRestrictedMethod = RestrictedMethodBase< >; export type RestrictedMethodConstraint< - MethodImplementation extends GenericRestrictedMethod, + MethodImplementation extends RestrictedMethodBase, > = MethodImplementation extends (args: infer Options) => Json | Promise ? Options extends RestrictedMethodOptions ? MethodImplementation @@ -397,10 +383,10 @@ export type PermissionSpecificationConstraint = */ export type PermissionSpecificationsMap< Specification extends PermissionSpecificationBase, -> = PermissionSpecificationConstraint extends never - ? never - : { +> = Specification extends PermissionSpecificationConstraint + ? { [TargetKey in Specification['targetKey']]: Specification extends PermissionSpecificationBase ? Specification : never; - }; + } + : never; diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 5c54af3165..f5e2cdda4b 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -216,9 +216,8 @@ function getDefaultPermissionSpecifications() { CaveatTypes.filterArrayResponse, CaveatTypes.reverseArrayResponse, ], - methodImplementation: (_args: RestrictedMethodOptions) => { + methodImplementation: (_args: RestrictedMethodOptions) => { return ['a', 'b', 'c']; - // return () => undefined; }, }, [PermissionKeys.wallet_getSecretObject]: { @@ -227,9 +226,8 @@ function getDefaultPermissionSpecifications() { CaveatTypes.filterObjectResponse, CaveatTypes.noopCaveat, ], - methodImplementation: (_args: RestrictedMethodOptions) => { + methodImplementation: (_args: RestrictedMethodOptions) => { return { a: 'x', b: 'y', c: 'z' }; - // return () => undefined; }, validator: (permission: GenericPermission) => { // A dummy validator for a caveat type that should be impossible to add @@ -249,7 +247,6 @@ function getDefaultPermissionSpecifications() { 'wallet_getSecret_', '', )}"!`; - // return Symbol('foo') }, factory: (options: PermissionOptions) => constructPermission({ @@ -267,20 +264,6 @@ function getDefaultPermissionSpecifications() { } as const; } -// TODO:delete -// type DoesExtend = T extends U ? T : never; - -// const specs = getDefaultPermissionSpecifications() -// type func = typeof specs['wallet_getSecret_*']['methodImplementation'] -// type x = DoesExtend -// type xx = DoesExtend) => Json | Promise>; -// type xxx = DoesExtend Json | Promise>; -// type xxxx = DoesExtend Json | Promise>; - type GetSpecMapValues = T extends PermissionSpecificationsMap< PermissionSpecificationBase > @@ -542,7 +525,8 @@ describe('PermissionController', () => { expect( controller.getRestrictedMethod(PermissionNames.wallet_getSecretArray), ).toStrictEqual( - controller.permissionSpecifications[ + // TODO:types what? + (controller.permissionSpecifications as any)[ PermissionKeys.wallet_getSecretArray ].methodImplementation, ); @@ -555,7 +539,8 @@ describe('PermissionController', () => { PermissionNames.wallet_getSecret_('foo'), ), ).toStrictEqual( - controller.permissionSpecifications[ + // TODO:types what? + (controller.permissionSpecifications as any)[ PermissionKeys['wallet_getSecret_*'] ].methodImplementation, ); diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index a5330c32e9..e6452ec94b 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -6,6 +6,7 @@ import { AcceptRequest as AcceptApprovalRequest, RejectRequest as RejectApprovalRequest, HasApprovalRequest, + StateMetadata, } from '@metamask/controllers'; // These are used in docstrings, but ESLint is ignorant of docstrings. /* eslint-disable @typescript-eslint/no-unused-vars */ @@ -132,7 +133,7 @@ export type PermissionControllerState = /** * The state metadata of the {@link PermissionController}. */ -const stateMetadata = { +const stateMetadata: StateMetadata> = { subjects: { persist: true, anonymous: true }, }; @@ -252,13 +253,11 @@ export type GenericPermissionController = PermissionController< export type ExtractPermission< PermissionSpecification extends PermissionSpecificationBase, CaveatSpecification extends CaveatSpecificationBase, -> = PermissionSpecification extends any - ? CaveatSpecification extends any - ? PermissionConstraint< - PermissionSpecification['targetKey'], - ExtractCaveats - > - : never +> = PermissionSpecification extends PermissionSpecificationConstraint + ? PermissionConstraint< + PermissionSpecification['targetKey'], + ExtractCaveats + > : never; /** @@ -392,7 +391,8 @@ export class PermissionController< } = options; super({ name: controllerName, - // TODO:types why? + // Typecast: The state metadata object is declared outside of the + // controller instance and therefore incorrectly genericized. metadata: stateMetadata as any, messenger, state: { @@ -541,15 +541,8 @@ export class PermissionController< * @param origin - The origin of the subject. * @returns The permissions of the subject, if any. */ - getPermissions( - origin: OriginString, // | SubjectPermissions< // ExtractPermission // > // | undefined { - ) { - // TODO:types why? - return this.state.subjects[origin]?.permissions as - | SubjectPermissions< - ExtractPermission - > - | undefined; + getPermissions(origin: OriginString) { + return this.state.subjects[origin]?.permissions; } /** @@ -911,9 +904,9 @@ export class PermissionController< ); if (caveatIndex === -1) { - permission.caveats.push(caveat as any); + permission.caveats.push(caveat); } else { - permission.caveats.splice(caveatIndex, 1, caveat as any); + permission.caveats.splice(caveatIndex, 1, caveat); } } else { // Typecast: At this point, we don't know if the specific permission @@ -924,7 +917,7 @@ export class PermissionController< } this.validateModifiedPermission( - // TODO:types why? + // Typecast: Immer WritableDraft incompatibility permission as unknown as ExtractPermission< PermissionSpecification, CaveatSpecification @@ -971,14 +964,14 @@ export class PermissionController< } if (permission.caveats.length === 1) { - // TODO:types why? + // Typecast: Immer WritableDraft incompatibility permission.caveats = null as any; } else { permission.caveats.splice(caveatIndex, 1); } this.validateModifiedPermission( - // TODO:types why? + // Typecast: Immer WritableDraft incompatibility permission as unknown as ExtractPermission< PermissionSpecification, CaveatSpecification @@ -1177,7 +1170,7 @@ export class PermissionController< this.update((draftState) => { if (!draftState.subjects[origin]) { // Typecast: immer's WritableDraft is incompatible with our generics - draftState.subjects[origin] = { origin, permissions: {} as any }; + draftState.subjects[origin] = { origin, permissions: {} }; } draftState.subjects[origin].permissions = permissions as any; From d27dcee929160b88d644ad317c4779037f38ea80 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sat, 9 Oct 2021 16:33:52 -0700 Subject: [PATCH 081/147] Restore caveat method type safety --- .../controllers/src/permissions/Caveat.ts | 9 --- .../controllers/src/permissions/Permission.ts | 58 +++++++++++++++++-- .../permissions/PermissionController.test.ts | 31 ++-------- .../src/permissions/PermissionController.ts | 33 ++++++++--- packages/controllers/src/permissions/enums.ts | 4 -- packages/controllers/src/permissions/index.ts | 5 +- .../permissions/rpc-methods/getPermissions.ts | 2 +- .../rpc-methods/requestPermissions.ts | 2 +- packages/controllers/src/permissions/utils.ts | 26 +++++++++ 9 files changed, 115 insertions(+), 55 deletions(-) delete mode 100644 packages/controllers/src/permissions/enums.ts create mode 100644 packages/controllers/src/permissions/utils.ts diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index 23cc0e281a..7d609e411e 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -144,9 +144,6 @@ export type ExtractCaveat< CaveatSpecification extends CaveatSpecificationBase, CaveatType extends string, > = _ExtractCaveat, CaveatType>; -// extends CaveatBase -// ? ExtractCaveats -// : never; /** * A utility type for extracting the {@link CaveatBase.value} type from @@ -159,12 +156,6 @@ export type ExtractCaveatValue< CaveatSpecification extends CaveatSpecificationBase, CaveatType extends string, > = ExtractCaveat['value']; -// export type ExtractCaveatValue< -// CaveatUnion extends GenericCaveat, -// CaveatType extends string, -// > = CaveatUnion extends CaveatBase -// ? CaveatValue -// : never; /** * Decorate a restricted method implementation with its caveats. diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 9ea6cb78e6..02c234adfd 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -116,10 +116,59 @@ export type GenericPermission = PermissionConstraint< GenericCaveat | never >; -// type ExtractCaveats = Permission extends PermissionConstraint< -// GenericTargetName, -// infer AllowedCaveats -// > ? AllowedCaveats : never; +/** + * An internal utility type used in {@link ExtractPermissionTargetKey}. + * + * @template Key - The target key type to extract from. + * @template Name - The name whose key to extract. + */ +type KeyOfTargetName< + Key extends string, + Name extends string, +> = Name extends ExtractPermissionTargetNames ? Key : never; + +/** + * A utility type for finding the permission target key corresponding to a + * target name. In a way, the inverse of {@link ExtractPermissionTargetNames}. + * + * See the documentation for the distinction between target names and keys. + * + * @template Key - The target key type to extract from. + * @template Name - The name whose key to extract. + */ +type ExtractPermissionTargetKey< + Key extends string, + Name extends string, +> = Name extends Key ? Name : Extract>; + +/** + * Internal utility for extracting the members types of an array. The type + * evalutes to `never` if the specified type is the empty tuple or neither + * an array nor a tuple. + * + * @template T - The array type whose members to extract. + */ +type ExtractArrayMembers = T extends readonly [...infer U] | [...infer U] + ? U[number] + : never; + +/** + * A utility type for extracting the allowed caveat types for a particular + * permission from a permission specification type or type union. + * + * @template PermissionSpecification - The permission specification type to + * extract valid caveat types from. + * @template TargetName - The target name of the permission whose allowed + * caveats to extract. + */ +export type ExtractAllowedCaveatTypes< + PermissionSpecification extends PermissionSpecificationBase, + TargetName extends string, +> = PermissionSpecification extends PermissionSpecificationBase< + ExtractPermissionTargetKey +> + ? ExtractArrayMembers + : never; /** * The options object of {@link constructPermission}. @@ -332,7 +381,6 @@ export type PermissionSpecificationBase = { * corresponds to. */ methodImplementation: GenericFunction; - // methodImplementation: RestrictedMethodConstraint; /** * The factory function used to get permission objects. Permissions returned diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index f5e2cdda4b..15396ca733 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -24,10 +24,7 @@ import { PermissionOptions, RestrictedMethodOptions, RestrictedMethodParameters, - PermissionSpecificationBase, - PermissionSpecificationsMap, - CaveatSpecificationsMap, - CaveatSpecificationBase, + ExtractSpecifications, } from '.'; // CaveatBase types and specifications @@ -56,12 +53,6 @@ type FilterObjectCaveat = CaveatBase< type NoopCaveat = CaveatBase; -// type DefaultCaveats = -// | FilterArrayCaveat -// | ReverseArrayCaveat -// | FilterObjectCaveat -// | NoopCaveat; - /** * Gets caveat specifications for: * - {@link FilterArrayCaveat} @@ -151,13 +142,7 @@ function getDefaultCaveatSpecifications() { } as const; } -type GetCaveatSpecMapValues = T extends CaveatSpecificationsMap< - CaveatSpecificationBase -> - ? T[keyof T] - : never; - -type DefaultCaveatSpecifications = GetCaveatSpecMapValues< +type DefaultCaveatSpecifications = ExtractSpecifications< ReturnType >; @@ -166,7 +151,8 @@ type DefaultCaveatSpecifications = GetCaveatSpecMapValues< // wallet_getSecret_* // We only define types for this permission because it's the only one with a // factory. -// Other permission types are extracted from the permission specifications. +// Other permission types are extracted from the permission specifications in +// the permission controller. type SecretNamespacedPermissionKey = 'wallet_getSecret_*'; @@ -264,13 +250,7 @@ function getDefaultPermissionSpecifications() { } as const; } -type GetSpecMapValues = T extends PermissionSpecificationsMap< - PermissionSpecificationBase -> - ? T[keyof T] - : never; - -type DefaultPermissionSpecifications = GetSpecMapValues< +type DefaultPermissionSpecifications = ExtractSpecifications< ReturnType >; @@ -387,7 +367,6 @@ function getDefaultPermissionController( DefaultPermissionSpecifications, DefaultCaveatSpecifications >(opts); - // return new PermissionController(opts); } /** diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index e6452ec94b..d6e60f82e0 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -41,6 +41,7 @@ import { PermissionSpecificationsMap, PermissionSpecificationBase, PermissionConstraint, + ExtractAllowedCaveatTypes, } from './Permission'; import { PermissionDoesNotExistError, @@ -61,7 +62,7 @@ import { userRejectedRequest, PermissionsRequestNotFoundError, } from './errors'; -import { MethodNames } from './enums'; +import { MethodNames } from './utils'; import { getPermissionMiddlewareFactory } from './permission-middleware'; /** @@ -725,7 +726,10 @@ export class PermissionController< PermissionSpecification, CaveatSpecification >['parentCapability'], - CaveatType extends CaveatSpecification['type'], + CaveatType extends ExtractAllowedCaveatTypes< + PermissionSpecification, + TargetName + >, >(origin: OriginString, target: TargetName, caveatType: CaveatType): boolean { return Boolean(this.getCaveat(origin, target, caveatType)); } @@ -750,7 +754,10 @@ export class PermissionController< PermissionSpecification, CaveatSpecification >['parentCapability'], - CaveatType extends CaveatSpecification['type'], + CaveatType extends ExtractAllowedCaveatTypes< + PermissionSpecification, + TargetName + >, >( origin: OriginString, target: TargetName, @@ -790,7 +797,10 @@ export class PermissionController< PermissionSpecification, CaveatSpecification >['parentCapability'], - CaveatType extends CaveatSpecification['type'], + CaveatType extends ExtractAllowedCaveatTypes< + PermissionSpecification, + TargetName + >, >( origin: OriginString, target: TargetName, @@ -827,7 +837,10 @@ export class PermissionController< PermissionSpecification, CaveatSpecification >['parentCapability'], - CaveatType extends CaveatSpecification['type'], + CaveatType extends ExtractAllowedCaveatTypes< + PermissionSpecification, + TargetName + >, CaveatValue extends ExtractCaveatValue, >( origin: OriginString, @@ -867,7 +880,10 @@ export class PermissionController< PermissionSpecification, CaveatSpecification >['parentCapability'], - CaveatType extends CaveatSpecification['type'], + CaveatType extends ExtractAllowedCaveatTypes< + PermissionSpecification, + TargetName + >, >( origin: OriginString, target: TargetName, @@ -944,7 +960,10 @@ export class PermissionController< PermissionSpecification, CaveatSpecification >['parentCapability'], - CaveatType extends CaveatSpecification['type'], + CaveatType extends ExtractAllowedCaveatTypes< + PermissionSpecification, + TargetName + >, >(origin: OriginString, target: TargetName, caveatType: CaveatType): void { this.update((draftState) => { const permission = draftState.subjects[origin]?.permissions[target]; diff --git a/packages/controllers/src/permissions/enums.ts b/packages/controllers/src/permissions/enums.ts deleted file mode 100644 index 41bc718318..0000000000 --- a/packages/controllers/src/permissions/enums.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum MethodNames { - requestPermissions = 'wallet_requestPermissions', - getPermissions = 'wallet_getPermissions', -} diff --git a/packages/controllers/src/permissions/index.ts b/packages/controllers/src/permissions/index.ts index a4d3df5b24..902e332008 100644 --- a/packages/controllers/src/permissions/index.ts +++ b/packages/controllers/src/permissions/index.ts @@ -1,6 +1,7 @@ export * from './Caveat'; -export * from './enums'; export * from './Permission'; export * from './PermissionController'; -// TODO: Move these to the appropriate package +export * from './utils'; + +// TODO:feat Move these to the appropriate package export * as permissionRpcMethods from './rpc-methods'; diff --git a/packages/controllers/src/permissions/rpc-methods/getPermissions.ts b/packages/controllers/src/permissions/rpc-methods/getPermissions.ts index 416d6d9a39..dcf5ec2321 100644 --- a/packages/controllers/src/permissions/rpc-methods/getPermissions.ts +++ b/packages/controllers/src/permissions/rpc-methods/getPermissions.ts @@ -3,7 +3,7 @@ import type { JsonRpcEngineEndCallback, PendingJsonRpcResponse, } from 'json-rpc-engine'; -import { MethodNames } from '../enums'; +import { MethodNames } from '../utils'; import type { GenericPermission } from '../Permission'; import type { SubjectPermissions } from '../PermissionController'; diff --git a/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts b/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts index f1af941467..ec39630b92 100644 --- a/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts +++ b/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts @@ -5,7 +5,7 @@ import type { PendingJsonRpcResponse, } from 'json-rpc-engine'; import { nanoid } from 'nanoid'; -import { MethodNames } from '../enums'; +import { MethodNames } from '../utils'; import { invalidParams } from '../errors'; import type { GenericPermission, RequestedPermissions } from '../Permission'; diff --git a/packages/controllers/src/permissions/utils.ts b/packages/controllers/src/permissions/utils.ts new file mode 100644 index 0000000000..994009862b --- /dev/null +++ b/packages/controllers/src/permissions/utils.ts @@ -0,0 +1,26 @@ +import { CaveatSpecificationBase, CaveatSpecificationsMap } from './Caveat'; +import { + PermissionSpecificationBase, + PermissionSpecificationsMap, +} from './Permission'; + +export enum MethodNames { + requestPermissions = 'wallet_requestPermissions', + getPermissions = 'wallet_getPermissions', +} + +/** + * Utility type for extracting a union of all individual + * {@link CaveatSpecificationBase} or + * {@link PermissionSpecificationBase} types + * from a {@link CaveatSpecificationsMap} or + * {@link PermissionSpecificationsMap}. + * + * @template SpecificationsMap - The caveat or permission specifications map + * whose specification type union to extract. + */ +export type ExtractSpecifications< + SpecificationsMap extends + | CaveatSpecificationsMap> + | PermissionSpecificationsMap>, +> = SpecificationsMap[keyof SpecificationsMap]; From b56b354c801de1a7afbdb636fc1df7998591a42a Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sat, 9 Oct 2021 16:56:59 -0700 Subject: [PATCH 082/147] fixup! Restore caveat method type safety --- packages/controllers/src/permissions/Permission.ts | 10 ++++------ .../src/permissions/PermissionController.ts | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 02c234adfd..b399a55ca3 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -25,7 +25,7 @@ export type GenericTargetName = string; */ export type PermissionBase< Target extends GenericTargetName, - AllowedCaveat extends GenericCaveat | never, + AllowedCaveat extends GenericCaveat, > = { /** * The context(s) in which this capability is meaningful. @@ -65,9 +65,7 @@ export type PermissionBase< * * In the context of MetaMask, this is always the name of an RPC method. */ - readonly parentCapability: Target extends `${string}_` - ? `${Target}${string}` - : Target; + readonly parentCapability: Target; }; /** @@ -103,7 +101,7 @@ type ExtractPermissionTargetNames = */ export type PermissionConstraint< TargetKey extends string, - AllowedCaveats extends GenericCaveat | never, + AllowedCaveats extends GenericCaveat, > = TargetNameConstraint> extends never ? never : PermissionBase, AllowedCaveats>; @@ -113,7 +111,7 @@ export type PermissionConstraint< */ export type GenericPermission = PermissionConstraint< GenericTargetName, - GenericCaveat | never + GenericCaveat >; /** diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index d6e60f82e0..6266d5234f 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -1408,7 +1408,7 @@ export class PermissionController< throw invalidParams({ data: { origin, requestedPermissions } }); } - if (!this.getRestrictedMethod(methodName)) { + if (!this.getTargetKey(methodName)) { throw methodNotFound({ method: methodName, data: { origin, requestedPermissions }, From 4078aee828808a0203efa6674c7ff2187c13798c Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sat, 9 Oct 2021 18:42:49 -0700 Subject: [PATCH 083/147] Status quo ante type refactor --- .../controllers/src/permissions/Permission.ts | 4 +- .../permissions/PermissionController.test.ts | 53 ++++++- .../src/permissions/PermissionController.ts | 132 ++++++++++++------ .../controllers/src/permissions/errors.ts | 40 ++++-- 4 files changed, 172 insertions(+), 57 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index b399a55ca3..2c4343f0ce 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -390,7 +390,7 @@ export type PermissionSpecificationBase = { * used, and the validator function (if specified) will be called on newly * constructed permissions. */ - factory?: PermissionFactory; + factory?: PermissionFactory>; /** * The validator function used to validate permissions of the associated type @@ -399,7 +399,7 @@ export type PermissionSpecificationBase = { * * The validator should throw an appropriate JSON-RPC error if validation fails. */ - validator?: PermissionValidator; + validator?: PermissionValidator; }; /** diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 15396ca733..67e60550a2 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -167,6 +167,7 @@ type SecretNamespacedPermission = PermissionConstraint< const PermissionKeys = { wallet_getSecretArray: 'wallet_getSecretArray', wallet_getSecretObject: 'wallet_getSecretObject', + wallet_noop: 'wallet_noop', 'wallet_getSecret_*': 'wallet_getSecret_*', } as const; @@ -177,6 +178,7 @@ const PermissionKeys = { const PermissionNames = { wallet_getSecretArray: PermissionKeys.wallet_getSecretArray, wallet_getSecretObject: PermissionKeys.wallet_getSecretObject, + wallet_noop: PermissionKeys.wallet_noop, wallet_getSecret_: (str: string) => `wallet_getSecret_${str}` as const, } as const; @@ -228,7 +230,7 @@ function getDefaultPermissionSpecifications() { [PermissionKeys['wallet_getSecret_*']]: { targetKey: PermissionKeys['wallet_getSecret_*'], allowedCaveats: [CaveatTypes.noopCaveat], - methodImplementation: (args: RestrictedMethodOptions<[]>) => { + methodImplementation: (args: RestrictedMethodOptions) => { return `Hello, secret friend "${args.method.replace( 'wallet_getSecret_', '', @@ -247,6 +249,12 @@ function getDefaultPermissionSpecifications() { ); }, }, + [PermissionKeys.wallet_noop]: { + targetKey: PermissionKeys.wallet_noop, + methodImplementation: (_args: RestrictedMethodOptions) => { + return null; + }, + }, } as const; } @@ -423,7 +431,7 @@ describe('PermissionController', () => { expect(controller.state).toStrictEqual(getExistingPermissionState()); }); - it('throws an error for invalid permission target keys', () => { + it('throws if a permission specification target key is invalid', () => { expect( () => new PermissionController< @@ -468,7 +476,9 @@ describe('PermissionController', () => { }), ), ).toThrow(`Invalid permission target key: "foo*"`); + }); + it('throws if a permission specification map key does not match its "targetKey" value', () => { expect( () => new PermissionController< @@ -486,6 +496,25 @@ describe('PermissionController', () => { `Invalid permission specification: key "foo" must match specification.target value "bar".`, ); }); + + it('throws if a permission specification lists unrecognized caveats', () => { + const permissionSpecifications = getDefaultPermissionSpecifications(); + ( + permissionSpecifications as any + ).wallet_getSecretArray.allowedCaveats.push('foo'); + + expect( + () => + new PermissionController< + DefaultPermissionSpecifications, + DefaultCaveatSpecifications + >( + getPermissionControllerOptions({ + permissionSpecifications, + }), + ), + ).toThrow(new errors.UnrecognizedCaveatTypeError('foo')); + }); }); describe('clearState', () => { @@ -1327,7 +1356,13 @@ describe('PermissionController', () => { CaveatTypes.filterArrayResponse as any, ['foo'], ), - ).toThrow(new Error('getSecretObject permission validation failed')); + ).toThrow( + new errors.ForbiddenCaveatError( + CaveatTypes.filterArrayResponse, + origin, + PermissionNames.wallet_getSecretObject, + ), + ); }); }); @@ -2051,8 +2086,8 @@ describe('PermissionController', () => { }, }), ).toThrow( - new errors.CaveatTypeDoesNotExistError( - constructCaveat('fooType', 'bar'), + new errors.UnrecognizedCaveatTypeError( + 'fooType', origin, PermissionNames.wallet_getSecretArray, ), @@ -2128,7 +2163,13 @@ describe('PermissionController', () => { }, }, }), - ).toThrow(new Error('getSecretObject permission validation failed')); + ).toThrow( + new errors.ForbiddenCaveatError( + CaveatTypes.filterArrayResponse, + origin, + PermissionNames.wallet_getSecretObject, + ), + ); }); }); diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 6266d5234f..9d5b3b1da9 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -48,7 +48,7 @@ import { methodNotFound, UnrecognizedSubjectError, CaveatDoesNotExistError, - CaveatTypeDoesNotExistError, + UnrecognizedCaveatTypeError, CaveatMissingValueError, InvalidCaveatFieldsError, InvalidSubjectIdentifierError, @@ -61,6 +61,7 @@ import { internalError, userRejectedRequest, PermissionsRequestNotFoundError, + ForbiddenCaveatError, } from './errors'; import { MethodNames } from './utils'; import { getPermissionMiddlewareFactory } from './permission-middleware'; @@ -407,7 +408,11 @@ export class PermissionController< this._unrestrictedMethods = new Set(unrestrictedMethods); this._caveatSpecifications = deepFreeze({ ...caveatSpecifications }); - this.validatePermissionSpecifications(permissionSpecifications); + this.validatePermissionSpecifications( + permissionSpecifications, + this._caveatSpecifications, + ); + this._permissionSpecifications = deepFreeze({ ...permissionSpecifications, }); @@ -416,8 +421,9 @@ export class PermissionController< this.createPermissionMiddleware = getPermissionMiddlewareFactory({ executeRestrictedMethod: this._executeRestrictedMethod.bind(this), getRestrictedMethod: this._getRestrictedMethod.bind(this), - isUnrestrictedMethod: (methodName: string) => - this.unrestrictedMethods.has(methodName), + isUnrestrictedMethod: this.unrestrictedMethods.has.bind( + this.unrestrictedMethods, + ), }); } @@ -432,10 +438,11 @@ export class PermissionController< * controller's constructor. */ private validatePermissionSpecifications( - specifications: PermissionSpecificationsMap, + permissionSpecifications: PermissionSpecificationsMap, + caveatSpecifications: CaveatSpecificationsMap, ) { - Object.entries(specifications).forEach( - ([targetKey, { targetKey: innerTargetKey }]) => { + Object.entries(permissionSpecifications).forEach( + ([targetKey, { targetKey: innerTargetKey, allowedCaveats }]) => { // Check if the target key is the empty string, ends with "_", or ends // with "*" but not "_*" if (!targetKey || /_$/u.test(targetKey) || /[^_]\*$/u.test(targetKey)) { @@ -447,6 +454,14 @@ export class PermissionController< `Invalid permission specification: key "${targetKey}" must match specification.target value "${innerTargetKey}".`, ); } + + if (allowedCaveats) { + allowedCaveats.forEach((caveatType) => { + if (!hasProperty(caveatSpecifications, caveatType)) { + throw new UnrecognizedCaveatTypeError(caveatType); + } + }); + } }, ); } @@ -938,6 +953,8 @@ export class PermissionController< PermissionSpecification, CaveatSpecification >, + origin, + target, ); }); } @@ -995,19 +1012,33 @@ export class PermissionController< PermissionSpecification, CaveatSpecification >, + origin, + target, ); }); } /** - * Validates the specified modified permission. Effectively, calls the - * validator function of the specified permission, if any. Should always - * be invoked on a permission after its caveats have been modified. + * Validates the specified modified permission. Should **always** be invoked + * on a permission after its caveats have been modified. + * + * Just like {@link PermissionController.validatePermission}, except that the + * corresponding target key and specification are retrieved first, and an + * error is thrown if the target key does not exist. * * @param permission - The modified permission to validate. + * @param origin - The origin associated with the permission. + * @param targetName - The target name name of the permission. */ - private validateModifiedPermission( - permission: ExtractPermission, + private validateModifiedPermission< + Permission extends ExtractPermission< + PermissionSpecification, + CaveatSpecification + >, + >( + permission: Permission, + origin: OriginString, + targetName: Permission['parentCapability'], ): void { const targetKey = this.getTargetKey(permission.parentCapability); /* istanbul ignore next: this should be impossible */ @@ -1016,7 +1047,13 @@ export class PermissionController< `Fatal: Existing permission target key "${targetKey}" has no specification.`, ); } - this.permissionSpecifications[targetKey].validator?.(permission); + + this.validatePermission( + this.permissionSpecifications[targetKey], + permission, + origin, + targetName, + ); } /** @@ -1161,7 +1198,7 @@ export class PermissionController< ? specification.factory(permissionOptions, requestData) : constructPermission(permissionOptions) ) as ExtractPermission; - specification.validator?.(permission, origin, targetName); + this.validatePermission(specification, permission, origin, targetName); permissions[targetName] = permission; } @@ -1169,6 +1206,43 @@ export class PermissionController< return permissions; } + /** + * Validates the specified permission by: + * - Ensuring that it only includes caveats allowed by its specification. + * - Calling the validator of its specification, if any. + * + * An error is thrown if validation fails. + * + * @param specification - The specification of the permission. + * @param permission - The permission to validate. + * @param origin - The origin associated with the permission. + * @param targetName - The target name of the permission. + */ + private validatePermission< + Permission extends ExtractPermission< + PermissionSpecification, + CaveatSpecification + >, + >( + specification: PermissionSpecificationBase, + permission: Permission, + origin: OriginString, + targetName: Permission['parentCapability'], + ): void { + const { allowedCaveats, validator } = specification; + if (permission.caveats !== null) { + permission.caveats.forEach(({ type }) => { + if (!allowedCaveats?.includes(type)) { + throw new ForbiddenCaveatError(type, origin, targetName); + } + }); + } + + if (validator) { + validator(permission, origin, targetName); + } + } + /** * Assigns the specified permissions to the subject with the given origin. * Overwrites all existing permissions, and creates a subject entry if it @@ -1265,7 +1339,11 @@ export class PermissionController< ]; if (!specification) { - throw new CaveatTypeDoesNotExistError(requestedCaveat, origin, target); + throw new UnrecognizedCaveatTypeError( + requestedCaveat.type, + origin, + target, + ); } if (!hasProperty(requestedCaveat, 'value')) { @@ -1531,10 +1609,6 @@ export class PermissionController< params, ); - if (resultIsError(result)) { - throw result; - } - if (result === undefined) { throw new Error( `Internal request for method "${methodName}" as origin "${origin}" has no result.`, @@ -1606,23 +1680,3 @@ export class PermissionController< return methodImplementation; } } - -/** - * Type guard for checking whether the value returned from a restricted method - * invocation is a result or an error. - * - * @param resultOrError - The value returned from a restricted method - * invocation. - * @returns Whether the value is an {@link Error}. - */ -export function resultIsError( - resultOrError: Json | Error, -): resultOrError is Error { - return Boolean( - resultOrError instanceof Error || - (resultOrError && - isPlainObject(resultOrError) && - hasProperty(resultOrError, 'message') && - typeof resultOrError.message === 'string'), - ); -} diff --git a/packages/controllers/src/permissions/errors.ts b/packages/controllers/src/permissions/errors.ts index d0d2c15eaa..fe8ce22be3 100644 --- a/packages/controllers/src/permissions/errors.ts +++ b/packages/controllers/src/permissions/errors.ts @@ -89,8 +89,26 @@ export class PermissionDoesNotExistError extends Error { } export class UnrecognizedCaveatTypeError extends Error { - constructor(caveatType: string) { + public data: { + caveatType: string; + origin?: string; + target?: string; + }; + + constructor(caveatType: string); + + constructor(caveatType: string, origin: string, target: string); + + constructor(caveatType: string, origin?: string, target?: string) { super(`Unrecognized caveat type: "${caveatType}"`); + this.data = { caveatType }; + if (origin !== undefined) { + this.data.origin = origin; + } + + if (target !== undefined) { + this.data.target = target; + } } } @@ -136,7 +154,7 @@ export class InvalidCaveatTypeError extends Error { } } -export class CaveatTypeDoesNotExistError extends Error { +export class CaveatMissingValueError extends Error { public data: { caveat: Record; origin: string; @@ -144,12 +162,12 @@ export class CaveatTypeDoesNotExistError extends Error { }; constructor(caveat: Record, origin: string, target: string) { - super(`Caveat type "${caveat.type}" does not exist.`); + super(`Caveat is missing "value" field.`); this.data = { caveat, origin, target }; } } -export class CaveatMissingValueError extends Error { +export class InvalidCaveatFieldsError extends Error { public data: { caveat: Record; origin: string; @@ -157,23 +175,25 @@ export class CaveatMissingValueError extends Error { }; constructor(caveat: Record, origin: string, target: string) { - super(`Caveat is missing "value" field.`); + super( + `Caveat has unexpected number of fields: "${Object.keys(caveat).length}"`, + ); this.data = { caveat, origin, target }; } } -export class InvalidCaveatFieldsError extends Error { +export class ForbiddenCaveatError extends Error { public data: { - caveat: Record; + caveatType: string; origin: string; target: string; }; - constructor(caveat: Record, origin: string, target: string) { + constructor(caveatType: string, origin: string, targetName: string) { super( - `Caveat has unexpected number of fields: "${Object.keys(caveat).length}"`, + `Permissions for target "${targetName}" may not have caveats of type "${caveatType}".`, ); - this.data = { caveat, origin, target }; + this.data = { caveatType, origin, target: targetName }; } } From 99c8454e9f22397a54bf32d80ed7e1756cbaed9b Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sat, 9 Oct 2021 21:43:15 -0700 Subject: [PATCH 084/147] Unit tests: PermissionController, complete --- .../permissions/PermissionController.test.ts | 164 +++++++++++++++++- .../src/permissions/PermissionController.ts | 31 +++- 2 files changed, 190 insertions(+), 5 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 67e60550a2..4ef7bfaf7c 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -165,9 +165,11 @@ type SecretNamespacedPermission = PermissionConstraint< * Permission key constants. */ const PermissionKeys = { + wallet_doubleNumber: 'wallet_doubleNumber', wallet_getSecretArray: 'wallet_getSecretArray', wallet_getSecretObject: 'wallet_getSecretObject', wallet_noop: 'wallet_noop', + wallet_noop2: 'wallet_noop2', 'wallet_getSecret_*': 'wallet_getSecret_*', } as const; @@ -176,9 +178,11 @@ const PermissionKeys = { * permissions are namespaced, it's a getter function. */ const PermissionNames = { + wallet_doubleNumber: PermissionKeys.wallet_doubleNumber, wallet_getSecretArray: PermissionKeys.wallet_getSecretArray, wallet_getSecretObject: PermissionKeys.wallet_getSecretObject, wallet_noop: PermissionKeys.wallet_noop, + wallet_noop2: PermissionKeys.wallet_noop2, wallet_getSecret_: (str: string) => `wallet_getSecret_${str}` as const, } as const; @@ -249,12 +253,40 @@ function getDefaultPermissionSpecifications() { ); }, }, + [PermissionKeys.wallet_doubleNumber]: { + targetKey: PermissionKeys.wallet_doubleNumber, + methodImplementation: ({ params }: RestrictedMethodOptions<[number]>) => { + if (!Array.isArray(params)) { + throw new Error( + `Invalid ${PermissionKeys.wallet_doubleNumber} request`, + ); + } + return params[0] * 2; + }, + }, [PermissionKeys.wallet_noop]: { targetKey: PermissionKeys.wallet_noop, methodImplementation: (_args: RestrictedMethodOptions) => { return null; }, }, + // This one exists just to check permission validator logic + [PermissionKeys.wallet_noop2]: { + targetKey: PermissionKeys.wallet_noop2, + methodImplementation: (_args: RestrictedMethodOptions) => { + return null; + }, + allowedCaveats: [CaveatTypes.noopCaveat, CaveatTypes.filterArrayResponse], + validator: (permission: GenericPermission) => { + if ( + permission.caveats?.some( + ({ type }) => type !== CaveatTypes.noopCaveat, + ) + ) { + throw new Error('noop permission validation failed'); + } + }, + }, } as const; } @@ -2145,7 +2177,7 @@ describe('PermissionController', () => { ).toThrow(new Error('NoopCaveat value must be null')); }); - it('throws if permission validation fails', () => { + it('throws if the requested permission specifies disallowed caveats', () => { const controller = getDefaultPermissionController(); const origin = 'metamask.io'; @@ -2171,6 +2203,51 @@ describe('PermissionController', () => { ), ); }); + + it('throws if the requested permission specifies caveats, and no caveats are allowed', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + expect(() => + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + wallet_doubleNumber: { + caveats: [ + { + type: CaveatTypes.filterArrayResponse, + value: ['bar'], + }, + ] as any, + }, + }, + }), + ).toThrow( + new errors.ForbiddenCaveatError( + CaveatTypes.filterArrayResponse, + origin, + PermissionNames.wallet_doubleNumber, + ), + ); + }); + + it('throws if the permission validator throws', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + expect(() => + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_noop2]: { + caveats: [ + constructCaveat(CaveatTypes.filterArrayResponse, ['foo']), + ], + }, + }, + }), + ).toThrow(new Error('noop permission validation failed')); + }); }); describe('requestPermissions', () => { @@ -2685,6 +2762,26 @@ describe('PermissionController', () => { ).toStrictEqual(['a', 'b', 'c']); }); + it('executes a restricted method with parameters', async () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_doubleNumber]: {}, + }, + }); + + expect( + await controller.executeRestrictedMethod( + origin, + PermissionNames.wallet_doubleNumber, + [10], + ), + ).toStrictEqual(20); + }); + it('executes a namespaced restricted method', async () => { const controller = getDefaultPermissionController(); const origin = 'metamask.io'; @@ -2748,6 +2845,71 @@ describe('PermissionController', () => { ), ).toStrictEqual(['c', 'a']); }); + + it('throws if the subject does not have the requisite permission', async () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + await expect( + controller.executeRestrictedMethod( + origin, + PermissionNames.wallet_doubleNumber, + ), + ).rejects.toThrow( + errors.unauthorized({ + data: { origin, method: PermissionNames.wallet_doubleNumber }, + }), + ); + }); + + it('throws if the requested method (i.e. target) does not exist', async () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + await expect( + controller.executeRestrictedMethod(origin, 'wallet_getMeTacos' as any), + ).rejects.toThrow( + errors.methodNotFound({ + method: 'wallet_getMeTacos', + data: { origin }, + }), + ); + }); + + it('throws if the restricted method returns undefined', async () => { + const permissionSpecifications = getDefaultPermissionSpecifications(); + ( + permissionSpecifications as any + ).wallet_doubleNumber.methodImplementation = () => undefined; + + const controller = new PermissionController< + DefaultPermissionSpecifications, + DefaultCaveatSpecifications + >( + getPermissionControllerOptions({ + permissionSpecifications, + }), + ); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_doubleNumber]: {}, + }, + }); + + await expect( + controller.executeRestrictedMethod( + origin, + PermissionNames.wallet_doubleNumber, + ), + ).rejects.toThrow( + new Error( + `Internal request for method "${PermissionNames.wallet_doubleNumber}" as origin "${origin}" returned no result.`, + ), + ); + }); }); describe('controller actions', () => { diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 9d5b3b1da9..b6cbfa6a02 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -1248,7 +1248,7 @@ export class PermissionController< * Overwrites all existing permissions, and creates a subject entry if it * doesn't already exist. * - * **ATTN: Assumes that the new permissions have been validated.** + * **ATTN:** Assumes that the new permissions have been validated. * * @param origin - The origin of the grantee subject. * @param permissions - The new permissions for the grantee subject. @@ -1583,7 +1583,22 @@ export class PermissionController< } /** - * TODO + * Executes a restricted method as the subject with the the given origin. + * The specified params, if any, will be passed to the method implementation. + * + * **ATTN:** Great caution should be exercised in the use of this method. + * Methods that cause side effects or affect application state should + * be avoided. + * + * This method will first attempt to retrieve the requested restrictded method + * implementation, throwing if it does not exist. The method will then be + * invoked as though the subject with the specified origin had invoked it with + * the specified parameters. This means that any existing caveats will be + * applied to the restricted method, and this method will throw if the + * restricted method or its caveat decorators throw. + * + * In addition, this method will throw if the subject does not have a + * permission for the specified restricted method. * * @param origin - The origin of the subject to execute the method on behalf * of. @@ -1611,7 +1626,7 @@ export class PermissionController< if (result === undefined) { throw new Error( - `Internal request for method "${methodName}" as origin "${origin}" has no result.`, + `Internal request for method "${methodName}" as origin "${origin}" returned no result.`, ); } @@ -1619,8 +1634,16 @@ export class PermissionController< } /** - * TODO + * An internal method used in the controller's `json-rpc-engine` middleware + * and {@link PermissionController.executeRestrictedMethod}. Calls the + * specified restricted method implementation after decorating it with the + * caveats of its permission. Throws if the subject does not have the + * requisite permission. * + * **ATTN:** Assumes that all parameters are valid. + * + * @see {@link PermissionController.executeRestrictedMethod} and + * {@link PermissionController.createPermissionMiddleware} for usage. * @param methodImplementation - The implementation of the method to call. * @param subject - Metadata about the subject that made the request. * @param req - The request object associated with the request. From 4df1d368769c6e09467f51a05c44efbf262e0c95 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sat, 9 Oct 2021 22:18:37 -0700 Subject: [PATCH 085/147] Unit tests: Complete Caveat, Permission, and errors files --- .../src/permissions/Caveat.test.ts | 59 ++++++++++++++++ .../src/permissions/Permission.test.ts | 67 +++++++++++++++++++ .../controllers/src/permissions/errors.ts | 32 ++++----- 3 files changed, 142 insertions(+), 16 deletions(-) create mode 100644 packages/controllers/src/permissions/Caveat.test.ts create mode 100644 packages/controllers/src/permissions/Permission.test.ts diff --git a/packages/controllers/src/permissions/Caveat.test.ts b/packages/controllers/src/permissions/Caveat.test.ts new file mode 100644 index 0000000000..947e29e3c2 --- /dev/null +++ b/packages/controllers/src/permissions/Caveat.test.ts @@ -0,0 +1,59 @@ +import * as errors from './errors'; +import { + CaveatSpecificationBase, + CaveatSpecificationsMap, + decorateWithCaveats, + GenericPermission, +} from '.'; + +// This function is mostly tested through the PermissionController tests, +// we just add a couple here for completeness. +describe('decorateWithCaveats', () => { + it('decorates a method with caveat', () => { + const methodImplementation = () => [1, 2, 3]; + + const caveatSpecifications = { + reverse: { + decorator: (method: any, _caveat: any) => () => + method().reverse() as any, + }, + } as unknown as CaveatSpecificationsMap>; + + const permission = { + caveats: [{ type: 'reverse', value: null }], + } as unknown as GenericPermission; + + expect(methodImplementation()).toStrictEqual([1, 2, 3]); + expect( + decorateWithCaveats( + methodImplementation, + permission, + caveatSpecifications, + )({} as any), + ).toStrictEqual([3, 2, 1]); + }); + + it('throws an error if the caveat type is unrecognized', () => { + const methodImplementation = () => [1, 2, 3]; + + const caveatSpecifications = { + reverse: { + decorator: (method: any, _caveat: any) => () => + method().reverse() as any, + }, + } as unknown as CaveatSpecificationsMap>; + + const permission = { + // This type doesn't exist + caveats: [{ type: 'kaplar', value: null }], + } as unknown as GenericPermission; + + expect(() => + decorateWithCaveats( + methodImplementation, + permission, + caveatSpecifications, + )({} as any), + ).toThrow(new errors.UnrecognizedCaveatTypeError('kaplar')); + }); +}); diff --git a/packages/controllers/src/permissions/Permission.test.ts b/packages/controllers/src/permissions/Permission.test.ts new file mode 100644 index 0000000000..ba39a86766 --- /dev/null +++ b/packages/controllers/src/permissions/Permission.test.ts @@ -0,0 +1,67 @@ +import { constructPermission, GenericCaveat } from '.'; + +describe('constructPermission', () => { + it('constructs a permission', () => { + const invoker = 'foo.io'; + const target = 'wallet_bar'; + + expect( + constructPermission({ + invoker, + target, + }), + ).toMatchObject( + expect.objectContaining({ + id: expect.any(String), + parentCapability: target, + invoker, + caveats: null, + date: expect.any(Number), + }), + ); + }); + + it('constructs a permission with caveats', () => { + const invoker = 'foo.io'; + const target = 'wallet_bar'; + const caveats = [{ type: 'foo', value: 'bar' }] as [GenericCaveat]; + + expect( + constructPermission({ + invoker, + target, + caveats, + }), + ).toMatchObject( + expect.objectContaining({ + id: expect.any(String), + parentCapability: target, + invoker, + caveats: [...caveats], + date: expect.any(Number), + }), + ); + }); + + it('constructs a permission with an id', () => { + const invoker = 'foo.io'; + const target = 'wallet_bar'; + const id = 'abc123'; + + expect( + constructPermission({ + invoker, + target, + id, + }), + ).toMatchObject( + expect.objectContaining({ + id, + parentCapability: target, + invoker, + caveats: null, + date: expect.any(Number), + }), + ); + }); +}); diff --git a/packages/controllers/src/permissions/errors.ts b/packages/controllers/src/permissions/errors.ts index fe8ce22be3..c30a105ae4 100644 --- a/packages/controllers/src/permissions/errors.ts +++ b/packages/controllers/src/permissions/errors.ts @@ -1,33 +1,33 @@ -import { JsonRpcRequest } from 'json-rpc-engine'; import { errorCodes, ethErrors, EthereumRpcError } from 'eth-rpc-errors'; -type ErrorArg = { - message?: string; - data?: JsonRpcRequest | unknown; -}; - -type MethodNotFoundArg = ErrorArg & { - method?: string; +type UnauthorizedArg = { + data?: Record; }; -export function unauthorized(arg?: ErrorArg) { +export function unauthorized(opts: UnauthorizedArg) { return ethErrors.provider.unauthorized({ message: - arg?.message || 'Unauthorized to perform action. Try requesting the required permission(s) first. For more information, see: https://docs.metamask.io/guide/rpc-api.html#permissions', - data: arg?.data || undefined, + data: opts.data, }); } -export function methodNotFound(opts: MethodNotFoundArg) { - const message = opts.method - ? `The method "${opts.method}" does not exist / is not available.` - : undefined; +type MethodNotFoundArg = { + method: string; + data?: unknown; +}; +export function methodNotFound(opts: MethodNotFoundArg) { + const message = `The method "${opts.method}" does not exist / is not available.`; return ethErrors.rpc.methodNotFound({ data: opts.data, message }); } -export function invalidParams(opts: ErrorArg) { +type InvalidParamsArg = { + message?: string; + data?: unknown; +}; + +export function invalidParams(opts: InvalidParamsArg) { return ethErrors.rpc.invalidParams({ data: opts.data, message: opts.message, From 8977b191f27a905cc3c18b1858340d67088770ec Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sat, 9 Oct 2021 23:30:38 -0700 Subject: [PATCH 086/147] Some comment / type fixup --- packages/controllers/src/permissions/Permission.ts | 2 +- .../controllers/src/permissions/PermissionController.test.ts | 4 ++-- packages/controllers/src/permissions/PermissionController.ts | 2 +- packages/controllers/src/permissions/index.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 2c4343f0ce..5d8e5a805c 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -191,7 +191,7 @@ export type PermissionOptions = { * See {@link CaveatBase}. */ caveats?: NonEmptyArray; - // TODO:feat Add runtime check for allowed caveats to ensure that we can do this. + // TODO: Refactor allowed caveats validation to enable this? // caveats?: NonEmptyArray>; }; diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 4ef7bfaf7c..4e5e91fc23 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -565,7 +565,7 @@ describe('PermissionController', () => { expect( controller.getRestrictedMethod(PermissionNames.wallet_getSecretArray), ).toStrictEqual( - // TODO:types what? + // TODO:types Fix permissionSpecifications indices (controller.permissionSpecifications as any)[ PermissionKeys.wallet_getSecretArray ].methodImplementation, @@ -579,7 +579,7 @@ describe('PermissionController', () => { PermissionNames.wallet_getSecret_('foo'), ), ).toStrictEqual( - // TODO:types what? + // TODO:types Fix permissionSpecifications indices (controller.permissionSpecifications as any)[ PermissionKeys['wallet_getSecret_*'] ].methodImplementation, diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index b6cbfa6a02..3c1abeb907 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -179,7 +179,7 @@ export type ClearPermissions = { handler: () => void; }; -// TODO:feat Implement all desired actions +// TODO: Implement all desired actions /** * The {@link ControllerMessenger} actions of the {@link PermissionController}. */ diff --git a/packages/controllers/src/permissions/index.ts b/packages/controllers/src/permissions/index.ts index 902e332008..4f471a9893 100644 --- a/packages/controllers/src/permissions/index.ts +++ b/packages/controllers/src/permissions/index.ts @@ -3,5 +3,5 @@ export * from './Permission'; export * from './PermissionController'; export * from './utils'; -// TODO:feat Move these to the appropriate package +// TODO: Move these to the appropriate package export * as permissionRpcMethods from './rpc-methods'; From 9b15377152215b5678c35b9beb586cc471279fa1 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sun, 10 Oct 2021 11:44:35 -0700 Subject: [PATCH 087/147] Add PermissionController.updateAllCaveatsOfType --- .../controllers/src/permissions/Permission.ts | 10 +- .../permissions/PermissionController.test.ts | 4 + .../src/permissions/PermissionController.ts | 300 +++++++++++++----- packages/controllers/src/utils.ts | 18 ++ 4 files changed, 258 insertions(+), 74 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 5d8e5a805c..f30765fe1d 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -1,6 +1,6 @@ import { Json } from 'json-rpc-engine'; import { nanoid } from 'nanoid'; -import { NonEmptyArray } from '../utils'; +import { Mutable, NonEmptyArray } from '../utils'; import { GenericCaveat } from './Caveat'; // This is used in a docstring, but ESLint doesn't notice it. /* eslint-disable @typescript-eslint/no-unused-vars */ @@ -114,6 +114,14 @@ export type GenericPermission = PermissionConstraint< GenericCaveat >; +/** + * A {@link GenericPermission} with mutable caveats. + */ +export type MutableGenericPermission = Mutable< + GenericPermission & { caveats: NonEmptyArray }, + 'caveats' +>; + /** * An internal utility type used in {@link ExtractPermissionTargetKey}. * diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 4e5e91fc23..50112baff4 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -1717,6 +1717,10 @@ describe('PermissionController', () => { }); }); + describe('updateAllCaveatsOfType', () => { + it.todo('updates all caveats of a type'); + }); + describe('grantPermissions', () => { it('grants new permission', () => { const controller = getDefaultPermissionController(); diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 3c1abeb907..1a88ce7a1d 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -18,7 +18,7 @@ import type { import type { Patch } from 'immer'; import deepFreeze from 'deep-freeze-strict'; import { nanoid } from 'nanoid'; -import { isPlainObject, hasProperty, NonEmptyArray } from '../utils'; +import { isPlainObject, hasProperty, NonEmptyArray, Mutable } from '../utils'; import { constructCaveat as _constructCaveat, decorateWithCaveats, @@ -27,6 +27,7 @@ import { ExtractCaveats, CaveatSpecificationsMap, ExtractCaveatValue, + GenericCaveat, } from './Caveat'; import { constructPermission, @@ -42,6 +43,7 @@ import { PermissionSpecificationBase, PermissionConstraint, ExtractAllowedCaveatTypes, + MutableGenericPermission, } from './Permission'; import { PermissionDoesNotExistError, @@ -227,6 +229,11 @@ export type PermissionControllerMessenger = RestrictedControllerMessenger< never >; +export type GenericPermissionController = PermissionController< + PermissionSpecificationBase, + CaveatSpecificationBase +>; + /** * Options for {@link PermissionController.grantPermissions}. */ @@ -237,10 +244,31 @@ type GrantPermissionsOptions = { requestData?: Record; }; -export type GenericPermissionController = PermissionController< - PermissionSpecificationBase, - CaveatSpecificationBase ->; +/** + * Describes the possible results of a {@link CaveatMutator} function. + */ +export enum CaveatMutatorResult { + noop, + updateValue, + deleteCaveat, + revokePermission, +} + +/** + * Given a caveat value, returns a {@link CaveatMutatorResult} and, optionally, + * a new caveat value. + * + * @see {@link PermissionController.updateAllCaveatsOfType} for more details. + * @template Caveat - The caveat type for which this mutator is intended. + * @param caveatValue - The existing value of the caveat being mutated. + * @returns A tuple of the mutation result and, optionally, the new caveat + * value. + */ +export type CaveatMutator = ( + caveatValue: Caveat['value'], +) => + | [Exclude] + | [CaveatMutatorResult.updateValue, GenericCaveat['value']]; /** * Extracts the permission type or types from the specified permission and @@ -656,10 +684,10 @@ export class PermissionController< throw new PermissionDoesNotExistError(origin, target); } - // Typecast: ts(2589) + // Typecast: Immer WritableDraft incompatibility this.deletePermission( - draftState as any, - permissions as any, + draftState.subjects as unknown as PermissionControllerSubjects, + permissions as unknown as SubjectPermissions, origin, target, ); @@ -690,9 +718,9 @@ export class PermissionController< if (hasProperty(permissions as Record, target)) { this.deletePermission( - // Typecast: ts(2589) - draftState as any, - permissions as any, + // Typecast: Immer WritableDraft incompatibility + draftState.subjects as unknown as PermissionControllerSubjects, + permissions as unknown as SubjectPermissions, origin, target, ); @@ -702,12 +730,8 @@ export class PermissionController< } private deletePermission( - draftState: PermissionControllerState< - ExtractPermission - >, - permissions: SubjectPermissions< - ExtractPermission - >, + subjects: PermissionControllerSubjects, + permissions: SubjectPermissions, origin: OriginString, target: ExtractPermission< PermissionSpecification, @@ -717,7 +741,7 @@ export class PermissionController< if (Object.keys(permissions).length > 1) { delete permissions[target]; } else { - delete draftState.subjects[origin]; + delete subjects[origin]; } } @@ -862,10 +886,6 @@ export class PermissionController< target: TargetName, caveatType: CaveatType, caveatValue: CaveatValue, - // caveatValue: ExtractCaveatValue< - // CaveatSpecification, - // CaveatType - // >, ): void { if (!this.hasCaveat(origin, target, caveatType)) { throw new CaveatDoesNotExistError(origin, target, caveatType); @@ -959,6 +979,104 @@ export class PermissionController< }); } + /** + * Updates all caveats with the specified type for all subjects and + * permissions by applying the specified mutator function to them. + * + * Caveat mutators are functions that receive a caveat value and return a + * tuple consisting of a {@link CaveatMutatorResult} and, optionally, a new + * value to update the existing caveat with. + * + * For each caveat, depending on the mutator result, this method will: + * - Do nothing ({@link CaveatMutatorResult.noop}) + * - Update the value of the caveat ({@link CaveatMutatorResult.updateValue}) + * - The caveat specification validator, if any, will be called after + * updating the value. + * - Delete the caveat ({@link CaveatMutatorResult.deleteCaveat}) + * - The permission specification validator, if any, will be called after + * deleting the caveat. + * - Revoke the parent permission ({@link CaveatMutatorResult.revokePermission}) + * + * This method throws if the validation of any caveat or permission fails. + * + * @param targetCaveatType - The type of the caveats to update. + * @param mutator - The mutator function which will be applied to all caveat + * values. + */ + updateAllCaveatsOfType< + CaveatType extends ExtractCaveats['type'], + Caveat extends ExtractCaveat, + >(targetCaveatType: CaveatType, mutator: CaveatMutator): void { + if (Object.keys(this.state.subjects).length === 0) { + return; + } + + this.update((draftState) => { + Object.values(draftState.subjects).forEach((subject) => { + Object.values(subject.permissions).forEach((permission) => { + const { caveats } = permission; + const targetCaveat = caveats?.find( + ({ type }) => type === targetCaveatType, + ) as Caveat | undefined; + if (!targetCaveat) { + return; + } + + // The mutator may modify the caveat value in place, and must always + // return a valid mutation result. + const [mutationResult, newValue] = mutator(targetCaveat.value); + switch (mutationResult) { + case CaveatMutatorResult.noop: + break; + + case CaveatMutatorResult.updateValue: + // Typecast: The mutator type guarantees that the new value will + // not be undefined in this case. In any case, the specification + // validator is called on the mutated caveat. + (targetCaveat as Mutable).value = + newValue as Caveat['value']; + + this.validateCaveat( + targetCaveat, + subject.origin, + permission.parentCapability, + ); + break; + + case CaveatMutatorResult.deleteCaveat: + // Typecast: Immer WritableDraft incompatibility + this.deleteCaveat( + permission as MutableGenericPermission, + targetCaveatType, + subject.origin, + permission.parentCapability, + ); + break; + + case CaveatMutatorResult.revokePermission: + // Typecast: Immer WritableDraft incompatibility + this.deletePermission( + draftState.subjects as unknown as PermissionControllerSubjects, + subject.permissions as unknown as SubjectPermissions, + origin, + permission.parentCapability, + ); + break; + + default: + // This type check ensures that the switch statement is + // exhaustive. + // eslint-disable-next-line no-case-declarations + const _exhaustiveCheck: never = mutationResult; + throw new Error( + `Unrecognized mutation result: "${_exhaustiveCheck}"`, + ); + } + }); + }); + }); + } + /** * Removes the caveat of the specified type from the permission corresponding * to the given subject origin and target name. @@ -992,30 +1110,59 @@ export class PermissionController< throw new CaveatDoesNotExistError(origin, target, caveatType); } - const caveatIndex = permission.caveats.findIndex( - (existingCaveat) => existingCaveat.type === caveatType, - ); - if (!permission.caveats || caveatIndex === -1) { - throw new CaveatDoesNotExistError(origin, target, caveatType); - } + // Typecast: Immer WritableDraft incompatibility + this.deleteCaveat(permission as any, caveatType, origin, target); + }); + } - if (permission.caveats.length === 1) { - // Typecast: Immer WritableDraft incompatibility - permission.caveats = null as any; - } else { - permission.caveats.splice(caveatIndex, 1); - } + /** + * Deletes the specified caveat from the specified permission. If no caveats + * remain after deletion, the permission's caveat property is set to `null`. + * The permission is validated after being modified. + * + * Throws an error if the permission does not have a caveat with the specified + * type. + * + * @param permission - The permission whose caveat to delete. + * @param caveatType - The type of the caveat to delete. + * @param origin - The origin the permission subject. + * @param target - The name of the permission target. + */ + private deleteCaveat< + TargetName extends ExtractPermission< + PermissionSpecification, + CaveatSpecification + >['parentCapability'], + CaveatType extends ExtractCaveats['type'], + >( + permission: MutableGenericPermission, + caveatType: CaveatType, + origin: OriginString, + target: TargetName, + ): void { + const caveatIndex = permission.caveats.findIndex( + (existingCaveat) => existingCaveat.type === caveatType, + ); + if (!permission.caveats || caveatIndex === -1) { + throw new CaveatDoesNotExistError(origin, target, caveatType); + } - this.validateModifiedPermission( - // Typecast: Immer WritableDraft incompatibility - permission as unknown as ExtractPermission< - PermissionSpecification, - CaveatSpecification - >, - origin, - target, - ); - }); + if (permission.caveats.length === 1) { + // Typecast: Immer WritableDraft incompatibility + permission.caveats = null as any; + } else { + permission.caveats.splice(caveatIndex, 1); + } + + this.validateModifiedPermission( + // Typecast: From generic to modified permission + permission as ExtractPermission< + PermissionSpecification, + CaveatSpecification + >, + origin, + target, + ); } /** @@ -1030,15 +1177,13 @@ export class PermissionController< * @param origin - The origin associated with the permission. * @param targetName - The target name name of the permission. */ - private validateModifiedPermission< - Permission extends ExtractPermission< + private validateModifiedPermission( + permission: GenericPermission, + origin: OriginString, + targetName: ExtractPermission< PermissionSpecification, CaveatSpecification - >, - >( - permission: Permission, - origin: OriginString, - targetName: Permission['parentCapability'], + >['parentCapability'], ): void { const targetKey = this.getTargetKey(permission.parentCapability); /* istanbul ignore next: this should be impossible */ @@ -1218,16 +1363,14 @@ export class PermissionController< * @param origin - The origin associated with the permission. * @param targetName - The target name of the permission. */ - private validatePermission< - Permission extends ExtractPermission< - PermissionSpecification, - CaveatSpecification - >, - >( + private validatePermission( specification: PermissionSpecificationBase, - permission: Permission, + permission: GenericPermission, origin: OriginString, - targetName: Permission['parentCapability'], + targetName: ExtractPermission< + PermissionSpecification, + CaveatSpecification + >['parentCapability'], ): void { const { allowedCaveats, validator } = specification; if (permission.caveats !== null) { @@ -1333,19 +1476,6 @@ export class PermissionController< throw new InvalidCaveatTypeError(requestedCaveat, origin, target); } - const specification = - this.caveatSpecifications[ - requestedCaveat.type as CaveatSpecification['type'] - ]; - - if (!specification) { - throw new UnrecognizedCaveatTypeError( - requestedCaveat.type, - origin, - target, - ); - } - if (!hasProperty(requestedCaveat, 'value')) { throw new CaveatMissingValueError(requestedCaveat, origin, target); } @@ -1355,10 +1485,34 @@ export class PermissionController< requestedCaveat.type, (requestedCaveat.value as Json) ?? null, ); - specification.validator?.(caveat, origin, target); + this.validateCaveat(caveat, origin, target); return caveat as ExtractCaveats; } + /** + * Validates the specified caveat by ensuring that a specification exists for + * its type and invoking the validator function with it, if any. + * + * @param caveat - The caveat object to validate. + * @param origin - The origin associated with the subject of the parent + * permission. + * @param target - The target name associated with the parent permission. + */ + private validateCaveat( + caveat: GenericCaveat, + origin: OriginString, + target: string, + ): void { + const specification = + this.caveatSpecifications[caveat.type as CaveatSpecification['type']]; + + if (!specification) { + throw new UnrecognizedCaveatTypeError(caveat.type, origin, target); + } + + specification.validator?.(caveat, origin, target); + } + /** * Initiates a permission request that requires user approval. This should * always be used to grant additional permissions to a subject, unless user diff --git a/packages/controllers/src/utils.ts b/packages/controllers/src/utils.ts index c38535aa42..7d7c7a1b1a 100644 --- a/packages/controllers/src/utils.ts +++ b/packages/controllers/src/utils.ts @@ -11,5 +11,23 @@ export const hasProperty = ( /** * Like {@link Array}, but always non-empty. + * + * @template T - The non-empty array member type. */ export type NonEmptyArray = [T, ...T[]]; + +/** + * Makes every specified property of the specified object type mutable. + * + * @template T - The object whose readonly properties to make mutable. + * @template TargetKey - The property key(s) to make mutable. + */ +export type Mutable< + T extends Record, + TargetKey extends string, +> = { + -readonly [Key in keyof Pick]: T[Key]; +} & + { + [Key in keyof Omit]: T[Key]; + }; From 8ef3113a8b1810648507210ad444c5cd45e37b2c Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sun, 10 Oct 2021 12:58:14 -0700 Subject: [PATCH 088/147] Fix permission specification getting --- .../controllers/src/permissions/Permission.ts | 14 +++++ .../permissions/PermissionController.test.ts | 14 ++--- .../src/permissions/PermissionController.ts | 59 +++++++++++-------- 3 files changed, 55 insertions(+), 32 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index f30765fe1d..ea776922d8 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -444,3 +444,17 @@ export type PermissionSpecificationsMap< : never; } : never; + +/** + * Extracts a specific {@link PermissionSpecificationBase} from a union of + * permission specifications. + * + * @template Specification - The specification union type to extract from. + * @template TargetKey - The `targetKey` of the specification to extract. + */ +export type ExtractPermissionSpecification< + Specification extends PermissionSpecificationBase, + TargetKey extends Specification['targetKey'], +> = Specification extends PermissionSpecificationBase + ? Specification + : never; diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 50112baff4..d982642436 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -565,10 +565,9 @@ describe('PermissionController', () => { expect( controller.getRestrictedMethod(PermissionNames.wallet_getSecretArray), ).toStrictEqual( - // TODO:types Fix permissionSpecifications indices - (controller.permissionSpecifications as any)[ - PermissionKeys.wallet_getSecretArray - ].methodImplementation, + controller.getPermissionSpecification( + PermissionNames.wallet_getSecretArray, + ).methodImplementation, ); }); @@ -579,10 +578,9 @@ describe('PermissionController', () => { PermissionNames.wallet_getSecret_('foo'), ), ).toStrictEqual( - // TODO:types Fix permissionSpecifications indices - (controller.permissionSpecifications as any)[ - PermissionKeys['wallet_getSecret_*'] - ].methodImplementation, + controller.getPermissionSpecification( + PermissionKeys['wallet_getSecret_*'], + ).methodImplementation, ); }); diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 1a88ce7a1d..91937e1d4b 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -44,6 +44,7 @@ import { PermissionConstraint, ExtractAllowedCaveatTypes, MutableGenericPermission, + ExtractPermissionSpecification, } from './Permission'; import { PermissionDoesNotExistError, @@ -348,28 +349,10 @@ export class PermissionController< CaveatSpecificationsMap >; - /** - * The {@link CaveatSpecifications} of the controller. - */ - public get caveatSpecifications(): Readonly< - CaveatSpecificationsMap - > { - return this._caveatSpecifications; - } - private readonly _permissionSpecifications: Readonly< PermissionSpecificationsMap >; - /** - * The {@link PermissionSpecificationsMap} of the controller. - */ - public get permissionSpecifications(): Readonly< - PermissionSpecificationsMap - > { - return this._permissionSpecifications; - } - private readonly _unrestrictedMethods: ReadonlySet; /** @@ -455,6 +438,35 @@ export class PermissionController< }); } + /** + * Gets a permission specification. + * + * @param targetKey - The target key of the permission specification to get. + * @returns The permission specification with the specified target key. + */ + getPermissionSpecification< + TargetKey extends PermissionSpecification['targetKey'], + >( + targetKey: TargetKey, + ): ExtractPermissionSpecification { + // Typecast: Due to constraints added to the PermissionSpecificationsMap, + // `keyof typeof this._permissionSpecifications` evalutes to `never`. + // Therefore, we have to cast it like this. + return (this._permissionSpecifications as any)[targetKey]; + } + + /** + * Gets a caveat specification. + * + * @param caveatType - The type of the caveat specification to get. + * @returns The caveat specification with the specified type. + */ + getCaveatSpecification( + caveatType: CaveatType, + ) { + return this._caveatSpecifications[caveatType]; + } + /** * Constructor helper for validating permission specifications. This is * intended to prevent the use of invalid target keys which, while impossible @@ -547,7 +559,7 @@ export class PermissionController< return undefined; } - return this.permissionSpecifications[targetKey].methodImplementation; + return this.getPermissionSpecification(targetKey).methodImplementation; } /** @@ -1194,7 +1206,7 @@ export class PermissionController< } this.validatePermission( - this.permissionSpecifications[targetKey], + this.getPermissionSpecification(targetKey), permission, origin, targetName, @@ -1322,7 +1334,7 @@ export class PermissionController< PermissionSpecification, CaveatSpecification >['parentCapability']; - const specification = this.permissionSpecifications[targetKey]; + const specification = this.getPermissionSpecification(targetKey); const caveats = this.constructCaveats( origin, @@ -1503,8 +1515,7 @@ export class PermissionController< origin: OriginString, target: string, ): void { - const specification = - this.caveatSpecifications[caveat.type as CaveatSpecification['type']]; + const specification = this.getCaveatSpecification(caveat.type); if (!specification) { throw new UnrecognizedCaveatTypeError(caveat.type, origin, target); @@ -1825,7 +1836,7 @@ export class PermissionController< return decorateWithCaveats( methodImplementation, permission, - this.caveatSpecifications, + this._caveatSpecifications, )({ method, params, context: { origin } }); } From 4c410be429a8fd66a3a67445b34353e446b6b77e Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sun, 10 Oct 2021 13:57:49 -0700 Subject: [PATCH 089/147] Unit tests: permission middleware --- .../permissions/PermissionController.test.ts | 198 +++++++++++++++++- .../permissions/permission-middleware.test.ts | 15 -- 2 files changed, 194 insertions(+), 19 deletions(-) delete mode 100644 packages/controllers/src/permissions/permission-middleware.test.ts diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index d982642436..23477b013e 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -7,6 +7,7 @@ import { RejectRequest as RejectApprovalRequest, HasApprovalRequest, } from '@metamask/controllers'; +import { JsonRpcEngine, PendingJsonRpcResponse } from 'json-rpc-engine'; import { hasProperty, isPlainObject } from '../utils'; import * as errors from './errors'; import { constructCaveat } from './Caveat'; @@ -2803,7 +2804,7 @@ describe('PermissionController', () => { ).toStrictEqual('Hello, secret friend "foo"!'); }); - it('executes a restricted method with caveats', async () => { + it('executes a restricted method with a caveat', async () => { const controller = getDefaultPermissionController(); const origin = 'metamask.io'; @@ -2915,7 +2916,7 @@ describe('PermissionController', () => { }); describe('controller actions', () => { - it('permissionController:clearPermissions', () => { + it('action: PermissionController:clearPermissions', () => { const options = getPermissionControllerOptions(); const { messenger } = options; const controller = new PermissionController< @@ -2938,7 +2939,7 @@ describe('PermissionController', () => { expect(controller.state).toStrictEqual({ subjects: {} }); }); - it('permissionController:getSubjectNames', () => { + it('action: PermissionController:getSubjectNames', () => { const options = getPermissionControllerOptions(); const { messenger } = options; const controller = new PermissionController< @@ -2964,7 +2965,7 @@ describe('PermissionController', () => { expect(getSubjectNamesSpy).toHaveBeenCalledTimes(2); }); - it('permissionController:hasPermissions', () => { + it('action: PermissionController:hasPermissions', () => { const options = getPermissionControllerOptions(); const { messenger } = options; const controller = new PermissionController< @@ -2992,4 +2993,193 @@ describe('PermissionController', () => { expect(hasPermissionsSpy).toHaveBeenNthCalledWith(2, 'foo'); }); }); + + describe('permission middleware', () => { + it('executes a restricted method', async () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: {}, + }, + }); + + const engine = new JsonRpcEngine(); + engine.push(controller.createPermissionMiddleware({ origin })); + + const response: any = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: PermissionNames.wallet_getSecretArray, + }); + + expect(response.result).toStrictEqual(['a', 'b', 'c']); + }); + + it('executes a restricted method with a caveat', async () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: { + caveats: [constructCaveat(CaveatTypes.filterArrayResponse, ['b'])], + }, + }, + }); + + const engine = new JsonRpcEngine(); + engine.push(controller.createPermissionMiddleware({ origin })); + + const response: any = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: PermissionNames.wallet_getSecretArray, + }); + + expect(response.result).toStrictEqual(['b']); + }); + + it('executes a restricted method with multiple caveats', async () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: { + caveats: [ + constructCaveat(CaveatTypes.filterArrayResponse, ['a', 'c']), + constructCaveat(CaveatTypes.reverseArrayResponse, null), + ], + }, + }, + }); + + const engine = new JsonRpcEngine(); + engine.push(controller.createPermissionMiddleware({ origin })); + + const response: any = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: PermissionNames.wallet_getSecretArray, + }); + + expect(response.result).toStrictEqual(['c', 'a']); + }); + + it('passes through unrestricted methods', async () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + const engine = new JsonRpcEngine(); + engine.push(controller.createPermissionMiddleware({ origin })); + engine.push( + ( + _req: any, + res: PendingJsonRpcResponse<'success'>, + _next: any, + end: () => any, + ) => { + res.result = 'success'; + end(); + }, + ); + + const response: any = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'wallet_unrestrictedMethod', + }); + + expect(response.result).toStrictEqual('success'); + }); + + it('returns an error if the subject does not have the requisite permission', async () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + const engine = new JsonRpcEngine(); + engine.push(controller.createPermissionMiddleware({ origin })); + + const request: any = { + jsonrpc: '2.0', + id: 1, + method: PermissionNames.wallet_getSecretArray, + }; + + const expectedError = errors.unauthorized({ + data: { origin, method: PermissionNames.wallet_getSecretArray }, + }); + + const { error }: any = await engine.handle(request); + expect(error).toMatchObject(expect.objectContaining(expectedError)); + }); + + it('returns an error if the method does not exist', async () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + const engine = new JsonRpcEngine(); + engine.push(controller.createPermissionMiddleware({ origin })); + + const request: any = { + jsonrpc: '2.0', + id: 1, + method: 'wallet_foo', + }; + + const expectedError = errors.methodNotFound({ + method: 'wallet_foo', + data: { origin }, + }); + + const { error }: any = await engine.handle(request); + expect(error).toMatchObject(expect.objectContaining(expectedError)); + }); + + it('returns an error if the restricted method returns undefined', async () => { + const permissionSpecifications = getDefaultPermissionSpecifications(); + ( + permissionSpecifications as any + ).wallet_doubleNumber.methodImplementation = () => undefined; + + const controller = new PermissionController< + DefaultPermissionSpecifications, + DefaultCaveatSpecifications + >( + getPermissionControllerOptions({ + permissionSpecifications, + }), + ); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_doubleNumber]: {}, + }, + }); + + const engine = new JsonRpcEngine(); + engine.push(controller.createPermissionMiddleware({ origin })); + + const request: any = { + jsonrpc: '2.0', + id: 1, + method: PermissionNames.wallet_doubleNumber, + }; + + const expectedError = errors.internalError( + `Request for method "${PermissionNames.wallet_doubleNumber}" returned undefined result.`, + { request: { ...request } }, + ); + + const { error }: any = await engine.handle(request); + expect(error).toMatchObject(expect.objectContaining(expectedError)); + }); + }); }); diff --git a/packages/controllers/src/permissions/permission-middleware.test.ts b/packages/controllers/src/permissions/permission-middleware.test.ts deleted file mode 100644 index 4715dd5e10..0000000000 --- a/packages/controllers/src/permissions/permission-middleware.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { getPermissionMiddlewareFactory } from './permission-middleware'; - -describe('permission middleware', () => { - describe('getPermissionMiddlewareFactory', () => { - it('returns a permission middleware function', () => { - expect( - typeof getPermissionMiddlewareFactory({ - executeRestrictedMethod: jest.fn(), - getRestrictedMethod: jest.fn(), - isUnrestrictedMethod: jest.fn(), - }), - ).toStrictEqual('function'); - }); - }); -}); From d80c31c6f30836332865cb094831282e56d9c718 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sun, 10 Oct 2021 23:57:51 -0700 Subject: [PATCH 090/147] Unit tests: fin. --- .../permissions/PermissionController.test.ts | 340 +++++++++++++++++- .../src/permissions/PermissionController.ts | 58 +-- 2 files changed, 374 insertions(+), 24 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 23477b013e..4bf6802f12 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -26,6 +26,7 @@ import { RestrictedMethodOptions, RestrictedMethodParameters, ExtractSpecifications, + CaveatMutatorOperation, } from '.'; // CaveatBase types and specifications @@ -83,6 +84,16 @@ function getDefaultCaveatSpecifications() { caveat.value.includes(resultValue), ); }, + validator: (caveat: { + type: typeof CaveatTypes.filterArrayResponse; + value: unknown; + }) => { + if (!Array.isArray(caveat.value)) { + throw new Error( + `${CaveatTypes.filterArrayResponse} values must be arrays`, + ); + } + }, }, reverseArrayResponse: { type: CaveatTypes.reverseArrayResponse, @@ -1716,8 +1727,333 @@ describe('PermissionController', () => { }); }); - describe('updateAllCaveatsOfType', () => { - it.todo('updates all caveats of a type'); + describe('updatePermissionsByCaveat', () => { + enum MultiCaveatOrigins { + a = 'a.com', + b = 'b.io', + c = 'c.biz', + } + + /** + * @returns A tuple of a permission controller and an object listing the + * origins of its subjects. Each subject has at least one permission + * with caveats. + */ + const getMultiCaveatController = () => { + const controller = getDefaultPermissionController(); + + controller.grantPermissions({ + subject: { origin: MultiCaveatOrigins.a }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: { + caveats: [constructCaveat(CaveatTypes.filterArrayResponse, ['a'])], + }, + }, + }); + + controller.grantPermissions({ + subject: { origin: MultiCaveatOrigins.b }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: { + caveats: [ + constructCaveat(CaveatTypes.filterArrayResponse, ['b']), + constructCaveat(CaveatTypes.reverseArrayResponse, null), + ], + }, + [PermissionNames.wallet_getSecretObject]: { + caveats: [ + constructCaveat(CaveatTypes.filterObjectResponse, ['b']), + constructCaveat(CaveatTypes.noopCaveat, null), + ], + }, + [PermissionNames.wallet_getSecret_('foo')]: { + caveats: [constructCaveat(CaveatTypes.noopCaveat, null)], + }, + [PermissionNames.wallet_doubleNumber]: {}, + }, + }); + + controller.grantPermissions({ + subject: { origin: MultiCaveatOrigins.c }, + approvedPermissions: { + [PermissionNames.wallet_getSecretObject]: { + caveats: [constructCaveat(CaveatTypes.filterObjectResponse, ['c'])], + }, + [PermissionNames.wallet_getSecret_('bar')]: { + caveats: [constructCaveat(CaveatTypes.noopCaveat, null)], + }, + }, + }); + + return controller; + }; + + const getMultiCaveatStateMatcher = ( + overrides: Partial< + Record> + > = {}, + ) => { + return { + subjects: { + [MultiCaveatOrigins.a]: { + origin: MultiCaveatOrigins.a, + permissions: { + [PermissionNames.wallet_getSecretArray]: getPermissionMatcher( + PermissionNames.wallet_getSecretArray, + [constructCaveat(CaveatTypes.filterArrayResponse, ['a'])], + MultiCaveatOrigins.a, + ), + ...overrides[MultiCaveatOrigins.a], + }, + }, + + [MultiCaveatOrigins.b]: { + origin: MultiCaveatOrigins.b, + permissions: { + [PermissionNames.wallet_getSecretArray]: getPermissionMatcher( + PermissionNames.wallet_getSecretArray, + [ + constructCaveat(CaveatTypes.filterArrayResponse, ['b']), + constructCaveat(CaveatTypes.reverseArrayResponse, null), + ], + MultiCaveatOrigins.b, + ), + [PermissionNames.wallet_getSecretObject]: getPermissionMatcher( + PermissionNames.wallet_getSecretObject, + [ + constructCaveat(CaveatTypes.filterObjectResponse, ['b']), + constructCaveat(CaveatTypes.noopCaveat, null), + ], + MultiCaveatOrigins.b, + ), + [PermissionNames.wallet_getSecret_('foo')]: getPermissionMatcher( + PermissionNames.wallet_getSecret_('foo'), + [constructCaveat(CaveatTypes.noopCaveat, null)], + MultiCaveatOrigins.b, + ), + [PermissionNames.wallet_doubleNumber]: getPermissionMatcher( + PermissionNames.wallet_doubleNumber, + null, + MultiCaveatOrigins.b, + ), + ...overrides[MultiCaveatOrigins.b], + }, + }, + + [MultiCaveatOrigins.c]: { + origin: MultiCaveatOrigins.c, + permissions: { + [PermissionNames.wallet_getSecretObject]: getPermissionMatcher( + PermissionNames.wallet_getSecretObject, + [constructCaveat(CaveatTypes.filterObjectResponse, ['c'])], + MultiCaveatOrigins.c, + ), + [PermissionNames.wallet_getSecret_('bar')]: getPermissionMatcher( + PermissionNames.wallet_getSecret_('bar'), + [constructCaveat(CaveatTypes.noopCaveat, null)], + MultiCaveatOrigins.c, + ), + ...overrides[MultiCaveatOrigins.c], + }, + }, + }, + }; + }; + + // This is effectively a test of the above test utilities. + it('multi-caveat controller has expected state', () => { + const controller = getMultiCaveatController(); + expect(controller.state).toStrictEqual(getMultiCaveatStateMatcher()); + }); + + it('does nothing if there are no subjects', () => { + const controller = getDefaultPermissionController(); + expect(controller.state).toStrictEqual({ subjects: {} }); + + // There are no caveats, so this does nothing. + controller.updatePermissionsByCaveat( + CaveatTypes.filterArrayResponse, + () => [CaveatMutatorOperation.updateValue, ['a', 'b']], + ); + expect(controller.state).toStrictEqual({ subjects: {} }); + }); + + it('does nothing if the mutator returns the "noop" operation', () => { + const controller = getMultiCaveatController(); + + // Although there are caveats, we always return the "noop" operation, and + // therefore nothing happens. + controller.updatePermissionsByCaveat( + CaveatTypes.filterArrayResponse, + () => [CaveatMutatorOperation.noop], + ); + expect(controller.state).toStrictEqual(getMultiCaveatStateMatcher()); + }); + + it('updates the value of all caveats of a particular type', () => { + const controller = getMultiCaveatController(); + + controller.updatePermissionsByCaveat( + CaveatTypes.filterArrayResponse, + () => [CaveatMutatorOperation.updateValue, ['a', 'b']], + ); + + expect(controller.state).toStrictEqual( + getMultiCaveatStateMatcher({ + [MultiCaveatOrigins.a]: { + [PermissionNames.wallet_getSecretArray]: getPermissionMatcher( + PermissionNames.wallet_getSecretArray, + [constructCaveat(CaveatTypes.filterArrayResponse, ['a', 'b'])], + MultiCaveatOrigins.a, + ), + }, + [MultiCaveatOrigins.b]: { + [PermissionNames.wallet_getSecretArray]: getPermissionMatcher( + PermissionNames.wallet_getSecretArray, + [ + constructCaveat(CaveatTypes.filterArrayResponse, ['a', 'b']), + constructCaveat(CaveatTypes.reverseArrayResponse, null), + ], + MultiCaveatOrigins.b, + ), + }, + }), + ); + }); + + it('selectively updates the value of all caveats of a particular type', () => { + const controller = getMultiCaveatController(); + + let counter = 0; + const mutator: any = () => { + counter += 1; + return counter === 1 + ? [CaveatMutatorOperation.noop] + : [CaveatMutatorOperation.updateValue, ['a', 'b']]; + }; + + controller.updatePermissionsByCaveat( + CaveatTypes.filterArrayResponse, + mutator, + ); + + expect(controller.state).toStrictEqual( + getMultiCaveatStateMatcher({ + [MultiCaveatOrigins.b]: { + [PermissionNames.wallet_getSecretArray]: getPermissionMatcher( + PermissionNames.wallet_getSecretArray, + [ + constructCaveat(CaveatTypes.filterArrayResponse, ['a', 'b']), + constructCaveat(CaveatTypes.reverseArrayResponse, null), + ], + MultiCaveatOrigins.b, + ), + }, + }), + ); + }); + + it('deletes all caveats of a particular type', () => { + const controller = getMultiCaveatController(); + + controller.updatePermissionsByCaveat( + CaveatTypes.filterArrayResponse, + () => [CaveatMutatorOperation.deleteCaveat], + ); + + expect(controller.state).toStrictEqual( + getMultiCaveatStateMatcher({ + [MultiCaveatOrigins.a]: { + [PermissionNames.wallet_getSecretArray]: getPermissionMatcher( + PermissionNames.wallet_getSecretArray, + null, + MultiCaveatOrigins.a, + ), + }, + [MultiCaveatOrigins.b]: { + [PermissionNames.wallet_getSecretArray]: getPermissionMatcher( + PermissionNames.wallet_getSecretArray, + [constructCaveat(CaveatTypes.reverseArrayResponse, null)], + MultiCaveatOrigins.b, + ), + }, + }), + ); + }); + + it('revokes permissions associated with a caveat', () => { + const controller = getMultiCaveatController(); + + controller.updatePermissionsByCaveat( + CaveatTypes.filterObjectResponse, + () => [CaveatMutatorOperation.revokePermission], + ); + + const matcher = getMultiCaveatStateMatcher(); + delete matcher.subjects[MultiCaveatOrigins.b].permissions[ + PermissionNames.wallet_getSecretObject + ]; + + delete matcher.subjects[MultiCaveatOrigins.c].permissions[ + PermissionNames.wallet_getSecretObject + ]; + + expect(controller.state).toStrictEqual(matcher); + }); + + it('deletes subject if all permissions are revoked', () => { + const controller = getMultiCaveatController(); + + let counter = 0; + const mutator: any = () => { + counter += 1; + return counter === 1 + ? [CaveatMutatorOperation.revokePermission] + : [CaveatMutatorOperation.noop]; + }; + + controller.updatePermissionsByCaveat( + CaveatTypes.filterArrayResponse, + mutator, + ); + + const matcher = getMultiCaveatStateMatcher(); + delete (matcher.subjects as any)[MultiCaveatOrigins.a]; + + expect(controller.state).toStrictEqual(matcher); + }); + + it('throws if caveat validation fails after a value is updated', () => { + const controller = getMultiCaveatController(); + + expect(() => + controller.updatePermissionsByCaveat( + CaveatTypes.filterArrayResponse, + () => [CaveatMutatorOperation.updateValue, 'foo'], + ), + ).toThrow(`${CaveatTypes.filterArrayResponse} values must be arrays`); + }); + + it('throws if permission validation fails after a value is updated', () => { + const controller = getMultiCaveatController(); + + expect(() => + controller.updatePermissionsByCaveat(CaveatTypes.noopCaveat, () => [ + CaveatMutatorOperation.deleteCaveat, + ]), + ).toThrow('getSecret_* permission validation failed'); + }); + + it('throws if mutator returns unrecognized operation', () => { + const controller = getMultiCaveatController(); + + expect(() => + controller.updatePermissionsByCaveat( + CaveatTypes.filterArrayResponse, + () => ['foobar'] as any, + ), + ).toThrow(`Unrecognized mutation result: "foobar"`); + }); }); describe('grantPermissions', () => { diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 91937e1d4b..a14cad65fb 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -248,7 +248,7 @@ type GrantPermissionsOptions = { /** * Describes the possible results of a {@link CaveatMutator} function. */ -export enum CaveatMutatorResult { +export enum CaveatMutatorOperation { noop, updateValue, deleteCaveat, @@ -256,10 +256,10 @@ export enum CaveatMutatorResult { } /** - * Given a caveat value, returns a {@link CaveatMutatorResult} and, optionally, + * Given a caveat value, returns a {@link CaveatMutatorOperation} and, optionally, * a new caveat value. * - * @see {@link PermissionController.updateAllCaveatsOfType} for more details. + * @see {@link PermissionController.updatePermissionsByCaveat} for more details. * @template Caveat - The caveat type for which this mutator is intended. * @param caveatValue - The existing value of the caveat being mutated. * @returns A tuple of the mutation result and, optionally, the new caveat @@ -268,8 +268,8 @@ export enum CaveatMutatorResult { export type CaveatMutator = ( caveatValue: Caveat['value'], ) => - | [Exclude] - | [CaveatMutatorResult.updateValue, GenericCaveat['value']]; + | [Exclude] + | [CaveatMutatorOperation.updateValue, GenericCaveat['value']]; /** * Extracts the permission type or types from the specified permission and @@ -699,7 +699,6 @@ export class PermissionController< // Typecast: Immer WritableDraft incompatibility this.deletePermission( draftState.subjects as unknown as PermissionControllerSubjects, - permissions as unknown as SubjectPermissions, origin, target, ); @@ -732,7 +731,6 @@ export class PermissionController< this.deletePermission( // Typecast: Immer WritableDraft incompatibility draftState.subjects as unknown as PermissionControllerSubjects, - permissions as unknown as SubjectPermissions, origin, target, ); @@ -741,15 +739,25 @@ export class PermissionController< }); } + /** + * Deletes the permission identified by the given origin and target. If the + * permission is the single remaining permission of its subject, the subject + * is also deleted. + * + * @param subjects - The draft permission controller subjects. + * @param origin - The origin of the subject associated with the permission + * to delete. + * @param target - The target name of the permission to delete. + */ private deletePermission( subjects: PermissionControllerSubjects, - permissions: SubjectPermissions, origin: OriginString, target: ExtractPermission< PermissionSpecification, CaveatSpecification >['parentCapability'], ): void { + const { permissions } = subjects[origin]; if (Object.keys(permissions).length > 1) { delete permissions[target]; } else { @@ -907,7 +915,9 @@ export class PermissionController< } /** - * Internal method for setting the caveat of a permission. + * Sets the specified caveat on the specified permission. Overwrites existing + * caveats of the same type in-place (preserving array order), and adds the + * caveat to the end of the array otherwise. * * Throws an error if the permission does not exist or fails to validate after * its caveats have been modified. @@ -995,19 +1005,22 @@ export class PermissionController< * Updates all caveats with the specified type for all subjects and * permissions by applying the specified mutator function to them. * + * **ATTN:** Permissions can be revoked entirely by the action of this method, + * read on for details. + * * Caveat mutators are functions that receive a caveat value and return a - * tuple consisting of a {@link CaveatMutatorResult} and, optionally, a new + * tuple consisting of a {@link CaveatMutatorOperation} and, optionally, a new * value to update the existing caveat with. * * For each caveat, depending on the mutator result, this method will: - * - Do nothing ({@link CaveatMutatorResult.noop}) - * - Update the value of the caveat ({@link CaveatMutatorResult.updateValue}) + * - Do nothing ({@link CaveatMutatorOperation.noop}) + * - Update the value of the caveat ({@link CaveatMutatorOperation.updateValue}) * - The caveat specification validator, if any, will be called after * updating the value. - * - Delete the caveat ({@link CaveatMutatorResult.deleteCaveat}) + * - Delete the caveat ({@link CaveatMutatorOperation.deleteCaveat}) * - The permission specification validator, if any, will be called after * deleting the caveat. - * - Revoke the parent permission ({@link CaveatMutatorResult.revokePermission}) + * - Revoke the parent permission ({@link CaveatMutatorOperation.revokePermission}) * * This method throws if the validation of any caveat or permission fails. * @@ -1015,7 +1028,7 @@ export class PermissionController< * @param mutator - The mutator function which will be applied to all caveat * values. */ - updateAllCaveatsOfType< + updatePermissionsByCaveat< CaveatType extends ExtractCaveats['type'], Caveat extends ExtractCaveat, >(targetCaveatType: CaveatType, mutator: CaveatMutator): void { @@ -1038,10 +1051,10 @@ export class PermissionController< // return a valid mutation result. const [mutationResult, newValue] = mutator(targetCaveat.value); switch (mutationResult) { - case CaveatMutatorResult.noop: + case CaveatMutatorOperation.noop: break; - case CaveatMutatorResult.updateValue: + case CaveatMutatorOperation.updateValue: // Typecast: The mutator type guarantees that the new value will // not be undefined in this case. In any case, the specification // validator is called on the mutated caveat. @@ -1055,7 +1068,7 @@ export class PermissionController< ); break; - case CaveatMutatorResult.deleteCaveat: + case CaveatMutatorOperation.deleteCaveat: // Typecast: Immer WritableDraft incompatibility this.deleteCaveat( permission as MutableGenericPermission, @@ -1065,12 +1078,11 @@ export class PermissionController< ); break; - case CaveatMutatorResult.revokePermission: + case CaveatMutatorOperation.revokePermission: // Typecast: Immer WritableDraft incompatibility this.deletePermission( draftState.subjects as unknown as PermissionControllerSubjects, - subject.permissions as unknown as SubjectPermissions, - origin, + subject.origin, permission.parentCapability, ); break; @@ -1732,7 +1744,9 @@ export class PermissionController< } /** - * Internal function for rejecting a permission request. + * Rejects the permissions request with the specified id, with the specified + * error as the reason. This method is effectively a wrapper around a + * messenger call for the `ApprovalController:rejectRequest` action. * * @see {@link PermissionController.acceptPermissionsRequest} and * {@link PermissionController.rejectPermissionsRequest} for usage. From f171a3581664042c4975ee99792b11c1d9f648d2 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 11 Oct 2021 00:04:20 -0700 Subject: [PATCH 091/147] Update README --- .../controllers/src/permissions/README.md | 33 +++++++++---------- .../rpc-methods/requestPermissions.ts | 1 - 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/packages/controllers/src/permissions/README.md b/packages/controllers/src/permissions/README.md index 02510418ab..c75e1c5135 100644 --- a/packages/controllers/src/permissions/README.md +++ b/packages/controllers/src/permissions/README.md @@ -18,15 +18,15 @@ Permissions can have **caveats**, which are host-defined attenuations of the aut Permission system concepts correspond to components of the MetaMask stack as follows: -| Concept | Implementation | -| :---------------- | :--------------------------------------------------------------------------------------- | -| Host | The MetaMask application | -| Subjects | Websites, Snaps, or other extensions | -| Targets | JSON-RPC methods | -| Invocations | JSON-RPC requests | -| Permissions | Permission objects | -| Caveats | Caveat objects | -| Permission system | The `PermissionController`, `PermissionEnforcer`, and their `json-rpc-engine` middleware | +| Concept | Implementation | +| :---------------- | :-------------------------------------------------------------- | +| Host | The MetaMask application | +| Subjects | Websites, Snaps, or other extensions | +| Targets | JSON-RPC methods | +| Invocations | JSON-RPC requests | +| Permissions | Permission objects | +| Caveats | Caveat objects | +| Permission system | The `PermissionController` and its `json-rpc-engine` middleware | For the time being, JSON-RPC methods are the only kind of permission target that exists. Therefore, our implementation of the permission system revolves around JSON-RPC requests and MetaMask's `json-rpc-engine` stack. @@ -35,15 +35,13 @@ Unrestricted methods can always be called by anyone. Restricted methods require the requisite permission in order to be called. At any given moment, the `PermissionController` state tree describes the complete state of the permissions of all subjects known to the current MetaMask instance. -The `PermissionController` also provides methods for adding, updating, and removing permissions. -The controller has a sibling module, the `PermissionEnforcer`, which furnishes methods for applying the rules described by the controller state to incoming JSON-RPC requests. -The also provides a `json-rpc-engine` middleware so that we can apply its logic to incoming JSON-RPC requests. +The `PermissionController` also provides methods for adding, updating, and removing permissions, and a `json-rpc-engine` middleware function factory so that the permissioning logic can be applied to incoming JSON-RPC requests. Once the permission middleware is injected into our middleware stack, every JSON-RPC request will be handled in one of the following ways: - If the requested method is neither restricted nor unrestricted, the request will be rejected with a `methodNotFound` error. - If the requested method is unrestricted, it will pass through the middleware unmodified. -- If the requested method is restricted, the enforcer will attempt to get the permission corresponding to the subject and target, and: +- If the requested method is restricted, the middleware will attempt to get the permission corresponding to the subject and target, and: - If a permission is found, call the corresponding method with the request parameters. - If a permission is not found, reject the request with an `unauthorized` error. @@ -105,8 +103,6 @@ const permissionController = new PermissionController({ permissionSpecifications, unrestrictedMethods: ['wallet_unrestrictedMethod'], }); - -const permissionEnforcer = permissionController.enforcer; ``` ### Adding the Permission Middleware @@ -120,19 +116,20 @@ const origin = getOrigin(subject); const engine = new JsonRpcEngine(); engine.push(/* your various middleware*/); -engine.push(permissionEnforcer.createPermissionMiddleware({ origin })); +engine.push(permissionController.createPermissionMiddleware({ origin })); // Your middleware stack is now permissioned +engine.push(/* your other various middleware*/); ``` ### Calling a Restricted Method Internally ```typescript // Sometimes, we need to call a restricted method internally, as a particular subject. -permissionEnforcer.executeRestrictedMethod(origin, 'wallet_getSecretArray'); +permissionController.executeRestrictedMethod(origin, 'wallet_getSecretArray'); // If the restricted method has any parameters, they are given as the third // argument to executeRestrictedMethod(). -permissionEnforcer.executeRestrictedMethod(origin, 'wallet_getSecret', { +permissionController.executeRestrictedMethod(origin, 'wallet_getSecret', { secretType: 'array', }); ``` diff --git a/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts b/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts index ec39630b92..77f524e1f3 100644 --- a/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts +++ b/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts @@ -23,7 +23,6 @@ export const requestPermissionsHandler: PermittedHandlerExport< }, }; -// PermissionEnforcer['requestPermissions'] but with the origin parameter bound type RequestPermissions = ( requestedPermissions: RequestedPermissions, id: string, From 0fed0969dcf2cb9c35463b62185ef6039fded455 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 13 Oct 2021 11:49:25 -0700 Subject: [PATCH 092/147] Minor test wording fixup --- .../src/permissions/PermissionController.test.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 4bf6802f12..af73b39c1f 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -10,10 +10,10 @@ import { import { JsonRpcEngine, PendingJsonRpcResponse } from 'json-rpc-engine'; import { hasProperty, isPlainObject } from '../utils'; import * as errors from './errors'; -import { constructCaveat } from './Caveat'; import { AsyncRestrictedMethod, CaveatBase, + constructCaveat, constructPermission, GenericCaveat, GenericPermission, @@ -323,12 +323,11 @@ type ApprovalActions = * * @returns The restricted messenger. */ -function getDefaultRestrictedMessenger() { - const controllerMessenger = new ControllerMessenger< +function getPermissionControllerMessenger() { + return new ControllerMessenger< PermissionControllerActions | ApprovalActions, PermissionControllerEvents - >(); - return controllerMessenger.getRestricted< + >().getRestricted< typeof controllerName, PermissionControllerActions['type'] | ApprovalActions['type'], PermissionControllerEvents['type'] @@ -385,7 +384,7 @@ function getExistingPermissionState() { * * The following defaults are used: * - `caveatSpecifications`: {@link getDefaultCaveatSpecifications} - * - `messenger`: {@link getDefaultRestrictedMessenger} + * - `messenger`: {@link getPermissionControllerMessenger} * - `permissionSpecifications`: {@link getDefaultPermissionSpecifications} * - `unrestrictedMethods`: {@link getDefaultUnrestrictedMethods} * - `state`: `undefined` @@ -396,7 +395,7 @@ function getExistingPermissionState() { function getPermissionControllerOptions(opts?: Record) { return { caveatSpecifications: getDefaultCaveatSpecifications(), - messenger: getDefaultRestrictedMessenger(), + messenger: getPermissionControllerMessenger(), permissionSpecifications: getDefaultPermissionSpecifications(), unrestrictedMethods: getDefaultUnrestrictedMethods(), state: undefined, From c492759067b8d79aebf3522ac8abbec908600a64 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 13 Oct 2021 12:12:19 -0700 Subject: [PATCH 093/147] Update packages/controllers/src/permissions/README.md --- packages/controllers/src/permissions/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/controllers/src/permissions/README.md b/packages/controllers/src/permissions/README.md index c75e1c5135..635fe35d33 100644 --- a/packages/controllers/src/permissions/README.md +++ b/packages/controllers/src/permissions/README.md @@ -8,7 +8,7 @@ It is the successor of the original MetaMask permission system, [`rpc-cap`](http The permission system itself belongs to a **host**, and it mediates the access to resources – called **targets** – of distinct **subjects**. A target can belong to the host itself, or another subject. -When a subject requests access to a target, we say that they **invoke** it. +When a subject attempts to access a target, we say that they **invoke** it. The system ensures that subjects can only invoke a target if they have the **permission** to do so. Permissions are associated with a subject and target, and they are part of the state of the permission system. From 33105e1c92bad1553bfba3f6291726fe905a3a42 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 13 Oct 2021 12:13:16 -0700 Subject: [PATCH 094/147] Update packages/controllers/src/permissions/README.md Co-authored-by: Mark Stacey --- packages/controllers/src/permissions/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/controllers/src/permissions/README.md b/packages/controllers/src/permissions/README.md index 635fe35d33..7f4cdfa3f6 100644 --- a/packages/controllers/src/permissions/README.md +++ b/packages/controllers/src/permissions/README.md @@ -42,8 +42,8 @@ Once the permission middleware is injected into our middleware stack, every JSON - If the requested method is neither restricted nor unrestricted, the request will be rejected with a `methodNotFound` error. - If the requested method is unrestricted, it will pass through the middleware unmodified. - If the requested method is restricted, the middleware will attempt to get the permission corresponding to the subject and target, and: - - If a permission is found, call the corresponding method with the request parameters. - - If a permission is not found, reject the request with an `unauthorized` error. + - If the request is authorized, call the corresponding method with the request parameters. + - If the request is not authorized, reject the request with an `unauthorized` error. ### Caveats From d724b56d0ab700144b3a156294f19b4ac6936f92 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 13 Oct 2021 12:13:37 -0700 Subject: [PATCH 095/147] Update packages/controllers/src/permissions/README.md Co-authored-by: Mark Stacey --- packages/controllers/src/permissions/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/controllers/src/permissions/README.md b/packages/controllers/src/permissions/README.md index 7f4cdfa3f6..281f19ddfa 100644 --- a/packages/controllers/src/permissions/README.md +++ b/packages/controllers/src/permissions/README.md @@ -47,7 +47,7 @@ Once the permission middleware is injected into our middleware stack, every JSON ### Caveats -Caveats are objects associated with permissions that impose arbitrary restrictions on restricted method requests. +Caveats are arbitrary restrictions on restricted method requests. Every permission has a `caveats` field, which is either an array of caveats or `null`. Every caveat has a string `type`, and every type has an associated function that is used to apply the caveat to a restricted method request. When the `PermissionController` is constructed, the consumer specifies the available caveat types and their implementations. From 0556f82349627c50a2f407d98e69f47fca4dcff3 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sun, 17 Oct 2021 14:09:30 -0700 Subject: [PATCH 096/147] Simplify types - Remove unnecessary generics - Rationalize use of "Constraint" types - Rename or remove all "Base" types - Minor fixups --- .../src/permissions/Caveat.test.ts | 8 +- .../controllers/src/permissions/Caveat.ts | 162 ++++-- .../src/permissions/Permission.test.ts | 4 +- .../controllers/src/permissions/Permission.ts | 183 +++---- .../permissions/PermissionController.test.ts | 23 +- .../src/permissions/PermissionController.ts | 464 +++++++++--------- .../src/permissions/permission-middleware.ts | 12 +- packages/controllers/src/permissions/utils.ts | 21 +- 8 files changed, 488 insertions(+), 389 deletions(-) diff --git a/packages/controllers/src/permissions/Caveat.test.ts b/packages/controllers/src/permissions/Caveat.test.ts index 947e29e3c2..d27586e767 100644 --- a/packages/controllers/src/permissions/Caveat.test.ts +++ b/packages/controllers/src/permissions/Caveat.test.ts @@ -1,7 +1,7 @@ import * as errors from './errors'; import { - CaveatSpecificationBase, - CaveatSpecificationsMap, + CaveatSpecification, + CaveatSpecificationMap, decorateWithCaveats, GenericPermission, } from '.'; @@ -17,7 +17,7 @@ describe('decorateWithCaveats', () => { decorator: (method: any, _caveat: any) => () => method().reverse() as any, }, - } as unknown as CaveatSpecificationsMap>; + } as unknown as CaveatSpecificationMap>; const permission = { caveats: [{ type: 'reverse', value: null }], @@ -41,7 +41,7 @@ describe('decorateWithCaveats', () => { decorator: (method: any, _caveat: any) => () => method().reverse() as any, }, - } as unknown as CaveatSpecificationsMap>; + } as unknown as CaveatSpecificationMap>; const permission = { // This type doesn't exist diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index 7d609e411e..3afea28552 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -3,19 +3,35 @@ import { Json } from 'json-rpc-engine'; import { UnrecognizedCaveatTypeError } from './errors'; import { AsyncRestrictedMethod, - RestrictedMethodBase, + RestrictedMethod, GenericPermission, RestrictedMethodParameters, } from './Permission'; +export type CaveatConstraint = { + /** + * The type of the caveat. The type is presumed to be meaningful in the + * context of the capability it is associated with. + * + * In MetaMask, every permission can only have one caveat of each type. + */ + readonly type: string; + + /** + * Any additional data necessary to enforce the caveat. + * + * TODO:TS4.4 Make optional + */ + readonly value: Json; +}; + /** - * Identical to instances of the Caveat class, useful for when TypeScript - * has a meltdown over assigning classes to the Json type. + * TODO:docs * * @template Type - The type of the caveat. * @template Value - The value associated with the caveat. */ -export type CaveatBase = { +export type Caveat = { /** * The type of the caveat. The type is presumed to be meaningful in the * context of the capability it is associated with. @@ -33,7 +49,7 @@ export type CaveatBase = { }; /** - * The {@link CaveatBase} factory function. Naively constructs a new caveat from the + * The {@link Caveat} factory function. Naively constructs a new caveat from the * inputs. Sets `value` to `null` if no value is provided. * * @param type - The type of the caveat. @@ -43,7 +59,7 @@ export type CaveatBase = { export function constructCaveat( type: Type, value: Value, -): CaveatBase { +): Caveat { return { type, value }; } @@ -55,39 +71,47 @@ export function constructCaveat( /** * A function for applying caveats to a restricted method request. * - * @template Caveat - The caveat type associated with this decorator. + * @template ParentCaveat - The caveat type associated with this decorator. * @param decorated - The restricted method implementation to be decorated. * The method may have already been decorated with other caveats. * @param caveat - The caveat object. * @returns The decorate restricted method implementation. */ -export type CaveatDecorator = ( +export type CaveatDecorator = ( decorated: AsyncRestrictedMethod, - caveat: Caveat, + caveat: ParentCaveat, ) => AsyncRestrictedMethod; -export type ExtractCaveatValueFromDecorator< - Decorator extends CaveatDecorator, -> = Decorator extends ( - decorated: any, - caveat: infer Caveat, -) => AsyncRestrictedMethod - ? Caveat extends GenericCaveat - ? Caveat['value'] - : never - : never; - -type CaveatValidator = ( - caveat: { type: Caveat['type']; value: unknown }, +/** + * TODO:docs + */ +type ExtractCaveatValueFromDecorator> = + Decorator extends ( + decorated: any, + caveat: infer ParentCaveat, + ) => AsyncRestrictedMethod + ? ParentCaveat extends CaveatConstraint + ? ParentCaveat['value'] + : never + : never; + +/** + * TODO:docs + */ +export type CaveatValidator = ( + caveat: { type: ParentCaveat['type']; value: unknown }, origin?: string, target?: string, ) => void; -export type CaveatSpecificationBase = { +/** + * TODO:docs + */ +export type CaveatSpecificationConstraint = { /** * The string type of the caveat. */ - type: Type; + type: string; /** * The decorator function used to apply the caveat to restricted method @@ -110,52 +134,88 @@ export type CaveatSpecificationBase = { }; /** - * A generic caveat. + * TODO:docs */ -export type GenericCaveat = CaveatBase; +export type CaveatSpecification = { + /** + * The string type of the caveat. + */ + type: Type; -export type CaveatSpecificationsMap< - Specification extends CaveatSpecificationBase, + /** + * The decorator function used to apply the caveat to restricted method + * requests. + */ + decorator: CaveatDecorator; + + /** + * The validator function used to validate caveats of the associated type + * whenever they are instantiated. Caveat are instantiated whenever they are + * created or mutated. + * + * The validator should throw an appropriate JSON-RPC error if validation fails. + * + * If no validator is specified, no validation of caveat values will be + * performed. Although caveats can also be validated by permission validators, + * validating caveat values separately is strongly recommended. + */ + validator?: CaveatValidator; +}; + +/** + * TODO:docs + */ +export type CaveatSpecificationMap< + Specification extends CaveatSpecification, > = { - [Key in Specification['type']]: Specification extends CaveatSpecificationBase + [Key in Specification['type']]: Specification extends CaveatSpecification ? Specification : never; }; -export type ExtractCaveats< - Specification extends CaveatSpecificationBase, -> = Specification extends any - ? CaveatBase< - Specification['type'], - ExtractCaveatValueFromDecorator - > - : never; +/** + * TODO:docs + */ +export type ExtractCaveats> = + Specification extends any + ? Caveat< + Specification['type'], + ExtractCaveatValueFromDecorator + > + : never; /** * Internal utility type, because using parameterized types in conditional types * causes weird things to happen. */ type _ExtractCaveat< - Caveat extends GenericCaveat, + CaveatUnion extends CaveatConstraint, CaveatType extends string, -> = Caveat extends CaveatBase ? Caveat : never; +> = CaveatUnion extends Caveat ? CaveatUnion : never; +/** + * Extracts the type of a specific {@link Caveat} from a union of caveat + * specifications. + * + * @template CaveatSpecifications - The union of all caveat specifications. + * @template CaveatType - The type of the caveat to extract. + */ export type ExtractCaveat< - CaveatSpecification extends CaveatSpecificationBase, + CaveatSpecifications extends CaveatSpecification, CaveatType extends string, -> = _ExtractCaveat, CaveatType>; +> = _ExtractCaveat, CaveatType>; /** - * A utility type for extracting the {@link CaveatBase.value} type from - * a union of caveat types. + * Extracts the value type of a specific {@link Caveat} from a union of caveat + * specifications. * - * @template CaveatUnion - The caveat type union to extract a value type from. + * @template CaveatSpecifications - The union of all caveat specifications. * @template CaveatType - The type of the caveat whose value to extract. */ export type ExtractCaveatValue< - CaveatSpecification extends CaveatSpecificationBase, + CaveatSpecifications extends CaveatSpecification, CaveatType extends string, -> = ExtractCaveat['value']; +> = ExtractCaveat['value']; /** * Decorate a restricted method implementation with its caveats. @@ -164,12 +224,12 @@ export type ExtractCaveatValue< * decorator) must be awaited. */ export function decorateWithCaveats< - CaveatSpecification extends CaveatSpecificationBase, + CaveatSpecifications extends CaveatSpecification, >( - methodImplementation: RestrictedMethodBase, + methodImplementation: RestrictedMethod, permission: Readonly, // bound to the requesting origin - caveatSpecifications: CaveatSpecificationsMap, // all caveat implementations -): RestrictedMethodBase { + caveatSpecifications: CaveatSpecificationMap, // all caveat implementations +): RestrictedMethod { const { caveats } = permission; if (!caveats) { return methodImplementation; @@ -182,7 +242,7 @@ export function decorateWithCaveats< for (const caveat of caveats) { const specification = - caveatSpecifications[caveat.type as CaveatSpecification['type']]; + caveatSpecifications[caveat.type as CaveatSpecifications['type']]; if (!specification) { throw new UnrecognizedCaveatTypeError(caveat.type); } diff --git a/packages/controllers/src/permissions/Permission.test.ts b/packages/controllers/src/permissions/Permission.test.ts index ba39a86766..4543dfabe1 100644 --- a/packages/controllers/src/permissions/Permission.test.ts +++ b/packages/controllers/src/permissions/Permission.test.ts @@ -1,4 +1,4 @@ -import { constructPermission, GenericCaveat } from '.'; +import { constructPermission, CaveatConstraint } from '.'; describe('constructPermission', () => { it('constructs a permission', () => { @@ -24,7 +24,7 @@ describe('constructPermission', () => { it('constructs a permission with caveats', () => { const invoker = 'foo.io'; const target = 'wallet_bar'; - const caveats = [{ type: 'foo', value: 'bar' }] as [GenericCaveat]; + const caveats = [{ type: 'foo', value: 'bar' }] as [CaveatConstraint]; expect( constructPermission({ diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index ea776922d8..6f79d421a0 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -1,11 +1,11 @@ import { Json } from 'json-rpc-engine'; import { nanoid } from 'nanoid'; import { Mutable, NonEmptyArray } from '../utils'; -import { GenericCaveat } from './Caveat'; +import { CaveatConstraint } from './Caveat'; // This is used in a docstring, but ESLint doesn't notice it. /* eslint-disable @typescript-eslint/no-unused-vars */ import type { PermissionController } from './PermissionController'; -import type { CaveatBase } from './Caveat'; +import type { Caveat } from './Caveat'; /* eslint-enable @typescript-eslint/no-unused-vars */ /** @@ -19,14 +19,7 @@ export type OriginString = string; */ export type GenericTargetName = string; -/** - * The base permission interface. Lacks important constraints on its generics; - * consumers should use {@link PermissionConstraint} instead. - */ -export type PermissionBase< - Target extends GenericTargetName, - AllowedCaveat extends GenericCaveat, -> = { +export type PermissionConstraint = { /** * The context(s) in which this capability is meaningful. * @@ -35,14 +28,12 @@ export type PermissionBase< */ readonly '@context'?: NonEmptyArray; - // TODO: Make optional in typescript@4.4.x + // TODO:TS4.4 Make optional /** * The caveats of the permission. - * @see {@link CaveatBase} For more information. + * @see {@link Caveat} For more information. */ - readonly caveats: AllowedCaveat extends never - ? null - : NonEmptyArray | null; + readonly caveats: null | NonEmptyArray; /** * The creation date of the permission, in UNIX epoch time. @@ -65,16 +56,46 @@ export type PermissionBase< * * In the context of MetaMask, this is always the name of an RPC method. */ - readonly parentCapability: Target; + readonly parentCapability: string; }; +/** + * @template TargetKey - They key of the permission target that the permission + * corresponds to. + * @template AllowedCaveat - A union of the allowed {@link Caveat} types + * for the permission. + */ +export type Permission< + TargetKey extends GenericTargetName, + AllowedCaveat extends CaveatConstraint, +> = ValidateTargetName> extends never + ? never + : PermissionConstraint & { + // TODO:TS4.4 Make optional + /** + * The caveats of the permission. + * @see {@link Caveat} For more information. + */ + readonly caveats: AllowedCaveat extends never + ? null + : NonEmptyArray | null; + + /** + * A pointer to the resource that possession of the capability grants + * access to. + * + * In the context of MetaMask, this is always the name of an RPC method. + */ + readonly parentCapability: ExtractPermissionTargetNames; + }; + /** * A utility type for ensuring that the given permission target name conforms to * our naming conventions. * * See the documentation for the distinction between target names and keys. */ -type TargetNameConstraint = Name extends `${string}*` +type ValidateTargetName = Name extends `${string}*` ? never : Name extends `${string}_` ? never @@ -88,7 +109,7 @@ type TargetNameConstraint = Name extends `${string}*` * * @template Key - The target key type to extract target names from. */ -type ExtractPermissionTargetNames = +export type ExtractPermissionTargetNames = Key extends `${infer Base}_*` ? `${Base}_${string}` : Key; /** @@ -96,29 +117,20 @@ type ExtractPermissionTargetNames = * * @template TargetKey - They key of the permission target that the permission * corresponds to. - * @template AllowedCaveats - A union of the allowed {@link CaveatBase} types + * @template AllowedCaveats - A union of the allowed {@link Caveat} types * for the permission. */ -export type PermissionConstraint< - TargetKey extends string, - AllowedCaveats extends GenericCaveat, -> = TargetNameConstraint> extends never - ? never - : PermissionBase, AllowedCaveats>; /** * A generic permission. */ -export type GenericPermission = PermissionConstraint< - GenericTargetName, - GenericCaveat ->; +export type GenericPermission = Permission; /** * A {@link GenericPermission} with mutable caveats. */ export type MutableGenericPermission = Mutable< - GenericPermission & { caveats: NonEmptyArray }, + GenericPermission & { caveats: NonEmptyArray | null }, 'caveats' >; @@ -128,7 +140,7 @@ export type MutableGenericPermission = Mutable< * @template Key - The target key type to extract from. * @template Name - The name whose key to extract. */ -type KeyOfTargetName< +export type KeyOfTargetName< Key extends string, Name extends string, > = Name extends ExtractPermissionTargetNames ? Key : never; @@ -142,10 +154,10 @@ type KeyOfTargetName< * @template Key - The target key type to extract from. * @template Name - The name whose key to extract. */ -type ExtractPermissionTargetKey< +export type ExtractPermissionTargetKey< Key extends string, Name extends string, -> = Name extends Key ? Name : Extract>; +> = Key extends Name ? Key : Extract>; /** * Internal utility for extracting the members types of an array. The type @@ -168,21 +180,24 @@ type ExtractArrayMembers = T extends readonly [...infer U] | [...infer U] * caveats to extract. */ export type ExtractAllowedCaveatTypes< - PermissionSpecification extends PermissionSpecificationBase, + PermissionSpecification extends PermissionSpecificationConstraint, TargetName extends string, -> = PermissionSpecification extends PermissionSpecificationBase< - ExtractPermissionTargetKey -> +> = PermissionSpecification extends PermissionSpecificationConstraint & { + targetKey: ExtractPermissionTargetKey< + PermissionSpecification['targetKey'], + TargetName + >; +} ? ExtractArrayMembers : never; /** * The options object of {@link constructPermission}. * - * @template Permission - The {@link PermissionBase} that will be constructed. + * @template TargetPermission - The {@link Permission} that will be constructed. */ -export type PermissionOptions = { - target: Permission['parentCapability']; +export type PermissionOptions = { + target: TargetPermission['parentCapability']; /** * The origin string of the subject that has the permission. */ @@ -196,25 +211,25 @@ export type PermissionOptions = { /** * The caveats of the permission. - * See {@link CaveatBase}. + * See {@link Caveat}. */ - caveats?: NonEmptyArray; + caveats?: NonEmptyArray; // TODO: Refactor allowed caveats validation to enable this? - // caveats?: NonEmptyArray>; + // caveats?: NonEmptyArray>; }; /** * The default permission factory function. Naively constructs a permission from * the inputs. Sets a default, random `id` if none is provided. * - * @see {@link PermissionBase} For more details. + * @see {@link Permission} For more details. * - * @template Permission - The {@link PermissionBase} that will be constructed. + * @template TargetPermission - The {@link Permission} that will be constructed. * @param options - The options for the permission. * @returns The new permission object. */ -export function constructPermission( - options: PermissionOptions, +export function constructPermission( + options: PermissionOptions, ): GenericPermission { const { caveats = null, id, invoker, target } = options; @@ -238,7 +253,7 @@ export function constructPermission( export function findCaveat( permission: GenericPermission, caveatType: string, -): GenericCaveat | undefined { +): CaveatConstraint | undefined { return permission.caveats?.find((caveat) => caveat.type === caveatType); } @@ -308,20 +323,20 @@ export type AsyncRestrictedMethod< * @template Params - The JSON-RPC parameters of the restricted method. * @template Result - The JSON-RPC result of the restricted method. */ -export type RestrictedMethodBase< +export type RestrictedMethod< Params extends RestrictedMethodParameters, Result extends Json, > = | SyncRestrictedMethod | AsyncRestrictedMethod; -export type GenericRestrictedMethod = RestrictedMethodBase< +export type GenericRestrictedMethod = RestrictedMethod< RestrictedMethodParameters, Json >; -export type RestrictedMethodConstraint< - MethodImplementation extends RestrictedMethodBase, +export type ValidateRestrictedMethod< + MethodImplementation extends RestrictedMethod, > = MethodImplementation extends (args: infer Options) => Json | Promise ? Options extends RestrictedMethodOptions ? MethodImplementation @@ -329,17 +344,17 @@ export type RestrictedMethodConstraint< : never; export type PermissionFactory< - Permission extends GenericPermission, + TargetPermission extends GenericPermission, RequestData extends Record, > = ( - options: PermissionOptions, + options: PermissionOptions, requestData?: RequestData, -) => Permission; +) => TargetPermission; -export type PermissionValidator = ( +export type PermissionValidator = ( permission: GenericPermission, origin?: OriginString, - target?: Permission['parentCapability'], + target?: TargetPermission['parentCapability'], ) => void; /** @@ -350,7 +365,7 @@ export type PermissionValidator = ( * * @template Key - The target key string to apply the constraint to. */ -type TargetKeyConstraint = Key extends `${string}_*` +type ValidateTargetKey = Key extends `${string}_*` ? Key : Key extends `${string}_` ? never @@ -363,18 +378,12 @@ type TargetKeyConstraint = Key extends `${string}_*` */ type GenericFunction = (...args: any[]) => any; -/** - * The base permission specification type. - * - * @template TargetKey - They key of the permission target that the permission - * corresponds to. - */ -export type PermissionSpecificationBase = { +export type PermissionSpecificationConstraint = { /** * The target resource of the permission. In other words, at the time of * writing, the RPC method name. */ - targetKey: TargetKey; + targetKey: string; /** * An array of the caveat types that may be added to instances of this @@ -411,16 +420,16 @@ export type PermissionSpecificationBase = { }; /** - * An individual permission specification object. Constrained such that the + * Constraint for {@link PermissionSpecificationConstraint} objects ensuring th + * that permission specifications are * given target key must be valid. * - * @template TargetKey - The key of the permission target. - * @template Permission - The type of the permission object. + * @template T - The key of the permission target. */ -export type PermissionSpecificationConstraint = - T extends PermissionSpecificationBase - ? T['targetKey'] extends TargetKeyConstraint - ? T['methodImplementation'] extends RestrictedMethodConstraint< +export type ValidatePermissionSpecification = + T extends PermissionSpecificationConstraint + ? T['targetKey'] extends ValidateTargetKey + ? T['methodImplementation'] extends ValidateRestrictedMethod< T['methodImplementation'] > ? T @@ -432,29 +441,31 @@ export type PermissionSpecificationConstraint = * The specifications for all permissions and restricted methods supported by * a particular {@link PermissionController}. * - * @template Specifications - A union of all {@link PermissionSpecificationBase} + * @template Specifications - A union of all {@link PermissionSpecificationConstraint} * types. */ -export type PermissionSpecificationsMap< - Specification extends PermissionSpecificationBase, -> = Specification extends PermissionSpecificationConstraint - ? { - [TargetKey in Specification['targetKey']]: Specification extends PermissionSpecificationBase - ? Specification - : never; - } - : never; +export type PermissionSpecificationMap< + Specification extends PermissionSpecificationConstraint, +> = { + [TargetKey in Specification['targetKey']]: Specification extends PermissionSpecificationConstraint & { + targetKey: TargetKey; + } + ? Specification + : never; +}; /** - * Extracts a specific {@link PermissionSpecificationBase} from a union of + * Extracts a specific {@link PermissionSpecificationConstraint} from a union of * permission specifications. * * @template Specification - The specification union type to extract from. * @template TargetKey - The `targetKey` of the specification to extract. */ export type ExtractPermissionSpecification< - Specification extends PermissionSpecificationBase, + Specification extends PermissionSpecificationConstraint, TargetKey extends Specification['targetKey'], -> = Specification extends PermissionSpecificationBase +> = Specification extends PermissionSpecificationConstraint & { + targetKey: TargetKey; +} ? Specification : never; diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index af73b39c1f..c769c29f96 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -12,13 +12,13 @@ import { hasProperty, isPlainObject } from '../utils'; import * as errors from './errors'; import { AsyncRestrictedMethod, - CaveatBase, + Caveat, constructCaveat, constructPermission, - GenericCaveat, + CaveatConstraint, GenericPermission, MethodNames, - PermissionConstraint, + Permission, PermissionController, PermissionControllerActions, PermissionControllerEvents, @@ -29,7 +29,7 @@ import { CaveatMutatorOperation, } from '.'; -// CaveatBase types and specifications +// Caveat types and specifications const CaveatTypes = { filterArrayResponse: 'filterArrayResponse', @@ -38,22 +38,19 @@ const CaveatTypes = { noopCaveat: 'noopCaveat', } as const; -type FilterArrayCaveat = CaveatBase< +type FilterArrayCaveat = Caveat< typeof CaveatTypes.filterArrayResponse, string[] >; -type ReverseArrayCaveat = CaveatBase< - typeof CaveatTypes.reverseArrayResponse, - null ->; +type ReverseArrayCaveat = Caveat; -type FilterObjectCaveat = CaveatBase< +type FilterObjectCaveat = Caveat< typeof CaveatTypes.filterObjectResponse, string[] >; -type NoopCaveat = CaveatBase; +type NoopCaveat = Caveat; /** * Gets caveat specifications for: @@ -168,7 +165,7 @@ type DefaultCaveatSpecifications = ExtractSpecifications< type SecretNamespacedPermissionKey = 'wallet_getSecret_*'; -type SecretNamespacedPermission = PermissionConstraint< +type SecretNamespacedPermission = Permission< SecretNamespacedPermissionKey, NoopCaveat >; @@ -446,7 +443,7 @@ function getDefaultPermissionControllerWithState() { */ function getPermissionMatcher( parentCapability: string, - caveats: GenericCaveat[] | null | typeof expect.objectContaining = null, + caveats: CaveatConstraint[] | null | typeof expect.objectContaining = null, invoker = 'metamask.io', ) { return expect.objectContaining({ diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index a14cad65fb..b53f839ada 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -23,11 +23,11 @@ import { constructCaveat as _constructCaveat, decorateWithCaveats, ExtractCaveat, - CaveatSpecificationBase, ExtractCaveats, - CaveatSpecificationsMap, + CaveatSpecificationMap, ExtractCaveatValue, - GenericCaveat, + CaveatConstraint, + CaveatSpecificationConstraint, } from './Caveat'; import { constructPermission, @@ -36,15 +36,15 @@ import { OriginString, PermissionOptions, RequestedPermissions, - RestrictedMethodBase, + RestrictedMethod, RestrictedMethodParameters, - PermissionSpecificationConstraint, - PermissionSpecificationsMap, - PermissionSpecificationBase, - PermissionConstraint, + ValidatePermissionSpecification, + PermissionSpecificationMap, + Permission, ExtractAllowedCaveatTypes, MutableGenericPermission, ExtractPermissionSpecification, + PermissionSpecificationConstraint, } from './Permission'; import { PermissionDoesNotExistError, @@ -105,33 +105,36 @@ const controllerName = 'PermissionController'; /** * Permissions associated with a {@link PermissionController} subject. */ -export type SubjectPermissions = Record< - Permission['parentCapability'], - Permission +export type SubjectPermissions = Record< + Permissions['parentCapability'], + Permissions >; /** * Permissions and metadata associated with a {@link PermissionController} * subject. */ -export type PermissionSubjectEntry = { - permissions: SubjectPermissions; +export type PermissionSubjectEntry< + SubjectPermission extends GenericPermission, +> = { + permissions: SubjectPermissions; } & PermissionSubjectMetadata; /** * All subjects of a {@link PermissionController}. */ -export type PermissionControllerSubjects = - Record>; +export type PermissionControllerSubjects< + SubjectPermission extends GenericPermission, +> = Record>; // TODO:TS4.4 Enable compiler flags to forbid unchecked member access /** * The state of a {@link PermissionController}. */ -export type PermissionControllerState = - Permission extends GenericPermission +export type PermissionControllerState = + Permissions extends GenericPermission ? { - subjects: PermissionControllerSubjects; + subjects: PermissionControllerSubjects; } : never; @@ -142,11 +145,10 @@ const stateMetadata: StateMetadata> = { subjects: { persist: true, anonymous: true }, }; -// Typecast: We can't reference the class generics, so we must resort to "any" /** * The default state of the {@link PermissionController}. */ -const defaultState: PermissionControllerState = { +const defaultState: PermissionControllerState = { subjects: {}, }; @@ -231,8 +233,8 @@ export type PermissionControllerMessenger = RestrictedControllerMessenger< >; export type GenericPermissionController = PermissionController< - PermissionSpecificationBase, - CaveatSpecificationBase + PermissionSpecificationConstraint, + CaveatSpecificationConstraint >; /** @@ -265,57 +267,57 @@ export enum CaveatMutatorOperation { * @returns A tuple of the mutation result and, optionally, the new caveat * value. */ -export type CaveatMutator = ( - caveatValue: Caveat['value'], +export type CaveatMutator = ( + caveatValue: TargetCaveat['value'], ) => | [Exclude] - | [CaveatMutatorOperation.updateValue, GenericCaveat['value']]; + | [CaveatMutatorOperation.updateValue, CaveatConstraint['value']]; /** * Extracts the permission type or types from the specified permission and * caveat specifications. * - * @template PermissionSpecification - The permission specification(s) to + * @template ControllerPermissionSpecification - The permission specification(s) to * extract a permission type union from. - * @template CaveatSpecification - The caveat specification(s) to extract a - * permission type union from. Necessary because {@link PermissionConstraint} + * @template ControllerCaveatSpecification - The caveat specification(s) to extract a + * permission type union from. Necessary because {@link Permission} * has a generic that describes the allowed caveats for the permission. */ export type ExtractPermission< - PermissionSpecification extends PermissionSpecificationBase, - CaveatSpecification extends CaveatSpecificationBase, -> = PermissionSpecification extends PermissionSpecificationConstraint - ? PermissionConstraint< - PermissionSpecification['targetKey'], - ExtractCaveats + ControllerPermissionSpecification extends PermissionSpecificationConstraint, + ControllerCaveatSpecification extends CaveatSpecificationConstraint, +> = ControllerPermissionSpecification extends ValidatePermissionSpecification + ? Permission< + ControllerPermissionSpecification['targetKey'], + ExtractCaveats > : never; /** * Options for the {@link PermissionController} constructor. * - * @template TargetKey - A union of string literal types corresponding to the - * keys of all permission target. Must match the given permission - * specifications object. - * @template Caveat - A union of the types of all caveats available to the new - * controller. Must match the given caveat specifications object. - * @template Permission - A union of the types of all permissions available to - * the new controller. Must match the given permission and caveat specification - * objects. + * @template ControllerPermissionSpecification - A union of the types of all + * permission specifications available to the controller. Any referenced caveats + * must be included in the controller's caveat specifications. + * @template ControllerCaveatSpecification - A union of the types of all + * caveat specifications available to the controller. */ export type PermissionControllerOptions< - PermissionSpecification extends PermissionSpecificationBase, - CaveatSpecification extends CaveatSpecificationBase, -> = PermissionSpecificationConstraint extends never + ControllerPermissionSpecification extends PermissionSpecificationConstraint, + ControllerCaveatSpecification extends CaveatSpecificationConstraint, +> = ValidatePermissionSpecification extends never ? never : { messenger: PermissionControllerMessenger; - caveatSpecifications: CaveatSpecificationsMap; - permissionSpecifications: PermissionSpecificationsMap; + caveatSpecifications: CaveatSpecificationMap; + permissionSpecifications: PermissionSpecificationMap; unrestrictedMethods: string[]; state?: Partial< PermissionControllerState< - ExtractPermission + ExtractPermission< + ControllerPermissionSpecification, + ControllerCaveatSpecification + > > >; }; @@ -326,31 +328,31 @@ export type PermissionControllerOptions< * Assumes the existence of an {@link ApprovalController} reachable via the * {@link ControllerMessenger}. * - * @template TargetKey - A union of string literal types corresponding to the - * keys of all permission target. Must match the permission specifications of - * the controller. - * @template Caveat - A union of the types of all caveats available to the new - * controller. Must match the caveat specifications of the controller. - * @template Permission - A union of the types of all permissions available to - * the new controller. Must match the the permission and caveat specifications - * of the controller. + * @template ControllerPermissionSpecification - A union of the types of all + * permission specifications available to the controller. Any referenced caveats + * must be included in the controller's caveat specifications. + * @template ControllerCaveatSpecification - A union of the types of all + * caveat specifications available to the controller. */ export class PermissionController< - PermissionSpecification extends PermissionSpecificationBase, - CaveatSpecification extends CaveatSpecificationBase, + ControllerPermissionSpecification extends PermissionSpecificationConstraint, + ControllerCaveatSpecification extends CaveatSpecificationConstraint, > extends BaseController< typeof controllerName, PermissionControllerState< - ExtractPermission + ExtractPermission< + ControllerPermissionSpecification, + ControllerCaveatSpecification + > >, PermissionControllerMessenger > { private readonly _caveatSpecifications: Readonly< - CaveatSpecificationsMap + CaveatSpecificationMap >; private readonly _permissionSpecifications: Readonly< - PermissionSpecificationsMap + PermissionSpecificationMap >; private readonly _unrestrictedMethods: ReadonlySet; @@ -377,11 +379,11 @@ export class PermissionController< /** * @param options - Permission controller options. * @param options.caveatSpecifications - The specifications of all caveats - * available to the controller. See {@link CaveatSpecificationsMap} and the + * available to the controller. See {@link CaveatSpecificationMap} and the * documentation for more details. * @param options.permissionSpecifications - The specifications of all * permissions available to the controller. See - * {@link PermissionSpecificationsMap} and the documentation for more details. + * {@link PermissionSpecificationMap} and the documentation for more details. * @param options.unrestrictedMethods - The callable names of all JSON-RPC * methods ignored by the new controller. * @param options.messenger - The controller messenger. See @@ -391,8 +393,8 @@ export class PermissionController< */ constructor( options: PermissionControllerOptions< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >, ) { const { @@ -402,15 +404,20 @@ export class PermissionController< messenger, state = {}, } = options; + + // Typecast: The state metadata and default state objects are declared + // outside of the controller instance and are therefore incorrectly + // genericized. super({ name: controllerName, - // Typecast: The state metadata object is declared outside of the - // controller instance and therefore incorrectly genericized. metadata: stateMetadata as any, messenger, state: { ...(defaultState as PermissionControllerState< - ExtractPermission + ExtractPermission< + ControllerPermissionSpecification, + ControllerCaveatSpecification + > >), ...state, }, @@ -445,14 +452,14 @@ export class PermissionController< * @returns The permission specification with the specified target key. */ getPermissionSpecification< - TargetKey extends PermissionSpecification['targetKey'], + TargetKey extends ControllerPermissionSpecification['targetKey'], >( targetKey: TargetKey, - ): ExtractPermissionSpecification { - // Typecast: Due to constraints added to the PermissionSpecificationsMap, - // `keyof typeof this._permissionSpecifications` evalutes to `never`. - // Therefore, we have to cast it like this. - return (this._permissionSpecifications as any)[targetKey]; + ): ExtractPermissionSpecification< + ControllerPermissionSpecification, + TargetKey + > { + return this._permissionSpecifications[targetKey]; } /** @@ -461,9 +468,9 @@ export class PermissionController< * @param caveatType - The type of the caveat specification to get. * @returns The caveat specification with the specified type. */ - getCaveatSpecification( - caveatType: CaveatType, - ) { + getCaveatSpecification< + CaveatType extends ControllerCaveatSpecification['type'], + >(caveatType: CaveatType) { return this._caveatSpecifications[caveatType]; } @@ -478,32 +485,32 @@ export class PermissionController< * controller's constructor. */ private validatePermissionSpecifications( - permissionSpecifications: PermissionSpecificationsMap, - caveatSpecifications: CaveatSpecificationsMap, + permissionSpecifications: PermissionSpecificationMap, + caveatSpecifications: CaveatSpecificationMap, ) { - Object.entries(permissionSpecifications).forEach( - ([targetKey, { targetKey: innerTargetKey, allowedCaveats }]) => { - // Check if the target key is the empty string, ends with "_", or ends - // with "*" but not "_*" - if (!targetKey || /_$/u.test(targetKey) || /[^_]\*$/u.test(targetKey)) { - throw new Error(`Invalid permission target key: "${targetKey}"`); - } + Object.entries( + permissionSpecifications, + ).forEach(([targetKey, { targetKey: innerTargetKey, allowedCaveats }]) => { + // Check if the target key is the empty string, ends with "_", or ends + // with "*" but not "_*" + if (!targetKey || /_$/u.test(targetKey) || /[^_]\*$/u.test(targetKey)) { + throw new Error(`Invalid permission target key: "${targetKey}"`); + } - if (targetKey !== innerTargetKey) { - throw new Error( - `Invalid permission specification: key "${targetKey}" must match specification.target value "${innerTargetKey}".`, - ); - } + if (targetKey !== innerTargetKey) { + throw new Error( + `Invalid permission specification: key "${targetKey}" must match specification.target value "${innerTargetKey}".`, + ); + } - if (allowedCaveats) { - allowedCaveats.forEach((caveatType) => { - if (!hasProperty(caveatSpecifications, caveatType)) { - throw new UnrecognizedCaveatTypeError(caveatType); - } - }); - } - }, - ); + if (allowedCaveats) { + allowedCaveats.forEach((caveatType) => { + if (!hasProperty(caveatSpecifications, caveatType)) { + throw new UnrecognizedCaveatTypeError(caveatType); + } + }); + } + }); } /** @@ -535,7 +542,10 @@ export class PermissionController< // Typecast: The default state is declared outside of the controller // instance and therefore incorrectly genericized. return { ...defaultState } as PermissionControllerState< - ExtractPermission + ExtractPermission< + ControllerPermissionSpecification, + ControllerCaveatSpecification + > >; }); } @@ -548,11 +558,11 @@ export class PermissionController< */ getRestrictedMethod( method: string, - ): RestrictedMethodBase | undefined { + ): RestrictedMethod | undefined { const targetKey = this.getTargetKey( method as ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >['parentCapability'], ); if (!targetKey) { @@ -579,8 +589,8 @@ export class PermissionController< */ getPermission< SubjectPermission extends ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >, >( origin: OriginString, @@ -612,8 +622,8 @@ export class PermissionController< hasPermission( origin: OriginString, target: ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >['parentCapability'], ): boolean { return Boolean(this.getPermission(origin, target)); @@ -658,8 +668,8 @@ export class PermissionController< revokePermission( origin: OriginString, target: ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >['parentCapability'], ): void { this.revokePermissions({ [origin]: [target] }); @@ -678,8 +688,8 @@ export class PermissionController< OriginString, NonEmptyArray< ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >['parentCapability'] > >, @@ -715,8 +725,8 @@ export class PermissionController< */ revokePermissionForAllSubjects( target: ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >['parentCapability'], ): void { if (this.getSubjectNames().length === 0) { @@ -753,8 +763,8 @@ export class PermissionController< subjects: PermissionControllerSubjects, origin: OriginString, target: ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >['parentCapability'], ): void { const { permissions } = subjects[origin]; @@ -782,11 +792,11 @@ export class PermissionController< */ hasCaveat< TargetName extends ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >['parentCapability'], CaveatType extends ExtractAllowedCaveatTypes< - PermissionSpecification, + ControllerPermissionSpecification, TargetName >, >(origin: OriginString, target: TargetName, caveatType: CaveatType): boolean { @@ -810,25 +820,25 @@ export class PermissionController< */ getCaveat< TargetName extends ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >['parentCapability'], CaveatType extends ExtractAllowedCaveatTypes< - PermissionSpecification, + ControllerPermissionSpecification, TargetName >, >( origin: OriginString, target: TargetName, caveatType: CaveatType, - ): ExtractCaveat | undefined { + ): ExtractCaveat | undefined { const permission = this.getPermission(origin, target); if (!permission) { throw new PermissionDoesNotExistError(origin, target); } return findCaveat(permission, caveatType) as - | ExtractCaveat + | ExtractCaveat | undefined; } @@ -853,18 +863,18 @@ export class PermissionController< */ addCaveat< TargetName extends ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >['parentCapability'], CaveatType extends ExtractAllowedCaveatTypes< - PermissionSpecification, + ControllerPermissionSpecification, TargetName >, >( origin: OriginString, target: TargetName, caveatType: CaveatType, - caveatValue: ExtractCaveatValue, + caveatValue: ExtractCaveatValue, ): void { if (this.hasCaveat(origin, target, caveatType)) { throw new CaveatAlreadyExistsError(origin, target, caveatType); @@ -893,14 +903,17 @@ export class PermissionController< */ updateCaveat< TargetName extends ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >['parentCapability'], CaveatType extends ExtractAllowedCaveatTypes< - PermissionSpecification, + ControllerPermissionSpecification, TargetName >, - CaveatValue extends ExtractCaveatValue, + CaveatValue extends ExtractCaveatValue< + ControllerCaveatSpecification, + CaveatType + >, >( origin: OriginString, target: TargetName, @@ -934,33 +947,30 @@ export class PermissionController< */ private setCaveat< TargetName extends ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >['parentCapability'], CaveatType extends ExtractAllowedCaveatTypes< - PermissionSpecification, + ControllerPermissionSpecification, TargetName >, >( origin: OriginString, target: TargetName, caveatType: CaveatType, - caveatValue: ExtractCaveatValue, + caveatValue: ExtractCaveatValue, ): void { this.update((draftState) => { const subject = draftState.subjects[origin]; - // This should be impossible in our usage, but TypeScript wants it - /* istanbul ignore if */ + /* istanbul ignore if: practically impossible, but TypeScript wants it */ if (!subject) { throw new UnrecognizedSubjectError(origin); } - // Typecast: immer's WritableDraft is incompatible with our generics const permission = subject.permissions[target]; - // This should also be impossible in our usage, but TypeScript wants it - /* istanbul ignore if */ + /* istanbul ignore if: practically impossible, but TypeScript wants it */ if (!permission) { throw new PermissionDoesNotExistError(origin, target); } @@ -970,7 +980,6 @@ export class PermissionController< value: caveatValue, }); - // Typecasts: ts(2589) if (permission.caveats) { const caveatIndex = permission.caveats.findIndex( (existingCaveat) => existingCaveat.type === caveat.type, @@ -992,8 +1001,8 @@ export class PermissionController< this.validateModifiedPermission( // Typecast: Immer WritableDraft incompatibility permission as unknown as ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >, origin, target, @@ -1029,9 +1038,12 @@ export class PermissionController< * values. */ updatePermissionsByCaveat< - CaveatType extends ExtractCaveats['type'], - Caveat extends ExtractCaveat, - >(targetCaveatType: CaveatType, mutator: CaveatMutator): void { + CaveatType extends ExtractCaveats['type'], + TargetCaveat extends ExtractCaveat< + ControllerCaveatSpecification, + CaveatType + >, + >(targetCaveatType: CaveatType, mutator: CaveatMutator): void { if (Object.keys(this.state.subjects).length === 0) { return; } @@ -1042,7 +1054,7 @@ export class PermissionController< const { caveats } = permission; const targetCaveat = caveats?.find( ({ type }) => type === targetCaveatType, - ) as Caveat | undefined; + ) as TargetCaveat | undefined; if (!targetCaveat) { return; } @@ -1058,8 +1070,8 @@ export class PermissionController< // Typecast: The mutator type guarantees that the new value will // not be undefined in this case. In any case, the specification // validator is called on the mutated caveat. - (targetCaveat as Mutable).value = - newValue as Caveat['value']; + (targetCaveat as Mutable).value = + newValue as TargetCaveat['value']; this.validateCaveat( targetCaveat, @@ -1116,11 +1128,11 @@ export class PermissionController< */ removeCaveat< TargetName extends ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >['parentCapability'], CaveatType extends ExtractAllowedCaveatTypes< - PermissionSpecification, + ControllerPermissionSpecification, TargetName >, >(origin: OriginString, target: TargetName, caveatType: CaveatType): void { @@ -1154,39 +1166,36 @@ export class PermissionController< */ private deleteCaveat< TargetName extends ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >['parentCapability'], - CaveatType extends ExtractCaveats['type'], + CaveatType extends ExtractCaveats['type'], >( permission: MutableGenericPermission, caveatType: CaveatType, origin: OriginString, target: TargetName, ): void { + /* istanbul ignore if: not possible in our usage */ + if (!permission.caveats) { + throw new CaveatDoesNotExistError(origin, target, caveatType); + } + const caveatIndex = permission.caveats.findIndex( (existingCaveat) => existingCaveat.type === caveatType, ); - if (!permission.caveats || caveatIndex === -1) { + + if (caveatIndex === -1) { throw new CaveatDoesNotExistError(origin, target, caveatType); } if (permission.caveats.length === 1) { - // Typecast: Immer WritableDraft incompatibility - permission.caveats = null as any; + permission.caveats = null; } else { permission.caveats.splice(caveatIndex, 1); } - this.validateModifiedPermission( - // Typecast: From generic to modified permission - permission as ExtractPermission< - PermissionSpecification, - CaveatSpecification - >, - origin, - target, - ); + this.validateModifiedPermission(permission, origin, target); } /** @@ -1205,12 +1214,12 @@ export class PermissionController< permission: GenericPermission, origin: OriginString, targetName: ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >['parentCapability'], ): void { const targetKey = this.getTargetKey(permission.parentCapability); - /* istanbul ignore next: this should be impossible */ + /* istanbul ignore if: this should be impossible */ if (!targetKey) { throw new Error( `Fatal: Existing permission target key "${targetKey}" has no specification.`, @@ -1237,12 +1246,12 @@ export class PermissionController< */ private getTargetKey( method: ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >['parentCapability'], - ): PermissionSpecification['targetKey'] | undefined { + ): ControllerPermissionSpecification['targetKey'] | undefined { if (hasProperty(this._permissionSpecifications, method)) { - return method as PermissionSpecification['targetKey']; + return method as ControllerPermissionSpecification['targetKey']; } const wildCardMethodsWithoutWildCard: Record = {}; @@ -1268,7 +1277,7 @@ export class PermissionController< } if (wildCardMethodsWithoutWildCard[targetKey]) { - return `${targetKey}*` as PermissionSpecification['targetKey']; + return `${targetKey}*` as ControllerPermissionSpecification['targetKey']; } return undefined; @@ -1299,7 +1308,10 @@ export class PermissionController< preserveExistingPermissions = true, subject, }: GrantPermissionsOptions): SubjectPermissions< - ExtractPermission + ExtractPermission< + ControllerPermissionSpecification, + ControllerCaveatSpecification + > > { const { origin } = subject; @@ -1308,12 +1320,18 @@ export class PermissionController< } const permissions: SubjectPermissions< - ExtractPermission + ExtractPermission< + ControllerPermissionSpecification, + ControllerCaveatSpecification + > > = ((preserveExistingPermissions && { ...this.getPermissions(origin), }) || {}) as SubjectPermissions< - ExtractPermission + ExtractPermission< + ControllerPermissionSpecification, + ControllerCaveatSpecification + > >; for (const [requestedTarget, approvedPermission] of Object.entries( @@ -1321,8 +1339,8 @@ export class PermissionController< )) { const targetKey = this.getTargetKey( requestedTarget as ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >['parentCapability'], ); if (!targetKey) { @@ -1343,8 +1361,8 @@ export class PermissionController< // The requested target must be a valid target name if we found its key. // We reassign it to change its type. const targetName = requestedTarget as ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >['parentCapability']; const specification = this.getPermissionSpecification(targetKey); @@ -1355,7 +1373,10 @@ export class PermissionController< ); const permissionOptions: PermissionOptions< - ExtractPermission + ExtractPermission< + ControllerPermissionSpecification, + ControllerCaveatSpecification + > > = { caveats, invoker: origin, @@ -1366,7 +1387,10 @@ export class PermissionController< specification.factory ? specification.factory(permissionOptions, requestData) : constructPermission(permissionOptions) - ) as ExtractPermission; + ) as ExtractPermission< + ControllerPermissionSpecification, + ControllerCaveatSpecification + >; this.validatePermission(specification, permission, origin, targetName); permissions[targetName] = permission; } @@ -1388,12 +1412,12 @@ export class PermissionController< * @param targetName - The target name of the permission. */ private validatePermission( - specification: PermissionSpecificationBase, + specification: PermissionSpecificationConstraint, permission: GenericPermission, origin: OriginString, targetName: ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >['parentCapability'], ): void { const { allowedCaveats, validator } = specification; @@ -1424,15 +1448,18 @@ export class PermissionController< origin: OriginString, permissions: Record< string, - ExtractPermission + ExtractPermission< + ControllerPermissionSpecification, + ControllerCaveatSpecification + > >, ): void { this.update((draftState) => { if (!draftState.subjects[origin]) { - // Typecast: immer's WritableDraft is incompatible with our generics draftState.subjects[origin] = { origin, permissions: {} }; } + // Typecast: Immer WritableDraft incompatibility draftState.subjects[origin].permissions = permissions as any; }); } @@ -1452,16 +1479,18 @@ export class PermissionController< private constructCaveats( origin: OriginString, target: ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >['parentCapability'], requestedCaveats?: unknown[] | null, - ): NonEmptyArray> | undefined { + ): NonEmptyArray> | undefined { const caveatArray = requestedCaveats?.map((requestedCaveat) => this.constructCaveat(origin, target, requestedCaveat), ); return caveatArray && caveatArray.length > 0 - ? (caveatArray as NonEmptyArray>) + ? (caveatArray as NonEmptyArray< + ExtractCaveats + >) : undefined; } @@ -1483,11 +1512,11 @@ export class PermissionController< private constructCaveat( origin: OriginString, target: ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >['parentCapability'], requestedCaveat: unknown, - ): ExtractCaveats { + ): ExtractCaveats { if (!isPlainObject(requestedCaveat)) { throw new InvalidCaveatError(requestedCaveat, origin, target); } @@ -1510,7 +1539,7 @@ export class PermissionController< (requestedCaveat.value as Json) ?? null, ); this.validateCaveat(caveat, origin, target); - return caveat as ExtractCaveats; + return caveat as ExtractCaveats; } /** @@ -1523,7 +1552,7 @@ export class PermissionController< * @param target - The target name associated with the parent permission. */ private validateCaveat( - caveat: GenericCaveat, + caveat: CaveatConstraint, origin: OriginString, target: string, ): void { @@ -1562,7 +1591,10 @@ export class PermissionController< [ Record< string, - ExtractPermission + ExtractPermission< + ControllerPermissionSpecification, + ControllerCaveatSpecification + > >, { id: string; origin: OriginString }, ] @@ -1655,8 +1687,8 @@ export class PermissionController< for (const methodName of Object.keys(perms)) { const target = ( perms[methodName] as unknown as ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification > ).parentCapability; if (target !== undefined && methodName !== target) { @@ -1733,12 +1765,9 @@ export class PermissionController< * @returns Whether the specified request exists. */ private hasApprovalRequest(options: { id: string }): boolean { - // Typecast: There are only some valid parameter combinations, but we - // just won't worry about that here. return this.messagingSystem.call( 'ApprovalController:hasRequest', - // Typecast: Passing just an id is definitely valid, so I don't know - // what's going on here. + // Typecast: Passing just an id is definitely valid, so we override it. options as any, ); } @@ -1788,8 +1817,8 @@ export class PermissionController< async executeRestrictedMethod( origin: OriginString, methodName: ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >['parentCapability'], params?: RestrictedMethodParameters, ): Promise { @@ -1829,17 +1858,14 @@ export class PermissionController< * @returns */ private _executeRestrictedMethod( - methodImplementation: RestrictedMethodBase< - RestrictedMethodParameters, - Json - >, + methodImplementation: RestrictedMethod, subject: PermissionSubjectMetadata, method: ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >['parentCapability'], params: RestrictedMethodParameters = [], - ): ReturnType> { + ): ReturnType> { const { origin } = subject; const permission = this.getPermission(origin, method); @@ -1847,7 +1873,7 @@ export class PermissionController< throw unauthorized({ data: { origin, method } }); } - return decorateWithCaveats( + return decorateWithCaveats( methodImplementation, permission, this._caveatSpecifications, @@ -1868,11 +1894,11 @@ export class PermissionController< private _getRestrictedMethod( method: string, origin: OriginString, - ): RestrictedMethodBase { + ): RestrictedMethod { const methodImplementation = this.getRestrictedMethod( method as ExtractPermission< - PermissionSpecification, - CaveatSpecification + ControllerPermissionSpecification, + ControllerCaveatSpecification >['parentCapability'], ); if (!methodImplementation) { diff --git a/packages/controllers/src/permissions/permission-middleware.ts b/packages/controllers/src/permissions/permission-middleware.ts index 5882fe463f..bba4016795 100644 --- a/packages/controllers/src/permissions/permission-middleware.ts +++ b/packages/controllers/src/permissions/permission-middleware.ts @@ -10,20 +10,24 @@ import { internalError } from './errors'; import { GenericPermission, PermissionSubjectMetadata, - RestrictedMethodBase, + RestrictedMethod, RestrictedMethodParameters, } from '.'; +/** + * TODO:docs + */ + type ExecuteRestrictedMethod = ( - methodImplementation: RestrictedMethodBase, + methodImplementation: RestrictedMethod, subject: PermissionSubjectMetadata, method: Permission['parentCapability'], params?: RestrictedMethodParameters, -) => ReturnType>; +) => ReturnType>; type GetRestrictedMethod = ( method: string, origin: string, -) => RestrictedMethodBase; +) => RestrictedMethod; type IsUnrestrictedMethod = (method: string) => boolean; type PermissionMiddlewareFactoryOptions = diff --git a/packages/controllers/src/permissions/utils.ts b/packages/controllers/src/permissions/utils.ts index 994009862b..8441d2d260 100644 --- a/packages/controllers/src/permissions/utils.ts +++ b/packages/controllers/src/permissions/utils.ts @@ -1,7 +1,10 @@ -import { CaveatSpecificationBase, CaveatSpecificationsMap } from './Caveat'; import { - PermissionSpecificationBase, - PermissionSpecificationsMap, + CaveatSpecificationConstraint, + CaveatSpecificationMap, +} from './Caveat'; +import { + PermissionSpecificationConstraint, + PermissionSpecificationMap, } from './Permission'; export enum MethodNames { @@ -10,17 +13,15 @@ export enum MethodNames { } /** - * Utility type for extracting a union of all individual - * {@link CaveatSpecificationBase} or - * {@link PermissionSpecificationBase} types - * from a {@link CaveatSpecificationsMap} or - * {@link PermissionSpecificationsMap}. + * Utility type for extracting a union of all individual caveat or permission + * specification types from a {@link CaveatSpecificationMap} or + * {@link PermissionSpecificationMap}. * * @template SpecificationsMap - The caveat or permission specifications map * whose specification type union to extract. */ export type ExtractSpecifications< SpecificationsMap extends - | CaveatSpecificationsMap> - | PermissionSpecificationsMap>, + | CaveatSpecificationMap + | PermissionSpecificationMap, > = SpecificationsMap[keyof SpecificationsMap]; From 6fed1c891df0eea1fc9f8dd327ccc81af99db426 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sun, 17 Oct 2021 23:51:57 -0700 Subject: [PATCH 097/147] fixup! Simplify types --- packages/controllers/src/permissions/PermissionController.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index b53f839ada..2709dcd169 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -1767,7 +1767,9 @@ export class PermissionController< private hasApprovalRequest(options: { id: string }): boolean { return this.messagingSystem.call( 'ApprovalController:hasRequest', - // Typecast: Passing just an id is definitely valid, so we override it. + // Typecast: For some reason, the type here expects all of the possible + // HasApprovalRequest options to be specified, when they're actually all + // optional. Passing just the id is definitely valid, so we just cast it. options as any, ); } From ea899da622d8bb0d803aa22eea6440642154af8b Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 18 Oct 2021 00:01:36 -0700 Subject: [PATCH 098/147] More documentation and type fixup --- .../controllers/src/permissions/Permission.ts | 23 +++++++++++-------- .../src/permissions/PermissionController.ts | 5 +++- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 6f79d421a0..4cfc610e05 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -60,6 +60,8 @@ export type PermissionConstraint = { }; /** + * TODO:docs + * * @template TargetKey - They key of the permission target that the permission * corresponds to. * @template AllowedCaveat - A union of the allowed {@link Caveat} types @@ -420,19 +422,20 @@ export type PermissionSpecificationConstraint = { }; /** - * Constraint for {@link PermissionSpecificationConstraint} objects ensuring th - * that permission specifications are - * given target key must be valid. + * Constraint for {@link PermissionSpecificationConstraint} objects that + * evaluates to `never` if the specification contains any invalid fields. * - * @template T - The key of the permission target. + * @template Specification - The permission specification to validate. */ -export type ValidatePermissionSpecification = - T extends PermissionSpecificationConstraint - ? T['targetKey'] extends ValidateTargetKey - ? T['methodImplementation'] extends ValidateRestrictedMethod< - T['methodImplementation'] +export type ValidatePermissionSpecification = + Specification extends PermissionSpecificationConstraint + ? Specification['targetKey'] extends ValidateTargetKey< + Specification['targetKey'] + > + ? Specification['methodImplementation'] extends ValidateRestrictedMethod< + Specification['methodImplementation'] > - ? T + ? Specification : never : never : never; diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 2709dcd169..a6191352ff 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -232,6 +232,9 @@ export type PermissionControllerMessenger = RestrictedControllerMessenger< never >; +/** + * A generic {@link PermissionController}. + */ export type GenericPermissionController = PermissionController< PermissionSpecificationConstraint, CaveatSpecificationConstraint @@ -274,7 +277,7 @@ export type CaveatMutator = ( | [CaveatMutatorOperation.updateValue, CaveatConstraint['value']]; /** - * Extracts the permission type or types from the specified permission and + * Extracts the constrained permission type(s) from the specified permission and * caveat specifications. * * @template ControllerPermissionSpecification - The permission specification(s) to From 4a1dd8302cd29907a236466459e10b8a198658cd Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 18 Oct 2021 00:06:20 -0700 Subject: [PATCH 099/147] Fix parameter names of ExtractArrayMembers --- packages/controllers/src/permissions/Permission.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 4cfc610e05..29706fcd95 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -166,10 +166,12 @@ export type ExtractPermissionTargetKey< * evalutes to `never` if the specified type is the empty tuple or neither * an array nor a tuple. * - * @template T - The array type whose members to extract. + * @template ArrayType - The array type whose members to extract. */ -type ExtractArrayMembers = T extends readonly [...infer U] | [...infer U] - ? U[number] +type ExtractArrayMembers = ArrayType extends + | readonly [...infer ArrayMembers] + | [...infer ArrayMembers] + ? ArrayMembers[number] : never; /** From 1d90813ee3868439cc0ab5ccacc4d00f28494287 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 18 Oct 2021 15:19:59 -0700 Subject: [PATCH 100/147] Fix the ExtractPermission docstring, take II --- .../src/permissions/PermissionController.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index a6191352ff..907c7660ca 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -277,14 +277,14 @@ export type CaveatMutator = ( | [CaveatMutatorOperation.updateValue, CaveatConstraint['value']]; /** - * Extracts the constrained permission type(s) from the specified permission and - * caveat specifications. + * Extracts the permission(s) specified by the given permission and caveat + * specifications. * - * @template ControllerPermissionSpecification - The permission specification(s) to - * extract a permission type union from. - * @template ControllerCaveatSpecification - The caveat specification(s) to extract a - * permission type union from. Necessary because {@link Permission} - * has a generic that describes the allowed caveats for the permission. + * @template ControllerPermissionSpecification - The permission specification(s) + * to extract from. + * @template ControllerCaveatSpecification - The caveat specification(s) to + * extract from. Necessary because {@link Permission} has a generic parameter + * that describes the allowed caveats for the permission. */ export type ExtractPermission< ControllerPermissionSpecification extends PermissionSpecificationConstraint, From 8fa50a5ab9a6020794e23399b593c31b0d474e4b Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 18 Oct 2021 15:38:00 -0700 Subject: [PATCH 101/147] Fix readme --- .../controllers/src/permissions/README.md | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/packages/controllers/src/permissions/README.md b/packages/controllers/src/permissions/README.md index 281f19ddfa..f4f989d0d6 100644 --- a/packages/controllers/src/permissions/README.md +++ b/packages/controllers/src/permissions/README.md @@ -70,18 +70,23 @@ const caveatSpecifications = { // If a permission has any caveats, its corresponding restricted method // implementation is decorated / wrapped with the implementations of its // caveats, using the caveat's decorator function. - decorator: (( + decorator: + ( // Restricted methods and other caveats can be async, so we have to // assume that the method is async. - method: AsyncRestrictedMethod, - caveat: { type: 'filterArrayResponse'; value: string[] }, + method: AsyncRestrictedMethod, + caveat: FilterArrayCaveat, ) => async (args: RestrictedMethodOptions) => { - const result = await method(args); - return Array.isArray(result) - ? result.filter(caveat.value.includes) - : result; - }) as CaveatDecorator, + const result: string[] | unknown = await method(args); + if (!Array.isArray(result)) { + throw Error('not an array'); + } + + return result.filter((resultValue) => + caveat.value.includes(resultValue), + ); + }, }, }; @@ -153,35 +158,34 @@ const existingPermissions = await ethereum.request({ ### Caveat Decorators +Here follows some more example caveat decorator implementations. + ```typescript // Validation / passthrough export function onlyArrayParams( - next: MethodImplementation, + method: AsyncRestrictedMethod, _caveat: Caveat, ) { - return async ( - req: JsonRpcRequest, - context: Record, - ) => { - if (!Array.isArray(req.params)) { + return async (args: RestrictedMethodOptions) => { + if (!Array.isArray(args.params)) { throw new EthereumJsonRpcError(); } - return next(req, context); + return method(args); }; } // "Return handler" example export function eth_accounts( - next: MethodImplementation, + method: AsyncRestrictedMethod, caveat: Caveat, ) { - return async ( - req: JsonRpcRequest, - context: Record, - ) => { - const accounts = await next(req, context); - return accounts.filter((account: string) => caveat.value.includes(account)); + return async (args: RestrictedMethodOptions) => { + const accounts: string[] | unknown = await method(args); + return ( + accounts?.filter((account: string) => caveat.value.includes(account)) ?? + [] + ); }; } ``` From b1105f98991bdf5254c87ae0a4cc0f1345a1b743 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 25 Oct 2021 21:52:50 -0700 Subject: [PATCH 102/147] Replace GenericFunction with something more specific --- packages/controllers/src/permissions/Permission.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 29706fcd95..770d895609 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -377,11 +377,6 @@ type ValidateTargetKey = Key extends `${string}_*` ? never : Key; -/** - * A type extended by any function. - */ -type GenericFunction = (...args: any[]) => any; - export type PermissionSpecificationConstraint = { /** * The target resource of the permission. In other words, at the time of @@ -399,7 +394,7 @@ export type PermissionSpecificationConstraint = { * The implementation of the restricted method that the permission * corresponds to. */ - methodImplementation: GenericFunction; + methodImplementation: RestrictedMethod; /** * The factory function used to get permission objects. Permissions returned From c3cbe8dffe424fa3489ad70f64597ee96b5e2cea Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 25 Oct 2021 21:54:16 -0700 Subject: [PATCH 103/147] Remove GenericPermission, use PermissionConstraint --- .../src/permissions/Caveat.test.ts | 6 +-- .../controllers/src/permissions/Caveat.ts | 4 +- .../controllers/src/permissions/Permission.ts | 38 +++++++++---------- .../permissions/PermissionController.test.ts | 8 ++-- .../src/permissions/PermissionController.ts | 34 ++++++++--------- .../src/permissions/permission-middleware.ts | 19 +++++----- .../permissions/rpc-methods/getPermissions.ts | 8 ++-- .../rpc-methods/requestPermissions.ts | 8 ++-- 8 files changed, 60 insertions(+), 65 deletions(-) diff --git a/packages/controllers/src/permissions/Caveat.test.ts b/packages/controllers/src/permissions/Caveat.test.ts index d27586e767..7b5a44c237 100644 --- a/packages/controllers/src/permissions/Caveat.test.ts +++ b/packages/controllers/src/permissions/Caveat.test.ts @@ -3,7 +3,7 @@ import { CaveatSpecification, CaveatSpecificationMap, decorateWithCaveats, - GenericPermission, + PermissionConstraint, } from '.'; // This function is mostly tested through the PermissionController tests, @@ -21,7 +21,7 @@ describe('decorateWithCaveats', () => { const permission = { caveats: [{ type: 'reverse', value: null }], - } as unknown as GenericPermission; + } as unknown as PermissionConstraint; expect(methodImplementation()).toStrictEqual([1, 2, 3]); expect( @@ -46,7 +46,7 @@ describe('decorateWithCaveats', () => { const permission = { // This type doesn't exist caveats: [{ type: 'kaplar', value: null }], - } as unknown as GenericPermission; + } as unknown as PermissionConstraint; expect(() => decorateWithCaveats( diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index 3afea28552..f57ca66158 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -4,7 +4,7 @@ import { UnrecognizedCaveatTypeError } from './errors'; import { AsyncRestrictedMethod, RestrictedMethod, - GenericPermission, + PermissionConstraint, RestrictedMethodParameters, } from './Permission'; @@ -227,7 +227,7 @@ export function decorateWithCaveats< CaveatSpecifications extends CaveatSpecification, >( methodImplementation: RestrictedMethod, - permission: Readonly, // bound to the requesting origin + permission: Readonly, // bound to the requesting origin caveatSpecifications: CaveatSpecificationMap, // all caveat implementations ): RestrictedMethod { const { caveats } = permission; diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 770d895609..6397b2ea16 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -124,15 +124,10 @@ export type ExtractPermissionTargetNames = */ /** - * A generic permission. - */ -export type GenericPermission = Permission; - -/** - * A {@link GenericPermission} with mutable caveats. + * A {@link PermissionConstraint} with mutable caveats. */ export type MutableGenericPermission = Mutable< - GenericPermission & { caveats: NonEmptyArray | null }, + PermissionConstraint & { caveats: NonEmptyArray | null }, 'caveats' >; @@ -200,7 +195,7 @@ export type ExtractAllowedCaveatTypes< * * @template TargetPermission - The {@link Permission} that will be constructed. */ -export type PermissionOptions = { +export type PermissionOptions = { target: TargetPermission['parentCapability']; /** * The origin string of the subject that has the permission. @@ -232,9 +227,9 @@ export type PermissionOptions = { * @param options - The options for the permission. * @returns The new permission object. */ -export function constructPermission( - options: PermissionOptions, -): GenericPermission { +export function constructPermission< + TargetPermission extends PermissionConstraint, +>(options: PermissionOptions): PermissionConstraint { const { caveats = null, id, invoker, target } = options; return { @@ -255,7 +250,7 @@ export function constructPermission( * @returns The caveat, or undefined if no such caveat exists. */ export function findCaveat( - permission: GenericPermission, + permission: PermissionConstraint, caveatType: string, ): CaveatConstraint | undefined { return permission.caveats?.find((caveat) => caveat.type === caveatType); @@ -263,9 +258,9 @@ export function findCaveat( /** * A requested permission object. Just an object with any of the properties - * of a {@link GenericPermission} object. + * of a {@link PermissionConstraint} object. */ -type RequestedPermission = Partial; +type RequestedPermission = Partial; /** * A record of target names and their {@link RequestedPermission} objects. @@ -348,18 +343,19 @@ export type ValidateRestrictedMethod< : never; export type PermissionFactory< - TargetPermission extends GenericPermission, + TargetPermission extends PermissionConstraint, RequestData extends Record, > = ( options: PermissionOptions, requestData?: RequestData, ) => TargetPermission; -export type PermissionValidator = ( - permission: GenericPermission, - origin?: OriginString, - target?: TargetPermission['parentCapability'], -) => void; +export type PermissionValidator = + ( + permission: PermissionConstraint, + origin?: OriginString, + target?: TargetPermission['parentCapability'], + ) => void; /** * A utility type for ensuring that the given permission target key conforms to @@ -415,7 +411,7 @@ export type PermissionSpecificationConstraint = { * * The validator should throw an appropriate JSON-RPC error if validation fails. */ - validator?: PermissionValidator; + validator?: PermissionValidator; }; /** diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index c769c29f96..2b3fb3cd0f 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -16,7 +16,7 @@ import { constructCaveat, constructPermission, CaveatConstraint, - GenericPermission, + PermissionConstraint, MethodNames, Permission, PermissionController, @@ -230,7 +230,7 @@ function getDefaultPermissionSpecifications() { methodImplementation: (_args: RestrictedMethodOptions) => { return { a: 'x', b: 'y', c: 'z' }; }, - validator: (permission: GenericPermission) => { + validator: (permission: PermissionConstraint) => { // A dummy validator for a caveat type that should be impossible to add assert.ok( !permission.caveats?.some( @@ -254,7 +254,7 @@ function getDefaultPermissionSpecifications() { ...options, caveats: [constructCaveat(CaveatTypes.noopCaveat, null)], }) as SecretNamespacedPermission, - validator: (permission: GenericPermission) => { + validator: (permission: PermissionConstraint) => { assert.deepStrictEqual( permission.caveats, [constructCaveat(CaveatTypes.noopCaveat, null)], @@ -286,7 +286,7 @@ function getDefaultPermissionSpecifications() { return null; }, allowedCaveats: [CaveatTypes.noopCaveat, CaveatTypes.filterArrayResponse], - validator: (permission: GenericPermission) => { + validator: (permission: PermissionConstraint) => { if ( permission.caveats?.some( ({ type }) => type !== CaveatTypes.noopCaveat, diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 907c7660ca..ed33ad8f01 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -32,7 +32,7 @@ import { import { constructPermission, findCaveat, - GenericPermission, + PermissionConstraint, OriginString, PermissionOptions, RequestedPermissions, @@ -105,17 +105,15 @@ const controllerName = 'PermissionController'; /** * Permissions associated with a {@link PermissionController} subject. */ -export type SubjectPermissions = Record< - Permissions['parentCapability'], - Permissions ->; +export type SubjectPermissions = + Record; /** * Permissions and metadata associated with a {@link PermissionController} * subject. */ export type PermissionSubjectEntry< - SubjectPermission extends GenericPermission, + SubjectPermission extends PermissionConstraint, > = { permissions: SubjectPermissions; } & PermissionSubjectMetadata; @@ -124,7 +122,7 @@ export type PermissionSubjectEntry< * All subjects of a {@link PermissionController}. */ export type PermissionControllerSubjects< - SubjectPermission extends GenericPermission, + SubjectPermission extends PermissionConstraint, > = Record>; // TODO:TS4.4 Enable compiler flags to forbid unchecked member access @@ -132,7 +130,7 @@ export type PermissionControllerSubjects< * The state of a {@link PermissionController}. */ export type PermissionControllerState = - Permissions extends GenericPermission + Permissions extends PermissionConstraint ? { subjects: PermissionControllerSubjects; } @@ -148,7 +146,7 @@ const stateMetadata: StateMetadata> = { /** * The default state of the {@link PermissionController}. */ -const defaultState: PermissionControllerState = { +const defaultState: PermissionControllerState = { subjects: {}, }; @@ -157,7 +155,7 @@ const defaultState: PermissionControllerState = { */ export type GetPermissionControllerState = { type: `${typeof controllerName}:getState`; - handler: () => PermissionControllerState; + handler: () => PermissionControllerState; }; /** @@ -165,7 +163,7 @@ export type GetPermissionControllerState = { */ export type GetSubjects = { type: `${typeof controllerName}:getSubjectNames`; - handler: () => (keyof PermissionControllerSubjects)[]; + handler: () => (keyof PermissionControllerSubjects)[]; }; /** @@ -199,7 +197,7 @@ export type PermissionControllerActions = */ export type PermissionControllerStateChange = { type: `${typeof controllerName}:stateChange`; - payload: [PermissionControllerState, Patch[]]; + payload: [PermissionControllerState, Patch[]]; }; /** @@ -711,7 +709,7 @@ export class PermissionController< // Typecast: Immer WritableDraft incompatibility this.deletePermission( - draftState.subjects as unknown as PermissionControllerSubjects, + draftState.subjects as unknown as PermissionControllerSubjects, origin, target, ); @@ -743,7 +741,7 @@ export class PermissionController< if (hasProperty(permissions as Record, target)) { this.deletePermission( // Typecast: Immer WritableDraft incompatibility - draftState.subjects as unknown as PermissionControllerSubjects, + draftState.subjects as unknown as PermissionControllerSubjects, origin, target, ); @@ -763,7 +761,7 @@ export class PermissionController< * @param target - The target name of the permission to delete. */ private deletePermission( - subjects: PermissionControllerSubjects, + subjects: PermissionControllerSubjects, origin: OriginString, target: ExtractPermission< ControllerPermissionSpecification, @@ -1096,7 +1094,7 @@ export class PermissionController< case CaveatMutatorOperation.revokePermission: // Typecast: Immer WritableDraft incompatibility this.deletePermission( - draftState.subjects as unknown as PermissionControllerSubjects, + draftState.subjects as unknown as PermissionControllerSubjects, subject.origin, permission.parentCapability, ); @@ -1214,7 +1212,7 @@ export class PermissionController< * @param targetName - The target name name of the permission. */ private validateModifiedPermission( - permission: GenericPermission, + permission: PermissionConstraint, origin: OriginString, targetName: ExtractPermission< ControllerPermissionSpecification, @@ -1416,7 +1414,7 @@ export class PermissionController< */ private validatePermission( specification: PermissionSpecificationConstraint, - permission: GenericPermission, + permission: PermissionConstraint, origin: OriginString, targetName: ExtractPermission< ControllerPermissionSpecification, diff --git a/packages/controllers/src/permissions/permission-middleware.ts b/packages/controllers/src/permissions/permission-middleware.ts index bba4016795..7af56b38fb 100644 --- a/packages/controllers/src/permissions/permission-middleware.ts +++ b/packages/controllers/src/permissions/permission-middleware.ts @@ -8,7 +8,7 @@ import { } from 'json-rpc-engine'; import { internalError } from './errors'; import { - GenericPermission, + PermissionConstraint, PermissionSubjectMetadata, RestrictedMethod, RestrictedMethodParameters, @@ -18,7 +18,7 @@ import { * TODO:docs */ -type ExecuteRestrictedMethod = ( +type ExecuteRestrictedMethod = ( methodImplementation: RestrictedMethod, subject: PermissionSubjectMetadata, method: Permission['parentCapability'], @@ -30,15 +30,16 @@ type GetRestrictedMethod = ( ) => RestrictedMethod; type IsUnrestrictedMethod = (method: string) => boolean; -type PermissionMiddlewareFactoryOptions = - { - executeRestrictedMethod: ExecuteRestrictedMethod; - getRestrictedMethod: GetRestrictedMethod; - isUnrestrictedMethod: IsUnrestrictedMethod; - }; +type PermissionMiddlewareFactoryOptions< + Permission extends PermissionConstraint, +> = { + executeRestrictedMethod: ExecuteRestrictedMethod; + getRestrictedMethod: GetRestrictedMethod; + isUnrestrictedMethod: IsUnrestrictedMethod; +}; export function getPermissionMiddlewareFactory< - Permission extends GenericPermission, + Permission extends PermissionConstraint, >({ executeRestrictedMethod, getRestrictedMethod, diff --git a/packages/controllers/src/permissions/rpc-methods/getPermissions.ts b/packages/controllers/src/permissions/rpc-methods/getPermissions.ts index dcf5ec2321..f57612d5d4 100644 --- a/packages/controllers/src/permissions/rpc-methods/getPermissions.ts +++ b/packages/controllers/src/permissions/rpc-methods/getPermissions.ts @@ -5,13 +5,13 @@ import type { } from 'json-rpc-engine'; import { MethodNames } from '../utils'; -import type { GenericPermission } from '../Permission'; +import type { PermissionConstraint } from '../Permission'; import type { SubjectPermissions } from '../PermissionController'; export const getPermissionsHandler: PermittedHandlerExport< GetPermissionsHooks, void, - GenericPermission[] + PermissionConstraint[] > = { methodNames: [MethodNames.getPermissions], implementation: getPermissionsImplementation, @@ -22,12 +22,12 @@ export const getPermissionsHandler: PermittedHandlerExport< }; export type GetPermissionsHooks = { - getPermissions: () => SubjectPermissions; + getPermissions: () => SubjectPermissions; }; async function getPermissionsImplementation( _req: unknown, - res: PendingJsonRpcResponse, + res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, { getPermissions }: GetPermissionsHooks, diff --git a/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts b/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts index 77f524e1f3..26a5cdc658 100644 --- a/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts +++ b/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts @@ -8,12 +8,12 @@ import { nanoid } from 'nanoid'; import { MethodNames } from '../utils'; import { invalidParams } from '../errors'; -import type { GenericPermission, RequestedPermissions } from '../Permission'; +import type { PermissionConstraint, RequestedPermissions } from '../Permission'; export const requestPermissionsHandler: PermittedHandlerExport< RequestPermissionsHooks, [RequestedPermissions], - GenericPermission[] + PermissionConstraint[] > = { methodNames: [MethodNames.requestPermissions], implementation: requestPermissionsImplementation, @@ -27,7 +27,7 @@ type RequestPermissions = ( requestedPermissions: RequestedPermissions, id: string, ) => Promise< - [Record, { id: string; origin: string }] + [Record, { id: string; origin: string }] >; export type RequestPermissionsHooks = { @@ -36,7 +36,7 @@ export type RequestPermissionsHooks = { async function requestPermissionsImplementation( req: JsonRpcRequest<[RequestedPermissions]>, - res: PendingJsonRpcResponse, + res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, { requestPermissions }: RequestPermissionsHooks, From fa86d3033c5c79b38af16b5e1da17540cf200cc1 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 25 Oct 2021 22:07:01 -0700 Subject: [PATCH 104/147] Fixup --- packages/controllers/src/permissions/Caveat.ts | 2 +- packages/controllers/src/permissions/Permission.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index f57ca66158..8f46a52c07 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -166,7 +166,7 @@ export type CaveatSpecification = { * TODO:docs */ export type CaveatSpecificationMap< - Specification extends CaveatSpecification, + Specification extends CaveatSpecificationConstraint, > = { [Key in Specification['type']]: Specification extends CaveatSpecification ? Specification diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 6397b2ea16..6768ebda1b 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -443,7 +443,7 @@ export type ValidatePermissionSpecification = export type PermissionSpecificationMap< Specification extends PermissionSpecificationConstraint, > = { - [TargetKey in Specification['targetKey']]: Specification extends PermissionSpecificationConstraint & { + [TargetKey in Specification['targetKey']]: Specification extends { targetKey: TargetKey; } ? Specification @@ -460,7 +460,7 @@ export type PermissionSpecificationMap< export type ExtractPermissionSpecification< Specification extends PermissionSpecificationConstraint, TargetKey extends Specification['targetKey'], -> = Specification extends PermissionSpecificationConstraint & { +> = Specification extends { targetKey: TargetKey; } ? Specification From b14367f81e378912360bab6c6e0c14fc42bd7662 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 25 Oct 2021 22:34:01 -0700 Subject: [PATCH 105/147] More type fixup --- .../controllers/src/permissions/Permission.ts | 23 +++++++++---------- .../src/permissions/PermissionController.ts | 9 ++++---- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 6768ebda1b..557fa53775 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -420,18 +420,17 @@ export type PermissionSpecificationConstraint = { * * @template Specification - The permission specification to validate. */ -export type ValidatePermissionSpecification = - Specification extends PermissionSpecificationConstraint - ? Specification['targetKey'] extends ValidateTargetKey< - Specification['targetKey'] - > - ? Specification['methodImplementation'] extends ValidateRestrictedMethod< - Specification['methodImplementation'] - > - ? Specification - : never - : never - : never; +export type ValidPermissionSpecification< + Specification extends PermissionSpecificationConstraint, +> = Specification['targetKey'] extends ValidateTargetKey< + Specification['targetKey'] +> + ? Specification['methodImplementation'] extends ValidateRestrictedMethod< + Specification['methodImplementation'] + > + ? Specification + : never + : never; /** * The specifications for all permissions and restricted methods supported by diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index ed33ad8f01..f6210337e0 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -38,7 +38,7 @@ import { RequestedPermissions, RestrictedMethod, RestrictedMethodParameters, - ValidatePermissionSpecification, + ValidPermissionSpecification, PermissionSpecificationMap, Permission, ExtractAllowedCaveatTypes, @@ -115,8 +115,9 @@ export type SubjectPermissions = export type PermissionSubjectEntry< SubjectPermission extends PermissionConstraint, > = { + origin: SubjectPermission['invoker']; permissions: SubjectPermissions; -} & PermissionSubjectMetadata; +}; /** * All subjects of a {@link PermissionController}. @@ -287,7 +288,7 @@ export type CaveatMutator = ( export type ExtractPermission< ControllerPermissionSpecification extends PermissionSpecificationConstraint, ControllerCaveatSpecification extends CaveatSpecificationConstraint, -> = ControllerPermissionSpecification extends ValidatePermissionSpecification +> = ControllerPermissionSpecification extends ValidPermissionSpecification ? Permission< ControllerPermissionSpecification['targetKey'], ExtractCaveats @@ -306,7 +307,7 @@ export type ExtractPermission< export type PermissionControllerOptions< ControllerPermissionSpecification extends PermissionSpecificationConstraint, ControllerCaveatSpecification extends CaveatSpecificationConstraint, -> = ValidatePermissionSpecification extends never +> = ValidPermissionSpecification extends never ? never : { messenger: PermissionControllerMessenger; From 8603b41017e1cd8d1b44bfd7a031e8b85905ab40 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 25 Oct 2021 22:42:24 -0700 Subject: [PATCH 106/147] Rename more types from Validate* to Valid* --- packages/controllers/src/permissions/Permission.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 557fa53775..f5d4e966b6 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -70,7 +70,7 @@ export type PermissionConstraint = { export type Permission< TargetKey extends GenericTargetName, AllowedCaveat extends CaveatConstraint, -> = ValidateTargetName> extends never +> = ValidTargetName> extends never ? never : PermissionConstraint & { // TODO:TS4.4 Make optional @@ -97,7 +97,7 @@ export type Permission< * * See the documentation for the distinction between target names and keys. */ -type ValidateTargetName = Name extends `${string}*` +type ValidTargetName = Name extends `${string}*` ? never : Name extends `${string}_` ? never @@ -334,7 +334,7 @@ export type GenericRestrictedMethod = RestrictedMethod< Json >; -export type ValidateRestrictedMethod< +export type ValidRestrictedMethod< MethodImplementation extends RestrictedMethod, > = MethodImplementation extends (args: infer Options) => Json | Promise ? Options extends RestrictedMethodOptions @@ -365,7 +365,7 @@ export type PermissionValidator = * * @template Key - The target key string to apply the constraint to. */ -type ValidateTargetKey = Key extends `${string}_*` +type ValidTargetKey = Key extends `${string}_*` ? Key : Key extends `${string}_` ? never @@ -422,10 +422,10 @@ export type PermissionSpecificationConstraint = { */ export type ValidPermissionSpecification< Specification extends PermissionSpecificationConstraint, -> = Specification['targetKey'] extends ValidateTargetKey< +> = Specification['targetKey'] extends ValidTargetKey< Specification['targetKey'] > - ? Specification['methodImplementation'] extends ValidateRestrictedMethod< + ? Specification['methodImplementation'] extends ValidRestrictedMethod< Specification['methodImplementation'] > ? Specification From 9204cfc336f2711d7bb217ad71759c0d55fc230c Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 25 Oct 2021 23:14:57 -0700 Subject: [PATCH 107/147] Get rid of PermissionValidator generic --- packages/controllers/src/permissions/Permission.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index f5d4e966b6..f976e15523 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -350,12 +350,11 @@ export type PermissionFactory< requestData?: RequestData, ) => TargetPermission; -export type PermissionValidator = - ( - permission: PermissionConstraint, - origin?: OriginString, - target?: TargetPermission['parentCapability'], - ) => void; +export type PermissionValidatorConstraint = ( + permission: PermissionConstraint, + origin?: OriginString, + target?: string, +) => void; /** * A utility type for ensuring that the given permission target key conforms to @@ -411,7 +410,7 @@ export type PermissionSpecificationConstraint = { * * The validator should throw an appropriate JSON-RPC error if validation fails. */ - validator?: PermissionValidator; + validator?: PermissionValidatorConstraint; }; /** From 3d3cdf2d8ff7eca8cb4d85da31c8faa1f448915b Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Mon, 25 Oct 2021 23:35:53 -0700 Subject: [PATCH 108/147] Improve caveat specification type safety --- .../src/permissions/Caveat.test.ts | 6 ++-- .../controllers/src/permissions/Caveat.ts | 33 ++++++++++--------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/packages/controllers/src/permissions/Caveat.test.ts b/packages/controllers/src/permissions/Caveat.test.ts index 7b5a44c237..97943e59a8 100644 --- a/packages/controllers/src/permissions/Caveat.test.ts +++ b/packages/controllers/src/permissions/Caveat.test.ts @@ -1,6 +1,6 @@ import * as errors from './errors'; import { - CaveatSpecification, + CaveatSpecificationConstraint, CaveatSpecificationMap, decorateWithCaveats, PermissionConstraint, @@ -17,7 +17,7 @@ describe('decorateWithCaveats', () => { decorator: (method: any, _caveat: any) => () => method().reverse() as any, }, - } as unknown as CaveatSpecificationMap>; + } as unknown as CaveatSpecificationMap; const permission = { caveats: [{ type: 'reverse', value: null }], @@ -41,7 +41,7 @@ describe('decorateWithCaveats', () => { decorator: (method: any, _caveat: any) => () => method().reverse() as any, }, - } as unknown as CaveatSpecificationMap>; + } as unknown as CaveatSpecificationMap; const permission = { // This type doesn't exist diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index 8f46a52c07..b4f8cf7846 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -136,17 +136,17 @@ export type CaveatSpecificationConstraint = { /** * TODO:docs */ -export type CaveatSpecification = { +export type CaveatSpecification = { /** * The string type of the caveat. */ - type: Type; + type: SpecifiedCaveat['type']; /** * The decorator function used to apply the caveat to restricted method * requests. */ - decorator: CaveatDecorator; + decorator: CaveatDecorator; /** * The validator function used to validate caveats of the associated type @@ -159,7 +159,7 @@ export type CaveatSpecification = { * performed. Although caveats can also be validated by permission validators, * validating caveat values separately is strongly recommended. */ - validator?: CaveatValidator; + validator?: CaveatValidator; }; /** @@ -168,7 +168,9 @@ export type CaveatSpecification = { export type CaveatSpecificationMap< Specification extends CaveatSpecificationConstraint, > = { - [Key in Specification['type']]: Specification extends CaveatSpecification + [Key in Specification['type']]: Specification extends CaveatSpecification< + Caveat + > ? Specification : never; }; @@ -176,13 +178,14 @@ export type CaveatSpecificationMap< /** * TODO:docs */ -export type ExtractCaveats> = - Specification extends any - ? Caveat< - Specification['type'], - ExtractCaveatValueFromDecorator - > - : never; +export type ExtractCaveats< + Specification extends CaveatSpecification, +> = Specification extends any + ? Caveat< + Specification['type'], + ExtractCaveatValueFromDecorator + > + : never; /** * Internal utility type, because using parameterized types in conditional types @@ -201,7 +204,7 @@ type _ExtractCaveat< * @template CaveatType - The type of the caveat to extract. */ export type ExtractCaveat< - CaveatSpecifications extends CaveatSpecification, + CaveatSpecifications extends CaveatSpecification, CaveatType extends string, > = _ExtractCaveat, CaveatType>; @@ -213,7 +216,7 @@ export type ExtractCaveat< * @template CaveatType - The type of the caveat whose value to extract. */ export type ExtractCaveatValue< - CaveatSpecifications extends CaveatSpecification, + CaveatSpecifications extends CaveatSpecification, CaveatType extends string, > = ExtractCaveat['value']; @@ -224,7 +227,7 @@ export type ExtractCaveatValue< * decorator) must be awaited. */ export function decorateWithCaveats< - CaveatSpecifications extends CaveatSpecification, + CaveatSpecifications extends CaveatSpecification, >( methodImplementation: RestrictedMethod, permission: Readonly, // bound to the requesting origin From 20298a68113d89a701ff0fb353f6be1de053d105 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 26 Oct 2021 08:59:50 -0700 Subject: [PATCH 109/147] Remove leftover comment --- packages/controllers/src/permissions/Permission.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index f976e15523..b5cb072f82 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -114,15 +114,6 @@ type ValidTargetName = Name extends `${string}*` export type ExtractPermissionTargetNames = Key extends `${infer Base}_*` ? `${Base}_${string}` : Key; -/** - * The consumer-facing base permission type. - * - * @template TargetKey - They key of the permission target that the permission - * corresponds to. - * @template AllowedCaveats - A union of the allowed {@link Caveat} types - * for the permission. - */ - /** * A {@link PermissionConstraint} with mutable caveats. */ From 362973479add6d904f09d5e437bc81a8d3671a9a Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 26 Oct 2021 09:15:50 -0700 Subject: [PATCH 110/147] Replace state metadata and default state consts with getters --- .../src/permissions/PermissionController.ts | 48 ++++++++++++------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index f6210337e0..e039b7e1c2 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -138,18 +138,24 @@ export type PermissionControllerState = : never; /** - * The state metadata of the {@link PermissionController}. + * Get the state metadata of the {@link PermissionController}. + * + * @template Permissions - The controller's permission type union. */ -const stateMetadata: StateMetadata> = { - subjects: { persist: true, anonymous: true }, -}; +function getStateMetadata() { + return { subjects: { anonymous: true, persist: true } } as StateMetadata< + PermissionControllerState + >; +} /** - * The default state of the {@link PermissionController}. + * Get the default state of the {@link PermissionController}. + * + * @template Permissions - The controller's permission type union. */ -const defaultState: PermissionControllerState = { - subjects: {}, -}; +function getDefaultState() { + return { subjects: {} } as PermissionControllerState; +} /** * Gets the state of the {@link PermissionController}. @@ -412,15 +418,21 @@ export class PermissionController< // genericized. super({ name: controllerName, - metadata: stateMetadata as any, + metadata: + getStateMetadata< + ExtractPermission< + ControllerPermissionSpecification, + ControllerCaveatSpecification + > + >(), messenger, state: { - ...(defaultState as PermissionControllerState< + ...getDefaultState< ExtractPermission< ControllerPermissionSpecification, ControllerCaveatSpecification > - >), + >(), ...state, }, }); @@ -543,12 +555,14 @@ export class PermissionController< this.update((_draftState) => { // Typecast: The default state is declared outside of the controller // instance and therefore incorrectly genericized. - return { ...defaultState } as PermissionControllerState< - ExtractPermission< - ControllerPermissionSpecification, - ControllerCaveatSpecification - > - >; + return { + ...getDefaultState< + ExtractPermission< + ControllerPermissionSpecification, + ControllerCaveatSpecification + > + >(), + }; }); } From fca8ab562dd1a34e7c3347c9a40e5faa3f24abb9 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 26 Oct 2021 09:19:56 -0700 Subject: [PATCH 111/147] Simplify PermissionControllerOptions --- .../src/permissions/PermissionController.ts | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index e039b7e1c2..a2be1767cd 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -311,24 +311,22 @@ export type ExtractPermission< * caveat specifications available to the controller. */ export type PermissionControllerOptions< - ControllerPermissionSpecification extends PermissionSpecificationConstraint, + ControllerPermissionSpecification extends ValidPermissionSpecification, ControllerCaveatSpecification extends CaveatSpecificationConstraint, -> = ValidPermissionSpecification extends never - ? never - : { - messenger: PermissionControllerMessenger; - caveatSpecifications: CaveatSpecificationMap; - permissionSpecifications: PermissionSpecificationMap; - unrestrictedMethods: string[]; - state?: Partial< - PermissionControllerState< - ExtractPermission< - ControllerPermissionSpecification, - ControllerCaveatSpecification - > - > - >; - }; +> = { + messenger: PermissionControllerMessenger; + caveatSpecifications: CaveatSpecificationMap; + permissionSpecifications: PermissionSpecificationMap; + unrestrictedMethods: string[]; + state?: Partial< + PermissionControllerState< + ExtractPermission< + ControllerPermissionSpecification, + ControllerCaveatSpecification + > + > + >; +}; /** * The permission controller. See the documentation for details. From a06545e9edbede8709ec4c87c36018d143667059 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 26 Oct 2021 09:22:37 -0700 Subject: [PATCH 112/147] Improve PermissionControllerSubjects --- packages/controllers/src/permissions/PermissionController.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index a2be1767cd..611ea8852e 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -124,7 +124,10 @@ export type PermissionSubjectEntry< */ export type PermissionControllerSubjects< SubjectPermission extends PermissionConstraint, -> = Record>; +> = Record< + SubjectPermission['invoker'], + PermissionSubjectEntry +>; // TODO:TS4.4 Enable compiler flags to forbid unchecked member access /** From d42c1198eedea8e61175eb654520fbebf9a9e71c Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 28 Oct 2021 13:36:44 -0700 Subject: [PATCH 113/147] Update packages/controllers/src/permissions/PermissionController.ts Co-authored-by: Mark Stacey --- packages/controllers/src/permissions/PermissionController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 611ea8852e..6357bf82e5 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -1071,7 +1071,7 @@ export class PermissionController< const { caveats } = permission; const targetCaveat = caveats?.find( ({ type }) => type === targetCaveatType, - ) as TargetCaveat | undefined; + ); if (!targetCaveat) { return; } From b0b7997da8b63dc0a4fef00eaeb0a3ccda8b64fb Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Thu, 28 Oct 2021 14:28:18 -0700 Subject: [PATCH 114/147] Address review feedback --- .../controllers/src/permissions/Permission.ts | 10 +- .../permissions/PermissionController.test.ts | 90 +++++++++------ .../src/permissions/PermissionController.ts | 107 ++++++++++-------- 3 files changed, 116 insertions(+), 91 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index b5cb072f82..d0f8cf8c10 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -1,6 +1,6 @@ import { Json } from 'json-rpc-engine'; import { nanoid } from 'nanoid'; -import { Mutable, NonEmptyArray } from '../utils'; +import { NonEmptyArray } from '../utils'; import { CaveatConstraint } from './Caveat'; // This is used in a docstring, but ESLint doesn't notice it. /* eslint-disable @typescript-eslint/no-unused-vars */ @@ -114,14 +114,6 @@ type ValidTargetName = Name extends `${string}*` export type ExtractPermissionTargetNames = Key extends `${infer Base}_*` ? `${Base}_${string}` : Key; -/** - * A {@link PermissionConstraint} with mutable caveats. - */ -export type MutableGenericPermission = Mutable< - PermissionConstraint & { caveats: NonEmptyArray | null }, - 'caveats' ->; - /** * An internal utility type used in {@link ExtractPermissionTargetKey}. * diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 2b3fb3cd0f..2d828db394 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -27,6 +27,7 @@ import { RestrictedMethodParameters, ExtractSpecifications, CaveatMutatorOperation, + RestrictedMethod, } from '.'; // Caveat types and specifications @@ -568,28 +569,24 @@ describe('PermissionController', () => { }); describe('getRestrictedMethod', () => { - it('gets the implementation of a restricted method', () => { + it('gets the implementation of a restricted method', async () => { const controller = getDefaultPermissionController(); - expect( - controller.getRestrictedMethod(PermissionNames.wallet_getSecretArray), - ).toStrictEqual( - controller.getPermissionSpecification( - PermissionNames.wallet_getSecretArray, - ).methodImplementation, - ); + const method = controller.getRestrictedMethod( + PermissionNames.wallet_getSecretArray, + ) as RestrictedMethod; + + expect(await method({} as any)).toStrictEqual(['a', 'b', 'c']); }); - it('gets the implementation of a namespaced restricted method', () => { + it('gets the implementation of a namespaced restricted method', async () => { const controller = getDefaultPermissionController(); + const method = controller.getRestrictedMethod( + PermissionNames.wallet_getSecret_('foo'), + ) as RestrictedMethod; + expect( - controller.getRestrictedMethod( - PermissionNames.wallet_getSecret_('foo'), - ), - ).toStrictEqual( - controller.getPermissionSpecification( - PermissionKeys['wallet_getSecret_*'], - ).methodImplementation, - ); + await method({ method: 'wallet_getSecret_foo' } as any), + ).toStrictEqual('Hello, secret friend "foo"!'); }); it('returns undefined if the method does not exist', () => { @@ -1869,7 +1866,12 @@ describe('PermissionController', () => { // There are no caveats, so this does nothing. controller.updatePermissionsByCaveat( CaveatTypes.filterArrayResponse, - () => [CaveatMutatorOperation.updateValue, ['a', 'b']], + () => { + return { + operation: CaveatMutatorOperation.updateValue, + value: ['a', 'b'], + }; + }, ); expect(controller.state).toStrictEqual({ subjects: {} }); }); @@ -1881,7 +1883,9 @@ describe('PermissionController', () => { // therefore nothing happens. controller.updatePermissionsByCaveat( CaveatTypes.filterArrayResponse, - () => [CaveatMutatorOperation.noop], + () => { + return { operation: CaveatMutatorOperation.noop }; + }, ); expect(controller.state).toStrictEqual(getMultiCaveatStateMatcher()); }); @@ -1891,7 +1895,12 @@ describe('PermissionController', () => { controller.updatePermissionsByCaveat( CaveatTypes.filterArrayResponse, - () => [CaveatMutatorOperation.updateValue, ['a', 'b']], + () => { + return { + operation: CaveatMutatorOperation.updateValue, + value: ['a', 'b'], + }; + }, ); expect(controller.state).toStrictEqual( @@ -1924,8 +1933,11 @@ describe('PermissionController', () => { const mutator: any = () => { counter += 1; return counter === 1 - ? [CaveatMutatorOperation.noop] - : [CaveatMutatorOperation.updateValue, ['a', 'b']]; + ? { operation: CaveatMutatorOperation.noop } + : { + operation: CaveatMutatorOperation.updateValue, + value: ['a', 'b'], + }; }; controller.updatePermissionsByCaveat( @@ -1954,7 +1966,9 @@ describe('PermissionController', () => { controller.updatePermissionsByCaveat( CaveatTypes.filterArrayResponse, - () => [CaveatMutatorOperation.deleteCaveat], + () => { + return { operation: CaveatMutatorOperation.deleteCaveat }; + }, ); expect(controller.state).toStrictEqual( @@ -1982,7 +1996,9 @@ describe('PermissionController', () => { controller.updatePermissionsByCaveat( CaveatTypes.filterObjectResponse, - () => [CaveatMutatorOperation.revokePermission], + () => { + return { operation: CaveatMutatorOperation.revokePermission }; + }, ); const matcher = getMultiCaveatStateMatcher(); @@ -2003,9 +2019,12 @@ describe('PermissionController', () => { let counter = 0; const mutator: any = () => { counter += 1; - return counter === 1 - ? [CaveatMutatorOperation.revokePermission] - : [CaveatMutatorOperation.noop]; + return { + operation: + counter === 1 + ? CaveatMutatorOperation.revokePermission + : CaveatMutatorOperation.noop, + }; }; controller.updatePermissionsByCaveat( @@ -2025,7 +2044,12 @@ describe('PermissionController', () => { expect(() => controller.updatePermissionsByCaveat( CaveatTypes.filterArrayResponse, - () => [CaveatMutatorOperation.updateValue, 'foo'], + () => { + return { + operation: CaveatMutatorOperation.updateValue, + value: 'foo', + }; + }, ), ).toThrow(`${CaveatTypes.filterArrayResponse} values must be arrays`); }); @@ -2034,9 +2058,9 @@ describe('PermissionController', () => { const controller = getMultiCaveatController(); expect(() => - controller.updatePermissionsByCaveat(CaveatTypes.noopCaveat, () => [ - CaveatMutatorOperation.deleteCaveat, - ]), + controller.updatePermissionsByCaveat(CaveatTypes.noopCaveat, () => { + return { operation: CaveatMutatorOperation.deleteCaveat }; + }), ).toThrow('getSecret_* permission validation failed'); }); @@ -2046,7 +2070,9 @@ describe('PermissionController', () => { expect(() => controller.updatePermissionsByCaveat( CaveatTypes.filterArrayResponse, - () => ['foobar'] as any, + () => { + return { operation: 'foobar' } as any; + }, ), ).toThrow(`Unrecognized mutation result: "foobar"`); }); diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 6357bf82e5..664210f3f5 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -15,7 +15,7 @@ import type { ControllerMessenger, } from '@metamask/controllers'; /* eslint-enable @typescript-eslint/no-unused-vars */ -import type { Patch } from 'immer'; +import { castDraft, castImmutable, Draft, Patch } from 'immer'; import deepFreeze from 'deep-freeze-strict'; import { nanoid } from 'nanoid'; import { isPlainObject, hasProperty, NonEmptyArray, Mutable } from '../utils'; @@ -42,7 +42,6 @@ import { PermissionSpecificationMap, Permission, ExtractAllowedCaveatTypes, - MutableGenericPermission, ExtractPermissionSpecification, PermissionSpecificationConstraint, } from './Permission'; @@ -105,8 +104,8 @@ const controllerName = 'PermissionController'; /** * Permissions associated with a {@link PermissionController} subject. */ -export type SubjectPermissions = - Record; +export type SubjectPermissions = + Record; /** * Permissions and metadata associated with a {@link PermissionController} @@ -121,6 +120,8 @@ export type PermissionSubjectEntry< /** * All subjects of a {@link PermissionController}. + * + * @template SubjectPermission - The permissions of the subject. */ export type PermissionControllerSubjects< SubjectPermission extends PermissionConstraint, @@ -132,32 +133,34 @@ export type PermissionControllerSubjects< // TODO:TS4.4 Enable compiler flags to forbid unchecked member access /** * The state of a {@link PermissionController}. + * + * @template Permission - The controller's permission type union. */ -export type PermissionControllerState = - Permissions extends PermissionConstraint +export type PermissionControllerState = + Permission extends PermissionConstraint ? { - subjects: PermissionControllerSubjects; + subjects: PermissionControllerSubjects; } : never; /** * Get the state metadata of the {@link PermissionController}. * - * @template Permissions - The controller's permission type union. + * @template Permission - The controller's permission type union. */ -function getStateMetadata() { +function getStateMetadata() { return { subjects: { anonymous: true, persist: true } } as StateMetadata< - PermissionControllerState + PermissionControllerState >; } /** * Get the default state of the {@link PermissionController}. * - * @template Permissions - The controller's permission type union. + * @template Permission - The controller's permission type union. */ -function getDefaultState() { - return { subjects: {} } as PermissionControllerState; +function getDefaultState() { + return { subjects: {} } as PermissionControllerState; } /** @@ -280,9 +283,19 @@ export enum CaveatMutatorOperation { */ export type CaveatMutator = ( caveatValue: TargetCaveat['value'], -) => - | [Exclude] - | [CaveatMutatorOperation.updateValue, CaveatConstraint['value']]; +) => CaveatMutatorResult; + +type CaveatMutatorResult = + | Readonly<{ + operation: CaveatMutatorOperation.updateValue; + value: CaveatConstraint['value']; + }> + | Readonly<{ + operation: Exclude< + CaveatMutatorOperation, + CaveatMutatorOperation.updateValue + >; + }>; /** * Extracts the permission(s) specified by the given permission and caveat @@ -466,7 +479,7 @@ export class PermissionController< * @param targetKey - The target key of the permission specification to get. * @returns The permission specification with the specified target key. */ - getPermissionSpecification< + private getPermissionSpecification< TargetKey extends ControllerPermissionSpecification['targetKey'], >( targetKey: TargetKey, @@ -483,7 +496,7 @@ export class PermissionController< * @param caveatType - The type of the caveat specification to get. * @returns The caveat specification with the specified type. */ - getCaveatSpecification< + private getCaveatSpecification< CaveatType extends ControllerCaveatSpecification['type'], >(caveatType: CaveatType) { return this._caveatSpecifications[caveatType]; @@ -496,8 +509,10 @@ export class PermissionController< * * Throws an error if validation fails. * - * @param specifications - The permission specifications passed to this - * controller's constructor. + * @param permissionSpecifications - The permission specifications passed to + * this controller's constructor. + * @param caveatSpecifications - The caveat specifications passed to this + * controller. */ private validatePermissionSpecifications( permissionSpecifications: PermissionSpecificationMap, @@ -724,11 +739,7 @@ export class PermissionController< } // Typecast: Immer WritableDraft incompatibility - this.deletePermission( - draftState.subjects as unknown as PermissionControllerSubjects, - origin, - target, - ); + this.deletePermission(draftState.subjects, origin, target); }); }); }); @@ -755,12 +766,7 @@ export class PermissionController< const { permissions } = subject; if (hasProperty(permissions as Record, target)) { - this.deletePermission( - // Typecast: Immer WritableDraft incompatibility - draftState.subjects as unknown as PermissionControllerSubjects, - origin, - target, - ); + this.deletePermission(draftState.subjects, origin, target); } }); }); @@ -777,7 +783,7 @@ export class PermissionController< * @param target - The target name of the permission to delete. */ private deletePermission( - subjects: PermissionControllerSubjects, + subjects: Draft>, origin: OriginString, target: ExtractPermission< ControllerPermissionSpecification, @@ -980,7 +986,9 @@ export class PermissionController< this.update((draftState) => { const subject = draftState.subjects[origin]; - /* istanbul ignore if: practically impossible, but TypeScript wants it */ + // Unreachable because `hasCaveat` is always called before this, and it + // throws if permissions are missing. TypeScript needs this, however. + /* istanbul ignore if */ if (!subject) { throw new UnrecognizedSubjectError(origin); } @@ -1017,10 +1025,7 @@ export class PermissionController< this.validateModifiedPermission( // Typecast: Immer WritableDraft incompatibility - permission as unknown as ExtractPermission< - ControllerPermissionSpecification, - ControllerCaveatSpecification - >, + permission, origin, target, ); @@ -1078,8 +1083,8 @@ export class PermissionController< // The mutator may modify the caveat value in place, and must always // return a valid mutation result. - const [mutationResult, newValue] = mutator(targetCaveat.value); - switch (mutationResult) { + const mutatorResult = mutator(targetCaveat.value); + switch (mutatorResult.operation) { case CaveatMutatorOperation.noop: break; @@ -1088,7 +1093,7 @@ export class PermissionController< // not be undefined in this case. In any case, the specification // validator is called on the mutated caveat. (targetCaveat as Mutable).value = - newValue as TargetCaveat['value']; + mutatorResult.value; this.validateCaveat( targetCaveat, @@ -1100,7 +1105,7 @@ export class PermissionController< case CaveatMutatorOperation.deleteCaveat: // Typecast: Immer WritableDraft incompatibility this.deleteCaveat( - permission as MutableGenericPermission, + permission, targetCaveatType, subject.origin, permission.parentCapability, @@ -1110,20 +1115,22 @@ export class PermissionController< case CaveatMutatorOperation.revokePermission: // Typecast: Immer WritableDraft incompatibility this.deletePermission( - draftState.subjects as unknown as PermissionControllerSubjects, + draftState.subjects, subject.origin, permission.parentCapability, ); break; - default: + default: { // This type check ensures that the switch statement is // exhaustive. - // eslint-disable-next-line no-case-declarations - const _exhaustiveCheck: never = mutationResult; + const _exhaustiveCheck: never = mutatorResult; throw new Error( - `Unrecognized mutation result: "${_exhaustiveCheck}"`, + `Unrecognized mutation result: "${ + (_exhaustiveCheck as any).operation + }"`, ); + } } }); }); @@ -1188,7 +1195,7 @@ export class PermissionController< >['parentCapability'], CaveatType extends ExtractCaveats['type'], >( - permission: MutableGenericPermission, + permission: Draft, caveatType: CaveatType, origin: OriginString, target: TargetName, @@ -1228,7 +1235,7 @@ export class PermissionController< * @param targetName - The target name name of the permission. */ private validateModifiedPermission( - permission: PermissionConstraint, + permission: Draft, origin: OriginString, targetName: ExtractPermission< ControllerPermissionSpecification, @@ -1245,7 +1252,7 @@ export class PermissionController< this.validatePermission( this.getPermissionSpecification(targetKey), - permission, + castImmutable(permission) as PermissionConstraint, origin, targetName, ); @@ -1477,7 +1484,7 @@ export class PermissionController< } // Typecast: Immer WritableDraft incompatibility - draftState.subjects[origin].permissions = permissions as any; + draftState.subjects[origin].permissions = castDraft(permissions); }); } From f2c8e495930a6f66859d07216d21b1088014f1a9 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Thu, 28 Oct 2021 18:14:04 -0700 Subject: [PATCH 115/147] Simplify permission-middleware types, add docs --- .../controllers/src/permissions/Permission.ts | 17 +++++- .../src/permissions/PermissionController.ts | 4 +- .../src/permissions/permission-middleware.ts | 60 +++++++++++-------- 3 files changed, 53 insertions(+), 28 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index d0f8cf8c10..463d328768 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -19,6 +19,15 @@ export type OriginString = string; */ export type GenericTargetName = string; +/** + * A `ZCAP-LD`-like permission object. A permission is associated with a + * particular `invoker`, which is the holder of the permission. Possessing the + * permission grants access to a particular restricted resource, identified by + * the `parentCapability`. The use of the restricted resource may be further + * restricted by any `caveats` associated with the permission. + * + * See the documentation for details. + */ export type PermissionConstraint = { /** * The context(s) in which this capability is meaningful. @@ -60,7 +69,13 @@ export type PermissionConstraint = { }; /** - * TODO:docs + * A `ZCAP-LD`-like permission object. A permission is associated with a + * particular `invoker`, which is the holder of the permission. Possessing the + * permission grants access to a particular restricted resource, identified by + * the `parentCapability`. The use of the restricted resource may be further + * restricted by any `caveats` associated with the permission. + * + * See the documentation for details. * * @template TargetKey - They key of the permission target that the permission * corresponds to. diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 664210f3f5..46ee84d289 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -40,7 +40,7 @@ import { RestrictedMethodParameters, ValidPermissionSpecification, PermissionSpecificationMap, - Permission, + Permission as SpecificPermission, ExtractAllowedCaveatTypes, ExtractPermissionSpecification, PermissionSpecificationConstraint, @@ -311,7 +311,7 @@ export type ExtractPermission< ControllerPermissionSpecification extends PermissionSpecificationConstraint, ControllerCaveatSpecification extends CaveatSpecificationConstraint, > = ControllerPermissionSpecification extends ValidPermissionSpecification - ? Permission< + ? SpecificPermission< ControllerPermissionSpecification['targetKey'], ExtractCaveats > diff --git a/packages/controllers/src/permissions/permission-middleware.ts b/packages/controllers/src/permissions/permission-middleware.ts index 7af56b38fb..48a4c6d5b8 100644 --- a/packages/controllers/src/permissions/permission-middleware.ts +++ b/packages/controllers/src/permissions/permission-middleware.ts @@ -8,43 +8,52 @@ import { } from 'json-rpc-engine'; import { internalError } from './errors'; import { + GenericPermissionController, PermissionConstraint, PermissionSubjectMetadata, - RestrictedMethod, RestrictedMethodParameters, } from '.'; +// This is used in a docstring, but ESLint doesn't notice it. +/* eslint-disable @typescript-eslint/no-unused-vars, import/order */ +import type { JsonRpcEngine } from 'json-rpc-engine'; +import type { PermissionController } from './PermissionController'; +/* eslint-enable @typescript-eslint/no-unused-vars, import/order */ -/** - * TODO:docs - */ - -type ExecuteRestrictedMethod = ( - methodImplementation: RestrictedMethod, - subject: PermissionSubjectMetadata, - method: Permission['parentCapability'], - params?: RestrictedMethodParameters, -) => ReturnType>; -type GetRestrictedMethod = ( - method: string, - origin: string, -) => RestrictedMethod; -type IsUnrestrictedMethod = (method: string) => boolean; - -type PermissionMiddlewareFactoryOptions< - Permission extends PermissionConstraint, -> = { - executeRestrictedMethod: ExecuteRestrictedMethod; - getRestrictedMethod: GetRestrictedMethod; - isUnrestrictedMethod: IsUnrestrictedMethod; +type PermissionMiddlewareFactoryOptions = { + executeRestrictedMethod: GenericPermissionController['_executeRestrictedMethod']; + getRestrictedMethod: GenericPermissionController['_getRestrictedMethod']; + isUnrestrictedMethod: (method: string) => boolean; }; +/** + * Creates a permission middleware function factory. Intended for internal use + * in the {@link PermissionController}. Like any {@link JsonRpcEngine} + * middleware, each middleware will only receive requests from a particular + * subject / origin. However, each middleware also requires access to some + * `PermissionController` internals, which is why this "factory factory" exists. + * + * The middlewares returned by the factory will pass through requests for + * unrestricted methods, and attempt to execute restricted methods. If a method + * is neither restricted nor unrestricted, a "method not found" error will be + * returned. + * If a method is restricted, the middleware will first attempt to retrieve the + * subject's permission for that method. If the permission is found, the method + * will be executed. Otherwise, an "unauthorized" error will be returned. + * + * @param options - Options bag. + * @param options.executeRestrictedMethod - {@link PermissionController._executeRestrictedMethod}. + * @param options.getRestrictedMethod - {@link PermissionController.getRestrictedMethod}. + * @param options.isUnrestrictedMethod - A function that checks whether a + * particular method is unrestricted. + * @returns A permission middleware factory function. + */ export function getPermissionMiddlewareFactory< Permission extends PermissionConstraint, >({ executeRestrictedMethod, getRestrictedMethod, isUnrestrictedMethod, -}: PermissionMiddlewareFactoryOptions) { +}: PermissionMiddlewareFactoryOptions) { return function createPermissionMiddleware( subject: PermissionSubjectMetadata, ): JsonRpcMiddleware { @@ -55,7 +64,7 @@ export function getPermissionMiddlewareFactory< ): Promise => { const { method, params } = req; - // skip registered safe/passthrough methods. + // Skip registered unrestricted methods. if (isUnrestrictedMethod(method)) { return next(); } @@ -63,6 +72,7 @@ export function getPermissionMiddlewareFactory< // This will throw if no restricted method implementation is found. const methodImplementation = getRestrictedMethod(method, subject.origin); + // This will throw if the permission does not exist. const result = await executeRestrictedMethod( methodImplementation, subject, From 799f9faf36b56e8d02bc122413b8792b0bc4ca6e Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Thu, 28 Oct 2021 23:27:10 -0700 Subject: [PATCH 116/147] Verify that caveat values are JSON-compatible --- packages/controllers/package.json | 1 + .../permissions/PermissionController.test.ts | 30 +++++++++++++++++++ .../src/permissions/PermissionController.ts | 16 ++++++---- .../controllers/src/permissions/errors.ts | 13 ++++++++ 4 files changed, 54 insertions(+), 6 deletions(-) diff --git a/packages/controllers/package.json b/packages/controllers/package.json index ffbd7c28dd..3e70645b69 100644 --- a/packages/controllers/package.json +++ b/packages/controllers/package.json @@ -34,6 +34,7 @@ "@types/deep-freeze-strict": "^1.1.0", "deep-freeze-strict": "^1.1.1", "eth-rpc-errors": "^4.0.2", + "fast-deep-equal": "^3.1.3", "immer": "^9.0.6", "json-rpc-engine": "^6.1.0", "json-rpc-middleware-stream": "^3.0.0", diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 2d828db394..193f050ece 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -2517,6 +2517,36 @@ describe('PermissionController', () => { ); }); + it('throws if a requested caveat has a value that is not valid JSON', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + expect(() => + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + wallet_getSecretArray: { + caveats: [ + { + type: CaveatTypes.filterArrayResponse, + value: { foo: () => undefined }, + }, + ] as any, + }, + }, + }), + ).toThrow( + new errors.CaveatInvalidJsonError( + { + type: CaveatTypes.filterArrayResponse, + foo: 'bar', + }, + origin, + PermissionNames.wallet_getSecretArray, + ), + ); + }); + it('throws if caveat validation fails', () => { const controller = getDefaultPermissionController(); const origin = 'metamask.io'; diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 46ee84d289..987fc5f9cf 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -8,6 +8,7 @@ import { HasApprovalRequest, StateMetadata, } from '@metamask/controllers'; +import deepEqual from 'fast-deep-equal'; // These are used in docstrings, but ESLint is ignorant of docstrings. /* eslint-disable @typescript-eslint/no-unused-vars */ import type { @@ -64,6 +65,7 @@ import { userRejectedRequest, PermissionsRequestNotFoundError, ForbiddenCaveatError, + CaveatInvalidJsonError, } from './errors'; import { MethodNames } from './utils'; import { getPermissionMiddlewareFactory } from './permission-middleware'; @@ -1549,7 +1551,8 @@ export class PermissionController< throw new InvalidCaveatFieldsError(requestedCaveat, origin, target); } - if (typeof requestedCaveat.type !== 'string') { + const { type, value } = requestedCaveat; + if (typeof type !== 'string') { throw new InvalidCaveatTypeError(requestedCaveat, origin, target); } @@ -1557,11 +1560,12 @@ export class PermissionController< throw new CaveatMissingValueError(requestedCaveat, origin, target); } - // TODO: Consider validating that this is Json? - const caveat = _constructCaveat( - requestedCaveat.type, - (requestedCaveat.value as Json) ?? null, - ); + // Assert that the caveat value is valid JSON + if (!deepEqual(value, JSON.parse(JSON.stringify(value)))) { + throw new CaveatInvalidJsonError(requestedCaveat, origin, target); + } + + const caveat = _constructCaveat(type, (value as Json) ?? null); this.validateCaveat(caveat, origin, target); return caveat as ExtractCaveats; } diff --git a/packages/controllers/src/permissions/errors.ts b/packages/controllers/src/permissions/errors.ts index c30a105ae4..32008714cd 100644 --- a/packages/controllers/src/permissions/errors.ts +++ b/packages/controllers/src/permissions/errors.ts @@ -167,6 +167,19 @@ export class CaveatMissingValueError extends Error { } } +export class CaveatInvalidJsonError extends Error { + public data: { + caveat: Record; + origin: string; + target: string; + }; + + constructor(caveat: Record, origin: string, target: string) { + super(`Caveat "value" is invalid JSON.`); + this.data = { caveat, origin, target }; + } +} + export class InvalidCaveatFieldsError extends Error { public data: { caveat: Record; From 226ecaa217e608194c54d9b411ffc62cf1df12e4 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Sat, 30 Oct 2021 21:47:26 -0700 Subject: [PATCH 117/147] Apply suggestions from code review Co-authored-by: Mark Stacey --- .../src/permissions/PermissionController.ts | 31 +++++++------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 987fc5f9cf..2b00fc565f 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -1091,9 +1091,11 @@ export class PermissionController< break; case CaveatMutatorOperation.updateValue: - // Typecast: The mutator type guarantees that the new value will - // not be undefined in this case. In any case, the specification - // validator is called on the mutated caveat. + // Typecast: `Mutable` is used here to assign to a readonly + // property. `targetConstraint` should already be mutable because + // it's part of a draft, but for some reason it's not. We can't + // use the more-correct `Draft` type here either because it + // results in an error. (targetCaveat as Mutable).value = mutatorResult.value; @@ -1172,8 +1174,7 @@ export class PermissionController< throw new CaveatDoesNotExistError(origin, target, caveatType); } - // Typecast: Immer WritableDraft incompatibility - this.deleteCaveat(permission as any, caveatType, origin, target); + this.deleteCaveat(permission, caveatType, origin, target); }); } @@ -1277,7 +1278,7 @@ export class PermissionController< >['parentCapability'], ): ControllerPermissionSpecification['targetKey'] | undefined { if (hasProperty(this._permissionSpecifications, method)) { - return method as ControllerPermissionSpecification['targetKey']; + return method; } const wildCardMethodsWithoutWildCard: Record = {}; @@ -1303,7 +1304,7 @@ export class PermissionController< } if (wildCardMethodsWithoutWildCard[targetKey]) { - return `${targetKey}*` as ControllerPermissionSpecification['targetKey']; + return `${targetKey}*`; } return undefined; @@ -1363,12 +1364,7 @@ export class PermissionController< for (const [requestedTarget, approvedPermission] of Object.entries( approvedPermissions, )) { - const targetKey = this.getTargetKey( - requestedTarget as ExtractPermission< - ControllerPermissionSpecification, - ControllerCaveatSpecification - >['parentCapability'], - ); + const targetKey = this.getTargetKey(requestedTarget); if (!targetKey) { throw methodNotFound({ method: requestedTarget }); } @@ -1409,14 +1405,9 @@ export class PermissionController< target: targetName, }; - const permission = ( - specification.factory + const permission = specification.factory ? specification.factory(permissionOptions, requestData) - : constructPermission(permissionOptions) - ) as ExtractPermission< - ControllerPermissionSpecification, - ControllerCaveatSpecification - >; + : constructPermission(permissionOptions); this.validatePermission(specification, permission, origin, targetName); permissions[targetName] = permission; } From 2aed96368ff2cafc2accaa71477b7fb6c14b613e Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sat, 30 Oct 2021 21:48:07 -0700 Subject: [PATCH 118/147] fixup! Apply suggestions from code review --- packages/controllers/src/permissions/PermissionController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 2b00fc565f..ca712b7bdb 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -1406,8 +1406,8 @@ export class PermissionController< }; const permission = specification.factory - ? specification.factory(permissionOptions, requestData) - : constructPermission(permissionOptions); + ? specification.factory(permissionOptions, requestData) + : constructPermission(permissionOptions); this.validatePermission(specification, permission, origin, targetName); permissions[targetName] = permission; } From 37b75f9c1bf08286e00d3cf0ee071637b3cfe88c Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sat, 30 Oct 2021 21:49:38 -0700 Subject: [PATCH 119/147] Remove leftover typecast comments --- .../controllers/src/permissions/PermissionController.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index ca712b7bdb..2db6a57799 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -429,9 +429,6 @@ export class PermissionController< state = {}, } = options; - // Typecast: The state metadata and default state objects are declared - // outside of the controller instance and are therefore incorrectly - // genericized. super({ name: controllerName, metadata: @@ -571,8 +568,6 @@ export class PermissionController< */ clearState(): void { this.update((_draftState) => { - // Typecast: The default state is declared outside of the controller - // instance and therefore incorrectly genericized. return { ...getDefaultState< ExtractPermission< @@ -740,7 +735,6 @@ export class PermissionController< throw new PermissionDoesNotExistError(origin, target); } - // Typecast: Immer WritableDraft incompatibility this.deletePermission(draftState.subjects, origin, target); }); }); @@ -1026,7 +1020,6 @@ export class PermissionController< } this.validateModifiedPermission( - // Typecast: Immer WritableDraft incompatibility permission, origin, target, @@ -1107,7 +1100,6 @@ export class PermissionController< break; case CaveatMutatorOperation.deleteCaveat: - // Typecast: Immer WritableDraft incompatibility this.deleteCaveat( permission, targetCaveatType, @@ -1117,7 +1109,6 @@ export class PermissionController< break; case CaveatMutatorOperation.revokePermission: - // Typecast: Immer WritableDraft incompatibility this.deletePermission( draftState.subjects, subject.origin, From 4d24c94977b0f31b5da29fba8d3f5eaeb49554a3 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Sat, 30 Oct 2021 21:55:01 -0700 Subject: [PATCH 120/147] Rename Permission generic type to ValidPermission --- packages/controllers/src/permissions/Permission.ts | 2 +- .../src/permissions/PermissionController.test.ts | 4 ++-- .../src/permissions/PermissionController.ts | 10 +++------- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 463d328768..1fe05f6609 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -82,7 +82,7 @@ export type PermissionConstraint = { * @template AllowedCaveat - A union of the allowed {@link Caveat} types * for the permission. */ -export type Permission< +export type ValidPermission< TargetKey extends GenericTargetName, AllowedCaveat extends CaveatConstraint, > = ValidTargetName> extends never diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 193f050ece..8c6ac6c7f3 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -18,7 +18,7 @@ import { CaveatConstraint, PermissionConstraint, MethodNames, - Permission, + ValidPermission, PermissionController, PermissionControllerActions, PermissionControllerEvents, @@ -166,7 +166,7 @@ type DefaultCaveatSpecifications = ExtractSpecifications< type SecretNamespacedPermissionKey = 'wallet_getSecret_*'; -type SecretNamespacedPermission = Permission< +type SecretNamespacedPermission = ValidPermission< SecretNamespacedPermissionKey, NoopCaveat >; diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 2db6a57799..324dca0eae 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -41,7 +41,7 @@ import { RestrictedMethodParameters, ValidPermissionSpecification, PermissionSpecificationMap, - Permission as SpecificPermission, + ValidPermission, ExtractAllowedCaveatTypes, ExtractPermissionSpecification, PermissionSpecificationConstraint, @@ -313,7 +313,7 @@ export type ExtractPermission< ControllerPermissionSpecification extends PermissionSpecificationConstraint, ControllerCaveatSpecification extends CaveatSpecificationConstraint, > = ControllerPermissionSpecification extends ValidPermissionSpecification - ? SpecificPermission< + ? ValidPermission< ControllerPermissionSpecification['targetKey'], ExtractCaveats > @@ -1019,11 +1019,7 @@ export class PermissionController< permission.caveats = [caveat] as any; } - this.validateModifiedPermission( - permission, - origin, - target, - ); + this.validateModifiedPermission(permission, origin, target); }); } From 5ce5f803f0f3d8947ba553a49a69b515932c12f0 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 2 Nov 2021 09:34:40 -0700 Subject: [PATCH 121/147] Add NonEmptyArray type guard --- .../src/permissions/PermissionController.ts | 14 +++++++++----- packages/controllers/src/utils.ts | 11 +++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 324dca0eae..117cf3248f 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -19,7 +19,13 @@ import type { import { castDraft, castImmutable, Draft, Patch } from 'immer'; import deepFreeze from 'deep-freeze-strict'; import { nanoid } from 'nanoid'; -import { isPlainObject, hasProperty, NonEmptyArray, Mutable } from '../utils'; +import { + isPlainObject, + hasProperty, + NonEmptyArray, + Mutable, + isNonEmptyArray, +} from '../utils'; import { constructCaveat as _constructCaveat, decorateWithCaveats, @@ -1491,10 +1497,8 @@ export class PermissionController< const caveatArray = requestedCaveats?.map((requestedCaveat) => this.constructCaveat(origin, target, requestedCaveat), ); - return caveatArray && caveatArray.length > 0 - ? (caveatArray as NonEmptyArray< - ExtractCaveats - >) + return caveatArray && isNonEmptyArray(caveatArray) + ? caveatArray : undefined; } diff --git a/packages/controllers/src/utils.ts b/packages/controllers/src/utils.ts index 7d7c7a1b1a..a7e89690b1 100644 --- a/packages/controllers/src/utils.ts +++ b/packages/controllers/src/utils.ts @@ -16,6 +16,17 @@ export const hasProperty = ( */ export type NonEmptyArray = [T, ...T[]]; +/** + * {@link NonEmptyArray} type guard. + * + * @template T - The non-empty array member type. + * @param value - The value to check. + * @returns Whether the value is a non-empty array. + */ +export function isNonEmptyArray(value: T[]): value is NonEmptyArray { + return Array.isArray(value) && value.length > 0; +} + /** * Makes every specified property of the specified object type mutable. * From 9307b5a66a17c31f8c205b65e47378196712cafa Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 2 Nov 2021 14:58:58 -0700 Subject: [PATCH 122/147] Apply suggestions from code review Co-authored-by: Mark Stacey --- .../src/permissions/PermissionController.ts | 44 +++++++------------ 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 117cf3248f..a9a108d630 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -1339,15 +1339,13 @@ export class PermissionController< throw new InvalidSubjectIdentifierError(origin); } - const permissions: SubjectPermissions< - ExtractPermission< - ControllerPermissionSpecification, - ControllerCaveatSpecification - > - > = ((preserveExistingPermissions && { - ...this.getPermissions(origin), - }) || - {}) as SubjectPermissions< + const permissions = ( + preserveExistingPermissions + ? { + ...this.getPermissions(origin), + } + : {} + ) as SubjectPermissions< ExtractPermission< ControllerPermissionSpecification, ControllerCaveatSpecification @@ -1387,12 +1385,7 @@ export class PermissionController< approvedPermission.caveats, ); - const permissionOptions: PermissionOptions< - ExtractPermission< - ControllerPermissionSpecification, - ControllerCaveatSpecification - > - > = { + const permissionOptions = { caveats, invoker: origin, target: targetName, @@ -1469,7 +1462,6 @@ export class PermissionController< draftState.subjects[origin] = { origin, permissions: {} }; } - // Typecast: Immer WritableDraft incompatibility draftState.subjects[origin].permissions = castDraft(permissions); }); } @@ -1505,10 +1497,9 @@ export class PermissionController< /** * Constructs a caveat for the permission of the specified subject origin and * target name, per the requested caveat argument. This methods validates - * everything about the requested caveat, except that its `value` property - * is JSON-compatible. It also ensures that a caveat specification exist for - * the requested type, and calls the specification validator, if it exists, on - * the constructed caveat. + * everything about the requested caveat. It also ensures that a caveat + * specification exist for the requested type, and calls the specification + * validator, if it exists, on the constructed caveat. * * Throws an error if validation fails. * @@ -1610,12 +1601,12 @@ export class PermissionController< ] > { this.validateRequestedPermissions(origin, requestedPermissions); - const metadata: PermissionsRequest['metadata'] = { + const metadata = { id, origin, }; - const permissionsRequest: PermissionsRequest = { + const permissionsRequest = { metadata, permissions: requestedPermissions, }; @@ -1692,15 +1683,10 @@ export class PermissionController< throw invalidParams({ data: { origin, requestedPermissions } }); } - const perms: RequestedPermissions = requestedPermissions; + const perms = requestedPermissions; for (const methodName of Object.keys(perms)) { - const target = ( - perms[methodName] as unknown as ExtractPermission< - ControllerPermissionSpecification, - ControllerCaveatSpecification - > - ).parentCapability; + const target = perms[methodName].parentCapability; if (target !== undefined && methodName !== target) { throw invalidParams({ data: { origin, requestedPermissions } }); } From b8fbba71e14b19d0a2ee5bf60b654bb05aa575f2 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 2 Nov 2021 14:59:38 -0700 Subject: [PATCH 123/147] fixup! Apply suggestions from code review --- packages/controllers/src/permissions/PermissionController.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index a9a108d630..1393af98c6 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -41,7 +41,6 @@ import { findCaveat, PermissionConstraint, OriginString, - PermissionOptions, RequestedPermissions, RestrictedMethod, RestrictedMethodParameters, From 11e318e6ac51e222f50ff983631e902a3e6dd1d1 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 2 Nov 2021 18:16:11 -0700 Subject: [PATCH 124/147] Improve some validation, address review feedback --- .../permissions/PermissionController.test.ts | 440 +++++++++++++++--- .../src/permissions/PermissionController.ts | 177 ++++--- packages/controllers/src/utils.ts | 17 + 3 files changed, 506 insertions(+), 128 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 8c6ac6c7f3..9d2faf1c27 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -2521,29 +2521,36 @@ describe('PermissionController', () => { const controller = getDefaultPermissionController(); const origin = 'metamask.io'; - expect(() => - controller.grantPermissions({ - subject: { origin }, - approvedPermissions: { - wallet_getSecretArray: { - caveats: [ - { - type: CaveatTypes.filterArrayResponse, - value: { foo: () => undefined }, + const circular: any = { foo: 'bar' }; + circular.circular = circular; + + [{ foo: () => undefined }, circular, { foo: BigInt(10) }].forEach( + (invalidValue) => { + expect(() => + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + wallet_getSecretArray: { + caveats: [ + { + type: CaveatTypes.filterArrayResponse, + value: invalidValue, + }, + ] as any, }, - ] as any, - }, - }, - }), - ).toThrow( - new errors.CaveatInvalidJsonError( - { - type: CaveatTypes.filterArrayResponse, - foo: 'bar', - }, - origin, - PermissionNames.wallet_getSecretArray, - ), + }, + }), + ).toThrow( + new errors.CaveatInvalidJsonError( + { + type: CaveatTypes.filterArrayResponse, + value: invalidValue, + }, + origin, + PermissionNames.wallet_getSecretArray, + ), + ); + }, ); }); @@ -2760,6 +2767,265 @@ describe('PermissionController', () => { ); }); + it('throws if requested permissions object is not a plain object', async () => { + const options = getPermissionControllerOptions(); + const { messenger } = options; + const origin = 'metamask.io'; + const controller = getDefaultPermissionController(options); + + const callActionSpy = jest.spyOn(messenger, 'call'); + + for (const invalidInput of [ + // not plain objects + null, + 'foo', + [{ [PermissionNames.wallet_getSecretArray]: {} }], + ]) { + await expect( + async () => + await controller.requestPermissions(origin, invalidInput as any), + ).rejects.toThrow( + errors.invalidParams({ + message: `Requested permissions for origin "${origin}" is not a plain object.`, + data: { origin, requestedPermissions: invalidInput }, + }), + ); + } + + expect(callActionSpy).not.toHaveBeenCalled(); + }); + + it('throws if requested permissions object has no permissions', async () => { + const options = getPermissionControllerOptions(); + const { messenger } = options; + const origin = 'metamask.io'; + + const callActionSpy = jest.spyOn(messenger, 'call'); + + const controller = getDefaultPermissionController(options); + await expect( + async () => + // No permissions in object + await controller.requestPermissions(origin, {}), + ).rejects.toThrow( + errors.invalidParams({ + message: `Permissions request for origin "${origin}" contains no permissions.`, + data: { origin, requestedPermissions: {} }, + }), + ); + + expect(callActionSpy).not.toHaveBeenCalled(); + }); + + it('throws if requested permissions contain a (key : value.parentCapability) mismatch', async () => { + const options = getPermissionControllerOptions(); + const { messenger } = options; + const origin = 'metamask.io'; + + const callActionSpy = jest.spyOn(messenger, 'call'); + + const controller = getDefaultPermissionController(options); + await expect( + async () => + await controller.requestPermissions(origin, { + [PermissionNames.wallet_getSecretArray]: { + parentCapability: PermissionNames.wallet_getSecretArray, + }, + // parentCapability value does not match key + [PermissionNames.wallet_getSecretObject]: { + parentCapability: PermissionNames.wallet_getSecretArray, + }, + }), + ).rejects.toThrow( + errors.invalidParams({ + message: `Permissions request for origin "${origin}" contains invalid requested permission(s).`, + data: { + origin, + requestedPermissions: { + [PermissionNames.wallet_getSecretArray]: { + [PermissionNames.wallet_getSecretArray]: { + parentCapability: PermissionNames.wallet_getSecretArray, + }, + [PermissionNames.wallet_getSecretObject]: { + parentCapability: PermissionNames.wallet_getSecretArray, + }, + }, + }, + }, + }), + ); + + expect(callActionSpy).not.toHaveBeenCalled(); + }); + + it('throws if requesting a permission for an unknown target', async () => { + const options = getPermissionControllerOptions(); + const { messenger } = options; + const origin = 'metamask.io'; + + const callActionSpy = jest.spyOn(messenger, 'call'); + + const controller = getDefaultPermissionController(options); + await expect( + async () => + await controller.requestPermissions(origin, { + [PermissionNames.wallet_getSecretArray]: {}, + wallet_getSecretKabob: {}, + }), + ).rejects.toThrow( + errors.methodNotFound({ + method: 'wallet_getSecretKabob', + data: { + origin, + requestedPermissions: { + [PermissionNames.wallet_getSecretArray]: { + [PermissionNames.wallet_getSecretArray]: {}, + wallet_getSecretKabob: {}, + }, + }, + }, + }), + ); + + expect(callActionSpy).not.toHaveBeenCalled(); + }); + + it('throws if the approved request object is invalid', async () => { + const options = getPermissionControllerOptions(); + const { messenger } = options; + const origin = 'metamask.io'; + const controller = getDefaultPermissionController(options); + const callActionSpy = jest.spyOn(messenger, 'call'); + + for (const invalidRequestObject of ['foo', null, { metadata: 'foo' }]) { + callActionSpy.mockClear(); + callActionSpy.mockImplementationOnce( + (async () => invalidRequestObject) as any, + ); + + await expect( + async () => + await controller.requestPermissions(origin, { + [PermissionNames.wallet_getSecretArray]: {}, + }), + ).rejects.toThrow( + errors.internalError( + `Approved permissions request for subject "${origin}" is invalid.`, + { data: { approvedRequest: invalidRequestObject } }, + ), + ); + + expect(callActionSpy).toHaveBeenCalledTimes(1); + expect(callActionSpy).toHaveBeenCalledWith( + 'ApprovalController:addRequest', + { + id: expect.any(String), + origin, + requestData: { + metadata: { id: expect.any(String), origin }, + permissions: { [PermissionNames.wallet_getSecretArray]: {} }, + }, + type: MethodNames.requestPermissions, + }, + true, + ); + } + }); + + it('throws if the approved request ID changed', async () => { + const options = getPermissionControllerOptions(); + const { messenger } = options; + const origin = 'metamask.io'; + + const callActionSpy = jest + .spyOn(messenger, 'call') + .mockImplementationOnce((async (...args: any) => { + const [, { requestData }] = args; + return { + // different id + metadata: { ...requestData.metadata, id: 'foo' }, + permissions: { + [PermissionNames.wallet_getSecretArray]: {}, + }, + }; + }) as any); + + const controller = getDefaultPermissionController(options); + await expect( + async () => + await controller.requestPermissions(origin, { + [PermissionNames.wallet_getSecretArray]: {}, + }), + ).rejects.toThrow( + errors.internalError( + `Approved permissions request for subject "${origin}" mutated its id.`, + { originalId: expect.any(String), mutatedId: 'foo' }, + ), + ); + + expect(callActionSpy).toHaveBeenCalledTimes(1); + expect(callActionSpy).toHaveBeenCalledWith( + 'ApprovalController:addRequest', + { + id: expect.any(String), + origin, + requestData: { + metadata: { id: expect.any(String), origin }, + permissions: { [PermissionNames.wallet_getSecretArray]: {} }, + }, + type: MethodNames.requestPermissions, + }, + true, + ); + }); + + it('throws if the approved request origin changed', async () => { + const options = getPermissionControllerOptions(); + const { messenger } = options; + const origin = 'metamask.io'; + + const callActionSpy = jest + .spyOn(messenger, 'call') + .mockImplementationOnce((async (...args: any) => { + const [, { requestData }] = args; + return { + // different origin + metadata: { ...requestData.metadata, origin: 'foo.com' }, + permissions: { + [PermissionNames.wallet_getSecretArray]: {}, + }, + }; + }) as any); + + const controller = getDefaultPermissionController(options); + await expect( + async () => + await controller.requestPermissions(origin, { + [PermissionNames.wallet_getSecretArray]: {}, + }), + ).rejects.toThrow( + errors.internalError( + `Approved permissions request for subject "${origin}" mutated its origin.`, + { originalOrigin: origin, mutatedOrigin: 'foo' }, + ), + ); + + expect(callActionSpy).toHaveBeenCalledTimes(1); + expect(callActionSpy).toHaveBeenCalledWith( + 'ApprovalController:addRequest', + { + id: expect.any(String), + origin, + requestData: { + metadata: { id: expect.any(String), origin }, + permissions: { [PermissionNames.wallet_getSecretArray]: {} }, + }, + type: MethodNames.requestPermissions, + }, + true, + ); + }); + it('throws if no permissions were approved', async () => { const options = getPermissionControllerOptions(); const { messenger } = options; @@ -2783,7 +3049,7 @@ describe('PermissionController', () => { }), ).rejects.toThrow( errors.internalError( - `Approved permissions request for origin "${origin}" contains no permissions.`, + `Invalid approved permissions request: Permissions request for origin "${origin}" contains no permissions.`, { [PermissionNames.wallet_getSecretArray]: {}, }, @@ -2806,55 +3072,102 @@ describe('PermissionController', () => { ); }); - it('throws if requested permissions are invalid', async () => { + it('throws if approved permissions object is not a plain object', async () => { const options = getPermissionControllerOptions(); const { messenger } = options; const origin = 'metamask.io'; + const id = 'arbitraryId'; + const controller = getDefaultPermissionController(options); const callActionSpy = jest.spyOn(messenger, 'call'); - const controller = getDefaultPermissionController(options); - // Iterate over some invalid inputs - [ - // not plain objects + // The metadata is valid, but the permissions are invalid + const getInvalidRequestObject = (invalidPermissions: any) => { + return { + metadata: { origin, id }, + permissions: invalidPermissions, + }; + }; + + for (const invalidRequestObject of [ null, 'foo', [{ [PermissionNames.wallet_getSecretArray]: {} }], - // no permissions - {}, - ].forEach(async (input) => { + ].map((invalidPermissions) => + getInvalidRequestObject(invalidPermissions), + )) { + callActionSpy.mockClear(); + callActionSpy.mockImplementationOnce( + (async () => invalidRequestObject) as any, + ); + await expect( - async () => await controller.requestPermissions(origin, input as any), + async () => + await controller.requestPermissions( + origin, + { + [PermissionNames.wallet_getSecretArray]: {}, + }, + id, + ), ).rejects.toThrow( - errors.invalidParams({ - data: { origin, requestedPermissions: input }, - }), + errors.internalError( + `Invalid approved permissions request: Requested permissions for origin "${origin}" is not a plain object.`, + { data: { approvedRequest: invalidRequestObject } }, + ), ); - }); - expect(callActionSpy).not.toHaveBeenCalled(); + expect(callActionSpy).toHaveBeenCalledTimes(1); + expect(callActionSpy).toHaveBeenCalledWith( + 'ApprovalController:addRequest', + { + id: expect.any(String), + origin, + requestData: { + metadata: { id: expect.any(String), origin }, + permissions: { [PermissionNames.wallet_getSecretArray]: {} }, + }, + type: MethodNames.requestPermissions, + }, + true, + ); + } }); - it('throws if requested permissions contain a (key : value.parentCapability) mismatch', async () => { + it('throws if approved permissions contain a (key : value.parentCapability) mismatch', async () => { const options = getPermissionControllerOptions(); const { messenger } = options; const origin = 'metamask.io'; + const controller = getDefaultPermissionController(options); - const callActionSpy = jest.spyOn(messenger, 'call'); + const callActionSpy = jest + .spyOn(messenger, 'call') + .mockImplementationOnce((async (...args: any) => { + const [, { requestData }] = args; + return { + metadata: { ...requestData.metadata }, + permissions: { + [PermissionNames.wallet_getSecretArray]: { + parentCapability: PermissionNames.wallet_getSecretArray, + }, + // parentCapability value does not match key + [PermissionNames.wallet_getSecretObject]: { + parentCapability: PermissionNames.wallet_getSecretArray, + }, + }, + }; + }) as any); - const controller = getDefaultPermissionController(options); await expect( async () => await controller.requestPermissions(origin, { [PermissionNames.wallet_getSecretArray]: { parentCapability: PermissionNames.wallet_getSecretArray, }, - [PermissionNames.wallet_getSecretObject]: { - parentCapability: PermissionNames.wallet_getSecretArray, - }, }), ).rejects.toThrow( errors.invalidParams({ + message: `Invalid approved permissions request: Permissions request for origin "${origin}" contains invalid requested permission(s).`, data: { origin, requestedPermissions: { @@ -2871,39 +3184,24 @@ describe('PermissionController', () => { }), ); - expect(callActionSpy).not.toHaveBeenCalled(); - }); - - it('throws if requesting a permission for an unre target', async () => { - const options = getPermissionControllerOptions(); - const { messenger } = options; - const origin = 'metamask.io'; - - const callActionSpy = jest.spyOn(messenger, 'call'); - - const controller = getDefaultPermissionController(options); - await expect( - async () => - await controller.requestPermissions(origin, { - [PermissionNames.wallet_getSecretArray]: {}, - wallet_getSecretKabob: {}, - }), - ).rejects.toThrow( - errors.methodNotFound({ - method: 'wallet_getSecretKabob', - data: { - origin, - requestedPermissions: { + expect(callActionSpy).toHaveBeenCalledTimes(1); + expect(callActionSpy).toHaveBeenCalledWith( + 'ApprovalController:addRequest', + { + id: expect.any(String), + origin, + requestData: { + metadata: { id: expect.any(String), origin }, + permissions: { [PermissionNames.wallet_getSecretArray]: { - [PermissionNames.wallet_getSecretArray]: {}, - wallet_getSecretKabob: {}, + parentCapability: PermissionNames.wallet_getSecretArray, }, }, }, - }), + type: MethodNames.requestPermissions, + }, + true, ); - - expect(callActionSpy).not.toHaveBeenCalled(); }); }); diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 1393af98c6..f592c4311d 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -8,7 +8,6 @@ import { HasApprovalRequest, StateMetadata, } from '@metamask/controllers'; -import deepEqual from 'fast-deep-equal'; // These are used in docstrings, but ESLint is ignorant of docstrings. /* eslint-disable @typescript-eslint/no-unused-vars */ import type { @@ -25,6 +24,7 @@ import { NonEmptyArray, Mutable, isNonEmptyArray, + isValidJson, } from '../utils'; import { constructCaveat as _constructCaveat, @@ -258,16 +258,6 @@ export type GenericPermissionController = PermissionController< CaveatSpecificationConstraint >; -/** - * Options for {@link PermissionController.grantPermissions}. - */ -type GrantPermissionsOptions = { - approvedPermissions: RequestedPermissions; - subject: PermissionSubjectMetadata; - preserveExistingPermissions?: boolean; - requestData?: Record; -}; - /** * Describes the possible results of a {@link CaveatMutator} function. */ @@ -1326,7 +1316,12 @@ export class PermissionController< requestData, preserveExistingPermissions = true, subject, - }: GrantPermissionsOptions): SubjectPermissions< + }: { + approvedPermissions: RequestedPermissions; + subject: PermissionSubjectMetadata; + preserveExistingPermissions?: boolean; + requestData?: Record; + }): SubjectPermissions< ExtractPermission< ControllerPermissionSpecification, ControllerCaveatSpecification @@ -1533,7 +1528,7 @@ export class PermissionController< } // Assert that the caveat value is valid JSON - if (!deepEqual(value, JSON.parse(JSON.stringify(value)))) { + if (!isValidJson(value)) { throw new CaveatInvalidJsonError(requestedCaveat, origin, target); } @@ -1570,6 +1565,9 @@ export class PermissionController< * always be used to grant additional permissions to a subject, unless user * approval has been obtained through some other means. * + * Permissions are validated at every step of the approval process, and this + * method will reject if validation fails. + * * @see {@link ApprovalController} For the user approval logic. * @see {@link PermissionController.acceptPermissionsRequest} For the method * that _accepts_ the request and resolves the user approval promise. @@ -1612,15 +1610,9 @@ export class PermissionController< const { permissions: approvedPermissions, ...requestData } = await this.requestUserApproval(permissionsRequest); - if (Object.keys(approvedPermissions).length === 0) { - // The request should already have been rejected if this is the case. - throw internalError( - `Approved permissions request for origin "${origin}" contains no permissions.`, - { requestedPermissions }, - ); - } return [ + // Further validation is performed here this.grantPermissions({ subject: { origin }, approvedPermissions, @@ -1631,16 +1623,71 @@ export class PermissionController< ]; } + /** + * Validates requested permissions. Throws if validation fails. + * + * This method ensures that the the requested permissions are a properly + * formatted {@link RequestedPermissions} object, that its keys correspond to + * the `parentCapability` fields of its values, and that the requested + * capability exists. + * + * Unrecognzied properties on requested permissions are ignored. Any requested + * caveats are validated in later stages of request processing. + * + * @param origin - The origin of the grantee subject. + * @param requestedPermissions - The requested permissions. + */ + private validateRequestedPermissions( + origin: OriginString, + requestedPermissions: RequestedPermissions | unknown, + ): void { + if (!isPlainObject(requestedPermissions)) { + throw invalidParams({ + message: `Requested permissions for origin "${origin}" is not a plain object.`, + data: { origin, requestedPermissions }, + }); + } + + if (Object.keys(requestedPermissions).length === 0) { + throw invalidParams({ + message: `Permissions request for origin "${origin}" contains no permissions.`, + data: { requestedPermissions }, + }); + } + + for (const methodName of Object.keys(requestedPermissions)) { + const permission = requestedPermissions[methodName]; + if ( + !isPlainObject(permission) || + (permission.parentCapability !== undefined && + methodName !== permission.parentCapability) + ) { + throw invalidParams({ + message: `Permissions request for origin "${origin}" contains invalid requested permission(s).`, + data: { origin, requestedPermissions }, + }); + } + + if (!this.getTargetKey(methodName)) { + throw methodNotFound({ + method: methodName, + data: { origin, requestedPermissions }, + }); + } + } + } + /** * Adds a request to the {@link ApprovalController} using the - * {@link AddApprovalRequest} action. + * {@link AddApprovalRequest} action. Also validates the resulting approved + * permissions request, and throws an error if validation fails. * * @param permissionsRequest - The permissions request object. * @returns The approved permissions request object. */ private async requestUserApproval(permissionsRequest: PermissionsRequest) { const { origin, id } = permissionsRequest.metadata; - return (await this.messagingSystem.call( + const approvedRequest = await this.messagingSystem.call( 'ApprovalController:addRequest', { id, @@ -1649,53 +1696,69 @@ export class PermissionController< type: MethodNames.requestPermissions, }, true, - )) as PermissionsRequest; + ); + + this.validateApprovedPermissions(approvedRequest, { id, origin }); + return approvedRequest as PermissionsRequest; } /** - * Validates requested permissions. Throws if validation fails. - * - * All properties of {@link RequestedPermissions} values are ignored during - * request processing in this controller except `caveats`, which is validated - * by {@link PermissionController.computeCaveats}, caveat validators, and - * permission validators. + * Validates an approved {@link PermissionsRequest} object. The approved + * request must have the required `metadata` and `permissions` properties, + * the `id` and `origin` of the `metadata` must match the original request + * metadata, and the requested permissions must be valid per + * {@link PermissionController.validateRequestedPermissions}. Any extra + * metadata properties are ignored. * - * This method still validates the `parentCapability` field, since a mismatch - * between a {@link RequestedPermissions} key and `parentCapability` makes it - * impossible to determine which permission the consumer intended to request. - * At the time of writing, our types also do not prevent this kind of - * mismatch. + * An error is thrown if validation fails. * - * @param origin - The origin of the grantee subject. - * @param requestedPermissions - The requested permissions. + * @param approvedRequest - The approved permissions request object. + * @param originalMetadata - The original request metadata. */ - private validateRequestedPermissions( - origin: OriginString, - requestedPermissions: RequestedPermissions, - ): void { + private validateApprovedPermissions( + approvedRequest: PermissionsRequest | unknown, + originalMetadata: PermissionsRequestMetadata, + ) { + const { id, origin } = originalMetadata; + if ( - !requestedPermissions || - typeof requestedPermissions !== 'object' || - Array.isArray(requestedPermissions) || - Object.keys(requestedPermissions).length === 0 + !isPlainObject(approvedRequest) || + !isPlainObject(approvedRequest.metadata) ) { - throw invalidParams({ data: { origin, requestedPermissions } }); + throw internalError( + `Approved permissions request for subject "${origin}" is invalid.`, + { data: { approvedRequest } }, + ); } - const perms = requestedPermissions; + const { + metadata: { id: newId, origin: newOrigin }, + permissions, + } = approvedRequest; - for (const methodName of Object.keys(perms)) { - const target = perms[methodName].parentCapability; - if (target !== undefined && methodName !== target) { - throw invalidParams({ data: { origin, requestedPermissions } }); - } + if (newId !== id) { + throw internalError( + `Approved permissions request for subject "${origin}" mutated its id.`, + { originalId: id, mutatedId: newId }, + ); + } - if (!this.getTargetKey(methodName)) { - throw methodNotFound({ - method: methodName, - data: { origin, requestedPermissions }, - }); - } + if (newOrigin !== origin) { + throw internalError( + `Approved permissions request for subject "${origin}" mutated its origin.`, + { originalOrigin: origin, mutatedOrigin: newOrigin }, + ); + } + + try { + this.validateRequestedPermissions(origin, permissions); + } catch (error) { + // Re-throw as an internal error; we should never receive invalid approved + // permissions. + throw internalError( + `Invalid approved permissions request: ${error.message}`, + error.data, + ); } } diff --git a/packages/controllers/src/utils.ts b/packages/controllers/src/utils.ts index a7e89690b1..90a7ab17ca 100644 --- a/packages/controllers/src/utils.ts +++ b/packages/controllers/src/utils.ts @@ -1,3 +1,6 @@ +import { Json } from '@metamask/controllers'; +import deepEqual from 'fast-deep-equal'; + type PlainObject = Record; export function isPlainObject(value: unknown): value is PlainObject { @@ -27,6 +30,20 @@ export function isNonEmptyArray(value: T[]): value is NonEmptyArray { return Array.isArray(value) && value.length > 0; } +/** + * {@link Json} type guard. + * + * @param value - The value to check. + * @returns Whether the value is valid JSON. + */ +export function isValidJson(value: unknown): value is Json { + try { + return deepEqual(value, JSON.parse(JSON.stringify(value))); + } catch (_) { + return false; + } +} + /** * Makes every specified property of the specified object type mutable. * From 606ed4f5c9f087f8259bf0bd26767b35f712b855 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 3 Nov 2021 20:57:28 -0700 Subject: [PATCH 125/147] Apply suggestions from code review Co-authored-by: Mark Stacey --- .../src/permissions/PermissionController.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index f592c4311d..7e5366d66f 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -818,7 +818,7 @@ export class PermissionController< } /** - * Gets the caveat of the specified type, if any, for the the permission of + * Gets the caveat of the specified type, if any, for the permission of * the subject corresponding to the given origin. * * Throws an error if the subject does not have a permission with the @@ -1532,7 +1532,7 @@ export class PermissionController< throw new CaveatInvalidJsonError(requestedCaveat, origin, target); } - const caveat = _constructCaveat(type, (value as Json) ?? null); + const caveat = _constructCaveat(type, value ?? null); this.validateCaveat(caveat, origin, target); return caveat as ExtractCaveats; } @@ -1626,7 +1626,7 @@ export class PermissionController< /** * Validates requested permissions. Throws if validation fails. * - * This method ensures that the the requested permissions are a properly + * This method ensures that the requested permissions are a properly * formatted {@link RequestedPermissions} object, that its keys correspond to * the `parentCapability` fields of its values, and that the requested * capability exists. @@ -1851,14 +1851,14 @@ export class PermissionController< } /** - * Executes a restricted method as the subject with the the given origin. + * Executes a restricted method as the subject with the given origin. * The specified params, if any, will be passed to the method implementation. * * **ATTN:** Great caution should be exercised in the use of this method. * Methods that cause side effects or affect application state should * be avoided. * - * This method will first attempt to retrieve the requested restrictded method + * This method will first attempt to retrieve the requested restricted method * implementation, throwing if it does not exist. The method will then be * invoked as though the subject with the specified origin had invoked it with * the specified parameters. This means that any existing caveats will be @@ -1933,7 +1933,7 @@ export class PermissionController< throw unauthorized({ data: { origin, method } }); } - return decorateWithCaveats( + return decorateWithCaveats( methodImplementation, permission, this._caveatSpecifications, From e1e50ddfb3f461f2e732194f7b65c8f471ebd6eb Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 3 Nov 2021 21:01:47 -0700 Subject: [PATCH 126/147] Clean up some target name and key parameter types --- .../src/permissions/PermissionController.ts | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 7e5366d66f..544bd4bd90 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -583,12 +583,7 @@ export class PermissionController< getRestrictedMethod( method: string, ): RestrictedMethod | undefined { - const targetKey = this.getTargetKey( - method as ExtractPermission< - ControllerPermissionSpecification, - ControllerCaveatSpecification - >['parentCapability'], - ); + const targetKey = this.getTargetKey(method); if (!targetKey) { return undefined; } @@ -1254,10 +1249,7 @@ export class PermissionController< * @returns The internal key of the method. */ private getTargetKey( - method: ExtractPermission< - ControllerPermissionSpecification, - ControllerCaveatSpecification - >['parentCapability'], + method: string, ): ControllerPermissionSpecification['targetKey'] | undefined { if (hasProperty(this._permissionSpecifications, method)) { return method; @@ -1955,12 +1947,7 @@ export class PermissionController< method: string, origin: OriginString, ): RestrictedMethod { - const methodImplementation = this.getRestrictedMethod( - method as ExtractPermission< - ControllerPermissionSpecification, - ControllerCaveatSpecification - >['parentCapability'], - ); + const methodImplementation = this.getRestrictedMethod(method); if (!methodImplementation) { throw methodNotFound({ method, data: { origin } }); } From fb667ff6d666dc565f770aa45502b9784d6d4033 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 3 Nov 2021 21:15:01 -0700 Subject: [PATCH 127/147] Rationalize getRestrictedMethod implementations --- .../permissions/PermissionController.test.ts | 6 ++- .../src/permissions/PermissionController.ts | 43 +++++++------------ 2 files changed, 20 insertions(+), 29 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 9d2faf1c27..bb2f86ac4a 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -589,9 +589,11 @@ describe('PermissionController', () => { ).toStrictEqual('Hello, secret friend "foo"!'); }); - it('returns undefined if the method does not exist', () => { + it('throws an error if the method does not exist', () => { const controller = getDefaultPermissionController(); - expect(controller.getRestrictedMethod('foo' as any)).toBeUndefined(); + expect(() => controller.getRestrictedMethod('foo')).toThrow( + errors.methodNotFound({ method: 'foo' }), + ); }); }); diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 544bd4bd90..150b071feb 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -460,7 +460,7 @@ export class PermissionController< this.registerMessageHandlers(); this.createPermissionMiddleware = getPermissionMiddlewareFactory({ executeRestrictedMethod: this._executeRestrictedMethod.bind(this), - getRestrictedMethod: this._getRestrictedMethod.bind(this), + getRestrictedMethod: this.getRestrictedMethod.bind(this), isUnrestrictedMethod: this.unrestrictedMethods.has.bind( this.unrestrictedMethods, ), @@ -577,15 +577,27 @@ export class PermissionController< /** * Gets the implementation of the specified restricted method. * + * A JSON-RPC error is thrown if the method does not exist. + * + * @see {@link PermissionController.executeRestrictedMethod} and + * {@link PermissionController.createPermissionMiddleware} for internal usage. * @param method - The name of the restricted method. + * @param origin - The origin associated with the request for the restricted + * method, if any. * @returns The restricted method implementation. */ getRestrictedMethod( method: string, - ): RestrictedMethod | undefined { + origin?: string, + ): RestrictedMethod { const targetKey = this.getTargetKey(method); if (!targetKey) { - return undefined; + const error = methodNotFound({ method }); + if (origin) { + error.data = { origin }; + } + + throw error; } return this.getPermissionSpecification(targetKey).methodImplementation; @@ -1875,7 +1887,7 @@ export class PermissionController< params?: RestrictedMethodParameters, ): Promise { // Throws if the method does not exist - const methodImplementation = this._getRestrictedMethod(methodName, origin); + const methodImplementation = this.getRestrictedMethod(methodName, origin); const result = await this._executeRestrictedMethod( methodImplementation, @@ -1931,27 +1943,4 @@ export class PermissionController< this._caveatSpecifications, )({ method, params, context: { origin } }); } - - /** - * Like {@link PermissionController.getRestrictedMethod}, except the request - * for the restricted method is associated with a specific subject, and a - * JSON-RPC error is thrown if the method does not exist. - * - * @see {@link PermissionController.executeRestrictedMethod} and - * {@link PermissionController.createPermissionMiddleware} for usage. - * @param method - The name of the method to get. - * @param request - The request associated with the request for the method. - * @returns - The restricted method implementation, if it exists. - */ - private _getRestrictedMethod( - method: string, - origin: OriginString, - ): RestrictedMethod { - const methodImplementation = this.getRestrictedMethod(method); - if (!methodImplementation) { - throw methodNotFound({ method, data: { origin } }); - } - - return methodImplementation; - } } From 47ff014736528848ce1d49910b8ed9e63753f339 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 3 Nov 2021 21:18:09 -0700 Subject: [PATCH 128/147] Fix a comment --- packages/controllers/src/permissions/PermissionController.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 150b071feb..faf60fb3ab 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -1912,7 +1912,8 @@ export class PermissionController< * caveats of its permission. Throws if the subject does not have the * requisite permission. * - * **ATTN:** Assumes that all parameters are valid. + * **ATTN:** Parameter validation is the responsibility of the caller, or + * the restricted method implementation in the case of `params`. * * @see {@link PermissionController.executeRestrictedMethod} and * {@link PermissionController.createPermissionMiddleware} for usage. From de43571e1fcc1c0b1d9ca6606430403bb4096984 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 3 Nov 2021 21:21:13 -0700 Subject: [PATCH 129/147] fixup! Fix a comment --- packages/controllers/src/permissions/permission-middleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/controllers/src/permissions/permission-middleware.ts b/packages/controllers/src/permissions/permission-middleware.ts index 48a4c6d5b8..a07ee36de4 100644 --- a/packages/controllers/src/permissions/permission-middleware.ts +++ b/packages/controllers/src/permissions/permission-middleware.ts @@ -21,7 +21,7 @@ import type { PermissionController } from './PermissionController'; type PermissionMiddlewareFactoryOptions = { executeRestrictedMethod: GenericPermissionController['_executeRestrictedMethod']; - getRestrictedMethod: GenericPermissionController['_getRestrictedMethod']; + getRestrictedMethod: GenericPermissionController['getRestrictedMethod']; isUnrestrictedMethod: (method: string) => boolean; }; From 91cf3df54c941a228cb3503b538073c1aed104ec Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 3 Nov 2021 21:45:00 -0700 Subject: [PATCH 130/147] Address review feedback --- packages/controllers/src/permissions/Permission.ts | 3 +-- packages/controllers/src/permissions/PermissionController.ts | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 1fe05f6609..19e650c70d 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -240,8 +240,7 @@ export function constructPermission< } /** - * Gets the the caveat of the specified type belonging to the specified - * permission. + * Gets the caveat of the specified type belonging to the specified permission. * * @param permission The permission whose caveat to retrieve. * @param caveatType The type of the caveat to retrieve. diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index faf60fb3ab..2d9229c5a7 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -1643,7 +1643,7 @@ export class PermissionController< */ private validateRequestedPermissions( origin: OriginString, - requestedPermissions: RequestedPermissions | unknown, + requestedPermissions: unknown, ): void { if (!isPlainObject(requestedPermissions)) { throw invalidParams({ @@ -1720,7 +1720,7 @@ export class PermissionController< * @param originalMetadata - The original request metadata. */ private validateApprovedPermissions( - approvedRequest: PermissionsRequest | unknown, + approvedRequest: unknown, originalMetadata: PermissionsRequestMetadata, ) { const { id, origin } = originalMetadata; @@ -1824,6 +1824,7 @@ export class PermissionController< * @see {@link PermissionController.acceptPermissionsRequest} and * {@link PermissionController.rejectPermissionsRequest} for usage. * @param options - The {@link HasApprovalRequest} options. + * @param options.id - The id of the approval request to check for. * @returns Whether the specified request exists. */ private hasApprovalRequest(options: { id: string }): boolean { From 0655728ba6af1d91e0c0307b10de1a70e6d6e1b4 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 9 Nov 2021 15:43:38 -0800 Subject: [PATCH 131/147] Apply suggestions from code review Co-authored-by: Mark Stacey --- packages/controllers/src/permissions/Caveat.ts | 18 ++++-------------- .../controllers/src/permissions/Permission.ts | 4 +--- .../src/permissions/PermissionController.ts | 3 +-- packages/controllers/src/permissions/README.md | 2 +- .../src/permissions/permission-middleware.ts | 2 +- 5 files changed, 8 insertions(+), 21 deletions(-) diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index b4f8cf7846..7349ef3409 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -187,15 +187,6 @@ export type ExtractCaveats< > : never; -/** - * Internal utility type, because using parameterized types in conditional types - * causes weird things to happen. - */ -type _ExtractCaveat< - CaveatUnion extends CaveatConstraint, - CaveatType extends string, -> = CaveatUnion extends Caveat ? CaveatUnion : never; - /** * Extracts the type of a specific {@link Caveat} from a union of caveat * specifications. @@ -206,7 +197,7 @@ type _ExtractCaveat< export type ExtractCaveat< CaveatSpecifications extends CaveatSpecification, CaveatType extends string, -> = _ExtractCaveat, CaveatType>; +> = Extract, { type: CaveatType }>; /** * Extracts the value type of a specific {@link Caveat} from a union of caveat @@ -238,10 +229,9 @@ export function decorateWithCaveats< return methodImplementation; } - let decorated = methodImplementation as AsyncRestrictedMethod< - RestrictedMethodParameters, - Json - >; + let decorated = async ( + args: Parameters>[0], + ) => methodImplementation(args); for (const caveat of caveats) { const specification = diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 19e650c70d..e312e2bdf1 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -169,7 +169,7 @@ type ExtractArrayMembers = ArrayType extends /** * A utility type for extracting the allowed caveat types for a particular - * permission from a permission specification type or type union. + * permission from a permission specification type. * * @template PermissionSpecification - The permission specification type to * extract valid caveat types from. @@ -211,8 +211,6 @@ export type PermissionOptions = { * See {@link Caveat}. */ caveats?: NonEmptyArray; - // TODO: Refactor allowed caveats validation to enable this? - // caveats?: NonEmptyArray>; }; /** diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 2d9229c5a7..d447f317ce 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -1591,8 +1591,7 @@ export class PermissionController< preserveExistingPermissions = true, ): Promise< [ - Record< - string, + SubjectPermissions< ExtractPermission< ControllerPermissionSpecification, ControllerCaveatSpecification diff --git a/packages/controllers/src/permissions/README.md b/packages/controllers/src/permissions/README.md index f4f989d0d6..40d76d9792 100644 --- a/packages/controllers/src/permissions/README.md +++ b/packages/controllers/src/permissions/README.md @@ -78,7 +78,7 @@ const caveatSpecifications = { caveat: FilterArrayCaveat, ) => async (args: RestrictedMethodOptions) => { - const result: string[] | unknown = await method(args); + const result = await method(args); if (!Array.isArray(result)) { throw Error('not an array'); } diff --git a/packages/controllers/src/permissions/permission-middleware.ts b/packages/controllers/src/permissions/permission-middleware.ts index a07ee36de4..e4343cc0c2 100644 --- a/packages/controllers/src/permissions/permission-middleware.ts +++ b/packages/controllers/src/permissions/permission-middleware.ts @@ -76,7 +76,7 @@ export function getPermissionMiddlewareFactory< const result = await executeRestrictedMethod( methodImplementation, subject, - method as Permission['parentCapability'], + method, params, ); From 2bb2c5501e85c53bf06b7ca0419c068e5c446b7b Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 9 Nov 2021 16:43:15 -0800 Subject: [PATCH 132/147] fixup! Apply suggestions from code review --- .../src/permissions/Caveat.test.ts | 21 ++++++++++--------- .../src/permissions/permission-middleware.ts | 5 +---- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/controllers/src/permissions/Caveat.test.ts b/packages/controllers/src/permissions/Caveat.test.ts index 97943e59a8..e3ff7f1431 100644 --- a/packages/controllers/src/permissions/Caveat.test.ts +++ b/packages/controllers/src/permissions/Caveat.test.ts @@ -9,13 +9,14 @@ import { // This function is mostly tested through the PermissionController tests, // we just add a couple here for completeness. describe('decorateWithCaveats', () => { - it('decorates a method with caveat', () => { + it('decorates a method with caveat', async () => { const methodImplementation = () => [1, 2, 3]; const caveatSpecifications = { reverse: { - decorator: (method: any, _caveat: any) => () => - method().reverse() as any, + decorator: (method: any, _caveat: any) => async () => { + return (await method()).reverse(); + }, }, } as unknown as CaveatSpecificationMap; @@ -23,14 +24,14 @@ describe('decorateWithCaveats', () => { caveats: [{ type: 'reverse', value: null }], } as unknown as PermissionConstraint; + const decorated = decorateWithCaveats( + methodImplementation, + permission, + caveatSpecifications, + ); + expect(methodImplementation()).toStrictEqual([1, 2, 3]); - expect( - decorateWithCaveats( - methodImplementation, - permission, - caveatSpecifications, - )({} as any), - ).toStrictEqual([3, 2, 1]); + expect(await decorated({} as any)).toStrictEqual([3, 2, 1]); }); it('throws an error if the caveat type is unrecognized', () => { diff --git a/packages/controllers/src/permissions/permission-middleware.ts b/packages/controllers/src/permissions/permission-middleware.ts index e4343cc0c2..3cc9d79eea 100644 --- a/packages/controllers/src/permissions/permission-middleware.ts +++ b/packages/controllers/src/permissions/permission-middleware.ts @@ -9,7 +9,6 @@ import { import { internalError } from './errors'; import { GenericPermissionController, - PermissionConstraint, PermissionSubjectMetadata, RestrictedMethodParameters, } from '.'; @@ -47,9 +46,7 @@ type PermissionMiddlewareFactoryOptions = { * particular method is unrestricted. * @returns A permission middleware factory function. */ -export function getPermissionMiddlewareFactory< - Permission extends PermissionConstraint, ->({ +export function getPermissionMiddlewareFactory({ executeRestrictedMethod, getRestrictedMethod, isUnrestrictedMethod, From c2fd150790ac2124c2337c55854f519f71eb1788 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 9 Nov 2021 16:57:28 -0800 Subject: [PATCH 133/147] Remove constructCaveat --- .../controllers/src/permissions/Caveat.ts | 15 -- .../permissions/PermissionController.test.ts | 142 +++++++++--------- .../src/permissions/PermissionController.ts | 3 +- 3 files changed, 72 insertions(+), 88 deletions(-) diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index 7349ef3409..1804f7daa4 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -48,21 +48,6 @@ export type Caveat = { readonly value: Value; }; -/** - * The {@link Caveat} factory function. Naively constructs a new caveat from the - * inputs. Sets `value` to `null` if no value is provided. - * - * @param type - The type of the caveat. - * @param value - The value associated with the caveat, if any. - * @returns The new caveat object. - */ -export function constructCaveat( - type: Type, - value: Value, -): Caveat { - return { type, value }; -} - // Next, we define types used for specifying caveats at the consumer layer, // and a function for applying caveats to a restricted method request. This is // Accomplished by decorating the restricted method implementation with the diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index bb2f86ac4a..5012b72b95 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -13,7 +13,6 @@ import * as errors from './errors'; import { AsyncRestrictedMethod, Caveat, - constructCaveat, constructPermission, CaveatConstraint, PermissionConstraint, @@ -253,12 +252,12 @@ function getDefaultPermissionSpecifications() { factory: (options: PermissionOptions) => constructPermission({ ...options, - caveats: [constructCaveat(CaveatTypes.noopCaveat, null)], + caveats: [{ type: CaveatTypes.noopCaveat, value: null }], }) as SecretNamespacedPermission, validator: (permission: PermissionConstraint) => { assert.deepStrictEqual( permission.caveats, - [constructCaveat(CaveatTypes.noopCaveat, null)], + [{ type: CaveatTypes.noopCaveat, value: null }], 'getSecret_* permission validation failed', ); }, @@ -957,7 +956,7 @@ describe('PermissionController', () => { permissions: { [PermissionNames.wallet_getSecret_('foo')]: getPermissionMatcher( PermissionNames.wallet_getSecret_('foo'), - [constructCaveat(CaveatTypes.noopCaveat, null)], + [{ type: CaveatTypes.noopCaveat, value: null }], origin3, ), }, @@ -1073,7 +1072,7 @@ describe('PermissionController', () => { ), [PermissionNames.wallet_getSecret_('foo')]: getPermissionMatcher( PermissionNames.wallet_getSecret_('foo'), - [constructCaveat(CaveatTypes.noopCaveat, null)], + [{ type: CaveatTypes.noopCaveat, value: null }], origin2, ), }, @@ -1083,7 +1082,7 @@ describe('PermissionController', () => { permissions: { [PermissionNames.wallet_getSecret_('foo')]: getPermissionMatcher( PermissionNames.wallet_getSecret_('foo'), - [constructCaveat(CaveatTypes.noopCaveat, null)], + [{ type: CaveatTypes.noopCaveat, value: null }], origin3, ), }, @@ -1098,7 +1097,7 @@ describe('PermissionController', () => { ), [PermissionNames.wallet_getSecret_('bar')]: getPermissionMatcher( PermissionNames.wallet_getSecret_('bar'), - [constructCaveat(CaveatTypes.noopCaveat, null)], + [{ type: CaveatTypes.noopCaveat, value: null }], origin4, ), }, @@ -1119,7 +1118,7 @@ describe('PermissionController', () => { [PermissionNames.wallet_getSecretArray]: {}, [PermissionNames.wallet_getSecretObject]: { caveats: [ - constructCaveat(CaveatTypes.filterObjectResponse, ['kaplar']), + { type: CaveatTypes.filterObjectResponse, value: ['kaplar'] }, ], }, [PermissionNames.wallet_getSecret_('foo')]: {}, @@ -1179,7 +1178,7 @@ describe('PermissionController', () => { [PermissionNames.wallet_getSecretArray]: {}, [PermissionNames.wallet_getSecretObject]: { caveats: [ - constructCaveat(CaveatTypes.filterObjectResponse, ['kaplar']), + { type: CaveatTypes.filterObjectResponse, value: ['kaplar'] }, ], }, [PermissionNames.wallet_getSecret_('foo')]: {}, @@ -1200,9 +1199,10 @@ describe('PermissionController', () => { PermissionNames.wallet_getSecretObject, CaveatTypes.filterObjectResponse, ), - ).toStrictEqual( - constructCaveat(CaveatTypes.filterObjectResponse, ['kaplar']), - ); + ).toStrictEqual({ + type: CaveatTypes.filterObjectResponse, + value: ['kaplar'], + }); expect( controller.getCaveat( @@ -1210,7 +1210,7 @@ describe('PermissionController', () => { PermissionNames.wallet_getSecret_('foo'), CaveatTypes.noopCaveat, ), - ).toStrictEqual(constructCaveat(CaveatTypes.noopCaveat, null)); + ).toStrictEqual({ type: CaveatTypes.noopCaveat, value: null }); }); it('throws an error if no corresponding permission exists', () => { @@ -1256,7 +1256,7 @@ describe('PermissionController', () => { permissions: { wallet_getSecretArray: getPermissionMatcher( 'wallet_getSecretArray', - [constructCaveat(CaveatTypes.filterArrayResponse, ['foo'])], + [{ type: CaveatTypes.filterArrayResponse, value: ['foo'] }], origin, ), }, @@ -1274,7 +1274,7 @@ describe('PermissionController', () => { approvedPermissions: { [PermissionNames.wallet_getSecretObject]: { caveats: [ - constructCaveat(CaveatTypes.filterObjectResponse, ['foo']), + { type: CaveatTypes.filterObjectResponse, value: ['foo'] }, ], }, }, @@ -1287,7 +1287,7 @@ describe('PermissionController', () => { permissions: { wallet_getSecretObject: getPermissionMatcher( 'wallet_getSecretObject', - [constructCaveat(CaveatTypes.filterObjectResponse, ['foo'])], + [{ type: CaveatTypes.filterObjectResponse, value: ['foo'] }], origin, ), }, @@ -1310,8 +1310,8 @@ describe('PermissionController', () => { wallet_getSecretObject: getPermissionMatcher( 'wallet_getSecretObject', [ - constructCaveat(CaveatTypes.filterObjectResponse, ['foo']), - constructCaveat(CaveatTypes.noopCaveat, null), + { type: CaveatTypes.filterObjectResponse, value: ['foo'] }, + { type: CaveatTypes.noopCaveat, value: null }, ], origin, ), @@ -1330,7 +1330,7 @@ describe('PermissionController', () => { approvedPermissions: { [PermissionNames.wallet_getSecretObject]: { caveats: [ - constructCaveat(CaveatTypes.filterObjectResponse, ['kaplar']), + { type: CaveatTypes.filterObjectResponse, value: ['kaplar'] }, ], }, }, @@ -1380,7 +1380,7 @@ describe('PermissionController', () => { approvedPermissions: { [PermissionNames.wallet_getSecretObject]: { caveats: [ - constructCaveat(CaveatTypes.filterObjectResponse, ['foo']), + { type: CaveatTypes.filterObjectResponse, value: ['foo'] }, ], }, }, @@ -1413,7 +1413,7 @@ describe('PermissionController', () => { approvedPermissions: { [PermissionNames.wallet_getSecretArray]: { caveats: [ - constructCaveat(CaveatTypes.filterArrayResponse, ['foo']), + { type: CaveatTypes.filterArrayResponse, value: ['foo'] }, ], }, }, @@ -1426,7 +1426,7 @@ describe('PermissionController', () => { permissions: { wallet_getSecretArray: getPermissionMatcher( 'wallet_getSecretArray', - [constructCaveat(CaveatTypes.filterArrayResponse, ['foo'])], + [{ type: CaveatTypes.filterArrayResponse, value: ['foo'] }], origin, ), }, @@ -1448,7 +1448,7 @@ describe('PermissionController', () => { permissions: { wallet_getSecretArray: getPermissionMatcher( 'wallet_getSecretArray', - [constructCaveat(CaveatTypes.filterArrayResponse, ['bar'])], + [{ type: CaveatTypes.filterArrayResponse, value: ['bar'] }], origin, ), }, @@ -1511,7 +1511,7 @@ describe('PermissionController', () => { subject: { origin }, approvedPermissions: { [PermissionNames.wallet_getSecret_('foo')]: { - caveats: [constructCaveat(CaveatTypes.noopCaveat, null)], + caveats: [{ type: CaveatTypes.noopCaveat, value: null }], }, }, }); @@ -1537,7 +1537,7 @@ describe('PermissionController', () => { approvedPermissions: { [PermissionNames.wallet_getSecretArray]: { caveats: [ - constructCaveat(CaveatTypes.filterArrayResponse, ['foo']), + { type: CaveatTypes.filterArrayResponse, value: ['foo'] }, ], }, }, @@ -1550,7 +1550,7 @@ describe('PermissionController', () => { permissions: { wallet_getSecretArray: getPermissionMatcher( 'wallet_getSecretArray', - [constructCaveat(CaveatTypes.filterArrayResponse, ['foo'])], + [{ type: CaveatTypes.filterArrayResponse, value: ['foo'] }], origin, ), }, @@ -1589,8 +1589,8 @@ describe('PermissionController', () => { approvedPermissions: { [PermissionNames.wallet_getSecretObject]: { caveats: [ - constructCaveat(CaveatTypes.noopCaveat, null), - constructCaveat(CaveatTypes.filterObjectResponse, ['foo']), + { type: CaveatTypes.noopCaveat, value: null }, + { type: CaveatTypes.filterObjectResponse, value: ['foo'] }, ], }, }, @@ -1604,8 +1604,8 @@ describe('PermissionController', () => { wallet_getSecretObject: getPermissionMatcher( 'wallet_getSecretObject', [ - constructCaveat(CaveatTypes.noopCaveat, null), - constructCaveat(CaveatTypes.filterObjectResponse, ['foo']), + { type: CaveatTypes.noopCaveat, value: null }, + { type: CaveatTypes.filterObjectResponse, value: ['foo'] }, ], origin, ), @@ -1627,7 +1627,7 @@ describe('PermissionController', () => { permissions: { wallet_getSecretObject: getPermissionMatcher( 'wallet_getSecretObject', - [constructCaveat(CaveatTypes.filterObjectResponse, ['foo'])], + [{ type: CaveatTypes.filterObjectResponse, value: ['foo'] }], origin, ), }, @@ -1664,7 +1664,7 @@ describe('PermissionController', () => { [PermissionNames.wallet_getSecretArray]: {}, [PermissionNames.wallet_getSecretObject]: { caveats: [ - constructCaveat(CaveatTypes.filterObjectResponse, ['foo']), + { type: CaveatTypes.filterObjectResponse, value: ['foo'] }, ], }, }, @@ -1707,7 +1707,7 @@ describe('PermissionController', () => { subject: { origin }, approvedPermissions: { [PermissionNames.wallet_getSecret_('foo')]: { - caveats: [constructCaveat(CaveatTypes.noopCaveat, null)], + caveats: [{ type: CaveatTypes.noopCaveat, value: null }], }, }, }); @@ -1741,7 +1741,7 @@ describe('PermissionController', () => { subject: { origin: MultiCaveatOrigins.a }, approvedPermissions: { [PermissionNames.wallet_getSecretArray]: { - caveats: [constructCaveat(CaveatTypes.filterArrayResponse, ['a'])], + caveats: [{ type: CaveatTypes.filterArrayResponse, value: ['a'] }], }, }, }); @@ -1751,18 +1751,18 @@ describe('PermissionController', () => { approvedPermissions: { [PermissionNames.wallet_getSecretArray]: { caveats: [ - constructCaveat(CaveatTypes.filterArrayResponse, ['b']), - constructCaveat(CaveatTypes.reverseArrayResponse, null), + { type: CaveatTypes.filterArrayResponse, value: ['b'] }, + { type: CaveatTypes.reverseArrayResponse, value: null }, ], }, [PermissionNames.wallet_getSecretObject]: { caveats: [ - constructCaveat(CaveatTypes.filterObjectResponse, ['b']), - constructCaveat(CaveatTypes.noopCaveat, null), + { type: CaveatTypes.filterObjectResponse, value: ['b'] }, + { type: CaveatTypes.noopCaveat, value: null }, ], }, [PermissionNames.wallet_getSecret_('foo')]: { - caveats: [constructCaveat(CaveatTypes.noopCaveat, null)], + caveats: [{ type: CaveatTypes.noopCaveat, value: null }], }, [PermissionNames.wallet_doubleNumber]: {}, }, @@ -1772,10 +1772,10 @@ describe('PermissionController', () => { subject: { origin: MultiCaveatOrigins.c }, approvedPermissions: { [PermissionNames.wallet_getSecretObject]: { - caveats: [constructCaveat(CaveatTypes.filterObjectResponse, ['c'])], + caveats: [{ type: CaveatTypes.filterObjectResponse, value: ['c'] }], }, [PermissionNames.wallet_getSecret_('bar')]: { - caveats: [constructCaveat(CaveatTypes.noopCaveat, null)], + caveats: [{ type: CaveatTypes.noopCaveat, value: null }], }, }, }); @@ -1795,7 +1795,7 @@ describe('PermissionController', () => { permissions: { [PermissionNames.wallet_getSecretArray]: getPermissionMatcher( PermissionNames.wallet_getSecretArray, - [constructCaveat(CaveatTypes.filterArrayResponse, ['a'])], + [{ type: CaveatTypes.filterArrayResponse, value: ['a'] }], MultiCaveatOrigins.a, ), ...overrides[MultiCaveatOrigins.a], @@ -1808,22 +1808,22 @@ describe('PermissionController', () => { [PermissionNames.wallet_getSecretArray]: getPermissionMatcher( PermissionNames.wallet_getSecretArray, [ - constructCaveat(CaveatTypes.filterArrayResponse, ['b']), - constructCaveat(CaveatTypes.reverseArrayResponse, null), + { type: CaveatTypes.filterArrayResponse, value: ['b'] }, + { type: CaveatTypes.reverseArrayResponse, value: null }, ], MultiCaveatOrigins.b, ), [PermissionNames.wallet_getSecretObject]: getPermissionMatcher( PermissionNames.wallet_getSecretObject, [ - constructCaveat(CaveatTypes.filterObjectResponse, ['b']), - constructCaveat(CaveatTypes.noopCaveat, null), + { type: CaveatTypes.filterObjectResponse, value: ['b'] }, + { type: CaveatTypes.noopCaveat, value: null }, ], MultiCaveatOrigins.b, ), [PermissionNames.wallet_getSecret_('foo')]: getPermissionMatcher( PermissionNames.wallet_getSecret_('foo'), - [constructCaveat(CaveatTypes.noopCaveat, null)], + [{ type: CaveatTypes.noopCaveat, value: null }], MultiCaveatOrigins.b, ), [PermissionNames.wallet_doubleNumber]: getPermissionMatcher( @@ -1840,12 +1840,12 @@ describe('PermissionController', () => { permissions: { [PermissionNames.wallet_getSecretObject]: getPermissionMatcher( PermissionNames.wallet_getSecretObject, - [constructCaveat(CaveatTypes.filterObjectResponse, ['c'])], + [{ type: CaveatTypes.filterObjectResponse, value: ['c'] }], MultiCaveatOrigins.c, ), [PermissionNames.wallet_getSecret_('bar')]: getPermissionMatcher( PermissionNames.wallet_getSecret_('bar'), - [constructCaveat(CaveatTypes.noopCaveat, null)], + [{ type: CaveatTypes.noopCaveat, value: null }], MultiCaveatOrigins.c, ), ...overrides[MultiCaveatOrigins.c], @@ -1910,7 +1910,7 @@ describe('PermissionController', () => { [MultiCaveatOrigins.a]: { [PermissionNames.wallet_getSecretArray]: getPermissionMatcher( PermissionNames.wallet_getSecretArray, - [constructCaveat(CaveatTypes.filterArrayResponse, ['a', 'b'])], + [{ type: CaveatTypes.filterArrayResponse, value: ['a', 'b'] }], MultiCaveatOrigins.a, ), }, @@ -1918,8 +1918,8 @@ describe('PermissionController', () => { [PermissionNames.wallet_getSecretArray]: getPermissionMatcher( PermissionNames.wallet_getSecretArray, [ - constructCaveat(CaveatTypes.filterArrayResponse, ['a', 'b']), - constructCaveat(CaveatTypes.reverseArrayResponse, null), + { type: CaveatTypes.filterArrayResponse, value: ['a', 'b'] }, + { type: CaveatTypes.reverseArrayResponse, value: null }, ], MultiCaveatOrigins.b, ), @@ -1953,8 +1953,8 @@ describe('PermissionController', () => { [PermissionNames.wallet_getSecretArray]: getPermissionMatcher( PermissionNames.wallet_getSecretArray, [ - constructCaveat(CaveatTypes.filterArrayResponse, ['a', 'b']), - constructCaveat(CaveatTypes.reverseArrayResponse, null), + { type: CaveatTypes.filterArrayResponse, value: ['a', 'b'] }, + { type: CaveatTypes.reverseArrayResponse, value: null }, ], MultiCaveatOrigins.b, ), @@ -1985,7 +1985,7 @@ describe('PermissionController', () => { [MultiCaveatOrigins.b]: { [PermissionNames.wallet_getSecretArray]: getPermissionMatcher( PermissionNames.wallet_getSecretArray, - [constructCaveat(CaveatTypes.reverseArrayResponse, null)], + [{ type: CaveatTypes.reverseArrayResponse, value: null }], MultiCaveatOrigins.b, ), }, @@ -2155,7 +2155,7 @@ describe('PermissionController', () => { permissions: { wallet_getSecret_kabob: getPermissionMatcher( 'wallet_getSecret_kabob', - [constructCaveat(CaveatTypes.noopCaveat, null)], + [{ type: CaveatTypes.noopCaveat, value: null }], ), }, }, @@ -2189,7 +2189,7 @@ describe('PermissionController', () => { permissions: { wallet_getSecret_kabob: getPermissionMatcher( 'wallet_getSecret_kabob', - [constructCaveat(CaveatTypes.noopCaveat, null)], + [{ type: CaveatTypes.noopCaveat, value: null }], origin1, ), }, @@ -2418,7 +2418,7 @@ describe('PermissionController', () => { wallet_getSecretArray: { caveats: [ { - ...constructCaveat(CaveatTypes.filterArrayResponse, ['foo']), + ...{ type: CaveatTypes.filterArrayResponse, value: ['foo'] }, bar: 'bar', }, ] as any, @@ -2428,7 +2428,7 @@ describe('PermissionController', () => { ).toThrow( new errors.InvalidCaveatFieldsError( { - ...constructCaveat(CaveatTypes.filterArrayResponse, ['foo']), + ...{ type: CaveatTypes.filterArrayResponse, value: ['foo'] }, bar: 'bar', }, origin, @@ -2476,7 +2476,7 @@ describe('PermissionController', () => { subject: { origin }, approvedPermissions: { wallet_getSecretArray: { - caveats: [constructCaveat('fooType', 'bar')] as any, + caveats: [{ type: 'fooType', value: 'bar' }] as any, }, }, }), @@ -2641,7 +2641,7 @@ describe('PermissionController', () => { approvedPermissions: { [PermissionNames.wallet_noop2]: { caveats: [ - constructCaveat(CaveatTypes.filterArrayResponse, ['foo']), + { type: CaveatTypes.filterArrayResponse, value: ['foo'] }, ], }, }, @@ -2719,7 +2719,7 @@ describe('PermissionController', () => { [PermissionNames.wallet_getSecretArray]: {}, [PermissionNames.wallet_getSecretObject]: { caveats: [ - constructCaveat(CaveatTypes.filterObjectResponse, ['baz']), + { type: CaveatTypes.filterObjectResponse, value: ['baz'] }, ], }, [PermissionNames.wallet_getSecret_('foo')]: {}, @@ -2733,12 +2733,12 @@ describe('PermissionController', () => { ), [PermissionNames.wallet_getSecretObject]: getPermissionMatcher( PermissionNames.wallet_getSecretObject, - [constructCaveat(CaveatTypes.filterObjectResponse, ['baz'])], + [{ type: CaveatTypes.filterObjectResponse, value: ['baz'] }], origin, ), [PermissionNames.wallet_getSecret_('foo')]: getPermissionMatcher( PermissionNames.wallet_getSecret_('foo'), - [constructCaveat(CaveatTypes.noopCaveat, null)], + [{ type: CaveatTypes.noopCaveat, value: null }], origin, ), }, @@ -2757,7 +2757,7 @@ describe('PermissionController', () => { [PermissionNames.wallet_getSecretArray]: {}, [PermissionNames.wallet_getSecretObject]: { caveats: [ - constructCaveat(CaveatTypes.filterObjectResponse, ['baz']), + { type: CaveatTypes.filterObjectResponse, value: ['baz'] }, ], }, [PermissionNames.wallet_getSecret_('foo')]: {}, @@ -3500,7 +3500,7 @@ describe('PermissionController', () => { subject: { origin }, approvedPermissions: { [PermissionNames.wallet_getSecretArray]: { - caveats: [constructCaveat(CaveatTypes.filterArrayResponse, ['b'])], + caveats: [{ type: CaveatTypes.filterArrayResponse, value: ['b'] }], }, }, }); @@ -3522,8 +3522,8 @@ describe('PermissionController', () => { approvedPermissions: { [PermissionNames.wallet_getSecretArray]: { caveats: [ - constructCaveat(CaveatTypes.filterArrayResponse, ['a', 'c']), - constructCaveat(CaveatTypes.reverseArrayResponse, null), + { type: CaveatTypes.filterArrayResponse, value: ['a', 'c'] }, + { type: CaveatTypes.reverseArrayResponse, value: null }, ], }, }, @@ -3714,7 +3714,7 @@ describe('PermissionController', () => { subject: { origin }, approvedPermissions: { [PermissionNames.wallet_getSecretArray]: { - caveats: [constructCaveat(CaveatTypes.filterArrayResponse, ['b'])], + caveats: [{ type: CaveatTypes.filterArrayResponse, value: ['b'] }], }, }, }); @@ -3740,8 +3740,8 @@ describe('PermissionController', () => { approvedPermissions: { [PermissionNames.wallet_getSecretArray]: { caveats: [ - constructCaveat(CaveatTypes.filterArrayResponse, ['a', 'c']), - constructCaveat(CaveatTypes.reverseArrayResponse, null), + { type: CaveatTypes.filterArrayResponse, value: ['a', 'c'] }, + { type: CaveatTypes.reverseArrayResponse, value: null }, ], }, }, diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index d447f317ce..13f8afee78 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -27,7 +27,6 @@ import { isValidJson, } from '../utils'; import { - constructCaveat as _constructCaveat, decorateWithCaveats, ExtractCaveat, ExtractCaveats, @@ -1536,7 +1535,7 @@ export class PermissionController< throw new CaveatInvalidJsonError(requestedCaveat, origin, target); } - const caveat = _constructCaveat(type, value ?? null); + const caveat = { type, value: value ?? null }; this.validateCaveat(caveat, origin, target); return caveat as ExtractCaveats; } From 36efcf7d21eecb90d96292635a1e3909edef50f1 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 9 Nov 2021 18:59:02 -0800 Subject: [PATCH 134/147] Improve requested permission / caveat validation --- .../permissions/PermissionController.test.ts | 89 +++++++ .../src/permissions/PermissionController.ts | 218 +++++++++++------- .../controllers/src/permissions/errors.ts | 26 +++ 3 files changed, 249 insertions(+), 84 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 5012b72b95..2f1b01a4c3 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -2368,6 +2368,32 @@ describe('PermissionController', () => { ); }); + it('throws if an approved permission has duplicate caveats', () => { + const controller = getDefaultPermissionController(); + const origin = 'metamask.io'; + + expect(() => + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + wallet_getSecretArray: { + parentCapability: 'wallet_getSecretArray', + caveats: [ + { type: CaveatTypes.filterArrayResponse, value: ['foo'] }, + { type: CaveatTypes.filterArrayResponse, value: ['foo'] }, + ], + }, + }, + }), + ).toThrow( + new errors.DuplicateCaveatError( + CaveatTypes.filterArrayResponse, + origin, + PermissionNames.wallet_getSecretArray, + ), + ); + }); + it('throws if a requested caveat is not a plain object', () => { const controller = getDefaultPermissionController(); const origin = 'metamask.io'; @@ -2892,6 +2918,69 @@ describe('PermissionController', () => { expect(callActionSpy).not.toHaveBeenCalled(); }); + it('throws if the "caveat" property of a requested permission is invalid', async () => { + const options = getPermissionControllerOptions(); + const { messenger } = options; + const origin = 'metamask.io'; + + const callActionSpy = jest.spyOn(messenger, 'call'); + + const controller = getDefaultPermissionController(options); + for (const invalidCaveatsValue of [ + [], // empty array + undefined, + 'foo', + 2, + Symbol('bar'), + ]) { + await expect( + async () => + await controller.requestPermissions(origin, { + [PermissionNames.wallet_getSecretArray]: { + caveats: invalidCaveatsValue as any, + }, + }), + ).rejects.toThrow( + new errors.InvalidCaveatsPropertyError( + origin, + PermissionNames.wallet_getSecretArray, + invalidCaveatsValue, + ), + ); + + expect(callActionSpy).not.toHaveBeenCalled(); + } + }); + + it('throws if a requested permission has duplicate caveats', async () => { + const options = getPermissionControllerOptions(); + const { messenger } = options; + const origin = 'metamask.io'; + + const callActionSpy = jest.spyOn(messenger, 'call'); + + const controller = getDefaultPermissionController(options); + await expect( + async () => + await controller.requestPermissions(origin, { + [PermissionNames.wallet_getSecretArray]: { + caveats: [ + { type: CaveatTypes.filterArrayResponse, value: ['foo'] }, + { type: CaveatTypes.filterArrayResponse, value: ['foo'] }, + ], + }, + }), + ).rejects.toThrow( + new errors.DuplicateCaveatError( + CaveatTypes.filterArrayResponse, + origin, + PermissionNames.wallet_getSecretArray, + ), + ); + + expect(callActionSpy).not.toHaveBeenCalled(); + }); + it('throws if the approved request object is invalid', async () => { const options = getPermissionControllerOptions(); const { messenger } = options; diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 13f8afee78..3f1cb5ddb6 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -15,8 +15,8 @@ import type { ControllerMessenger, } from '@metamask/controllers'; /* eslint-enable @typescript-eslint/no-unused-vars */ -import { castDraft, castImmutable, Draft, Patch } from 'immer'; import deepFreeze from 'deep-freeze-strict'; +import { castDraft, Draft, Patch } from 'immer'; import { nanoid } from 'nanoid'; import { isPlainObject, @@ -70,6 +70,8 @@ import { PermissionsRequestNotFoundError, ForbiddenCaveatError, CaveatInvalidJsonError, + DuplicateCaveatError, + InvalidCaveatsPropertyError, } from './errors'; import { MethodNames } from './utils'; import { getPermissionMiddlewareFactory } from './permission-middleware'; @@ -997,10 +999,11 @@ export class PermissionController< throw new PermissionDoesNotExistError(origin, target); } - const caveat = this.constructCaveat(origin, target, { + const caveat = { type: caveatType, value: caveatValue, - }); + }; + this.validateCaveat(caveat, origin, target); if (permission.caveats) { const caveatIndex = permission.caveats.findIndex( @@ -1243,7 +1246,7 @@ export class PermissionController< this.validatePermission( this.getPermissionSpecification(targetKey), - castImmutable(permission) as PermissionConstraint, + permission as PermissionConstraint, origin, targetName, ); @@ -1376,6 +1379,7 @@ export class PermissionController< >['parentCapability']; const specification = this.getPermissionSpecification(targetKey); + // The requested caveats are validated here. const caveats = this.constructCaveats( origin, targetName, @@ -1388,10 +1392,33 @@ export class PermissionController< target: targetName, }; - const permission = specification.factory - ? specification.factory(permissionOptions, requestData) - : constructPermission(permissionOptions); - this.validatePermission(specification, permission, origin, targetName); + let permission: ExtractPermission< + ControllerPermissionSpecification, + ControllerCaveatSpecification + >; + if (specification.factory) { + permission = specification.factory(permissionOptions, requestData); + + // Full caveat and permission validation is performed here since the + // factory function can arbitrarily modify the entire permission object, + // including its caveats. + this.validatePermission(specification, permission, origin, targetName); + } else { + permission = constructPermission( + permissionOptions, + ) as ExtractPermission< + ControllerPermissionSpecification, + ControllerCaveatSpecification + >; + + // We do not need to validate caveats in this case, because the plain + // permission constructor function does not modify the caveats, which + // were already validated by `constructCaveats` above. + this.validatePermission(specification, permission, origin, targetName, { + invokePermissionValidator: true, + performCaveatValidation: false, + }); + } permissions[targetName] = permission; } @@ -1401,8 +1428,13 @@ export class PermissionController< /** * Validates the specified permission by: + * - Ensuring that its `caveats` property is either `null` or a non-empty + * array. * - Ensuring that it only includes caveats allowed by its specification. - * - Calling the validator of its specification, if any. + * - Ensuring that it includes no duplicate caveats (by caveat type). + * - Validating each caveat object, if `performCaveatValidation` is `true`. + * - Calling the validator of its specification, if one exists and + * `invokePermissionValidator` is `true`. * * An error is thrown if validation fails. * @@ -1410,6 +1442,12 @@ export class PermissionController< * @param permission - The permission to validate. * @param origin - The origin associated with the permission. * @param targetName - The target name of the permission. + * @param validationOptions - Validation options. + * @param validationOptions.invokePermissionValidator - Whether to invoke the + * permission's consumer-specified validator function, if any. + * @param validationOptions.performCaveatValidation - Whether to invoke + * {@link PermissionController.validateCaveat} on each of the permission's + * caveats. */ private validatePermission( specification: PermissionSpecificationConstraint, @@ -1419,17 +1457,37 @@ export class PermissionController< ControllerPermissionSpecification, ControllerCaveatSpecification >['parentCapability'], + { invokePermissionValidator, performCaveatValidation } = { + invokePermissionValidator: true, + performCaveatValidation: true, + }, ): void { const { allowedCaveats, validator } = specification; - if (permission.caveats !== null) { - permission.caveats.forEach(({ type }) => { - if (!allowedCaveats?.includes(type)) { - throw new ForbiddenCaveatError(type, origin, targetName); + if (hasProperty(permission, 'caveats')) { + const { caveats } = permission; + + if (caveats !== null && !(Array.isArray(caveats) && caveats.length > 0)) { + throw new InvalidCaveatsPropertyError(origin, targetName, caveats); + } + + const seenCaveatTypes = new Set(); + caveats?.forEach((caveat) => { + if (performCaveatValidation) { + this.validateCaveat(caveat, origin, targetName); + } + + if (!allowedCaveats?.includes(caveat.type)) { + throw new ForbiddenCaveatError(caveat.type, origin, targetName); + } + + if (seenCaveatTypes.has(caveat.type)) { + throw new DuplicateCaveatError(caveat.type, origin, targetName); } + seenCaveatTypes.add(caveat.type); }); } - if (validator) { + if (invokePermissionValidator && validator) { validator(permission, origin, targetName); } } @@ -1464,12 +1522,11 @@ export class PermissionController< } /** - * Constructs the requested caveats for the permission of the specified - * subject origin and target name. + * Validates the requested caveats for the permission of the specified + * subject origin and target name and returns the validated caveat array. * * Throws an error if validation fails. * - * @see {@link PermissionController.constructCaveat} for details. * @param origin - The origin of the permission subject. * @param target - The permission target name. * @param requestedCaveats - The requested caveats to construct. @@ -1483,84 +1540,64 @@ export class PermissionController< >['parentCapability'], requestedCaveats?: unknown[] | null, ): NonEmptyArray> | undefined { - const caveatArray = requestedCaveats?.map((requestedCaveat) => - this.constructCaveat(origin, target, requestedCaveat), - ); + const caveatArray = requestedCaveats?.map((requestedCaveat) => { + this.validateCaveat(requestedCaveat, origin, target); + + // Reassign so that we have a fresh object. + const { type, value } = requestedCaveat as CaveatConstraint; + return { type, value } as ExtractCaveats; + }); + return caveatArray && isNonEmptyArray(caveatArray) ? caveatArray : undefined; } /** - * Constructs a caveat for the permission of the specified subject origin and - * target name, per the requested caveat argument. This methods validates - * everything about the requested caveat. It also ensures that a caveat - * specification exist for the requested type, and calls the specification - * validator, if it exists, on the constructed caveat. + * This methods validates that the specified caveat is an object with the + * expected properties and types. It also ensures that a caveat specification + * exists for the requested caveat type, and calls the specification + * validator, if it exists, on the caveat object. * * Throws an error if validation fails. * - * @param origin - The origin of the permission subject. - * @param target - The permission target name. - * @param requestedCaveat - The requested caveat to construct. - * @returns The constructed caveat. - */ - private constructCaveat( - origin: OriginString, - target: ExtractPermission< - ControllerPermissionSpecification, - ControllerCaveatSpecification - >['parentCapability'], - requestedCaveat: unknown, - ): ExtractCaveats { - if (!isPlainObject(requestedCaveat)) { - throw new InvalidCaveatError(requestedCaveat, origin, target); - } - - if (Object.keys(requestedCaveat).length !== 2) { - throw new InvalidCaveatFieldsError(requestedCaveat, origin, target); - } - - const { type, value } = requestedCaveat; - if (typeof type !== 'string') { - throw new InvalidCaveatTypeError(requestedCaveat, origin, target); - } - - if (!hasProperty(requestedCaveat, 'value')) { - throw new CaveatMissingValueError(requestedCaveat, origin, target); - } - - // Assert that the caveat value is valid JSON - if (!isValidJson(value)) { - throw new CaveatInvalidJsonError(requestedCaveat, origin, target); - } - - const caveat = { type, value: value ?? null }; - this.validateCaveat(caveat, origin, target); - return caveat as ExtractCaveats; - } - - /** - * Validates the specified caveat by ensuring that a specification exists for - * its type and invoking the validator function with it, if any. - * * @param caveat - The caveat object to validate. * @param origin - The origin associated with the subject of the parent * permission. * @param target - The target name associated with the parent permission. */ private validateCaveat( - caveat: CaveatConstraint, + caveat: unknown, origin: OriginString, target: string, ): void { - const specification = this.getCaveatSpecification(caveat.type); + if (!isPlainObject(caveat)) { + throw new InvalidCaveatError(caveat, origin, target); + } + if (Object.keys(caveat).length !== 2) { + throw new InvalidCaveatFieldsError(caveat, origin, target); + } + + if (typeof caveat.type !== 'string') { + throw new InvalidCaveatTypeError(caveat, origin, target); + } + + const specification = this.getCaveatSpecification(caveat.type); if (!specification) { throw new UnrecognizedCaveatTypeError(caveat.type, origin, target); } - specification.validator?.(caveat, origin, target); + if (!hasProperty(caveat, 'value') || caveat.value === undefined) { + throw new CaveatMissingValueError(caveat, origin, target); + } + + if (!isValidJson(caveat.value)) { + throw new CaveatInvalidJsonError(caveat, origin, target); + } + + // Typecast: TypeScript still believes that the caveat is a PlainObject. + specification.validator?.(caveat as CaveatConstraint, origin, target); } /** @@ -1614,7 +1651,6 @@ export class PermissionController< await this.requestUserApproval(permissionsRequest); return [ - // Further validation is performed here this.grantPermissions({ subject: { origin }, approvedPermissions, @@ -1629,12 +1665,13 @@ export class PermissionController< * Validates requested permissions. Throws if validation fails. * * This method ensures that the requested permissions are a properly - * formatted {@link RequestedPermissions} object, that its keys correspond to - * the `parentCapability` fields of its values, and that the requested - * capability exists. + * formatted {@link RequestedPermissions} object, and performs the same + * validation as {@link PermissionController.grantPermissions}, except that + * consumer-specified permission validator functions are not called, since + * they are only called on fully constructed, approved permissions that are + * otherwise completely valid. * - * Unrecognzied properties on requested permissions are ignored. Any requested - * caveats are validated in later stages of request processing. + * Unrecognzied properties on requested permissions are ignored. * * @param origin - The origin of the grantee subject. * @param requestedPermissions - The requested permissions. @@ -1659,6 +1696,15 @@ export class PermissionController< for (const methodName of Object.keys(requestedPermissions)) { const permission = requestedPermissions[methodName]; + const targetKey = this.getTargetKey(methodName); + + if (!targetKey) { + throw methodNotFound({ + method: methodName, + data: { origin, requestedPermissions }, + }); + } + if ( !isPlainObject(permission) || (permission.parentCapability !== undefined && @@ -1670,12 +1716,16 @@ export class PermissionController< }); } - if (!this.getTargetKey(methodName)) { - throw methodNotFound({ - method: methodName, - data: { origin, requestedPermissions }, - }); - } + // Here we validate the permission without invoking its validator, if any. + // The validator will be invoked after the permission has been approved. + this.validatePermission( + this.getPermissionSpecification(targetKey), + // Typecast: The permission is still a "PlainObject" here. + permission as PermissionConstraint, + origin, + methodName, + { invokePermissionValidator: false, performCaveatValidation: true }, + ); } } diff --git a/packages/controllers/src/permissions/errors.ts b/packages/controllers/src/permissions/errors.ts index 32008714cd..2699cab065 100644 --- a/packages/controllers/src/permissions/errors.ts +++ b/packages/controllers/src/permissions/errors.ts @@ -112,6 +112,17 @@ export class UnrecognizedCaveatTypeError extends Error { } } +export class InvalidCaveatsPropertyError extends Error { + public data: { origin: string; target: string; caveatsProperty: unknown }; + + constructor(origin: string, target: string, caveatsProperty: unknown) { + super( + `The "caveats" property of permission for "${target}" of subject "${origin}" is invalid. It must be a non-empty array if specified.`, + ); + this.data = { origin, target, caveatsProperty }; + } +} + export class CaveatDoesNotExistError extends Error { constructor(origin: string, target: string, caveatType: string) { super( @@ -210,6 +221,21 @@ export class ForbiddenCaveatError extends Error { } } +export class DuplicateCaveatError extends Error { + public data: { + caveatType: string; + origin: string; + target: string; + }; + + constructor(caveatType: string, origin: string, targetName: string) { + super( + `Permissions for target "${targetName}" contains multiple caveats of type "${caveatType}".`, + ); + this.data = { caveatType, origin, target: targetName }; + } +} + export class PermissionsRequestNotFoundError extends Error { constructor(id: string) { super(`Permissions request with id "${id}" not found.`); From 28a636517da0ea8345319de7849abf0bf8dc9173 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 10 Nov 2021 12:23:33 -0800 Subject: [PATCH 135/147] Make wallet_requestPermissions more readable --- .../controllers/src/permissions/PermissionController.ts | 2 +- .../src/permissions/rpc-methods/requestPermissions.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 3f1cb5ddb6..a45502472e 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -1413,7 +1413,7 @@ export class PermissionController< // We do not need to validate caveats in this case, because the plain // permission constructor function does not modify the caveats, which - // were already validated by `constructCaveats` above. + // were already validated by `constructCaveats` above. this.validatePermission(specification, permission, origin, targetName, { invokePermissionValidator: true, performCaveatValidation: false, diff --git a/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts b/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts index 26a5cdc658..4cb880bb7f 100644 --- a/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts +++ b/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts @@ -54,9 +54,14 @@ async function requestPermissionsImplementation( typeof req.id === 'number' || req.id ? req.id.toString() : nanoid(); try { - res.result = Object.values( - (await requestPermissions(req.params[0], id))[0], + const [requestedPermissions] = req.params; + const [grantedPermissions] = await requestPermissions( + requestedPermissions, + id, ); + + // `wallet_requestPermission` is specified to return an array. + res.result = Object.values(grantedPermissions); return end(); } catch (error) { return end(error); From 04bcbc42338ebc7704ed9d32f3cbfe2ef133f180 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 10 Nov 2021 12:25:27 -0800 Subject: [PATCH 136/147] Improve variable name in wallet_getPermissions implementation --- .../src/permissions/rpc-methods/getPermissions.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/controllers/src/permissions/rpc-methods/getPermissions.ts b/packages/controllers/src/permissions/rpc-methods/getPermissions.ts index f57612d5d4..a5aad701da 100644 --- a/packages/controllers/src/permissions/rpc-methods/getPermissions.ts +++ b/packages/controllers/src/permissions/rpc-methods/getPermissions.ts @@ -17,12 +17,13 @@ export const getPermissionsHandler: PermittedHandlerExport< implementation: getPermissionsImplementation, methodDescription: 'Get permissions.', hookNames: { - getPermissions: true, + getPermissionsForOrigin: true, }, }; export type GetPermissionsHooks = { - getPermissions: () => SubjectPermissions; + // This must be bound to the requesting origin. + getPermissionsForOrigin: () => SubjectPermissions; }; async function getPermissionsImplementation( @@ -30,9 +31,8 @@ async function getPermissionsImplementation( res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { getPermissions }: GetPermissionsHooks, + { getPermissionsForOrigin }: GetPermissionsHooks, ): Promise { - // getPermissions is already bound to the origin - res.result = Object.values(getPermissions() || {}); + res.result = Object.values(getPermissionsForOrigin() || {}); return end(); } From 72524c396a8c98fb9dae1c9a9b9a9fb182447fd6 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 10 Nov 2021 12:28:46 -0800 Subject: [PATCH 137/147] Improve KeyOfTargetName docstring --- packages/controllers/src/permissions/Permission.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index e312e2bdf1..00a939cc3c 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -130,12 +130,13 @@ export type ExtractPermissionTargetNames = Key extends `${infer Base}_*` ? `${Base}_${string}` : Key; /** + * Extracts the permission key of a particular name from a union of keys. * An internal utility type used in {@link ExtractPermissionTargetKey}. * * @template Key - The target key type to extract from. * @template Name - The name whose key to extract. */ -export type KeyOfTargetName< +type KeyOfTargetName< Key extends string, Name extends string, > = Name extends ExtractPermissionTargetNames ? Key : never; From ff0b6eb54e06531fe6180df5651303bf2840c8f4 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 10 Nov 2021 12:32:10 -0800 Subject: [PATCH 138/147] Simplify ExtractArrayMembers --- packages/controllers/src/permissions/Permission.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index 00a939cc3c..b01e53d41c 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -162,10 +162,8 @@ export type ExtractPermissionTargetKey< * * @template ArrayType - The array type whose members to extract. */ -type ExtractArrayMembers = ArrayType extends - | readonly [...infer ArrayMembers] - | [...infer ArrayMembers] - ? ArrayMembers[number] +type ExtractArrayMembers = ArrayType extends any[] | readonly any[] + ? ArrayType[number] : never; /** From bc7fdc3a598791a257298424ade052b067968b16 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 10 Nov 2021 12:35:45 -0800 Subject: [PATCH 139/147] Make ExtractArrayMembers resolve to never for the empty tuple --- packages/controllers/src/permissions/Permission.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index b01e53d41c..f62f249480 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -162,7 +162,9 @@ export type ExtractPermissionTargetKey< * * @template ArrayType - The array type whose members to extract. */ -type ExtractArrayMembers = ArrayType extends any[] | readonly any[] +type ExtractArrayMembers = ArrayType extends [] + ? never + : ArrayType extends any[] | readonly any[] ? ArrayType[number] : never; From 9299cae0e17f43ed86d0afdc8397de9f0f76bd68 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 10 Nov 2021 12:48:04 -0800 Subject: [PATCH 140/147] Simplify ExtractAllowedCaveatTypes --- .../controllers/src/permissions/Permission.ts | 16 ++------ .../permissions/PermissionController.test.ts | 2 + .../src/permissions/PermissionController.ts | 37 ++++--------------- 3 files changed, 13 insertions(+), 42 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index f62f249480..d925e298b6 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -179,15 +179,7 @@ type ExtractArrayMembers = ArrayType extends [] */ export type ExtractAllowedCaveatTypes< PermissionSpecification extends PermissionSpecificationConstraint, - TargetName extends string, -> = PermissionSpecification extends PermissionSpecificationConstraint & { - targetKey: ExtractPermissionTargetKey< - PermissionSpecification['targetKey'], - TargetName - >; -} - ? ExtractArrayMembers - : never; +> = ExtractArrayMembers; /** * The options object of {@link constructPermission}. @@ -226,7 +218,7 @@ export type PermissionOptions = { */ export function constructPermission< TargetPermission extends PermissionConstraint, ->(options: PermissionOptions): PermissionConstraint { +>(options: PermissionOptions): TargetPermission { const { caveats = null, id, invoker, target } = options; return { @@ -235,7 +227,7 @@ export function constructPermission< invoker, caveats, date: new Date().getTime(), - }; + } as TargetPermission; } /** @@ -379,7 +371,7 @@ export type PermissionSpecificationConstraint = { * An array of the caveat types that may be added to instances of this * permission. */ - allowedCaveats?: Readonly>; + allowedCaveats: Readonly> | null; /** * The implementation of the restricted method that the permission diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 2f1b01a4c3..45a512a167 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -264,6 +264,7 @@ function getDefaultPermissionSpecifications() { }, [PermissionKeys.wallet_doubleNumber]: { targetKey: PermissionKeys.wallet_doubleNumber, + allowedCaveats: null, methodImplementation: ({ params }: RestrictedMethodOptions<[number]>) => { if (!Array.isArray(params)) { throw new Error( @@ -275,6 +276,7 @@ function getDefaultPermissionSpecifications() { }, [PermissionKeys.wallet_noop]: { targetKey: PermissionKeys.wallet_noop, + allowedCaveats: null, methodImplementation: (_args: RestrictedMethodOptions) => { return null; }, diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index a45502472e..1ed63e17c1 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -817,10 +817,7 @@ export class PermissionController< ControllerPermissionSpecification, ControllerCaveatSpecification >['parentCapability'], - CaveatType extends ExtractAllowedCaveatTypes< - ControllerPermissionSpecification, - TargetName - >, + CaveatType extends ExtractAllowedCaveatTypes, >(origin: OriginString, target: TargetName, caveatType: CaveatType): boolean { return Boolean(this.getCaveat(origin, target, caveatType)); } @@ -845,10 +842,7 @@ export class PermissionController< ControllerPermissionSpecification, ControllerCaveatSpecification >['parentCapability'], - CaveatType extends ExtractAllowedCaveatTypes< - ControllerPermissionSpecification, - TargetName - >, + CaveatType extends ExtractAllowedCaveatTypes, >( origin: OriginString, target: TargetName, @@ -888,10 +882,7 @@ export class PermissionController< ControllerPermissionSpecification, ControllerCaveatSpecification >['parentCapability'], - CaveatType extends ExtractAllowedCaveatTypes< - ControllerPermissionSpecification, - TargetName - >, + CaveatType extends ExtractAllowedCaveatTypes, >( origin: OriginString, target: TargetName, @@ -928,10 +919,7 @@ export class PermissionController< ControllerPermissionSpecification, ControllerCaveatSpecification >['parentCapability'], - CaveatType extends ExtractAllowedCaveatTypes< - ControllerPermissionSpecification, - TargetName - >, + CaveatType extends ExtractAllowedCaveatTypes, CaveatValue extends ExtractCaveatValue< ControllerCaveatSpecification, CaveatType @@ -972,10 +960,7 @@ export class PermissionController< ControllerPermissionSpecification, ControllerCaveatSpecification >['parentCapability'], - CaveatType extends ExtractAllowedCaveatTypes< - ControllerPermissionSpecification, - TargetName - >, + CaveatType extends ExtractAllowedCaveatTypes, >( origin: OriginString, target: TargetName, @@ -1150,10 +1135,7 @@ export class PermissionController< ControllerPermissionSpecification, ControllerCaveatSpecification >['parentCapability'], - CaveatType extends ExtractAllowedCaveatTypes< - ControllerPermissionSpecification, - TargetName - >, + CaveatType extends ExtractAllowedCaveatTypes, >(origin: OriginString, target: TargetName, caveatType: CaveatType): void { this.update((draftState) => { const permission = draftState.subjects[origin]?.permissions[target]; @@ -1404,12 +1386,7 @@ export class PermissionController< // including its caveats. this.validatePermission(specification, permission, origin, targetName); } else { - permission = constructPermission( - permissionOptions, - ) as ExtractPermission< - ControllerPermissionSpecification, - ControllerCaveatSpecification - >; + permission = constructPermission(permissionOptions); // We do not need to validate caveats in this case, because the plain // permission constructor function does not modify the caveats, which From 2f6729330f34d51862ce805f2c6a1d14d55b9427 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 10 Nov 2021 12:50:50 -0800 Subject: [PATCH 141/147] Remove unnecessary constructPermission cast --- .../controllers/src/permissions/PermissionController.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 45a512a167..f5d90cf87d 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -253,7 +253,7 @@ function getDefaultPermissionSpecifications() { constructPermission({ ...options, caveats: [{ type: CaveatTypes.noopCaveat, value: null }], - }) as SecretNamespacedPermission, + }), validator: (permission: PermissionConstraint) => { assert.deepStrictEqual( permission.caveats, From 1ccbaf7f419999ca2ad692e21419a38a2dc6bf51 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 10 Nov 2021 12:55:48 -0800 Subject: [PATCH 142/147] Rename GenericTargetName to TargetName --- packages/controllers/src/permissions/Permission.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index d925e298b6..f866391999 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -17,7 +17,7 @@ export type OriginString = string; /** * The name of a permission target. */ -export type GenericTargetName = string; +type TargetName = string; /** * A `ZCAP-LD`-like permission object. A permission is associated with a @@ -83,7 +83,7 @@ export type PermissionConstraint = { * for the permission. */ export type ValidPermission< - TargetKey extends GenericTargetName, + TargetKey extends TargetName, AllowedCaveat extends CaveatConstraint, > = ValidTargetName> extends never ? never @@ -174,8 +174,6 @@ type ExtractArrayMembers = ArrayType extends [] * * @template PermissionSpecification - The permission specification type to * extract valid caveat types from. - * @template TargetName - The target name of the permission whose allowed - * caveats to extract. */ export type ExtractAllowedCaveatTypes< PermissionSpecification extends PermissionSpecificationConstraint, @@ -253,10 +251,7 @@ type RequestedPermission = Partial; /** * A record of target names and their {@link RequestedPermission} objects. */ -export type RequestedPermissions = Record< - GenericTargetName, - RequestedPermission ->; +export type RequestedPermissions = Record; /** * The restricted method context object. Essentially a way to pass internal @@ -277,7 +272,7 @@ export type RestrictedMethodParameters = Json[] | Record | void; */ export type RestrictedMethodOptions = { - method: GenericTargetName; + method: TargetName; params?: Params; context: RestrictedMethodContext; }; From 56ca0784916cba4c8da64be7da0a5f4a1847c64e Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 10 Nov 2021 13:03:08 -0800 Subject: [PATCH 143/147] Encapsulate ValidTargetName inside ExtractPermissionTargetNames --- packages/controllers/src/permissions/Permission.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index f866391999..c4ff012399 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -85,7 +85,7 @@ export type PermissionConstraint = { export type ValidPermission< TargetKey extends TargetName, AllowedCaveat extends CaveatConstraint, -> = ValidTargetName> extends never +> = ExtractPermissionTargetNames extends never ? never : PermissionConstraint & { // TODO:TS4.4 Make optional @@ -126,8 +126,9 @@ type ValidTargetName = Name extends `${string}*` * * @template Key - The target key type to extract target names from. */ -export type ExtractPermissionTargetNames = - Key extends `${infer Base}_*` ? `${Base}_${string}` : Key; +export type ExtractPermissionTargetNames = ValidTargetName< + Key extends `${infer Base}_*` ? `${Base}_${string}` : Key +>; /** * Extracts the permission key of a particular name from a union of keys. From 14ec1846efbd52ee9af838fa8d3a6c9018026c7b Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 10 Nov 2021 13:31:22 -0800 Subject: [PATCH 144/147] Simply CaveatSpecificationMap type --- packages/controllers/src/permissions/Caveat.ts | 8 +------- .../src/permissions/PermissionController.test.ts | 8 ++++---- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index 1804f7daa4..1349938505 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -152,13 +152,7 @@ export type CaveatSpecification = { */ export type CaveatSpecificationMap< Specification extends CaveatSpecificationConstraint, -> = { - [Key in Specification['type']]: Specification extends CaveatSpecification< - Caveat - > - ? Specification - : never; -}; +> = Record; /** * TODO:docs diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index f5d90cf87d..0ffcb55d2e 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -64,7 +64,7 @@ type NoopCaveat = Caveat; */ function getDefaultCaveatSpecifications() { return { - filterArrayResponse: { + [CaveatTypes.filterArrayResponse]: { type: CaveatTypes.filterArrayResponse, decorator: ( @@ -92,7 +92,7 @@ function getDefaultCaveatSpecifications() { } }, }, - reverseArrayResponse: { + [CaveatTypes.reverseArrayResponse]: { type: CaveatTypes.reverseArrayResponse, decorator: ( @@ -108,7 +108,7 @@ function getDefaultCaveatSpecifications() { return result.reverse(); }, }, - filterObjectResponse: { + [CaveatTypes.filterObjectResponse]: { type: CaveatTypes.filterObjectResponse, decorator: ( @@ -129,7 +129,7 @@ function getDefaultCaveatSpecifications() { return result; }, }, - noopCaveat: { + [CaveatTypes.noopCaveat]: { type: CaveatTypes.noopCaveat, decorator: ( From 3014a63f9053f319ac95ac19118dfe6a2419ce1b Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 10 Nov 2021 13:33:28 -0800 Subject: [PATCH 145/147] Add a TODO comment --- packages/controllers/src/permissions/Caveat.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/controllers/src/permissions/Caveat.ts b/packages/controllers/src/permissions/Caveat.ts index 1349938505..988cb671ae 100644 --- a/packages/controllers/src/permissions/Caveat.ts +++ b/packages/controllers/src/permissions/Caveat.ts @@ -154,6 +154,7 @@ export type CaveatSpecificationMap< Specification extends CaveatSpecificationConstraint, > = Record; +// TODO: Figure out why we have to extend "any" in this type. /** * TODO:docs */ From 118bbc912401b94cee01c27934b6d3387f554b65 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Wed, 10 Nov 2021 13:55:10 -0800 Subject: [PATCH 146/147] Remove unused default branches --- .../src/permissions/Permission.test.ts | 22 ------------------- .../controllers/src/permissions/Permission.ts | 10 ++------- .../rpc-methods/requestPermissions.ts | 14 ++++++++---- 3 files changed, 12 insertions(+), 34 deletions(-) diff --git a/packages/controllers/src/permissions/Permission.test.ts b/packages/controllers/src/permissions/Permission.test.ts index 4543dfabe1..62a368cebb 100644 --- a/packages/controllers/src/permissions/Permission.test.ts +++ b/packages/controllers/src/permissions/Permission.test.ts @@ -42,26 +42,4 @@ describe('constructPermission', () => { }), ); }); - - it('constructs a permission with an id', () => { - const invoker = 'foo.io'; - const target = 'wallet_bar'; - const id = 'abc123'; - - expect( - constructPermission({ - invoker, - target, - id, - }), - ).toMatchObject( - expect.objectContaining({ - id, - parentCapability: target, - invoker, - caveats: null, - date: expect.any(Number), - }), - ); - }); }); diff --git a/packages/controllers/src/permissions/Permission.ts b/packages/controllers/src/permissions/Permission.ts index c4ff012399..efe8e93275 100644 --- a/packages/controllers/src/permissions/Permission.ts +++ b/packages/controllers/src/permissions/Permission.ts @@ -192,12 +192,6 @@ export type PermissionOptions = { */ invoker: OriginString; - /** - * The GUID of the permission object. - * Assigned if not provided. - */ - id?: string; - /** * The caveats of the permission. * See {@link Caveat}. @@ -218,10 +212,10 @@ export type PermissionOptions = { export function constructPermission< TargetPermission extends PermissionConstraint, >(options: PermissionOptions): TargetPermission { - const { caveats = null, id, invoker, target } = options; + const { caveats = null, invoker, target } = options; return { - id: id ?? nanoid(), + id: nanoid(), parentCapability: target, invoker, caveats, diff --git a/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts b/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts index 4cb880bb7f..37582f5d1b 100644 --- a/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts +++ b/packages/controllers/src/permissions/rpc-methods/requestPermissions.ts @@ -1,10 +1,10 @@ +import { ethErrors } from 'eth-rpc-errors'; import type { PermittedHandlerExport } from '@metamask/rpc-methods/types'; import type { JsonRpcEngineEndCallback, JsonRpcRequest, PendingJsonRpcResponse, } from 'json-rpc-engine'; -import { nanoid } from 'nanoid'; import { MethodNames } from '../utils'; import { invalidParams } from '../errors'; @@ -50,14 +50,20 @@ async function requestPermissionsImplementation( return end(invalidParams({ data: { request: req } })); } - const id = - typeof req.id === 'number' || req.id ? req.id.toString() : nanoid(); + if (typeof req.id !== 'number' && !req.id) { + return end( + ethErrors.rpc.invalidRequest({ + message: 'Invalid request: Must specify an id.', + data: { request: req }, + }), + ); + } try { const [requestedPermissions] = req.params; const [grantedPermissions] = await requestPermissions( requestedPermissions, - id, + req.id.toString(), ); // `wallet_requestPermission` is specified to return an array. From b4fae68d3079b49879aa97c45815dbffedeaf5f0 Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Thu, 11 Nov 2021 12:36:39 -0800 Subject: [PATCH 147/147] Change requestPermission origin parameter to subject metadata --- .../permissions/PermissionController.test.ts | 142 +++++++++++------- .../src/permissions/PermissionController.ts | 7 +- 2 files changed, 93 insertions(+), 56 deletions(-) diff --git a/packages/controllers/src/permissions/PermissionController.test.ts b/packages/controllers/src/permissions/PermissionController.test.ts index 0ffcb55d2e..dfa31fe9dc 100644 --- a/packages/controllers/src/permissions/PermissionController.test.ts +++ b/packages/controllers/src/permissions/PermissionController.test.ts @@ -2696,9 +2696,12 @@ describe('PermissionController', () => { const controller = getDefaultPermissionController(options); expect( - await controller.requestPermissions(origin, { - [PermissionNames.wallet_getSecretArray]: {}, - }), + await controller.requestPermissions( + { origin }, + { + [PermissionNames.wallet_getSecretArray]: {}, + }, + ), ).toMatchObject([ { [PermissionNames.wallet_getSecretArray]: getPermissionMatcher( @@ -2743,15 +2746,18 @@ describe('PermissionController', () => { const controller = getDefaultPermissionController(options); expect( - await controller.requestPermissions(origin, { - [PermissionNames.wallet_getSecretArray]: {}, - [PermissionNames.wallet_getSecretObject]: { - caveats: [ - { type: CaveatTypes.filterObjectResponse, value: ['baz'] }, - ], + await controller.requestPermissions( + { origin }, + { + [PermissionNames.wallet_getSecretArray]: {}, + [PermissionNames.wallet_getSecretObject]: { + caveats: [ + { type: CaveatTypes.filterObjectResponse, value: ['baz'] }, + ], + }, + [PermissionNames.wallet_getSecret_('foo')]: {}, }, - [PermissionNames.wallet_getSecret_('foo')]: {}, - }), + ), ).toMatchObject([ { [PermissionNames.wallet_getSecretArray]: getPermissionMatcher( @@ -2813,7 +2819,10 @@ describe('PermissionController', () => { ]) { await expect( async () => - await controller.requestPermissions(origin, invalidInput as any), + await controller.requestPermissions( + { origin }, + invalidInput as any, + ), ).rejects.toThrow( errors.invalidParams({ message: `Requested permissions for origin "${origin}" is not a plain object.`, @@ -2836,7 +2845,7 @@ describe('PermissionController', () => { await expect( async () => // No permissions in object - await controller.requestPermissions(origin, {}), + await controller.requestPermissions({ origin }, {}), ).rejects.toThrow( errors.invalidParams({ message: `Permissions request for origin "${origin}" contains no permissions.`, @@ -2857,15 +2866,18 @@ describe('PermissionController', () => { const controller = getDefaultPermissionController(options); await expect( async () => - await controller.requestPermissions(origin, { - [PermissionNames.wallet_getSecretArray]: { - parentCapability: PermissionNames.wallet_getSecretArray, - }, - // parentCapability value does not match key - [PermissionNames.wallet_getSecretObject]: { - parentCapability: PermissionNames.wallet_getSecretArray, + await controller.requestPermissions( + { origin }, + { + [PermissionNames.wallet_getSecretArray]: { + parentCapability: PermissionNames.wallet_getSecretArray, + }, + // parentCapability value does not match key + [PermissionNames.wallet_getSecretObject]: { + parentCapability: PermissionNames.wallet_getSecretArray, + }, }, - }), + ), ).rejects.toThrow( errors.invalidParams({ message: `Permissions request for origin "${origin}" contains invalid requested permission(s).`, @@ -2898,10 +2910,13 @@ describe('PermissionController', () => { const controller = getDefaultPermissionController(options); await expect( async () => - await controller.requestPermissions(origin, { - [PermissionNames.wallet_getSecretArray]: {}, - wallet_getSecretKabob: {}, - }), + await controller.requestPermissions( + { origin }, + { + [PermissionNames.wallet_getSecretArray]: {}, + wallet_getSecretKabob: {}, + }, + ), ).rejects.toThrow( errors.methodNotFound({ method: 'wallet_getSecretKabob', @@ -2937,11 +2952,14 @@ describe('PermissionController', () => { ]) { await expect( async () => - await controller.requestPermissions(origin, { - [PermissionNames.wallet_getSecretArray]: { - caveats: invalidCaveatsValue as any, + await controller.requestPermissions( + { origin }, + { + [PermissionNames.wallet_getSecretArray]: { + caveats: invalidCaveatsValue as any, + }, }, - }), + ), ).rejects.toThrow( new errors.InvalidCaveatsPropertyError( origin, @@ -2964,14 +2982,17 @@ describe('PermissionController', () => { const controller = getDefaultPermissionController(options); await expect( async () => - await controller.requestPermissions(origin, { - [PermissionNames.wallet_getSecretArray]: { - caveats: [ - { type: CaveatTypes.filterArrayResponse, value: ['foo'] }, - { type: CaveatTypes.filterArrayResponse, value: ['foo'] }, - ], + await controller.requestPermissions( + { origin }, + { + [PermissionNames.wallet_getSecretArray]: { + caveats: [ + { type: CaveatTypes.filterArrayResponse, value: ['foo'] }, + { type: CaveatTypes.filterArrayResponse, value: ['foo'] }, + ], + }, }, - }), + ), ).rejects.toThrow( new errors.DuplicateCaveatError( CaveatTypes.filterArrayResponse, @@ -2998,9 +3019,12 @@ describe('PermissionController', () => { await expect( async () => - await controller.requestPermissions(origin, { - [PermissionNames.wallet_getSecretArray]: {}, - }), + await controller.requestPermissions( + { origin }, + { + [PermissionNames.wallet_getSecretArray]: {}, + }, + ), ).rejects.toThrow( errors.internalError( `Approved permissions request for subject "${origin}" is invalid.`, @@ -3046,9 +3070,12 @@ describe('PermissionController', () => { const controller = getDefaultPermissionController(options); await expect( async () => - await controller.requestPermissions(origin, { - [PermissionNames.wallet_getSecretArray]: {}, - }), + await controller.requestPermissions( + { origin }, + { + [PermissionNames.wallet_getSecretArray]: {}, + }, + ), ).rejects.toThrow( errors.internalError( `Approved permissions request for subject "${origin}" mutated its id.`, @@ -3093,9 +3120,12 @@ describe('PermissionController', () => { const controller = getDefaultPermissionController(options); await expect( async () => - await controller.requestPermissions(origin, { - [PermissionNames.wallet_getSecretArray]: {}, - }), + await controller.requestPermissions( + { origin }, + { + [PermissionNames.wallet_getSecretArray]: {}, + }, + ), ).rejects.toThrow( errors.internalError( `Approved permissions request for subject "${origin}" mutated its origin.`, @@ -3137,9 +3167,12 @@ describe('PermissionController', () => { const controller = getDefaultPermissionController(options); await expect( async () => - await controller.requestPermissions(origin, { - [PermissionNames.wallet_getSecretArray]: {}, - }), + await controller.requestPermissions( + { origin }, + { + [PermissionNames.wallet_getSecretArray]: {}, + }, + ), ).rejects.toThrow( errors.internalError( `Invalid approved permissions request: Permissions request for origin "${origin}" contains no permissions.`, @@ -3197,7 +3230,7 @@ describe('PermissionController', () => { await expect( async () => await controller.requestPermissions( - origin, + { origin }, { [PermissionNames.wallet_getSecretArray]: {}, }, @@ -3253,11 +3286,14 @@ describe('PermissionController', () => { await expect( async () => - await controller.requestPermissions(origin, { - [PermissionNames.wallet_getSecretArray]: { - parentCapability: PermissionNames.wallet_getSecretArray, + await controller.requestPermissions( + { origin }, + { + [PermissionNames.wallet_getSecretArray]: { + parentCapability: PermissionNames.wallet_getSecretArray, + }, }, - }), + ), ).rejects.toThrow( errors.invalidParams({ message: `Invalid approved permissions request: Permissions request for origin "${origin}" contains invalid requested permission(s).`, diff --git a/packages/controllers/src/permissions/PermissionController.ts b/packages/controllers/src/permissions/PermissionController.ts index 1ed63e17c1..0a9a5ef6c1 100644 --- a/packages/controllers/src/permissions/PermissionController.ts +++ b/packages/controllers/src/permissions/PermissionController.ts @@ -1598,9 +1598,9 @@ export class PermissionController< * @returns The granted permissions and request metadata. */ async requestPermissions( - origin: OriginString, + subject: PermissionSubjectMetadata, requestedPermissions: RequestedPermissions, - id: string = nanoid(), + id = nanoid(), preserveExistingPermissions = true, ): Promise< [ @@ -1613,6 +1613,7 @@ export class PermissionController< { id: string; origin: OriginString }, ] > { + const { origin } = subject; this.validateRequestedPermissions(origin, requestedPermissions); const metadata = { id, @@ -1629,7 +1630,7 @@ export class PermissionController< return [ this.grantPermissions({ - subject: { origin }, + subject, approvedPermissions, preserveExistingPermissions, requestData,