diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index 4821a3f90b..45c92c250d 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -24,6 +24,7 @@ module.exports = AuthenticationController = first_name: user.first_name last_name: user.last_name isAdmin: user.isAdmin + staffAccess: user.staffAccess email: user.email referal_id: user.referal_id session_created: (new Date()).toISOString() diff --git a/services/web/app/coffee/Features/UserMembership/UserMembershipAuthorization.coffee b/services/web/app/coffee/Features/UserMembership/UserMembershipAuthorization.coffee index f9be14b9ba..40b969885f 100644 --- a/services/web/app/coffee/Features/UserMembership/UserMembershipAuthorization.coffee +++ b/services/web/app/coffee/Features/UserMembership/UserMembershipAuthorization.coffee @@ -8,22 +8,31 @@ settings = require 'settings-sharelatex' request = require 'request' module.exports = UserMembershipAuthorization = - requireTeamAccess: (req, res, next) -> - requireAccessToEntity('team', req.params.id, req, res, next) + requireTeamMetricsAccess: (req, res, next) -> + requireAccessToEntity('team', req.params.id, req, res, next, 'groupMetrics') - requireGroupAccess: (req, res, next) -> - requireAccessToEntity('group', req.params.id, req, res, next) + requireGroupManagementAccess: (req, res, next) -> + requireAccessToEntity('group', req.params.id, req, res, next, 'groupManagement') - requireGroupManagersAccess: (req, res, next) -> - requireAccessToEntity('groupManagers', req.params.id, req, res, next) + requireGroupMetricsAccess: (req, res, next) -> + requireAccessToEntity('group', req.params.id, req, res, next, 'groupMetrics') - requireInstitutionAccess: (req, res, next) -> - requireAccessToEntity('institution', req.params.id, req, res, next) + requireGroupManagersManagementAccess: (req, res, next) -> + requireAccessToEntity('groupManagers', req.params.id, req, res, next, 'groupManagement') - requirePublisherAccess: (req, res, next) -> - requireAccessToEntity('publisher', req.params.id, req, res, next) + requireInstitutionMetricsAccess: (req, res, next) -> + requireAccessToEntity('institution', req.params.id, req, res, next, 'institutionMetrics') - requireTemplateAccess: (req, res, next) -> + requireInstitutionManagementAccess: (req, res, next) -> + requireAccessToEntity('institution', req.params.id, req, res, next, 'institutionManagement') + + requirePublisherMetricsAccess: (req, res, next) -> + requireAccessToEntity('publisher', req.params.id, req, res, next, 'publisherMetrics') + + requirePublisherManagementAccess: (req, res, next) -> + requireAccessToEntity('publisher', req.params.id, req, res, next, 'publisherManagement') + + requireTemplateMetricsAccess: (req, res, next) -> templateId = req.params.id request { baseUrl: settings.apis.v1.url @@ -51,26 +60,29 @@ module.exports = UserMembershipAuthorization = id: body.id title: body.title if body?.brand?.slug - requireAccessToEntity('publisher', body.brand.slug, req, res, next) + req.params.id = body.brand.slug + UserMembershipAuthorization.requirePublisherMetricsAccess(req, res, next) else AuthorizationMiddlewear.ensureUserIsSiteAdmin(req, res, next) requireGraphAccess: (req, res, next) -> + req.params.id = req.query.resource_id if req.query.resource_type == 'template' - # templates are a special case; can't use requireaccesstoentity directly - req.params.id = req.query.resource_id - return UserMembershipAuthorization.requireTemplateAccess(req, res, next) + return UserMembershipAuthorization.requireTemplateMetricsAccess(req, res, next) + else if req.query.resource_type == 'institution' + return UserMembershipAuthorization.requireInstitutionMetricsAccess(req, res, next) + else if req.query.resource_type == 'group' + return UserMembershipAuthorization.requireGroupMetricsAccess(req, res, next) + else if req.query.resource_type == 'team' + return UserMembershipAuthorization.requireTeamMetricsAccess(req, res, next) + requireAccessToEntity(req.query.resource_type, req.query.resource_id, req, res, next) - requireAccessToEntity( - req.query.resource_type, req.query.resource_id, req, res, next - ) - -requireAccessToEntity = (entityName, entityId, req, res, next) -> +requireAccessToEntity = (entityName, entityId, req, res, next, requiredStaffAccess=null) -> loggedInUser = AuthenticationController.getSessionUser(req) unless loggedInUser return AuthorizationMiddlewear.redirectToRestricted req, res, next - getEntity entityName, entityId, loggedInUser, (error, entity, entityConfig, entityExists) -> + getEntity entityName, entityId, loggedInUser, requiredStaffAccess, (error, entity, entityConfig, entityExists) -> return next(error) if error? if entity? @@ -87,12 +99,12 @@ requireAccessToEntity = (entityName, entityId, req, res, next) -> next(new Errors.NotFoundError()) -getEntity = (entityName, entityId, user, callback = (error, entity, entityConfig, entityExists)->) -> +getEntity = (entityName, entityId, user, requiredStaffAccess, callback = (error, entity, entityConfig, entityExists)->) -> entityConfig = EntityConfigs[entityName] unless entityConfig return callback(new Errors.NotFoundError("No such entity: #{entityName}")) - UserMembershipHandler.getEntity entityId, entityConfig, user, (error, entity)-> + UserMembershipHandler.getEntity entityId, entityConfig, user, requiredStaffAccess, (error, entity)-> return callback(error) if error? return callback(null, entity, entityConfig, true) if entity? diff --git a/services/web/app/coffee/Features/UserMembership/UserMembershipHandler.coffee b/services/web/app/coffee/Features/UserMembership/UserMembershipHandler.coffee index 5ec0d1a487..0371cdbb09 100644 --- a/services/web/app/coffee/Features/UserMembership/UserMembershipHandler.coffee +++ b/services/web/app/coffee/Features/UserMembership/UserMembershipHandler.coffee @@ -11,9 +11,9 @@ logger = require('logger-sharelatex') UserMembershipEntityConfigs = require "./UserMembershipEntityConfigs" module.exports = - getEntity: (entityId, entityConfig, loggedInUser, callback = (error, entity) ->) -> + getEntity: (entityId, entityConfig, loggedInUser, requiredStaffAccess, callback = (error, entity) ->) -> query = buildEntityQuery(entityId, entityConfig) - unless loggedInUser.isAdmin + unless loggedInUser.isAdmin or loggedInUser.staffAccess?[requiredStaffAccess] query[entityConfig.fields.access] = ObjectId(loggedInUser._id) EntityModels[entityConfig.modelName].findOne query, callback diff --git a/services/web/app/coffee/Features/UserMembership/UserMembershipRouter.coffee b/services/web/app/coffee/Features/UserMembership/UserMembershipRouter.coffee index d29d6571e5..7221f67b8c 100644 --- a/services/web/app/coffee/Features/UserMembership/UserMembershipRouter.coffee +++ b/services/web/app/coffee/Features/UserMembership/UserMembershipRouter.coffee @@ -8,52 +8,52 @@ module.exports = apply: (webRouter) -> # group members routes webRouter.get '/manage/groups/:id/members', - UserMembershipAuthorization.requireGroupAccess, + UserMembershipAuthorization.requireGroupManagementAccess, UserMembershipController.index webRouter.post '/manage/groups/:id/invites', - UserMembershipAuthorization.requireGroupAccess, + UserMembershipAuthorization.requireGroupManagementAccess, TeamInvitesController.createInvite webRouter.delete '/manage/groups/:id/user/:user_id', - UserMembershipAuthorization.requireGroupAccess, + UserMembershipAuthorization.requireGroupManagementAccess, SubscriptionGroupController.removeUserFromGroup webRouter.delete '/manage/groups/:id/invites/:email', - UserMembershipAuthorization.requireGroupAccess, + UserMembershipAuthorization.requireGroupManagementAccess, TeamInvitesController.revokeInvite webRouter.get '/manage/groups/:id/members/export', - UserMembershipAuthorization.requireGroupAccess, + UserMembershipAuthorization.requireGroupManagementAccess, UserMembershipController.exportCsv # group managers routes webRouter.get "/manage/groups/:id/managers", - UserMembershipAuthorization.requireGroupManagersAccess, + UserMembershipAuthorization.requireGroupManagersManagementAccess, UserMembershipController.index webRouter.post "/manage/groups/:id/managers", - UserMembershipAuthorization.requireGroupManagersAccess, + UserMembershipAuthorization.requireGroupManagersManagementAccess, UserMembershipController.add webRouter.delete "/manage/groups/:id/managers/:userId", - UserMembershipAuthorization.requireGroupManagersAccess, + UserMembershipAuthorization.requireGroupManagersManagementAccess, UserMembershipController.remove # institution members routes webRouter.get "/manage/institutions/:id/managers", - UserMembershipAuthorization.requireInstitutionAccess, + UserMembershipAuthorization.requireInstitutionManagementAccess, UserMembershipController.index webRouter.post "/manage/institutions/:id/managers", - UserMembershipAuthorization.requireInstitutionAccess, + UserMembershipAuthorization.requireInstitutionManagementAccess, UserMembershipController.add webRouter.delete "/manage/institutions/:id/managers/:userId", - UserMembershipAuthorization.requireInstitutionAccess, + UserMembershipAuthorization.requireInstitutionManagementAccess, UserMembershipController.remove # publisher members routes webRouter.get "/manage/publishers/:id/managers", - UserMembershipAuthorization.requirePublisherAccess, + UserMembershipAuthorization.requirePublisherManagementAccess, UserMembershipController.index webRouter.post "/manage/publishers/:id/managers", - UserMembershipAuthorization.requirePublisherAccess, + UserMembershipAuthorization.requirePublisherManagementAccess, UserMembershipController.add webRouter.delete "/manage/publishers/:id/managers/:userId", - UserMembershipAuthorization.requirePublisherAccess, + UserMembershipAuthorization.requirePublisherManagementAccess, UserMembershipController.remove # create new entitites diff --git a/services/web/app/coffee/models/User.coffee b/services/web/app/coffee/models/User.coffee index 663a86b469..230e8835a0 100644 --- a/services/web/app/coffee/models/User.coffee +++ b/services/web/app/coffee/models/User.coffee @@ -20,6 +20,14 @@ UserSchema = new Schema institution : {type : String, default : ''} hashedPassword : String isAdmin : {type : Boolean, default : false} + staffAccess: { + publisherMetrics: {type : Boolean, default: false} + publisherManagement: {type : Boolean, default: false} + institutionMetrics: {type : Boolean, default: false} + institutionManagement: {type : Boolean, default: false} + groupMetrics: {type : Boolean, default: false} + groupManagement: {type : Boolean, default: false} + } signUpDate : {type : Date, default: () -> new Date() } lastLoggedIn : {type : Date} lastLoginIp : {type : String, default : ''} diff --git a/services/web/test/unit/coffee/UserMembership/UserMembershipAuthorizationTests.coffee b/services/web/test/unit/coffee/UserMembership/UserMembershipAuthorizationTests.coffee index a708b6106d..7702e6ae4c 100644 --- a/services/web/test/unit/coffee/UserMembership/UserMembershipAuthorizationTests.coffee +++ b/services/web/test/unit/coffee/UserMembership/UserMembershipAuthorizationTests.coffee @@ -35,7 +35,7 @@ describe "UserMembershipAuthorization", -> describe 'requireAccessToEntity', -> it 'get entity', (done) -> - @UserMembershipAuthorization.requireGroupAccess @req, null, (error) => + @UserMembershipAuthorization.requireGroupMetricsAccess @req, null, (error) => expect(error).to.not.extist sinon.assert.calledWithMatch( @UserMembershipHandler.getEntity, @@ -50,7 +50,7 @@ describe "UserMembershipAuthorization", -> it 'handle entity not found as non-admin', (done) -> @UserMembershipHandler.getEntity.yields(null, null) @UserMembershipHandler.getEntityWithoutAuthorizationCheck.yields(null, null) - @UserMembershipAuthorization.requireGroupAccess @req, null, (error) => + @UserMembershipAuthorization.requireGroupMetricsAccess @req, null, (error) => expect(error).to.extist expect(error).to.be.instanceof(Error) expect(error.constructor.name).to.equal('NotFoundError') @@ -62,7 +62,7 @@ describe "UserMembershipAuthorization", -> @user.isAdmin = true @UserMembershipHandler.getEntity.yields(null, null) @UserMembershipHandler.getEntityWithoutAuthorizationCheck.yields(null, null) - @UserMembershipAuthorization.requirePublisherAccess @req, redirect: (path) => + @UserMembershipAuthorization.requirePublisherMetricsAccess @req, redirect: (path) => expect(path).to.extist expect(path).to.match /create/ done() @@ -71,7 +71,7 @@ describe "UserMembershipAuthorization", -> @user.isAdmin = true @UserMembershipHandler.getEntity.yields(null, null) @UserMembershipHandler.getEntityWithoutAuthorizationCheck.yields(null, null) - @UserMembershipAuthorization.requireGroupAccess @req, null, (error) => + @UserMembershipAuthorization.requireGroupMetricsAccess @req, null, (error) => expect(error).to.extist expect(error).to.be.instanceof(Error) expect(error.constructor.name).to.equal('NotFoundError') @@ -79,13 +79,13 @@ describe "UserMembershipAuthorization", -> it 'handle entity no access', (done) -> @UserMembershipHandler.getEntity.yields(null, null) - @UserMembershipAuthorization.requireGroupAccess @req, null, (error) => + @UserMembershipAuthorization.requireGroupMetricsAccess @req, null, (error) => sinon.assert.called(@AuthorizationMiddlewear.redirectToRestricted) done() it 'handle anonymous user', (done) -> @AuthenticationController.getSessionUser.returns(null) - @UserMembershipAuthorization.requireGroupAccess @req, null, (error) => + @UserMembershipAuthorization.requireGroupMetricsAccess @req, null, (error) => expect(error).to.extist sinon.assert.called(@AuthorizationMiddlewear.redirectToRestricted) sinon.assert.notCalled(@UserMembershipHandler.getEntity) @@ -94,7 +94,7 @@ describe "UserMembershipAuthorization", -> describe 'requireEntityAccess', -> it 'handle team access', (done) -> - @UserMembershipAuthorization.requireTeamAccess @req, null, (error) => + @UserMembershipAuthorization.requireTeamMetricsAccess @req, null, (error) => expect(error).to.not.extist sinon.assert.calledWithMatch( @UserMembershipHandler.getEntity, @@ -104,7 +104,7 @@ describe "UserMembershipAuthorization", -> done() it 'handle group access', (done) -> - @UserMembershipAuthorization.requireGroupAccess @req, null, (error) => + @UserMembershipAuthorization.requireGroupMetricsAccess @req, null, (error) => expect(error).to.not.extist sinon.assert.calledWithMatch( @UserMembershipHandler.getEntity, @@ -114,7 +114,7 @@ describe "UserMembershipAuthorization", -> done() it 'handle group managers access', (done) -> - @UserMembershipAuthorization.requireGroupManagersAccess @req, null, (error) => + @UserMembershipAuthorization.requireGroupManagersManagementAccess @req, null, (error) => expect(error).to.not.extist sinon.assert.calledWithMatch( @UserMembershipHandler.getEntity, @@ -124,7 +124,7 @@ describe "UserMembershipAuthorization", -> done() it 'handle institution access', (done) -> - @UserMembershipAuthorization.requireInstitutionAccess @req, null, (error) => + @UserMembershipAuthorization.requireInstitutionMetricsAccess @req, null, (error) => expect(error).to.not.extist sinon.assert.calledWithMatch( @UserMembershipHandler.getEntity, @@ -139,7 +139,7 @@ describe "UserMembershipAuthorization", -> title: 'Template Title' brand: { slug: 'brand-slug' } @request.yields(null, { statusCode: 200 }, JSON.stringify(templateData)) - @UserMembershipAuthorization.requireTemplateAccess @req, null, (error) => + @UserMembershipAuthorization.requireTemplateMetricsAccess @req, null, (error) => expect(error).to.not.extist sinon.assert.calledWithMatch( @UserMembershipHandler.getEntity, @@ -154,7 +154,7 @@ describe "UserMembershipAuthorization", -> title: 'Template Title' brand: null @request.yields(null, { statusCode: 200 }, JSON.stringify(templateData)) - @UserMembershipAuthorization.requireTemplateAccess @req, null, (error) => + @UserMembershipAuthorization.requireTemplateMetricsAccess @req, null, (error) => expect(error).to.not.extist sinon.assert.notCalled(@UserMembershipHandler.getEntity) sinon.assert.calledOnce(@AuthorizationMiddlewear.ensureUserIsSiteAdmin) diff --git a/services/web/test/unit/coffee/UserMembership/UserMembershipHandlerTests.coffee b/services/web/test/unit/coffee/UserMembership/UserMembershipHandlerTests.coffee index 49a15de212..ae4d702afa 100644 --- a/services/web/test/unit/coffee/UserMembership/UserMembershipHandlerTests.coffee +++ b/services/web/test/unit/coffee/UserMembership/UserMembershipHandlerTests.coffee @@ -61,7 +61,7 @@ describe 'UserMembershipHandler', -> describe 'getEntity', -> describe 'group subscriptions', -> it 'get subscription', (done) -> - @UserMembershipHandler.getEntity @fakeEntityId, EntityConfigs.group, @user, (error, subscription) => + @UserMembershipHandler.getEntity @fakeEntityId, EntityConfigs.group, @user, null, (error, subscription) => should.not.exist(error) expectedQuery = groupPlan: true @@ -73,7 +73,16 @@ describe 'UserMembershipHandler', -> done() it 'get for admin', (done) -> - @UserMembershipHandler.getEntity @fakeEntityId, EntityConfigs.group, { isAdmin: true }, (error, subscription) => + @UserMembershipHandler.getEntity @fakeEntityId, EntityConfigs.group, { isAdmin: true }, null, (error, subscription) => + should.not.exist(error) + expectedQuery = + groupPlan: true + _id: @fakeEntityId + assertCalledWith(@Subscription.findOne, expectedQuery) + done() + + it 'get with staffAccess field', (done) -> + @UserMembershipHandler.getEntity @fakeEntityId, EntityConfigs.group, { staffAccess: {institutionMetrics: true}}, 'institutionMetrics', (error, subscription) => should.not.exist(error) expectedQuery = groupPlan: true @@ -83,7 +92,7 @@ describe 'UserMembershipHandler', -> it 'handle error', (done) -> @Subscription.findOne.yields(new Error('some error')) - @UserMembershipHandler.getEntity @fakeEntityId, EntityConfigs.group, @user._id, (error, subscription) => + @UserMembershipHandler.getEntity @fakeEntityId, EntityConfigs.group, @user._id, null, (error, subscription) => should.exist(error) done() @@ -98,7 +107,7 @@ describe 'UserMembershipHandler', -> describe 'institutions', -> it 'get institution', (done) -> - @UserMembershipHandler.getEntity @institution.v1Id, EntityConfigs.institution, @user, (error, institution) => + @UserMembershipHandler.getEntity @institution.v1Id, EntityConfigs.institution, @user, null, (error, institution) => should.not.exist(error) expectedQuery = v1Id: @institution.v1Id, managerIds: ObjectId(@user._id) assertCalledWith(@Institution.findOne, expectedQuery) @@ -107,14 +116,14 @@ describe 'UserMembershipHandler', -> it 'handle errors', (done) -> @Institution.findOne.yields(new Error('nope')) - @UserMembershipHandler.getEntity @fakeEntityId, EntityConfigs.institution, @user._id, (error, institution) => + @UserMembershipHandler.getEntity @fakeEntityId, EntityConfigs.institution, @user._id, null, (error, institution) => should.exist(error) expect(error).to.not.be.an.instanceof(Errors.NotFoundError) done() describe 'publishers', -> it 'get publisher', (done) -> - @UserMembershipHandler.getEntity @publisher.slug, EntityConfigs.publisher, @user, (error, institution) => + @UserMembershipHandler.getEntity @publisher.slug, EntityConfigs.publisher, @user, null, (error, institution) => should.not.exist(error) expectedQuery = slug: @publisher.slug, managerIds: ObjectId(@user._id) assertCalledWith(@Publisher.findOne, expectedQuery)