From 4c03ebe4ee0017b80e99e10bc22dfb41c67f2909 Mon Sep 17 00:00:00 2001 From: Antoine Clausse Date: Mon, 14 Jul 2025 11:12:49 +0200 Subject: [PATCH] [web] Add some types for existing capabilities and PermissionController (#27048) * Add types on existing Capabilities code * Add ts-expect-error comments * Minor code changes to satisfy types * Remove ts-check because of unrelated errors * Remove some ts-expect-error comments * Revert "Remove some ts-expect-error comments" This reverts commit 76cc0a073710eecf4f8b88f8579405838607f4d5. * Remove the `@ts-check`s for now It looks like typescript is somewhat flaky. We can re-enable this later * Remove the `@ts-expect-error`s * Remove return type GitOrigin-RevId: 57bbd370654592c0662047e72e61f91bf38e0949 --- .../Authorization/PermissionsController.js | 25 +++++++-- .../Authorization/PermissionsManager.js | 51 +++++++++++-------- services/web/types/capabilities.ts | 11 ++++ 3 files changed, 61 insertions(+), 26 deletions(-) create mode 100644 services/web/types/capabilities.ts diff --git a/services/web/app/src/Features/Authorization/PermissionsController.js b/services/web/app/src/Features/Authorization/PermissionsController.js index c9ffac3c7c..27b99081c6 100644 --- a/services/web/app/src/Features/Authorization/PermissionsController.js +++ b/services/web/app/src/Features/Authorization/PermissionsController.js @@ -10,9 +10,16 @@ const Modules = require('../../infrastructure/Modules') const { expressify } = require('@overleaf/promise-utils') const Features = require('../../infrastructure/Features') +/** + * @typedef {(import('express').Request)} Request + * @typedef {(import('express').Response)} Response + * @typedef {(import('express').NextFunction)} NextFunction + * @typedef {import('./PermissionsManager').Capability} Capability + */ + /** * Function that returns middleware to add an `assertPermission` function to the request object to check if the user has a specific capability. - * @returns {Function} The middleware function that adds the `assertPermission` function to the request object. + * @returns {() => (req: Request, res: Response, next: NextFunction) => void} The middleware function that adds the `assertPermission` function to the request object. */ function useCapabilities() { const middleware = async function (req, res, next) { @@ -30,12 +37,15 @@ function useCapabilities() { return next() } try { - let results = await Modules.promises.hooks.fire( + /** + * @type {{groupPolicy: Record}[][]} + */ + const hookResponses = await Modules.promises.hooks.fire( 'getGroupPolicyForUser', req.user ) // merge array of all results from all modules - results = results.flat() + const results = hookResponses.flat() if (results.length > 0) { // get the combined group policy applying to the user @@ -70,8 +80,8 @@ function useCapabilities() { /** * Function that returns middleware to check if the user has permission to access a resource. - * @param {[string]} requiredCapabilities - the capabilities required to access the resource. - * @returns {Function} The middleware function that checks if the user has the required capabilities. + * @param {...Capability} requiredCapabilities - the capabilities required to access the resource. + * @returns {(req: Request, res: Response, next: NextFunction) => void} The middleware function that checks if the user has the required capabilities. */ function requirePermission(...requiredCapabilities) { if ( @@ -80,6 +90,11 @@ function requirePermission(...requiredCapabilities) { ) { throw new Error('invalid required capabilities') } + /** + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ const doRequest = async function (req, res, next) { if (!Features.hasFeature('saas')) { return next() diff --git a/services/web/app/src/Features/Authorization/PermissionsManager.js b/services/web/app/src/Features/Authorization/PermissionsManager.js index a20280a941..aad1021b24 100644 --- a/services/web/app/src/Features/Authorization/PermissionsManager.js +++ b/services/web/app/src/Features/Authorization/PermissionsManager.js @@ -45,16 +45,23 @@ const { callbackify } = require('util') const { ForbiddenError } = require('../Errors/Errors') const Modules = require('../../infrastructure/Modules') +/** + * @typedef {(import('../../../../types/capabilities').Capability)} Capability + */ + +/** @type {Map>} */ const POLICY_TO_CAPABILITY_MAP = new Map() const POLICY_TO_VALIDATOR_MAP = new Map() +/** @type {Map} */ const DEFAULT_PERMISSIONS = new Map() +/** @type {Set} */ const ALLOWED_PROPERTIES = new Set() /** * Throws an error if the given capability is not registered. * * @private - * @param {string} capability - The name of the capability to check. + * @param {Capability} capability - The name of the capability to check. * @throws {Error} If the capability is not registered. */ function ensureCapabilityExists(capability) { @@ -66,7 +73,7 @@ function ensureCapabilityExists(capability) { /** * Validates an group policy object * - * @param {Object} policies - An object containing policy names and booleans + * @param {Record} policies - An object containing policy names and booleans * as key-value entries. * @throws {Error} if the `policies` object contains a policy that is not * registered, or the policy value is not a boolean @@ -85,7 +92,7 @@ function validatePolicies(policies) { /** * Registers a new capability with the given name and options. * - * @param {string} name - The name of the capability to register. + * @param {Capability} name - The name of the capability to register. * @param {Object} options - The options for the capability. * @param {boolean} options.default - The default value for the capability * (required). @@ -108,7 +115,7 @@ function registerCapability(name, options) { * Registers a new policy with the given name, capabilities, and options. * * @param {string} name - The name of the policy to register. - * @param {Object} capabilities - The capabilities for the policy. + * @param {Partial>|Map} capabilities - The capabilities for the policy. * @param {Object} [options] - The options for the policy. * @param {Function?} [options.validator] - The optional validator function for the * policy. @@ -122,10 +129,11 @@ function registerPolicy(name, capabilities, options = {}) { if (POLICY_TO_CAPABILITY_MAP.has(name)) { throw new Error(`policy already registered: ${name}`) } + + /** @type {[Capability, boolean][]} */ + const entries = Object.entries(capabilities) // check that all the entries in the capability set exist and are booleans - for (const [capabilityName, capabilityValue] of Object.entries( - capabilities - )) { + for (const [capabilityName, capabilityValue] of entries) { // check that the capability exists (look in the default permissions) if (!DEFAULT_PERMISSIONS.has(capabilityName)) { throw new Error(`unknown capability: ${capabilityName}`) @@ -158,19 +166,18 @@ function registerAllowedProperty(name) { /** * returns the set of allowed properties that have been registered - * - * @returns {Set} ALLOWED_PROPERTIES */ function getAllowedProperties() { return ALLOWED_PROPERTIES } + /** * Returns an array of policy names that are enforced based on the provided * group policy object. * * @private - * @param {Object} groupPolicy - The group policy object to check. - * @returns {Array} An array of policy names that are enforced. + * @param {Partial>} groupPolicy - The group policy object to check. + * @returns {Capability[]} An array of policy names that are enforced. */ function getEnforcedPolicyNames(groupPolicy = {}) { if (!groupPolicy) { @@ -192,7 +199,7 @@ function getEnforcedPolicyNames(groupPolicy = {}) { * @private * @param {string} policyName - The name of the policy to retrieve the * capability value from. - * @param {string} capability - The name of the capability to retrieve the value + * @param {Capability} capability - The name of the capability to retrieve the value * for. * @returns {boolean | undefined} The value of the capability for the policy, or * undefined if the policy or capability is not found. @@ -205,7 +212,7 @@ function getCapabilityValueFromPolicy(policyName, capability) { * Returns the default value for the specified capability. * * @private - * @param {string} capability - The name of the capability to retrieve the + * @param {Capability} capability - The name of the capability to retrieve the * default value for. * @returns {boolean | undefined} The default value for the capability, or * undefined if the capability is not found. @@ -222,9 +229,10 @@ function getValidatorFromPolicy(policyName) { * Returns a set of default capabilities based on the DEFAULT_PERMISSIONS map. * * @private - * @returns {Set} A set of default capabilities. + * @returns {Set} A set of default capabilities. */ function getDefaultCapabilities() { + /** @type {Set} */ const defaultCapabilities = new Set() for (const [ capabilityName, @@ -242,7 +250,7 @@ function getDefaultCapabilities() { * which are not allowed by the policy. * * @private - * @param {Set} capabilitySet - The set of capabilities to apply the policy to. + * @param {Set} capabilitySet - The set of capabilities to apply the policy to. * @param {string} policyName - The name of the policy to apply. * @throws {Error} If the policy is unknown. */ @@ -281,10 +289,11 @@ function getUserCapabilities(groupPolicy) { /** * Combines an array of group policies into a single policy object. * - * @param {Array} groupPolicies - An array of group policies. - * @returns {Object} - The combined group policy object. + * @param {Record[]} groupPolicies - An array of group policies. + * @returns {Record} - The combined group policy object. */ function combineGroupPolicies(groupPolicies) { + /** @type {Record} */ const combinedGroupPolicy = {} for (const groupPolicy of groupPolicies) { const enforcedPolicyNames = getEnforcedPolicyNames(groupPolicy) @@ -335,7 +344,7 @@ function getUserRestrictions(groupPolicy) { * policy. * * @param {Object} groupPolicy - The group policy object for the user. - * @param {string} capability - The name of the capability to check permission + * @param {Capability} capability - The name of the capability to check permission * for. * @returns {boolean} True if the user has permission for the capability, false * otherwise. @@ -349,7 +358,7 @@ function hasPermission(groupPolicy, capability) { ) // if there are no results, or none of the policies apply, return the default permission if (results.length === 0 || results.every(result => result === undefined)) { - return getDefaultPermission(capability) + return !!getDefaultPermission(capability) } // only allow the permission if all the results are true, otherwise deny it return results.every(result => result === true) @@ -395,7 +404,7 @@ async function getUserValidationStatus({ user, groupPolicy, subscription }) { * * @param {Object} user - The user object to retrieve the group policy for. * Only the user's _id is required - * @param {Array} capabilities - The list of the capabilities to check permission for. + * @param {Capability[]} requiredCapabilities - The list of the capabilities to check permission for. * @returns {Promise} * @throws {Error} If the user does not have permission */ @@ -417,7 +426,7 @@ async function assertUserPermissions(user, requiredCapabilities) { * * @param {Object} user - The user object to retrieve the group policy for. * Only the user's _id is required - * @param {Array} capabilities - The list of the capabilities to check permission for. + * @param {Capability[]} requiredCapabilities - The list of the capabilities to check permission for. * @returns {Promise} - true if the user has all permissions, false if not */ async function checkUserPermissions(user, requiredCapabilities) { diff --git a/services/web/types/capabilities.ts b/services/web/types/capabilities.ts new file mode 100644 index 0000000000..961c554cfa --- /dev/null +++ b/services/web/types/capabilities.ts @@ -0,0 +1,11 @@ +export type Capability = + | 'add-secondary-email' + | 'change-password' + | 'chat' + | 'delete-own-account' + | 'dropbox' + | 'endorse-email' + | 'join-subscription' + | 'leave-group-subscription' + | 'start-subscription' + | 'use-ai'