diff --git a/services/web/app/coffee/Features/UserMembership/UserMembershipAuthorization.coffee b/services/web/app/coffee/Features/UserMembership/UserMembershipAuthorization.coffee index c21e89a1fa..19bff5b2e0 100644 --- a/services/web/app/coffee/Features/UserMembership/UserMembershipAuthorization.coffee +++ b/services/web/app/coffee/Features/UserMembership/UserMembershipAuthorization.coffee @@ -67,20 +67,33 @@ requireAccessToEntity = (entityName, entityId, req, res, next) -> unless loggedInUser return AuthorizationMiddlewear.redirectToRestricted req, res, next - getEntity entityName, entityId, loggedInUser, (error, entity, entityConfig) -> + getEntity entityName, entityId, loggedInUser, (error, entity, entityConfig, entityExists) -> return next(error) if error? - unless entity? + + if entity? + req.entity = entity + req.entityConfig = entityConfig + return next() + + if entityExists # user doesn't have access to entity return AuthorizationMiddlewear.redirectToRestricted(req, res, next) - req.entity = entity - req.entityConfig = entityConfig - next() + if loggedInUser.isAdmin and entityConfig.canCreate + # entity doesn't exists, admin can create it + return res.redirect "/entities/#{entityName}/create/#{entityId}" -getEntity = (entityName, entityId, userId, callback = (error, entity, entityConfig)->) -> + next(new Errors.NotFoundError()) + +getEntity = (entityName, entityId, user, callback = (error, entity, entityConfig, entityExists)->) -> entityConfig = EntityConfigs[entityName] unless entityConfig return callback(new Errors.NotFoundError("No such entity: #{entityName}")) - UserMembershipHandler.getEntity entityId, entityConfig, userId, (error, entity)-> + UserMembershipHandler.getEntity entityId, entityConfig, user, (error, entity)-> return callback(error) if error? - callback(null, entity, entityConfig) + return callback(null, entity, entityConfig, true) if entity? + + # no access to entity. Check if entity exists + UserMembershipHandler.getEntityWithoutAuthorizationCheck entityId, entityConfig, (error, entity)-> + return callback(error) if error? + callback(null, null, entityConfig, entity?) diff --git a/services/web/app/coffee/Features/UserMembership/UserMembershipController.coffee b/services/web/app/coffee/Features/UserMembership/UserMembershipController.coffee index 3a8e26ca1d..1aced1c2a6 100644 --- a/services/web/app/coffee/Features/UserMembership/UserMembershipController.coffee +++ b/services/web/app/coffee/Features/UserMembership/UserMembershipController.coffee @@ -80,3 +80,21 @@ module.exports = ) res.contentType('text/csv') res.send(csvOutput) + + new: (req, res, next)-> + res.render "user_membership/new", + entityName: req.params.name + entityId: req.params.id + + create: (req, res, next)-> + entityName = req.params.name + entityId = req.params.id + entityConfig = EntityConfigs[entityName] + unless entityConfig + return next(new Errors.NotFoundError("No such entity: #{entityName}")) + unless entityConfig.canCreate + return next(new Errors.NotFoundError("Cannot create new #{entityName}")) + + UserMembershipHandler.createEntity entityId, entityConfig, (error, entity) -> + return next(error) if error? + res.redirect entityConfig.pathsFor(entityId).index diff --git a/services/web/app/coffee/Features/UserMembership/UserMembershipEntityConfigs.coffee b/services/web/app/coffee/Features/UserMembership/UserMembershipEntityConfigs.coffee index 22d81c8821..b5e5b9a8b4 100644 --- a/services/web/app/coffee/Features/UserMembership/UserMembershipEntityConfigs.coffee +++ b/services/web/app/coffee/Features/UserMembership/UserMembershipEntityConfigs.coffee @@ -49,6 +49,7 @@ module.exports = institution: modelName: 'Institution' + canCreate: true fields: primaryKey: 'v1Id' read: ['managerIds'] @@ -60,11 +61,13 @@ module.exports = subtitle: 'managers_management' remove: 'remove_manager' pathsFor: (id) -> + index: "/manage/institutions/#{id}/managers" addMember: "/manage/institutions/#{id}/managers" removeMember: "/manage/institutions/#{id}/managers" publisher: modelName: 'Publisher' + canCreate: true fields: primaryKey: 'slug' read: ['managerIds'] @@ -76,5 +79,6 @@ module.exports = subtitle: 'managers_management' remove: 'remove_manager' pathsFor: (id) -> + index: "/manage/publishers/#{id}/managers" addMember: "/manage/publishers/#{id}/managers" removeMember: "/manage/publishers/#{id}/managers" diff --git a/services/web/app/coffee/Features/UserMembership/UserMembershipHandler.coffee b/services/web/app/coffee/Features/UserMembership/UserMembershipHandler.coffee index ef197b83d9..5ec0d1a487 100644 --- a/services/web/app/coffee/Features/UserMembership/UserMembershipHandler.coffee +++ b/services/web/app/coffee/Features/UserMembership/UserMembershipHandler.coffee @@ -12,13 +12,19 @@ UserMembershipEntityConfigs = require "./UserMembershipEntityConfigs" module.exports = getEntity: (entityId, entityConfig, loggedInUser, callback = (error, entity) ->) -> - entityId = ObjectId(entityId) if ObjectId.isValid(entityId.toString()) - query = Object.assign({}, entityConfig.baseQuery) - query[entityConfig.fields.primaryKey] = entityId + query = buildEntityQuery(entityId, entityConfig) unless loggedInUser.isAdmin query[entityConfig.fields.access] = ObjectId(loggedInUser._id) EntityModels[entityConfig.modelName].findOne query, callback + getEntityWithoutAuthorizationCheck: (entityId, entityConfig, callback = (error, entity) ->) -> + query = buildEntityQuery(entityId, entityConfig) + EntityModels[entityConfig.modelName].findOne query, callback + + createEntity: (entityId, entityConfig, callback = (error, entity) ->) -> + data = buildEntityQuery(entityId, entityConfig) + EntityModels[entityConfig.modelName].create data, callback + getUsers: (entity, entityConfig, callback = (error, users) ->) -> attributes = entityConfig.fields.read getPopulatedListOfMembers(entity, attributes, callback) @@ -72,3 +78,9 @@ removeUserFromEntity = (entity, attribute, userId, callback = (error)->) -> fieldUpdate = {} fieldUpdate[attribute] = userId entity.update { $pull: fieldUpdate }, callback + +buildEntityQuery = (entityId, entityConfig, loggedInUser) -> + entityId = ObjectId(entityId) if ObjectId.isValid(entityId.toString()) + query = Object.assign({}, entityConfig.baseQuery) + query[entityConfig.fields.primaryKey] = entityId + query diff --git a/services/web/app/coffee/Features/UserMembership/UserMembershipRouter.coffee b/services/web/app/coffee/Features/UserMembership/UserMembershipRouter.coffee index eb36f34114..d29d6571e5 100644 --- a/services/web/app/coffee/Features/UserMembership/UserMembershipRouter.coffee +++ b/services/web/app/coffee/Features/UserMembership/UserMembershipRouter.coffee @@ -2,6 +2,7 @@ UserMembershipAuthorization = require './UserMembershipAuthorization' UserMembershipController = require './UserMembershipController' SubscriptionGroupController = require '../Subscription/SubscriptionGroupController' TeamInvitesController = require '../Subscription/TeamInvitesController' +AuthorizationMiddlewear = require('../Authorization/AuthorizationMiddlewear') module.exports = apply: (webRouter) -> @@ -54,3 +55,11 @@ module.exports = webRouter.delete "/manage/publishers/:id/managers/:userId", UserMembershipAuthorization.requirePublisherAccess, UserMembershipController.remove + + # create new entitites + webRouter.get "/entities/:name/create/:id", + AuthorizationMiddlewear.ensureUserIsSiteAdmin, + UserMembershipController.new + webRouter.post "/entities/:name/create/:id", + AuthorizationMiddlewear.ensureUserIsSiteAdmin, + UserMembershipController.create diff --git a/services/web/app/views/user_membership/new.pug b/services/web/app/views/user_membership/new.pug new file mode 100644 index 0000000000..68e54c04bd --- /dev/null +++ b/services/web/app/views/user_membership/new.pug @@ -0,0 +1,15 @@ +extends ../layout + +block content + .content.content-alt + .container + .row + .col-md-10.col-md-offset-1 + h3 #{entityName} "#{entityId}" does not exists in v2 + form( + enctype='multipart/form-data', + method='post', + action='' + ) + input(name="_csrf", type="hidden", value=csrfToken) + button.btn.btn-primary.text-capitalize(type="submit") Create #{entityName} in v2 diff --git a/services/web/test/unit/coffee/UserMembership/UserMembershipAuthorizationTests.coffee b/services/web/test/unit/coffee/UserMembership/UserMembershipAuthorizationTests.coffee index 6e2514cb0f..d21de25c1f 100644 --- a/services/web/test/unit/coffee/UserMembership/UserMembershipAuthorizationTests.coffee +++ b/services/web/test/unit/coffee/UserMembership/UserMembershipAuthorizationTests.coffee @@ -18,6 +18,7 @@ describe "UserMembershipAuthorization", -> getSessionUser: sinon.stub().returns(@user) @UserMembershipHandler = getEntity: sinon.stub().yields(null, @subscription) + getEntityWithoutAuthorizationCheck: sinon.stub().yields(null, @subscription) @AuthorizationMiddlewear = redirectToRestricted: sinon.stub().yields() @UserMembershipAuthorization = SandboxedModule.require modulePath, requires: @@ -45,15 +46,42 @@ describe "UserMembershipAuthorization", -> expect(@req.entityConfig).to.exist done() - it 'handle entity not found', (done) -> + it 'handle entity not found as non-admin', (done) -> @UserMembershipHandler.getEntity.yields(null, null) + @UserMembershipHandler.getEntityWithoutAuthorizationCheck.yields(null, null) @UserMembershipAuthorization.requireGroupAccess @req, null, (error) => expect(error).to.extist - sinon.assert.called(@AuthorizationMiddlewear.redirectToRestricted) + expect(error).to.be.instanceof(Error) + expect(error.constructor.name).to.equal('NotFoundError') sinon.assert.called(@UserMembershipHandler.getEntity) expect(@req.entity).to.not.exist done() + it 'handle entity not found an admin can create', (done) -> + @user.isAdmin = true + @UserMembershipHandler.getEntity.yields(null, null) + @UserMembershipHandler.getEntityWithoutAuthorizationCheck.yields(null, null) + @UserMembershipAuthorization.requirePublisherAccess @req, redirect: (path) => + expect(path).to.extist + expect(path).to.match /create/ + done() + + it 'handle entity not found an admin cannot create', (done) -> + @user.isAdmin = true + @UserMembershipHandler.getEntity.yields(null, null) + @UserMembershipHandler.getEntityWithoutAuthorizationCheck.yields(null, null) + @UserMembershipAuthorization.requireGroupAccess @req, null, (error) => + expect(error).to.extist + expect(error).to.be.instanceof(Error) + expect(error.constructor.name).to.equal('NotFoundError') + done() + + it 'handle entity no access', (done) -> + @UserMembershipHandler.getEntity.yields(null, null) + @UserMembershipAuthorization.requireGroupAccess @req, null, (error) => + sinon.assert.called(@AuthorizationMiddlewear.redirectToRestricted) + done() + it 'handle anonymous user', (done) -> @AuthenticationController.getSessionUser.returns(null) @UserMembershipAuthorization.requireGroupAccess @req, null, (error) => diff --git a/services/web/test/unit/coffee/UserMembership/UserMembershipControllerTests.coffee b/services/web/test/unit/coffee/UserMembership/UserMembershipControllerTests.coffee index 32df7c85ad..e2f11e7f92 100644 --- a/services/web/test/unit/coffee/UserMembership/UserMembershipControllerTests.coffee +++ b/services/web/test/unit/coffee/UserMembership/UserMembershipControllerTests.coffee @@ -38,6 +38,7 @@ describe "UserMembershipController", -> getLoggedInUserId: sinon.stub().returns(@user._id) @UserMembershipHandler = getEntity: sinon.stub().yields(null, @subscription) + createEntity: sinon.stub().yields(null, @institution) getUsers: sinon.stub().yields(null, @users) addUser: sinon.stub().yields(null, @newUser) removeUser: sinon.stub().yields(null) @@ -204,3 +205,37 @@ describe "UserMembershipController", -> it "should export the correct csv", -> assertCalledWith(@res.send, "mock-email-1@foo.com\nmock-email-2@foo.com\n") + + describe 'new', -> + beforeEach -> + @req.params.name = 'publisher' + @req.params.id = 'abc' + + it 'renders view', (done) -> + @UserMembershipController.new @req, render: (viewPath, data) => + expect(data.entityName).to.eq 'publisher' + expect(data.entityId).to.eq 'abc' + done() + + describe 'create', -> + beforeEach -> + @req.params.name = 'institution' + @req.params.id = 123 + + it 'creates institution', (done) -> + @UserMembershipController.create @req, redirect: (path) => + expect(path).to.eq EntityConfigs['institution'].pathsFor(123).index + sinon.assert.calledWithMatch( + @UserMembershipHandler.createEntity, + 123, + modelName: 'Institution', + ) + done() + + it 'checks canCreate', (done) -> + @req.params.name = 'group' + @UserMembershipController.create @req, null, (error) => + expect(error).to.extist + expect(error).to.be.an.instanceof(Errors.NotFoundError) + sinon.assert.notCalled(@UserMembershipHandler.createEntity) + done() diff --git a/services/web/test/unit/coffee/UserMembership/UserMembershipHandlerTests.coffee b/services/web/test/unit/coffee/UserMembership/UserMembershipHandlerTests.coffee index eaea0791b7..49a15de212 100644 --- a/services/web/test/unit/coffee/UserMembership/UserMembershipHandlerTests.coffee +++ b/services/web/test/unit/coffee/UserMembership/UserMembershipHandlerTests.coffee @@ -46,6 +46,7 @@ describe 'UserMembershipHandler', -> findOne: sinon.stub().yields(null, @subscription) @Publisher = findOne: sinon.stub().yields(null, @publisher) + create: sinon.stub().yields(null, @publisher) @UserMembershipHandler = SandboxedModule.require modulePath, requires: './UserMembershipViewModel': @UserMembershipViewModel '../User/UserGetter': @UserGetter @@ -57,7 +58,7 @@ describe 'UserMembershipHandler', -> log: -> err: -> - describe 'getEntty', -> + describe 'getEntity', -> describe 'group subscriptions', -> it 'get subscription', (done) -> @UserMembershipHandler.getEntity @fakeEntityId, EntityConfigs.group, @user, (error, subscription) => @@ -86,6 +87,15 @@ describe 'UserMembershipHandler', -> should.exist(error) done() + describe 'getEntityWithoutAuthorizationCheck', -> + it 'get publisher', (done) -> + @UserMembershipHandler.getEntityWithoutAuthorizationCheck @fakeEntityId, EntityConfigs.publisher, (error, subscription) => + should.not.exist(error) + expectedQuery = slug: @fakeEntityId + assertCalledWith(@Publisher.findOne, expectedQuery) + expect(subscription).to.equal @publisher + done() + describe 'institutions', -> it 'get institution', (done) -> @UserMembershipHandler.getEntity @institution.v1Id, EntityConfigs.institution, @user, (error, institution) => @@ -136,6 +146,13 @@ describe 'UserMembershipHandler', -> expect(@UserMembershipViewModel.buildAsync.callCount).to.equal expectedCallcount done() + describe 'createEntity', -> + it 'creates publisher', (done) -> + @UserMembershipHandler.createEntity @fakeEntityId, EntityConfigs.publisher, (error, publisher) => + should.not.exist(error) + assertCalledWith(@Publisher.create, slug: @fakeEntityId) + done() + describe 'addUser', -> beforeEach -> @email = @newUser.email