Răsfoiți Sursa

[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
Antoine Clausse 1 an în urmă
părinte
comite
4c03ebe4ee

+ 20 - 5
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<string, boolean>}[][]}
+       */
+      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()

+ 30 - 21
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<string, Map<Capability, boolean>>} */
 const POLICY_TO_CAPABILITY_MAP = new Map()
 const POLICY_TO_VALIDATOR_MAP = new Map()
+/** @type {Map<Capability, boolean>} */
 const DEFAULT_PERMISSIONS = new Map()
+/** @type {Set<string>} */
 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<string, boolean>} 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<Record<Capability, boolean>>|Map<Capability, boolean>} 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<Map<Capability , boolean>>} 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<Capability>} A set of default capabilities.
  */
 function getDefaultCapabilities() {
+  /** @type {Set<Capability>} */
   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<Capability>} 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<string, boolean>[]} groupPolicies - An array of group policies.
+ * @returns {Record<string, boolean>} - The combined group policy object.
  */
 function combineGroupPolicies(groupPolicies) {
+  /** @type {Record<string, boolean>} */
   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<void>}
  * @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<Boolean>} - true if the user has all permissions, false if not
  */
 async function checkUserPermissions(user, requiredCapabilities) {

+ 11 - 0
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'