From f0420000c5e3f6754e5577a622e0dce524b06e14 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Tue, 4 Jul 2023 15:19:48 +0100 Subject: [PATCH] Merge pull request #13583 from overleaf/bg-add-permissions-middleware add permissions middleware for managed users GitOrigin-RevId: debd2398a3b75ce71023463ad3c0781750983b53 --- .../Authorization/PermissionsController.js | 48 +++++++++++++++++++ .../web/app/src/Features/Errors/Errors.js | 7 +++ .../Subscription/ManagedUsersHandler.js | 20 +++++++- 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 services/web/app/src/Features/Authorization/PermissionsController.js diff --git a/services/web/app/src/Features/Authorization/PermissionsController.js b/services/web/app/src/Features/Authorization/PermissionsController.js new file mode 100644 index 0000000000..e02130e401 --- /dev/null +++ b/services/web/app/src/Features/Authorization/PermissionsController.js @@ -0,0 +1,48 @@ +const { ForbiddenError } = require('../Errors/Errors') +const { hasPermission } = require('./PermissionsManager') +const ManagedUsersHandler = require('../Subscription/ManagedUsersHandler') + +/** + * 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. + */ +function requirePermission(...requiredCapabilities) { + if ( + requiredCapabilities.length === 0 || + requiredCapabilities.some(capability => typeof capability !== 'string') + ) { + throw new Error('invalid required capabilities') + } + const doRequest = async function (req, res, next) { + if (!req.user) { + return next(new Error('no user')) + } + try { + // get the group policy applying to the user + const groupPolicy = + await ManagedUsersHandler.promises.getGroupPolicyForUser(req.user) + // if there is no group policy, the user is not managed + if (!groupPolicy) { + return next() + } + // check that the user has all the required capabilities + for (const requiredCapability of requiredCapabilities) { + // if the user has the permission, continue + if (!hasPermission(groupPolicy, requiredCapability)) { + throw new ForbiddenError( + `user does not have permission for ${requiredCapability}` + ) + } + } + next() + } catch (error) { + next(error) + } + } + return doRequest +} + +module.exports = { + requirePermission, +} diff --git a/services/web/app/src/Features/Errors/Errors.js b/services/web/app/src/Features/Errors/Errors.js index 7d3940ab31..2394c4000c 100644 --- a/services/web/app/src/Features/Errors/Errors.js +++ b/services/web/app/src/Features/Errors/Errors.js @@ -148,6 +148,12 @@ class SubscriptionAdminDeletionError extends OErrorV2CompatibleError { } } +class SubscriptionNotFoundError extends OErrorV2CompatibleError { + constructor(options) { + super('subscription not found', options) + } +} + class ProjectNotFoundError extends OErrorV2CompatibleError { constructor(options) { super('project not found', options) @@ -213,6 +219,7 @@ module.exports = { ThirdPartyIdentityExistsError, ThirdPartyUserNotFoundError, SubscriptionAdminDeletionError, + SubscriptionNotFoundError, ProjectNotFoundError, UserNotFoundError, UserNotCollaboratorError, diff --git a/services/web/app/src/Features/Subscription/ManagedUsersHandler.js b/services/web/app/src/Features/Subscription/ManagedUsersHandler.js index 1380397b7b..f85df06255 100644 --- a/services/web/app/src/Features/Subscription/ManagedUsersHandler.js +++ b/services/web/app/src/Features/Subscription/ManagedUsersHandler.js @@ -4,6 +4,10 @@ const { GroupPolicy } = require('../../models/GroupPolicy') const { User } = require('../../models/User') const ManagedUsersPolicy = require('./ManagedUsersPolicy') const OError = require('@overleaf/o-error') +const { + UserNotFoundError, + SubscriptionNotFoundError, +} = require('../Errors/Errors') /** * This module contains functions for handling managed users in a @@ -40,7 +44,14 @@ async function enableManagedUsers(subscriptionId) { * @returns {Promise} - A Promise that resolves with the group policy * object for the user's enrollment, or undefined if it does not exist. */ -async function getGroupPolicyForUser(user) { +async function getGroupPolicyForUser(requestedUser) { + // Don't rely on the user being populated, it may be a session user without + // the enrollment property. Force the user to be loaded from mongo. + const user = await User.findById(requestedUser._id, 'enrollment') + if (!user) { + throw new UserNotFoundError({ info: { userId: requestedUser._id } }) + } + // Now we are sure the user exists and we have the full contents if (user.enrollment?.managedBy == null) { return } @@ -48,8 +59,13 @@ async function getGroupPolicyForUser(user) { const subscription = await Subscription.findById(user.enrollment.managedBy) .populate('groupPolicy', '-_id') .exec() + if (!subscription) { + throw new SubscriptionNotFoundError({ + info: { subscriptionId: user.enrollment.managedBy, userId: user._id }, + }) + } // return the group policy as a plain object (without the __v field) - const groupPolicy = subscription?.groupPolicy.toObject({ + const groupPolicy = subscription.groupPolicy.toObject({ versionKey: false, }) return groupPolicy