From 00bdc52fab489b7efe450735f6139939b11c357f Mon Sep 17 00:00:00 2001 From: Eric Mc Sween Date: Wed, 20 May 2020 10:21:13 -0400 Subject: [PATCH] Merge pull request #2840 from overleaf/jel-sso-redundant-subscription-notification Redundant subscription notification if entitlement via SSO GitOrigin-RevId: 8529204e78c3a43d87acbb375fea15c62cad48a3 --- .../Notifications/NotificationsBuilder.js | 351 +++++++++--------- .../src/Features/User/SAMLIdentityManager.js | 26 ++ .../unit/src/User/SAMLIdentityManagerTests.js | 55 +++ 3 files changed, 266 insertions(+), 166 deletions(-) diff --git a/services/web/app/src/Features/Notifications/NotificationsBuilder.js b/services/web/app/src/Features/Notifications/NotificationsBuilder.js index f7be3568f1..f71d2d54ab 100644 --- a/services/web/app/src/Features/Notifications/NotificationsBuilder.js +++ b/services/web/app/src/Features/Notifications/NotificationsBuilder.js @@ -11,177 +11,196 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ const NotificationsHandler = require('./NotificationsHandler') +const { promisifyAll } = require('../../util/promises') const request = require('request') const settings = require('settings-sharelatex') -module.exports = { - // Note: notification keys should be url-safe - - featuresUpgradedByAffiliation(affiliation, user) { - return { - key: `features-updated-by=${affiliation.institutionId}`, - create(callback) { - if (callback == null) { - callback = function() {} - } - const messageOpts = { institutionName: affiliation.institutionName } - return NotificationsHandler.createNotification( - user._id, - this.key, - 'notification_features_upgraded_by_affiliation', - messageOpts, - null, - false, - callback - ) - }, - read(callback) { - if (callback == null) { - callback = function() {} - } - return NotificationsHandler.markAsReadByKeyOnly(this.key, callback) +function featuresUpgradedByAffiliation(affiliation, user) { + return { + key: `features-updated-by=${affiliation.institutionId}`, + create(callback) { + if (callback == null) { + callback = function() {} } - } - }, - - redundantPersonalSubscription(affiliation, user) { - return { - key: `redundant-personal-subscription-${affiliation.institutionId}`, - create(callback) { - if (callback == null) { - callback = function() {} - } - const messageOpts = { institutionName: affiliation.institutionName } - return NotificationsHandler.createNotification( - user._id, - this.key, - 'notification_personal_subscription_not_required_due_to_affiliation', - messageOpts, - null, - false, - callback - ) - }, - read(callback) { - if (callback == null) { - callback = function() {} - } - return NotificationsHandler.markAsReadByKeyOnly(this.key, callback) - } - } - }, - - projectInvite(invite, project, sendingUser, user) { - return { - key: `project-invite-${invite._id}`, - create(callback) { - if (callback == null) { - callback = function() {} - } - const messageOpts = { - userName: sendingUser.first_name, - projectName: project.name, - projectId: project._id.toString(), - token: invite.token - } - return NotificationsHandler.createNotification( - user._id, - this.key, - 'notification_project_invite', - messageOpts, - invite.expires, - callback - ) - }, - read(callback) { - if (callback == null) { - callback = function() {} - } - return NotificationsHandler.markAsReadByKeyOnly(this.key, callback) - } - } - }, - - ipMatcherAffiliation(userId) { - return { - create(ip, callback) { - if (callback == null) { - callback = function() {} - } - if (!settings.apis.v1.url) { - return null - } // service is not configured - return request( - { - method: 'GET', - url: `${settings.apis.v1.url}/api/v2/users/${userId}/ip_matcher`, - auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass }, - body: { ip }, - json: true, - timeout: 20 * 1000 - }, - function(error, response, body) { - if (error != null) { - return error - } - if (response.statusCode !== 200) { - return null - } - - const key = `ip-matched-affiliation-${body.id}` - const messageOpts = { - university_name: body.name, - content: body.enrolment_ad_html - } - return NotificationsHandler.createNotification( - userId, - key, - 'notification_ip_matched_affiliation', - messageOpts, - null, - false, - callback - ) - } - ) - }, - - read(university_id, callback) { - if (callback == null) { - callback = function() {} - } - const key = `ip-matched-affiliation-${university_id}` - return NotificationsHandler.markAsReadWithKey(userId, key, callback) - } - } - }, - - tpdsFileLimit(user_id) { - return { - key: `tpdsFileLimit-${user_id}`, - create(projectName, callback) { - if (callback == null) { - callback = function() {} - } - const messageOpts = { - projectName: projectName - } - return NotificationsHandler.createNotification( - user_id, - this.key, - 'notification_tpds_file_limit', - messageOpts, - null, - true, - callback - ) - }, - read(callback) { - if (callback == null) { - callback = function() {} - } - return NotificationsHandler.markAsReadByKeyOnly(this.key, callback) + const messageOpts = { institutionName: affiliation.institutionName } + return NotificationsHandler.createNotification( + user._id, + this.key, + 'notification_features_upgraded_by_affiliation', + messageOpts, + null, + false, + callback + ) + }, + read(callback) { + if (callback == null) { + callback = function() {} } + return NotificationsHandler.markAsReadByKeyOnly(this.key, callback) } } } + +function redundantPersonalSubscription(affiliation, user) { + return { + key: `redundant-personal-subscription-${affiliation.institutionId}`, + create(callback) { + if (callback == null) { + callback = function() {} + } + const messageOpts = { institutionName: affiliation.institutionName } + return NotificationsHandler.createNotification( + user._id, + this.key, + 'notification_personal_subscription_not_required_due_to_affiliation', + messageOpts, + null, + false, + callback + ) + }, + read(callback) { + if (callback == null) { + callback = function() {} + } + return NotificationsHandler.markAsReadByKeyOnly(this.key, callback) + } + } +} + +function projectInvite(invite, project, sendingUser, user) { + return { + key: `project-invite-${invite._id}`, + create(callback) { + if (callback == null) { + callback = function() {} + } + const messageOpts = { + userName: sendingUser.first_name, + projectName: project.name, + projectId: project._id.toString(), + token: invite.token + } + return NotificationsHandler.createNotification( + user._id, + this.key, + 'notification_project_invite', + messageOpts, + invite.expires, + callback + ) + }, + read(callback) { + if (callback == null) { + callback = function() {} + } + return NotificationsHandler.markAsReadByKeyOnly(this.key, callback) + } + } +} + +function ipMatcherAffiliation(userId) { + return { + create(ip, callback) { + if (callback == null) { + callback = function() {} + } + if (!settings.apis.v1.url) { + return null + } // service is not configured + return request( + { + method: 'GET', + url: `${settings.apis.v1.url}/api/v2/users/${userId}/ip_matcher`, + auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass }, + body: { ip }, + json: true, + timeout: 20 * 1000 + }, + function(error, response, body) { + if (error != null) { + return error + } + if (response.statusCode !== 200) { + return null + } + + const key = `ip-matched-affiliation-${body.id}` + const messageOpts = { + university_name: body.name, + content: body.enrolment_ad_html + } + return NotificationsHandler.createNotification( + userId, + key, + 'notification_ip_matched_affiliation', + messageOpts, + null, + false, + callback + ) + } + ) + }, + + read(university_id, callback) { + if (callback == null) { + callback = function() {} + } + const key = `ip-matched-affiliation-${university_id}` + return NotificationsHandler.markAsReadWithKey(userId, key, callback) + } + } +} + +function tpdsFileLimit(user_id) { + return { + key: `tpdsFileLimit-${user_id}`, + create(projectName, callback) { + if (callback == null) { + callback = function() {} + } + const messageOpts = { + projectName: projectName + } + return NotificationsHandler.createNotification( + user_id, + this.key, + 'notification_tpds_file_limit', + messageOpts, + null, + true, + callback + ) + }, + read(callback) { + if (callback == null) { + callback = function() {} + } + return NotificationsHandler.markAsReadByKeyOnly(this.key, callback) + } + } +} + +const NotificationsBuilder = { + // Note: notification keys should be url-safe + + featuresUpgradedByAffiliation, + + redundantPersonalSubscription, + + projectInvite, + + ipMatcherAffiliation, + + tpdsFileLimit +} + +NotificationsBuilder.promises = { + redundantPersonalSubscription: function(affiliation, user) { + return promisifyAll(redundantPersonalSubscription(affiliation, user)) + } +} + +module.exports = NotificationsBuilder diff --git a/services/web/app/src/Features/User/SAMLIdentityManager.js b/services/web/app/src/Features/User/SAMLIdentityManager.js index 639aa392d7..dc24b2e950 100644 --- a/services/web/app/src/Features/User/SAMLIdentityManager.js +++ b/services/web/app/src/Features/User/SAMLIdentityManager.js @@ -1,7 +1,9 @@ const EmailHandler = require('../Email/EmailHandler') const Errors = require('../Errors/Errors') const InstitutionsAPI = require('../Institutions/InstitutionsAPI') +const NotificationsBuilder = require('../Notifications/NotificationsBuilder') const OError = require('@overleaf/o-error') +const SubscriptionLocator = require('../Subscription/SubscriptionLocator') const UserGetter = require('../User/UserGetter') const UserUpdater = require('../User/UserUpdater') const logger = require('logger-sharelatex') @@ -151,6 +153,24 @@ async function getUser(providerId, externalUserId) { return user } +async function redundantSubscription(userId, providerId, providerName) { + const subscription = await SubscriptionLocator.promises.getUserIndividualSubscription( + userId + ) + + if (subscription) { + await NotificationsBuilder.promises + .redundantPersonalSubscription( + { + institutionId: providerId, + institutionName: providerName + }, + { _id: userId } + ) + .create() + } +} + async function linkAccounts( userId, externalUserId, @@ -171,6 +191,11 @@ async function linkAccounts( // update v1 affiliations record if (hasEntitlement) { await InstitutionsAPI.promises.addEntitlement(userId, institutionEmail) + try { + await redundantSubscription(userId, providerId, providerName) + } catch (error) { + logger.err({ err: error }, 'error checking redundant subscription') + } } else { await InstitutionsAPI.promises.removeEntitlement(userId, institutionEmail) } @@ -271,6 +296,7 @@ const SAMLIdentityManager = { entitlementAttributeMatches, getUser, linkAccounts, + redundantSubscription, unlinkAccounts, updateEntitlement, userHasEntitlement diff --git a/services/web/test/unit/src/User/SAMLIdentityManagerTests.js b/services/web/test/unit/src/User/SAMLIdentityManagerTests.js index 7dcd0140fc..c17df26737 100644 --- a/services/web/test/unit/src/User/SAMLIdentityManagerTests.js +++ b/services/web/test/unit/src/User/SAMLIdentityManagerTests.js @@ -49,6 +49,18 @@ describe('SAMLIdentityManager', function() { sendEmail: sinon.stub().yields() }), '../Errors/Errors': this.Errors, + '../Notifications/NotificationsBuilder': (this.NotificationsBuilder = { + promises: { + redundantPersonalSubscription: sinon + .stub() + .returns({ create: sinon.stub().resolves() }) + } + }), + '../Subscription/SubscriptionLocator': (this.SubscriptionLocator = { + promises: { + getUserIndividualSubscription: sinon.stub().resolves() + } + }), '../../models/User': { User: (this.User = { findOneAndUpdate: sinon.stub(), @@ -212,4 +224,47 @@ describe('SAMLIdentityManager', function() { ).should.equal(false) }) }) + + describe('redundantSubscription', function() { + const userId = '1bv' + const providerId = 123 + const providerName = 'University Name' + describe('with a personal subscription', function() { + beforeEach(function() { + this.SubscriptionLocator.promises.getUserIndividualSubscription.resolves( + { + planCode: 'professional' + } + ) + }) + it('should create redundant personal subscription notification ', async function() { + try { + await this.SAMLIdentityManager.redundantSubscription( + userId, + providerId, + providerName + ) + } catch (error) { + expect(error).to.not.exist + } + expect(this.NotificationsBuilder.promises.redundantPersonalSubscription) + .to.have.been.calledOnce + }) + }) + describe('without a personal subscription', function() { + it('should create redundant personal subscription notification ', async function() { + try { + await this.SAMLIdentityManager.redundantSubscription( + userId, + providerId, + providerName + ) + } catch (error) { + expect(error).to.not.exist + } + expect(this.NotificationsBuilder.promises.redundantPersonalSubscription) + .to.not.have.been.called + }) + }) + }) })