mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-27 12:01:17 +00:00
Merge pull request #23006 from overleaf/msm-chat-capabilities-poc-2
[web] Add option to disable chat for subscription GitOrigin-RevId: 0052d060c74c39400496f7f9f54c820398d60012
This commit is contained in:
parent
ba60f885a4
commit
8ff8e7a4bf
14 changed files with 138 additions and 5 deletions
|
@ -8,6 +8,7 @@ const {
|
|||
const { assertUserPermissions } = require('./PermissionsManager').promises
|
||||
const Modules = require('../../infrastructure/Modules')
|
||||
const { expressify } = require('@overleaf/promise-utils')
|
||||
const Features = require('../../infrastructure/Features')
|
||||
|
||||
/**
|
||||
* Function that returns middleware to add an `assertPermission` function to the request object to check if the user has a specific capability.
|
||||
|
@ -80,6 +81,9 @@ function requirePermission(...requiredCapabilities) {
|
|||
throw new Error('invalid required capabilities')
|
||||
}
|
||||
const doRequest = async function (req, res, next) {
|
||||
if (!Features.hasFeature('saas')) {
|
||||
return next()
|
||||
}
|
||||
if (!req.user) {
|
||||
return next(new Error('no user'))
|
||||
}
|
||||
|
|
|
@ -63,6 +63,25 @@ function ensureCapabilityExists(capability) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an group policy object
|
||||
*
|
||||
* @param {Object} 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
|
||||
*/
|
||||
function validatePolicies(policies) {
|
||||
for (const [policy, value] of Object.entries(policies)) {
|
||||
if (!POLICY_TO_CAPABILITY_MAP.has(policy)) {
|
||||
throw new Error(`unknown policy: ${policy}`)
|
||||
}
|
||||
if (typeof value !== 'boolean') {
|
||||
throw new Error(`policy value must be a boolean: ${policy} = ${value}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new capability with the given name and options.
|
||||
*
|
||||
|
@ -439,6 +458,7 @@ async function checkUserListPermissions(userList, capabilities) {
|
|||
}
|
||||
|
||||
module.exports = {
|
||||
validatePolicies,
|
||||
registerCapability,
|
||||
registerPolicy,
|
||||
registerAllowedProperty,
|
||||
|
|
|
@ -801,7 +801,8 @@ const _ProjectController = {
|
|||
isTokenMember,
|
||||
isInvitedMember
|
||||
),
|
||||
chatEnabled: Features.hasFeature('chat'),
|
||||
chatEnabled:
|
||||
Features.hasFeature('chat') && req.capabilitySet.has('chat'),
|
||||
projectHistoryBlobsEnabled: Features.hasFeature(
|
||||
'project-history-blobs'
|
||||
),
|
||||
|
|
|
@ -149,7 +149,8 @@ async function projectListPage(req, res, next) {
|
|||
// TODO use helper function
|
||||
if (!user.enrollment?.managedBy) {
|
||||
groupSubscriptionsPendingEnrollment = subscriptions.filter(
|
||||
subscription => subscription.groupPlan && subscription.groupPolicy
|
||||
subscription =>
|
||||
subscription.groupPlan && subscription.managedUsersEnabled
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
@ -31,10 +31,12 @@ const SubscriptionLocator = {
|
|||
.exec()
|
||||
},
|
||||
|
||||
async getMemberSubscriptions(userOrId) {
|
||||
async getMemberSubscriptions(userOrId, populate = []) {
|
||||
const userId = SubscriptionLocator._getUserId(userOrId)
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return await Subscription.find({ member_ids: userId })
|
||||
.populate('admin_id', 'email')
|
||||
.populate(populate)
|
||||
.exec()
|
||||
},
|
||||
|
||||
|
|
|
@ -96,7 +96,7 @@ async function viewInvite(req, res, next) {
|
|||
personalSubscription.recurlySubscription_id &&
|
||||
personalSubscription.recurlySubscription_id !== ''
|
||||
|
||||
if (subscription?.groupPolicy) {
|
||||
if (subscription?.managedUsersEnabled) {
|
||||
if (!subscription.populated('groupPolicy')) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
await subscription.populate('groupPolicy')
|
||||
|
|
|
@ -24,6 +24,9 @@ const GroupPolicySchema = new Schema(
|
|||
|
||||
// User can't use any of our AI features, such as the compile-assistant
|
||||
userCannotUseAIFeatures: Boolean,
|
||||
|
||||
// User can't use the chat feature
|
||||
userCannotUseChat: Boolean,
|
||||
},
|
||||
{ minimize: false }
|
||||
)
|
||||
|
|
|
@ -1059,12 +1059,14 @@ async function initialize(webRouter, privateApiRouter, publicApiRouter) {
|
|||
'/project/:project_id/messages',
|
||||
AuthorizationMiddleware.blockRestrictedUserFromProject,
|
||||
AuthorizationMiddleware.ensureUserCanReadProject,
|
||||
PermissionsController.requirePermission('chat'),
|
||||
ChatController.getMessages
|
||||
)
|
||||
webRouter.post(
|
||||
'/project/:project_id/messages',
|
||||
AuthorizationMiddleware.blockRestrictedUserFromProject,
|
||||
AuthorizationMiddleware.ensureUserCanReadProject,
|
||||
PermissionsController.requirePermission('chat'),
|
||||
RateLimiterMiddleware.rateLimit(rateLimiters.sendChatMessage),
|
||||
ChatController.sendMessage
|
||||
)
|
||||
|
|
|
@ -27,6 +27,7 @@ import { GetProjectsResponseBody } from '../../../types/project/dashboard/api'
|
|||
import { Tag } from '../../../app/src/Features/Tags/types'
|
||||
import { Institution } from '../../../types/institution'
|
||||
import {
|
||||
GroupPolicy,
|
||||
ManagedGroupSubscription,
|
||||
MemberGroupSubscription,
|
||||
} from '../../../types/subscription/dashboard/subscription'
|
||||
|
@ -99,6 +100,7 @@ export interface Meta {
|
|||
'ol-groupId': string
|
||||
'ol-groupName': string
|
||||
'ol-groupPlans': GroupPlans
|
||||
'ol-groupPolicy': GroupPolicy
|
||||
'ol-groupSSOActive': boolean
|
||||
'ol-groupSSOTestResult': GroupSSOTestResult
|
||||
'ol-groupSettingsEnabledFor': string[]
|
||||
|
|
|
@ -112,7 +112,7 @@ h3.group-settings-title {
|
|||
}
|
||||
}
|
||||
|
||||
.below-managed-users {
|
||||
.below-settings-section {
|
||||
border-top: 1px solid @gray-lighter;
|
||||
padding-top: 25px;
|
||||
margin-top: 25px;
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
const tags = ['saas']
|
||||
|
||||
const migrate = async client => {
|
||||
const { db } = client
|
||||
await db.grouppolicies.updateMany({}, { $set: { userCannotUseChat: false } })
|
||||
}
|
||||
|
||||
const rollback = async client => {
|
||||
const { db } = client
|
||||
await db.grouppolicies.updateMany({}, { $unset: { userCannotUseChat: '' } })
|
||||
}
|
||||
|
||||
export default {
|
||||
tags,
|
||||
migrate,
|
||||
rollback,
|
||||
}
|
|
@ -678,6 +678,11 @@ describe('Authorization', function () {
|
|||
})
|
||||
|
||||
it('should allow an anonymous user chat messages access', function (done) {
|
||||
// chat access for anonymous users is a CE/SP-only feature, although currently broken
|
||||
// https://github.com/overleaf/internal/issues/10944
|
||||
if (Features.hasFeature('saas')) {
|
||||
this.skip()
|
||||
}
|
||||
expectChatAccess(this.anon, this.projectId, done)
|
||||
})
|
||||
|
||||
|
|
|
@ -64,6 +64,48 @@ describe('PermissionsManager', function () {
|
|||
]
|
||||
})
|
||||
|
||||
describe('validatePolicies', function () {
|
||||
it('accepts empty object', function () {
|
||||
expect(() => this.PermissionsManager.validatePolicies({})).not.to.throw
|
||||
})
|
||||
|
||||
it('accepts object with registered policies', function () {
|
||||
expect(() =>
|
||||
this.PermissionsManager.validatePolicies({
|
||||
openPolicy: true,
|
||||
restrictivePolicy: false,
|
||||
})
|
||||
).not.to.throw
|
||||
})
|
||||
|
||||
it('accepts object with policies containing non-boolean values', function () {
|
||||
expect(() =>
|
||||
this.PermissionsManager.validatePolicies({
|
||||
openPolicy: 1,
|
||||
})
|
||||
).to.throw('policy value must be a boolean: openPolicy = 1')
|
||||
expect(() =>
|
||||
this.PermissionsManager.validatePolicies({
|
||||
openPolicy: undefined,
|
||||
})
|
||||
).to.throw('policy value must be a boolean: openPolicy = undefined')
|
||||
expect(() =>
|
||||
this.PermissionsManager.validatePolicies({
|
||||
openPolicy: null,
|
||||
})
|
||||
).to.throw('policy value must be a boolean: openPolicy = null')
|
||||
})
|
||||
|
||||
it('throws error on object with policies that are not registered', function () {
|
||||
expect(() =>
|
||||
this.PermissionsManager.validatePolicies({
|
||||
openPolicy: true,
|
||||
unregisteredPolicy: false,
|
||||
})
|
||||
).to.throw('unknown policy: unregisteredPolicy')
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasPermission', function () {
|
||||
describe('when no policies apply to the user', function () {
|
||||
it('should return true if default permission is true', function () {
|
||||
|
|
|
@ -1132,6 +1132,40 @@ describe('ProjectController', function () {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('chatEnabled flag', function () {
|
||||
it('should be set to false when the feature is disabled', function (done) {
|
||||
this.Features.hasFeature = sinon.stub().withArgs('chat').returns(false)
|
||||
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.chatEnabled).to.be.false
|
||||
done()
|
||||
}
|
||||
this.ProjectController.loadEditor(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should be set to false when the feature is enabled but the capability is not available', function (done) {
|
||||
this.Features.hasFeature = sinon.stub().withArgs('chat').returns(false)
|
||||
this.req.capabilitySet = new Set()
|
||||
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.chatEnabled).to.be.false
|
||||
done()
|
||||
}
|
||||
this.ProjectController.loadEditor(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should be set to true when the feature is enabled and the capability is available', function (done) {
|
||||
this.Features.hasFeature = sinon.stub().withArgs('chat').returns(true)
|
||||
this.req.capabilitySet = new Set(['chat'])
|
||||
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.chatEnabled).to.be.true
|
||||
done()
|
||||
}
|
||||
this.ProjectController.loadEditor(this.req, this.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('userProjectsJson', function () {
|
||||
|
|
Loading…
Add table
Reference in a new issue