UNPKG

sparrow-controllers

Version:

Collection of platform-agnostic modules for creating secure data models for cryptocurrency wallets

1,045 lines 58.5 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PermissionController = exports.CaveatMutatorOperation = void 0; const deep_freeze_strict_1 = __importDefault(require("deep-freeze-strict")); const immer_1 = require("immer"); const nanoid_1 = require("nanoid"); const eth_rpc_errors_1 = require("eth-rpc-errors"); const BaseControllerV2_1 = require("../BaseControllerV2"); const util_1 = require("../util"); const Caveat_1 = require("./Caveat"); const errors_1 = require("./errors"); const Permission_1 = require("./Permission"); const permission_middleware_1 = require("./permission-middleware"); const utils_1 = require("./utils"); /** * The name of the {@link PermissionController}. */ const controllerName = 'PermissionController'; /** * Get the state metadata of the {@link PermissionController}. * * @template Permission - The controller's permission type union. * @returns The state metadata */ function getStateMetadata() { return { subjects: { anonymous: true, persist: true } }; } /** * Get the default state of the {@link PermissionController}. * * @template Permission - The controller's permission type union. * @returns The default state of the controller */ function getDefaultState() { return { subjects: {} }; } /** * Describes the possible results of a {@link CaveatMutator} function. */ var CaveatMutatorOperation; (function (CaveatMutatorOperation) { CaveatMutatorOperation[CaveatMutatorOperation["noop"] = 0] = "noop"; CaveatMutatorOperation[CaveatMutatorOperation["updateValue"] = 1] = "updateValue"; CaveatMutatorOperation[CaveatMutatorOperation["deleteCaveat"] = 2] = "deleteCaveat"; CaveatMutatorOperation[CaveatMutatorOperation["revokePermission"] = 3] = "revokePermission"; })(CaveatMutatorOperation = exports.CaveatMutatorOperation || (exports.CaveatMutatorOperation = {})); /** * The permission controller. See the README for details. * * Assumes the existence of an {@link ApprovalController} reachable via the * {@link ControllerMessenger}. * * @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. */ class PermissionController extends BaseControllerV2_1.BaseController { /** * Constructs the PermissionController. * * @param options - Permission controller options. * @param options.caveatSpecifications - The specifications of all caveats * 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 PermissionSpecificationMap} and the README 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) { const { caveatSpecifications, permissionSpecifications, unrestrictedMethods, messenger, state = {}, } = options; super({ name: controllerName, metadata: getStateMetadata(), messenger, state: Object.assign(Object.assign({}, getDefaultState()), state), }); this._unrestrictedMethods = new Set(unrestrictedMethods); this._caveatSpecifications = (0, deep_freeze_strict_1.default)(Object.assign({}, caveatSpecifications)); this.validatePermissionSpecifications(permissionSpecifications, this._caveatSpecifications); this._permissionSpecifications = (0, deep_freeze_strict_1.default)(Object.assign({}, permissionSpecifications)); this.registerMessageHandlers(); this.createPermissionMiddleware = (0, permission_middleware_1.getPermissionMiddlewareFactory)({ executeRestrictedMethod: this._executeRestrictedMethod.bind(this), getRestrictedMethod: this.getRestrictedMethod.bind(this), isUnrestrictedMethod: this.unrestrictedMethods.has.bind(this.unrestrictedMethods), }); } /** * The names of all JSON-RPC methods that will be ignored by the controller. * * @returns The names of all unrestricted JSON-RPC methods */ get unrestrictedMethods() { return this._unrestrictedMethods; } /** * 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) { return this._permissionSpecifications[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) { return this._caveatSpecifications[caveatType]; } /** * 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 permissionSpecifications - The permission specifications passed to * this controller's constructor. * @param caveatSpecifications - The caveat specifications passed to this * controller. */ validatePermissionSpecifications(permissionSpecifications, caveatSpecifications) { Object.entries(permissionSpecifications).forEach(([targetKey, { permissionType, targetKey: innerTargetKey, allowedCaveats },]) => { if (!permissionType || !(0, util_1.hasProperty)(Permission_1.PermissionType, permissionType)) { throw new Error(`Invalid permission type: "${permissionType}"`); } // 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 (allowedCaveats) { allowedCaveats.forEach((caveatType) => { if (!(0, util_1.hasProperty)(caveatSpecifications, caveatType)) { throw new errors_1.UnrecognizedCaveatTypeError(caveatType); } }); } }); } /** * Constructor helper for registering the controller's messaging system * actions. */ registerMessageHandlers() { this.messagingSystem.registerActionHandler(`${controllerName}:clearPermissions`, () => this.clearState()); this.messagingSystem.registerActionHandler(`${controllerName}:getEndowments`, (origin, targetName, requestData) => this.getEndowments(origin, targetName, requestData)); this.messagingSystem.registerActionHandler(`${controllerName}:getSubjectNames`, () => this.getSubjectNames()); this.messagingSystem.registerActionHandler(`${controllerName}:getPermissions`, (origin) => this.getPermissions(origin)); this.messagingSystem.registerActionHandler(`${controllerName}:hasPermission`, (origin, targetName) => this.hasPermission(origin, targetName)); this.messagingSystem.registerActionHandler(`${controllerName}:hasPermissions`, (origin) => this.hasPermissions(origin)); this.messagingSystem.registerActionHandler(`${controllerName}:grantPermissions`, this.grantPermissions.bind(this)); this.messagingSystem.registerActionHandler(`${controllerName}:requestPermissions`, (subject, permissions) => this.requestPermissions(subject, permissions)); this.messagingSystem.registerActionHandler(`${controllerName}:revokeAllPermissions`, (origin) => this.revokeAllPermissions(origin)); this.messagingSystem.registerActionHandler(`${controllerName}:revokePermissionForAllSubjects`, (target) => this.revokePermissionForAllSubjects(target)); this.messagingSystem.registerActionHandler(`${controllerName}:revokePermissions`, this.revokePermissions.bind(this)); } /** * Clears the state of the controller. */ clearState() { this.update((_draftState) => { return Object.assign({}, getDefaultState()); }); } /** * Gets the permission specification corresponding to the given permission * type and target name. Throws an error if the target name does not * correspond to a permission, or if the specification is not of the * given permission type. * * @template Type - The type of the permission specification to get. * @param permissionType - The type of the permission specification to get. * @param targetName - The name of the permission whose specification to get. * @param requestingOrigin - The origin of the requesting subject, if any. * Will be added to any thrown errors. * @returns The specification object corresponding to the given type and * target name. */ getTypedPermissionSpecification(permissionType, targetName, requestingOrigin) { const failureError = permissionType === Permission_1.PermissionType.RestrictedMethod ? (0, errors_1.methodNotFound)(targetName, requestingOrigin ? { origin: requestingOrigin } : undefined) : new errors_1.EndowmentPermissionDoesNotExistError(targetName, requestingOrigin); const targetKey = this.getTargetKey(targetName); if (!targetKey) { throw failureError; } const specification = this.getPermissionSpecification(targetKey); if (!(0, Permission_1.hasSpecificationType)(specification, permissionType)) { throw failureError; } return specification; } /** * 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, origin) { return this.getTypedPermissionSpecification(Permission_1.PermissionType.RestrictedMethod, method, origin).methodImplementation; } /** * Gets a list of all origins of subjects. * * @returns The origins (i.e. IDs) of all subjects. */ getSubjectNames() { 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 targetName - 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, targetName) { var _a; return (_a = this.state.subjects[origin]) === null || _a === void 0 ? void 0 : _a.permissions[targetName]; } /** * 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) { var _a; return (_a = this.state.subjects[origin]) === null || _a === void 0 ? void 0 : _a.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, target) { 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) { return Boolean(this.state.subjects[origin]); } /** * 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) { this.update((draftState) => { if (!draftState.subjects[origin]) { throw new errors_1.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, target) { 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) { this.update((draftState) => { Object.keys(subjectsAndPermissions).forEach((origin) => { if (!(0, util_1.hasProperty)(draftState.subjects, origin)) { throw new errors_1.UnrecognizedSubjectError(origin); } subjectsAndPermissions[origin].forEach((target) => { const { permissions } = draftState.subjects[origin]; if (!(0, util_1.hasProperty)(permissions, target)) { throw new errors_1.PermissionDoesNotExistError(origin, target); } this.deletePermission(draftState.subjects, origin, target); }); }); }); } /** * 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) { if (this.getSubjectNames().length === 0) { return; } this.update((draftState) => { Object.entries(draftState.subjects).forEach(([origin, subject]) => { const { permissions } = subject; if ((0, util_1.hasProperty)(permissions, target)) { this.deletePermission(draftState.subjects, origin, target); } }); }); } /** * 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. */ deletePermission(subjects, origin, target) { const { permissions } = subjects[origin]; if (Object.keys(permissions).length > 1) { delete permissions[target]; } else { delete subjects[origin]; } } /** * 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(origin, target, caveatType) { return Boolean(this.getCaveat(origin, target, caveatType)); } /** * 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 * 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(origin, target, caveatType) { const permission = this.getPermission(origin, target); if (!permission) { throw new errors_1.PermissionDoesNotExistError(origin, target); } return (0, Permission_1.findCaveat)(permission, caveatType); } /** * 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 * 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(origin, target, caveatType, caveatValue) { if (this.hasCaveat(origin, target, caveatType)) { throw new errors_1.CaveatAlreadyExistsError(origin, target, caveatType); } this.setCaveat(origin, target, caveatType, caveatValue); } /** * 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 * 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(origin, target, caveatType, caveatValue) { if (!this.hasCaveat(origin, target, caveatType)) { throw new errors_1.CaveatDoesNotExistError(origin, target, caveatType); } this.setCaveat(origin, target, caveatType, caveatValue); } /** * 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. * * @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. */ setCaveat(origin, target, caveatType, caveatValue) { this.update((draftState) => { const subject = draftState.subjects[origin]; // 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 errors_1.UnrecognizedSubjectError(origin); } const permission = subject.permissions[target]; /* istanbul ignore if: practically impossible, but TypeScript wants it */ if (!permission) { throw new errors_1.PermissionDoesNotExistError(origin, target); } const caveat = { type: caveatType, value: caveatValue, }; this.validateCaveat(caveat, origin, target); if (permission.caveats) { const caveatIndex = permission.caveats.findIndex((existingCaveat) => existingCaveat.type === caveat.type); if (caveatIndex === -1) { permission.caveats.push(caveat); } else { permission.caveats.splice(caveatIndex, 1, caveat); } } else { // 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]; } this.validateModifiedPermission(permission, origin, target); }); } /** * 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 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 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 CaveatMutatorOperation.deleteCaveat}). The permission specification validator, if any, will be called after deleting the caveat. * - Revoke the parent permission ({@link CaveatMutatorOperation.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. */ updatePermissionsByCaveat(targetCaveatType, mutator) { 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 === null || caveats === void 0 ? void 0 : caveats.find(({ type }) => type === targetCaveatType); if (!targetCaveat) { return; } // The mutator may modify the caveat value in place, and must always // return a valid mutation result. const mutatorResult = mutator(targetCaveat.value); switch (mutatorResult.operation) { case CaveatMutatorOperation.noop: break; case CaveatMutatorOperation.updateValue: // 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.value = mutatorResult.value; this.validateCaveat(targetCaveat, subject.origin, permission.parentCapability); break; case CaveatMutatorOperation.deleteCaveat: this.deleteCaveat(permission, targetCaveatType, subject.origin, permission.parentCapability); break; case CaveatMutatorOperation.revokePermission: this.deletePermission(draftState.subjects, subject.origin, permission.parentCapability); break; default: { // This type check ensures that the switch statement is // exhaustive. const _exhaustiveCheck = mutatorResult; throw new Error(`Unrecognized mutation result: "${_exhaustiveCheck.operation}"`); } } }); }); }); } /** * 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 * 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(origin, target, caveatType) { this.update((draftState) => { var _a; const permission = (_a = draftState.subjects[origin]) === null || _a === void 0 ? void 0 : _a.permissions[target]; if (!permission) { throw new errors_1.PermissionDoesNotExistError(origin, target); } if (!permission.caveats) { throw new errors_1.CaveatDoesNotExistError(origin, target, caveatType); } this.deleteCaveat(permission, caveatType, origin, target); }); } /** * 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. */ deleteCaveat(permission, caveatType, origin, target) { /* istanbul ignore if: not possible in our usage */ if (!permission.caveats) { throw new errors_1.CaveatDoesNotExistError(origin, target, caveatType); } const caveatIndex = permission.caveats.findIndex((existingCaveat) => existingCaveat.type === caveatType); if (caveatIndex === -1) { throw new errors_1.CaveatDoesNotExistError(origin, target, caveatType); } if (permission.caveats.length === 1) { permission.caveats = null; } else { permission.caveats.splice(caveatIndex, 1); } this.validateModifiedPermission(permission, origin, target); } /** * 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. */ validateModifiedPermission(permission, origin, targetName) { const targetKey = this.getTargetKey(permission.parentCapability); /* istanbul ignore if: this should be impossible */ if (!targetKey) { throw new Error(`Fatal: Existing permission target key "${targetKey}" has no specification.`); } this.validatePermission(this.getPermissionSpecification(targetKey), permission, origin, targetName); } /** * Gets the key for the specified permission target. * * Used to support our namespaced permission target feature, which is used * to implement namespaced restricted JSON-RPC methods. * * @param target - The requested permission target. * @returns The internal key of the permission target. */ getTargetKey(target) { if ((0, util_1.hasProperty)(this._permissionSpecifications, target)) { return target; } const namespacedTargetsWithoutWildcard = {}; for (const targetKey of Object.keys(this._permissionSpecifications)) { const wildCardMatch = targetKey.match(/(.+)\*$/u); if (wildCardMatch) { namespacedTargetsWithoutWildcard[wildCardMatch[1]] = true; } } // Check for potentially nested namespaces: // Ex: wildzone_ // Ex: eth_plugin_ const segments = target.split('_'); let targetKey = ''; while (segments.length > 0 && !(0, util_1.hasProperty)(this._permissionSpecifications, targetKey) && !namespacedTargetsWithoutWildcard[targetKey]) { targetKey += `${segments.shift()}_`; } if (namespacedTargetsWithoutWildcard[targetKey]) { return `${targetKey}*`; } 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. * * 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. * @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({ approvedPermissions, requestData, preserveExistingPermissions = true, subject, }) { const { origin } = subject; if (!origin || typeof origin !== 'string') { throw new errors_1.InvalidSubjectIdentifierError(origin); } const permissions = (preserveExistingPermissions ? Object.assign({}, this.getPermissions(origin)) : {}); for (const [requestedTarget, approvedPermission] of Object.entries(approvedPermissions)) { const targetKey = this.getTargetKey(requestedTarget); if (!targetKey) { throw (0, errors_1.methodNotFound)(requestedTarget); } if (approvedPermission.parentCapability !== undefined && requestedTarget !== approvedPermission.parentCapability) { throw new errors_1.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; const specification = this.getPermissionSpecification(targetKey); // The requested caveats are validated here. const caveats = this.constructCaveats(origin, targetName, approvedPermission.caveats); const permissionOptions = { caveats, invoker: origin, target: targetName, }; let permission; 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 = (0, Permission_1.constructPermission)(permissionOptions); // 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; } this.setValidatedPermissions(origin, permissions); return permissions; } /** * 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. * - 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. * * @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. * @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. */ validatePermission(specification, permission, origin, targetName, { invokePermissionValidator, performCaveatValidation } = { invokePermissionValidator: true, performCaveatValidation: true, }) { const { allowedCaveats, validator } = specification; if ((0, util_1.hasProperty)(permission, 'caveats')) { const { caveats } = permission; if (caveats !== null && !(Array.isArray(caveats) && caveats.length > 0)) { throw new errors_1.InvalidCaveatsPropertyError(origin, targetName, caveats); } const seenCaveatTypes = new Set(); caveats === null || caveats === void 0 ? void 0 : caveats.forEach((caveat) => { if (performCaveatValidation) { this.validateCaveat(caveat, origin, targetName); } if (!(allowedCaveats === null || allowedCaveats === void 0 ? void 0 : allowedCaveats.includes(caveat.type))) { throw new errors_1.ForbiddenCaveatError(caveat.type, origin, targetName); } if (seenCaveatTypes.has(caveat.type)) { throw new errors_1.DuplicateCaveatError(caveat.type, origin, targetName); } seenCaveatTypes.add(caveat.type); }); } if (invokePermissionValidator && 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 * doesn't already exist. * * 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. */ setValidatedPermissions(origin, permissions) { this.update((draftState) => { if (!draftState.subjects[origin]) { draftState.subjects[origin] = { origin, permissions: {} }; } draftState.subjects[origin].permissions = (0, immer_1.castDraft)(permissions); }); } /** * 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. * * @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. */ constructCaveats(origin, target, requestedCaveats) { const caveatArray = requestedCaveats === null || requestedCaveats === void 0 ? void 0 : requestedCaveats.map((requestedCaveat) => { this.validateCaveat(requestedCaveat, origin, target); // Reassign so that we have a fresh object. const { type, value } = requestedCaveat; return { type, value }; }); return caveatArray && (0, util_1.isNonEmptyArray)(caveatArray) ? caveatArray : undefined; } /** * 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 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. */ validateCaveat(caveat, origin, target) { var _a; if (!(0, util_1.isPlainObject)(caveat)) { throw new errors_1.InvalidCaveatError(caveat, origin, target); } if (Object.keys(caveat).length !== 2) { throw new errors_1.InvalidCaveatFieldsError(caveat, origin, target); } if (typeof caveat.type !== 'string') { throw new errors_1.InvalidCaveatTypeError(caveat, origin, target); } const specification = this.getCaveatSpecification(caveat.type); if (!specification) { throw new errors_1.UnrecognizedCaveatTypeError(caveat.type, origin, target); } if (!(0, util_1.hasProperty)(caveat, 'value') || caveat.value === undefined) { throw new errors_1.CaveatMissingValueError(caveat, origin, target); } if (!(0, util_1.isValidJson)(caveat.value)) { throw new errors_1.CaveatInvalidJsonError(caveat, origin, target); } // Typecast: TypeScript still believes that the caveat is a PlainObject. (_a = specification.validator) === null || _a === void 0 ? void 0 : _a.call(specification, 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 * 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. * @see {@link PermissionController.rejectPermissionsRequest} For the method * that _rejects_ the request and the user approval promise. * @param subject - The grantee subject. * @param requestedPermissions - The requested permissions. * @param options - Additional options. * @param options.id - The id of the permissions request. Defaults to a unique * id. * @param options.preserveExistingPermissions - Whether to preserve the * subject's existing permissions. Defaults to `true`. * @returns The granted permissions and request metadata. */ requestPermissions(subject, requestedPermissions, options = {}) { return __awaiter(this, void 0, void 0, function* () { const { origin } = subject; const { id = (0, nanoid_1.nanoid)(), preserveExistingPermissions = true } = options; this.validateRequestedPermissions(origin, requestedPermissions); const metadata = { id, origin, }; const permissionsRequest = { metadata, permissions: requestedPermissions, }; const _a = yield this.requestUserApproval(permissionsRequest), { permissions: approvedPermissions } = _a, requestData = __rest(_a, ["permissions"]); return [ this.grantPermissions({ subject, approvedPermissions, preserveExistingPermissions, requestData, }), metadata, ]; }); } /** * Validates requested permissions. Throws if validation fails. * * This method ensures that the requested permissions are a properly * 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. * * @param origin - The origin of the grantee subject. * @param requestedPermissions - The requested permissions. */ validateRequestedPermissions(origin, requestedPermissions) { if (!(0, util_1.isPlainObject)(requestedPermissions)) { throw (0, errors_1.invalidParams)({ message: `Requested permissions for origin "${origin}" is not a plain object.`, data: { origin, requestedPermissions }, }); } if (Object.keys(requestedPermissions).length === 0) { throw (0, errors_1.invalidParams)({ message: `Permissions request for origin "${origin}" contains no permissions.`, data: { requestedPermissions }, }); } for (const targetName of Object.keys(requestedPermissions)) { const permission = requestedPermissions[targetName]; const targetKey = this.getTargetKey(targetName); if (!targetKey) { throw (0, errors_1.methodNotFound)(targetName, { origin, requestedPermissions }); } if (!(0, util_1.isPlainObject)(permission) || (permission.parentCapability !== undefined && targetName !== permission.parentCapability)) { throw (0, errors_1.invalidParams)({ message: `Permissions request for origin "${origin}" contains invalid requested permission(s).`, 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, origin, targetName, { invokePermissionValidator: false, performCaveatValidation: true }); } } /** * Adds a request to the {@link ApprovalController} using the * {@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. */ requestUserApproval(permissionsRequest) { return __awaiter(this, void 0, void 0, function* () { const { origin, id } = permissionsRequest.metadata; const approvedRequest = yield this.messagingSystem.call('ApprovalController:addRequest', { id, origin, requestData: permissionsRequest, type: utils_1.MethodNames.requestPermissions, }, true); this.validateApprovedPermissions(approvedRequest, { id, origin }); return approvedRequest; }); } /** * 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. * * An error is thrown if validation fails. * * @param approvedRequest - The approved permissions request object. * @param originalMetadata - The original request metadata. */ validateApprovedPermissions(approvedRequest, originalMetadata) { const { id, origin } = originalMetadata; if (!(0, util_1.isPlainObject)(approvedRequest) || !(0, util_1.isPlainObject)(approvedRequest.metadata)) { throw (0, errors_1.internalError)(`Approved permissions request for subject "${origin}" is invalid.`, { data: { approvedRequest } }); } const { metadata: { id: newId, origin: newOrigin }, permissions, } = approvedRequest; if (newId !== id) { throw (0, errors_1.internalError)(`A