mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #13717 from overleaf/bg-managed-users-block-affiliations-fix
fix for block affiliations for managed users GitOrigin-RevId: cac54288592323ea3f1cd7655d4e2b89ee301002
This commit is contained in:
parent
e0a3dfec36
commit
bc3d6c3636
9 changed files with 87 additions and 9 deletions
|
@ -1,7 +1,34 @@
|
||||||
const { ForbiddenError } = require('../Errors/Errors')
|
const { ForbiddenError } = require('../Errors/Errors')
|
||||||
const { hasPermission } = require('./PermissionsManager')
|
const { hasPermission, getUserCapabilities } = require('./PermissionsManager')
|
||||||
const ManagedUsersHandler = require('../Subscription/ManagedUsersHandler')
|
const ManagedUsersHandler = require('../Subscription/ManagedUsersHandler')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
function useCapabilities() {
|
||||||
|
return async function (req, res, next) {
|
||||||
|
if (!req.user) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// get the group policy applying to the user
|
||||||
|
const groupPolicy =
|
||||||
|
await ManagedUsersHandler.promises.getGroupPolicyForUser(req.user)
|
||||||
|
const capabilitySet = getUserCapabilities(groupPolicy)
|
||||||
|
req.assertPermission = capability => {
|
||||||
|
if (!capabilitySet.has(capability)) {
|
||||||
|
throw new ForbiddenError(
|
||||||
|
`user does not have permission for ${capability}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
} catch (error) {
|
||||||
|
next(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Function that returns middleware to check if the user has permission to access a resource.
|
* 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.
|
* @param {[string]} requiredCapabilities - the capabilities required to access the resource.
|
||||||
|
@ -45,4 +72,5 @@ function requirePermission(...requiredCapabilities) {
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
requirePermission,
|
requirePermission,
|
||||||
|
useCapabilities,
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
*
|
*
|
||||||
* Validator: a function that takes a user and returns a boolean indicating
|
* Validator: a function that takes a user and returns a boolean indicating
|
||||||
* whether the user satisfies the policy or not. For example, a validator for
|
* whether the user satisfies the policy or not. For example, a validator for
|
||||||
* the `userCannotAddSecondaryEmail` policy would check whether the user has
|
* the `userCannotHaveSecondaryEmail` policy would check whether the user has
|
||||||
* more than one email address.
|
* more than one email address.
|
||||||
*
|
*
|
||||||
* Group Policies: a collection of policies with a setting indicating whether
|
* Group Policies: a collection of policies with a setting indicating whether
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
*
|
*
|
||||||
* {
|
* {
|
||||||
* "userCannotDeleteOwnAccount": true, // enforced
|
* "userCannotDeleteOwnAccount": true, // enforced
|
||||||
* "userCannotAddSecondaryEmail": false // not enforced
|
* "userCannotHaveSecondaryEmail": false // not enforced
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,12 @@ registerCapability('delete-own-account', { default: true })
|
||||||
// Register the capability for a user to add a secondary email to their account.
|
// Register the capability for a user to add a secondary email to their account.
|
||||||
registerCapability('add-secondary-email', { default: true })
|
registerCapability('add-secondary-email', { default: true })
|
||||||
|
|
||||||
|
// Register the capability for a user to add an affiliation to their account.
|
||||||
|
registerCapability('add-affiliation', { default: true })
|
||||||
|
|
||||||
|
// Register the capability for a user to endorse an email address.
|
||||||
|
registerCapability('endorse-email', { default: true })
|
||||||
|
|
||||||
// Register the capability for a user to sign in with Google to their account
|
// Register the capability for a user to sign in with Google to their account
|
||||||
registerCapability('link-google-sso', { default: true })
|
registerCapability('link-google-sso', { default: true })
|
||||||
|
|
||||||
|
@ -20,11 +26,14 @@ registerCapability('link-google-sso', { default: true })
|
||||||
registerCapability('link-other-third-party-sso', { default: true })
|
registerCapability('link-other-third-party-sso', { default: true })
|
||||||
|
|
||||||
// Register the capability for a user to leave a managed group subscription.
|
// Register the capability for a user to leave a managed group subscription.
|
||||||
registerCapability('leave-managing-group-subscription', { default: true })
|
registerCapability('leave-group-subscription', { default: true })
|
||||||
|
|
||||||
// Register the capability for a user to start a subscription.
|
// Register the capability for a user to start a subscription.
|
||||||
registerCapability('start-subscription', { default: true })
|
registerCapability('start-subscription', { default: true })
|
||||||
|
|
||||||
|
// Register the capability for a user to join a subscription.
|
||||||
|
registerCapability('join-subscription', { default: true })
|
||||||
|
|
||||||
// Register a policy to prevent a user deleting their own account.
|
// Register a policy to prevent a user deleting their own account.
|
||||||
registerPolicy('userCannotDeleteOwnAccount', {
|
registerPolicy('userCannotDeleteOwnAccount', {
|
||||||
'delete-own-account': false,
|
'delete-own-account': false,
|
||||||
|
@ -32,9 +41,11 @@ registerPolicy('userCannotDeleteOwnAccount', {
|
||||||
|
|
||||||
// Register a policy to prevent a user having secondary email addresses on their account.
|
// Register a policy to prevent a user having secondary email addresses on their account.
|
||||||
registerPolicy(
|
registerPolicy(
|
||||||
'userCannotAddSecondaryEmail',
|
'userCannotHaveSecondaryEmail',
|
||||||
{
|
{
|
||||||
'add-secondary-email': false,
|
'add-secondary-email': false,
|
||||||
|
'add-affiliation': false,
|
||||||
|
'endorse-email': false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
validator: async user => {
|
validator: async user => {
|
||||||
|
@ -46,7 +57,7 @@ registerPolicy(
|
||||||
|
|
||||||
// Register a policy to prevent a user leaving the group subscription they are managed by.
|
// Register a policy to prevent a user leaving the group subscription they are managed by.
|
||||||
registerPolicy('userCannotLeaveManagingGroupSubscription', {
|
registerPolicy('userCannotLeaveManagingGroupSubscription', {
|
||||||
'leave-managing-group-subscription': false,
|
'leave-group-subscription': false,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Register a policy to prevent a user having third-party SSO linked to their account.
|
// Register a policy to prevent a user having third-party SSO linked to their account.
|
||||||
|
@ -78,7 +89,7 @@ registerPolicy(
|
||||||
// Register a policy to prevent a user having an active personal subscription.
|
// Register a policy to prevent a user having an active personal subscription.
|
||||||
registerPolicy(
|
registerPolicy(
|
||||||
'userCannotHaveSubscription',
|
'userCannotHaveSubscription',
|
||||||
{ 'start-subscription': false },
|
{ 'start-subscription': false, 'join-subscription': false },
|
||||||
{
|
{
|
||||||
validator: async user => {
|
validator: async user => {
|
||||||
return !(await SubscriptionLocator.promises.getUserIndividualSubscription(
|
return !(await SubscriptionLocator.promises.getUserIndividualSubscription(
|
||||||
|
@ -101,7 +112,7 @@ registerPolicy(
|
||||||
function getDefaultPolicy() {
|
function getDefaultPolicy() {
|
||||||
return {
|
return {
|
||||||
userCannotDeleteOwnAccount: true,
|
userCannotDeleteOwnAccount: true,
|
||||||
userCannotAddSecondaryEmail: true,
|
userCannotHaveSecondaryEmail: true,
|
||||||
userCannotHaveSubscription: true,
|
userCannotHaveSubscription: true,
|
||||||
userCannotLeaveManagingGroupSubscription: true,
|
userCannotLeaveManagingGroupSubscription: true,
|
||||||
userCannotHaveGoogleSSO: false, // we want to allow google SSO by default
|
userCannotHaveGoogleSSO: false, // we want to allow google SSO by default
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const AuthenticationController = require('../Authentication/AuthenticationController')
|
const AuthenticationController = require('../Authentication/AuthenticationController')
|
||||||
|
const PermissionsController = require('../Authorization/PermissionsController')
|
||||||
const SubscriptionController = require('./SubscriptionController')
|
const SubscriptionController = require('./SubscriptionController')
|
||||||
const SubscriptionGroupController = require('./SubscriptionGroupController')
|
const SubscriptionGroupController = require('./SubscriptionGroupController')
|
||||||
const TeamInvitesController = require('./TeamInvitesController')
|
const TeamInvitesController = require('./TeamInvitesController')
|
||||||
|
@ -58,6 +59,7 @@ module.exports = {
|
||||||
webRouter.delete(
|
webRouter.delete(
|
||||||
'/subscription/group/user',
|
'/subscription/group/user',
|
||||||
AuthenticationController.requireLogin(),
|
AuthenticationController.requireLogin(),
|
||||||
|
PermissionsController.requirePermission('leave-group-subscription'),
|
||||||
SubscriptionGroupController.removeSelfFromGroup
|
SubscriptionGroupController.removeSelfFromGroup
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -70,6 +72,7 @@ module.exports = {
|
||||||
'/subscription/invites/:token/',
|
'/subscription/invites/:token/',
|
||||||
AuthenticationController.requireLogin(),
|
AuthenticationController.requireLogin(),
|
||||||
RateLimiterMiddleware.rateLimit(teamInviteRateLimiter),
|
RateLimiterMiddleware.rateLimit(teamInviteRateLimiter),
|
||||||
|
PermissionsController.requirePermission('join-subscription'),
|
||||||
TeamInvitesController.acceptInvite
|
TeamInvitesController.acceptInvite
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -87,6 +90,7 @@ module.exports = {
|
||||||
webRouter.post(
|
webRouter.post(
|
||||||
'/user/subscription/create',
|
'/user/subscription/create',
|
||||||
AuthenticationController.requireLogin(),
|
AuthenticationController.requireLogin(),
|
||||||
|
PermissionsController.requirePermission('start-subscription'),
|
||||||
SubscriptionController.createSubscription
|
SubscriptionController.createSubscription
|
||||||
)
|
)
|
||||||
webRouter.post(
|
webRouter.post(
|
||||||
|
|
|
@ -194,6 +194,14 @@ async function ensureAffiliationMiddleware(req, res, next) {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return new Errors.UserNotFoundError({ info: { userId } })
|
return new Errors.UserNotFoundError({ info: { userId } })
|
||||||
}
|
}
|
||||||
|
// if the user does not have permission to add an affiliation, we skip this middleware
|
||||||
|
try {
|
||||||
|
req.assertPermission('add-affiliation')
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Errors.ForbiddenError) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await ensureAffiliation(user)
|
await ensureAffiliation(user)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -8,7 +8,7 @@ const GroupPolicySchema = new Schema(
|
||||||
userCannotDeleteOwnAccount: Boolean,
|
userCannotDeleteOwnAccount: Boolean,
|
||||||
|
|
||||||
// User can't add a secondary email address, or affiliation
|
// User can't add a secondary email address, or affiliation
|
||||||
userCannotAddSecondaryEmail: Boolean,
|
userCannotHaveSecondaryEmail: Boolean,
|
||||||
|
|
||||||
// User can't have an active (currently auto-renewing) personal subscription, nor can they start one
|
// User can't have an active (currently auto-renewing) personal subscription, nor can they start one
|
||||||
userCannotHaveSubscription: Boolean,
|
userCannotHaveSubscription: Boolean,
|
||||||
|
|
|
@ -12,6 +12,7 @@ const UploadsRouter = require('./Features/Uploads/UploadsRouter')
|
||||||
const metrics = require('@overleaf/metrics')
|
const metrics = require('@overleaf/metrics')
|
||||||
const ReferalController = require('./Features/Referal/ReferalController')
|
const ReferalController = require('./Features/Referal/ReferalController')
|
||||||
const AuthenticationController = require('./Features/Authentication/AuthenticationController')
|
const AuthenticationController = require('./Features/Authentication/AuthenticationController')
|
||||||
|
const PermissionsController = require('./Features/Authorization/PermissionsController')
|
||||||
const SessionManager = require('./Features/Authentication/SessionManager')
|
const SessionManager = require('./Features/Authentication/SessionManager')
|
||||||
const TagsController = require('./Features/Tags/TagsController')
|
const TagsController = require('./Features/Tags/TagsController')
|
||||||
const NotificationsController = require('./Features/Notifications/NotificationsController')
|
const NotificationsController = require('./Features/Notifications/NotificationsController')
|
||||||
|
@ -300,6 +301,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
|
||||||
webRouter.get(
|
webRouter.get(
|
||||||
'/user/emails',
|
'/user/emails',
|
||||||
AuthenticationController.requireLogin(),
|
AuthenticationController.requireLogin(),
|
||||||
|
PermissionsController.useCapabilities(),
|
||||||
UserController.promises.ensureAffiliationMiddleware,
|
UserController.promises.ensureAffiliationMiddleware,
|
||||||
UserEmailsController.list
|
UserEmailsController.list
|
||||||
)
|
)
|
||||||
|
@ -332,6 +334,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
|
||||||
webRouter.post(
|
webRouter.post(
|
||||||
'/user/emails',
|
'/user/emails',
|
||||||
AuthenticationController.requireLogin(),
|
AuthenticationController.requireLogin(),
|
||||||
|
PermissionsController.requirePermission('add-secondary-email'),
|
||||||
RateLimiterMiddleware.rateLimit(rateLimiters.addEmail),
|
RateLimiterMiddleware.rateLimit(rateLimiters.addEmail),
|
||||||
CaptchaMiddleware.validateCaptcha('addEmail'),
|
CaptchaMiddleware.validateCaptcha('addEmail'),
|
||||||
UserEmailsController.add
|
UserEmailsController.add
|
||||||
|
@ -350,6 +353,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
|
||||||
webRouter.post(
|
webRouter.post(
|
||||||
'/user/emails/endorse',
|
'/user/emails/endorse',
|
||||||
AuthenticationController.requireLogin(),
|
AuthenticationController.requireLogin(),
|
||||||
|
PermissionsController.requirePermission('endorse-email'),
|
||||||
RateLimiterMiddleware.rateLimit(rateLimiters.endorseEmail),
|
RateLimiterMiddleware.rateLimit(rateLimiters.endorseEmail),
|
||||||
UserEmailsController.endorse
|
UserEmailsController.endorse
|
||||||
)
|
)
|
||||||
|
@ -395,6 +399,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
|
||||||
'/user/delete',
|
'/user/delete',
|
||||||
RateLimiterMiddleware.rateLimit(rateLimiters.deleteUser),
|
RateLimiterMiddleware.rateLimit(rateLimiters.deleteUser),
|
||||||
AuthenticationController.requireLogin(),
|
AuthenticationController.requireLogin(),
|
||||||
|
PermissionsController.requirePermission('delete-own-account'),
|
||||||
UserController.tryDeleteUser
|
UserController.tryDeleteUser
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -205,6 +205,10 @@ class User {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setEmails(emails, callback) {
|
||||||
|
UserModel.updateOne({ _id: this.id }, { emails }, callback)
|
||||||
|
}
|
||||||
|
|
||||||
logout(callback) {
|
logout(callback) {
|
||||||
this.getCsrfToken(error => {
|
this.getCsrfToken(error => {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
|
|
|
@ -928,12 +928,18 @@ describe('UserController', function () {
|
||||||
]
|
]
|
||||||
this.Features.hasFeature.withArgs('affiliations').returns(true)
|
this.Features.hasFeature.withArgs('affiliations').returns(true)
|
||||||
this.req.query.ensureAffiliation = true
|
this.req.query.ensureAffiliation = true
|
||||||
|
this.req.assertPermission = sinon.stub()
|
||||||
await this.UserController.promises.ensureAffiliationMiddleware(
|
await this.UserController.promises.ensureAffiliationMiddleware(
|
||||||
this.req,
|
this.req,
|
||||||
this.res,
|
this.res,
|
||||||
this.next
|
this.next
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
it('should check the user has permission', function () {
|
||||||
|
expect(this.req.assertPermission).to.have.been.calledWith(
|
||||||
|
'add-affiliation'
|
||||||
|
)
|
||||||
|
})
|
||||||
it('should unflag the emails but not confirm', function () {
|
it('should unflag the emails but not confirm', function () {
|
||||||
expect(
|
expect(
|
||||||
this.UserUpdater.promises.addAffiliationForNewUser
|
this.UserUpdater.promises.addAffiliationForNewUser
|
||||||
|
@ -960,12 +966,18 @@ describe('UserController', function () {
|
||||||
]
|
]
|
||||||
this.Features.hasFeature.withArgs('affiliations').returns(true)
|
this.Features.hasFeature.withArgs('affiliations').returns(true)
|
||||||
this.req.query.ensureAffiliation = true
|
this.req.query.ensureAffiliation = true
|
||||||
|
this.req.assertPermission = sinon.stub()
|
||||||
await this.UserController.promises.ensureAffiliationMiddleware(
|
await this.UserController.promises.ensureAffiliationMiddleware(
|
||||||
this.req,
|
this.req,
|
||||||
this.res,
|
this.res,
|
||||||
this.next
|
this.next
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
it('should check the user has permission', function () {
|
||||||
|
expect(this.req.assertPermission).to.have.been.calledWith(
|
||||||
|
'add-affiliation'
|
||||||
|
)
|
||||||
|
})
|
||||||
it('should add affiliation to v1, unflag and confirm on v2', function () {
|
it('should add affiliation to v1, unflag and confirm on v2', function () {
|
||||||
expect(this.UserUpdater.promises.addAffiliationForNewUser).to.have.not
|
expect(this.UserUpdater.promises.addAffiliationForNewUser).to.have.not
|
||||||
.been.called
|
.been.called
|
||||||
|
@ -992,12 +1004,18 @@ describe('UserController', function () {
|
||||||
]
|
]
|
||||||
this.Features.hasFeature.withArgs('affiliations').returns(true)
|
this.Features.hasFeature.withArgs('affiliations').returns(true)
|
||||||
this.req.query.ensureAffiliation = true
|
this.req.query.ensureAffiliation = true
|
||||||
|
this.req.assertPermission = sinon.stub()
|
||||||
await this.UserController.promises.ensureAffiliationMiddleware(
|
await this.UserController.promises.ensureAffiliationMiddleware(
|
||||||
this.req,
|
this.req,
|
||||||
this.res,
|
this.res,
|
||||||
this.next
|
this.next
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
it('should check the user has permission', function () {
|
||||||
|
expect(this.req.assertPermission).to.have.been.calledWith(
|
||||||
|
'add-affiliation'
|
||||||
|
)
|
||||||
|
})
|
||||||
it('should return the error', function () {
|
it('should return the error', function () {
|
||||||
expect(this.next).to.be.calledWith(sinon.match.instanceOf(Error))
|
expect(this.next).to.be.calledWith(sinon.match.instanceOf(Error))
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue