mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-02 05:41:33 +02:00
Merge pull request #13907 from overleaf/bg-managed-users-allow-cancelled-subscriptions
allow cancelled subscriptions for managed users GitOrigin-RevId: 56262ce4bd4cc93d4e5ea92222c76a874d6cad1e
This commit is contained in:
@@ -27,8 +27,12 @@ function useCapabilities() {
|
||||
}
|
||||
try {
|
||||
// get the group policy applying to the user
|
||||
const groupPolicy =
|
||||
await ManagedUsersHandler.promises.getGroupPolicyForUser(req.user)
|
||||
const { groupPolicy, managedBy, isManagedGroupAdmin } =
|
||||
await ManagedUsersHandler.promises.getEnrollmentForUser(req.user)
|
||||
// attach the subscription ID to the request object
|
||||
req.managedBy = managedBy
|
||||
// attach the subscription admin status to the request object
|
||||
req.isManagedGroupAdmin = isManagedGroupAdmin
|
||||
// attach the new capabilities to the request object
|
||||
for (const cap of getUserCapabilities(groupPolicy)) {
|
||||
req.capabilitySet.add(cap)
|
||||
@@ -66,8 +70,8 @@ function requirePermission(...requiredCapabilities) {
|
||||
}
|
||||
try {
|
||||
// get the group policy applying to the user
|
||||
const groupPolicy =
|
||||
await ManagedUsersHandler.promises.getGroupPolicyForUser(req.user)
|
||||
const { groupPolicy } =
|
||||
await ManagedUsersHandler.promises.getEnrollmentForUser(req.user)
|
||||
// if there is no group policy, the user is not managed
|
||||
if (!groupPolicy) {
|
||||
return next()
|
||||
|
||||
@@ -42,9 +42,9 @@ async function enableManagedUsers(subscriptionId) {
|
||||
* @function
|
||||
* @param {Object} user - The user object to retrieve the group policy for.
|
||||
* @returns {Promise<Object>} - A Promise that resolves with the group policy
|
||||
* object for the user's enrollment, or undefined if it does not exist.
|
||||
* and subscription objects for the user's enrollment, or null if it does not exist.
|
||||
*/
|
||||
async function getGroupPolicyForUser(requestedUser) {
|
||||
async function getEnrollmentForUser(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')
|
||||
@@ -53,7 +53,7 @@ async function getGroupPolicyForUser(requestedUser) {
|
||||
}
|
||||
// Now we are sure the user exists and we have the full contents
|
||||
if (user.enrollment?.managedBy == null) {
|
||||
return
|
||||
return {}
|
||||
}
|
||||
// retrieve the subscription and the group policy (without the _id field)
|
||||
const subscription = await Subscription.findById(user.enrollment.managedBy)
|
||||
@@ -64,11 +64,20 @@ async function getGroupPolicyForUser(requestedUser) {
|
||||
info: { subscriptionId: user.enrollment.managedBy, userId: user._id },
|
||||
})
|
||||
}
|
||||
|
||||
// check whether the user is an admin of the subscription
|
||||
const isManagedGroupAdmin = user._id.equals(subscription.admin_id)
|
||||
|
||||
// return the group policy as a plain object (without the __v field)
|
||||
const groupPolicy = subscription.groupPolicy.toObject({
|
||||
versionKey: false,
|
||||
})
|
||||
return groupPolicy
|
||||
|
||||
return {
|
||||
groupPolicy,
|
||||
managedBy: user.enrollment.managedBy,
|
||||
isManagedGroupAdmin,
|
||||
}
|
||||
}
|
||||
|
||||
async function enrollInSubscription(userId, subscription) {
|
||||
@@ -109,10 +118,10 @@ async function enrollInSubscription(userId, subscription) {
|
||||
module.exports = {
|
||||
promises: {
|
||||
enableManagedUsers,
|
||||
getGroupPolicyForUser,
|
||||
getEnrollmentForUser,
|
||||
enrollInSubscription,
|
||||
},
|
||||
enableManagedUsers: callbackify(enableManagedUsers),
|
||||
getGroupPolicyForUser: callbackify(getGroupPolicyForUser),
|
||||
getEnrollmentForUser: callbackify(getEnrollmentForUser),
|
||||
enrollInSubscription: callbackify(enrollInSubscription),
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ const {
|
||||
} = require('../Authorization/PermissionsManager')
|
||||
const { getUsersSubscription, getGroupSubscriptionsMemberOf } =
|
||||
require('./SubscriptionLocator').promises
|
||||
const { subscriptionIsCanceledOrExpired } = require('./RecurlyClient')
|
||||
|
||||
// This file defines the capabilities and policies that are used to
|
||||
// determine what managed users can and cannot do.
|
||||
@@ -35,6 +36,9 @@ registerCapability('start-subscription', { default: true })
|
||||
// Register the capability for a user to join a subscription.
|
||||
registerCapability('join-subscription', { default: true })
|
||||
|
||||
// Register the capability for a user to reactivate a subscription.
|
||||
registerCapability('reactivate-subscription', { default: true })
|
||||
|
||||
// Register a policy to prevent a user deleting their own account.
|
||||
registerPolicy('userCannotDeleteOwnAccount', {
|
||||
'delete-own-account': false,
|
||||
@@ -91,20 +95,54 @@ registerPolicy(
|
||||
// being a member of another group subscription.
|
||||
registerPolicy(
|
||||
'userCannotHaveSubscription',
|
||||
{ 'start-subscription': false, 'join-subscription': false },
|
||||
{
|
||||
'start-subscription': false,
|
||||
'join-subscription': false,
|
||||
'reactivate-subscription': false,
|
||||
},
|
||||
{
|
||||
validator: async ({ user, subscription }) => {
|
||||
const usersSubscription = await getUsersSubscription(user)
|
||||
const userHasSubscription = Boolean(usersSubscription)
|
||||
|
||||
// The user can be enrolled if:
|
||||
// 1. they do not have a subscription and are not a member of another subscription (apart from the managed group subscription)
|
||||
// 2. they have a subscription and it is canceled or expired
|
||||
// 3. they have a subscription and it is the subscription they are trying to join as a managed user
|
||||
// The last case is to allow the admin to join their own subscription as a managed user
|
||||
|
||||
const userHasSubscription =
|
||||
Boolean(usersSubscription) &&
|
||||
!subscriptionIsCanceledOrExpired(usersSubscription)
|
||||
|
||||
const userIsThisGroupAdmin =
|
||||
Boolean(usersSubscription) &&
|
||||
usersSubscription._id.toString() === subscription._id.toString()
|
||||
|
||||
const userMemberOfSubscriptions = await getGroupSubscriptionsMemberOf(
|
||||
user
|
||||
)
|
||||
// filter out the subscription of the managed group itself
|
||||
// the user will always be a member of this subscription
|
||||
|
||||
const isMemberOfOtherSubscriptions = userMemberOfSubscriptions.some(
|
||||
sub => sub._id.toString() !== subscription._id.toString()
|
||||
sub => {
|
||||
// ignore the subscription of the managed group itself
|
||||
if (sub._id.toString() === subscription._id.toString()) {
|
||||
return false
|
||||
}
|
||||
// ignore the user's own subscription
|
||||
if (
|
||||
usersSubscription &&
|
||||
sub._id.toString() === usersSubscription._id.toString()
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
(!userHasSubscription || userIsThisGroupAdmin) &&
|
||||
!isMemberOfOtherSubscriptions
|
||||
)
|
||||
return !userHasSubscription && !isMemberOfOtherSubscriptions
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -90,6 +90,11 @@ async function cancelSubscriptionByUuid(subscriptionUuid) {
|
||||
}
|
||||
}
|
||||
|
||||
function subscriptionIsCanceledOrExpired(subscription) {
|
||||
const state = subscription?.recurlyStatus?.state
|
||||
return state === 'canceled' || state === 'expired'
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
errors: recurly.errors,
|
||||
|
||||
@@ -102,6 +107,7 @@ module.exports = {
|
||||
removeSubscriptionChangeByUuid: callbackify(removeSubscriptionChangeByUuid),
|
||||
reactivateSubscriptionByUuid: callbackify(reactivateSubscriptionByUuid),
|
||||
cancelSubscriptionByUuid: callbackify(cancelSubscriptionByUuid),
|
||||
subscriptionIsCanceledOrExpired,
|
||||
|
||||
promises: {
|
||||
getSubscription,
|
||||
|
||||
@@ -574,6 +574,16 @@ function updateAccountEmailAddress(req, res, next) {
|
||||
function reactivateSubscription(req, res, next) {
|
||||
const user = SessionManager.getSessionUser(req.session)
|
||||
logger.debug({ userId: user._id }, 'reactivating subscription')
|
||||
try {
|
||||
if (req.isManagedGroupAdmin) {
|
||||
// allow admins to reactivate subscriptions
|
||||
} else {
|
||||
// otherwise require the user to have the reactivate-subscription permission
|
||||
req.assertPermission('reactivate-subscription')
|
||||
}
|
||||
} catch (error) {
|
||||
return next(error)
|
||||
}
|
||||
SubscriptionHandler.reactivateSubscription(user, function (err) {
|
||||
if (err) {
|
||||
OError.tag(err, 'something went wrong reactivating subscription', {
|
||||
|
||||
@@ -111,6 +111,7 @@ module.exports = {
|
||||
webRouter.post(
|
||||
'/user/subscription/reactivate',
|
||||
AuthenticationController.requireLogin(),
|
||||
PermissionsController.useCapabilities(),
|
||||
SubscriptionController.reactivateSubscription
|
||||
)
|
||||
|
||||
|
||||
@@ -66,8 +66,8 @@ class Subscription {
|
||||
ManagedUsersHandler.enableManagedUsers(this._id, callback)
|
||||
}
|
||||
|
||||
getGroupPolicyForUser(user, callback) {
|
||||
ManagedUsersHandler.getGroupPolicyForUser(user, callback)
|
||||
getEnrollmentForUser(user, callback) {
|
||||
ManagedUsersHandler.getEnrollmentForUser(user, callback)
|
||||
}
|
||||
|
||||
getCapabilities(groupPolicy) {
|
||||
|
||||
@@ -653,26 +653,76 @@ describe('SubscriptionController', function () {
|
||||
})
|
||||
|
||||
describe('reactivateSubscription', function () {
|
||||
beforeEach(function (done) {
|
||||
this.res = {
|
||||
redirect() {
|
||||
describe('when the user has permission', function () {
|
||||
beforeEach(function (done) {
|
||||
this.res = {
|
||||
redirect() {
|
||||
done()
|
||||
},
|
||||
}
|
||||
this.req.assertPermission = sinon.stub()
|
||||
this.next = sinon.stub().callsFake(error => {
|
||||
done(error)
|
||||
})
|
||||
sinon.spy(this.res, 'redirect')
|
||||
this.SubscriptionController.reactivateSubscription(
|
||||
this.req,
|
||||
this.res,
|
||||
this.next
|
||||
)
|
||||
})
|
||||
|
||||
it('should assert the user has permission to reactivate their subscription', function (done) {
|
||||
this.req.assertPermission
|
||||
.calledWith('reactivate-subscription')
|
||||
.should.equal(true)
|
||||
done()
|
||||
})
|
||||
|
||||
it('should tell the handler to reactivate this user', function (done) {
|
||||
this.SubscriptionHandler.reactivateSubscription
|
||||
.calledWith(this.user)
|
||||
.should.equal(true)
|
||||
done()
|
||||
})
|
||||
|
||||
it('should redurect to the subscription page', function (done) {
|
||||
this.res.redirect.calledWith('/user/subscription').should.equal(true)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the user does not have permission', function () {
|
||||
beforeEach(function (done) {
|
||||
this.res = {
|
||||
redirect() {
|
||||
done()
|
||||
},
|
||||
}
|
||||
this.req.assertPermission = sinon.stub().throws()
|
||||
this.next = sinon.stub().callsFake(() => {
|
||||
done()
|
||||
},
|
||||
}
|
||||
sinon.spy(this.res, 'redirect')
|
||||
this.SubscriptionController.reactivateSubscription(this.req, this.res)
|
||||
})
|
||||
})
|
||||
sinon.spy(this.res, 'redirect')
|
||||
this.SubscriptionController.reactivateSubscription(
|
||||
this.req,
|
||||
this.res,
|
||||
this.next
|
||||
)
|
||||
})
|
||||
|
||||
it('should tell the handler to reactivate this user', function (done) {
|
||||
this.SubscriptionHandler.reactivateSubscription
|
||||
.calledWith(this.user)
|
||||
.should.equal(true)
|
||||
done()
|
||||
})
|
||||
it('should not reactivate the user', function (done) {
|
||||
this.req.assertPermission = sinon.stub().throws()
|
||||
this.SubscriptionHandler.reactivateSubscription.called.should.equal(
|
||||
false
|
||||
)
|
||||
done()
|
||||
})
|
||||
|
||||
it('should redurect to the subscription page', function (done) {
|
||||
this.res.redirect.calledWith('/user/subscription').should.equal(true)
|
||||
done()
|
||||
it('should call next with an error', function (done) {
|
||||
this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user