2019-10-03 10:10:22 -04:00
|
|
|
const EmailHandler = require('../Email/EmailHandler')
|
2019-08-27 16:48:00 -04:00
|
|
|
const Errors = require('../Errors/Errors')
|
2019-10-21 08:33:53 -04:00
|
|
|
const InstitutionsAPI = require('../Institutions/InstitutionsAPI')
|
2019-09-12 10:01:12 -04:00
|
|
|
const OError = require('@overleaf/o-error')
|
|
|
|
const UserGetter = require('../User/UserGetter')
|
|
|
|
const UserUpdater = require('../User/UserUpdater')
|
2019-10-21 08:33:53 -04:00
|
|
|
const logger = require('logger-sharelatex')
|
|
|
|
const { User } = require('../../models/User')
|
2019-09-12 10:01:12 -04:00
|
|
|
|
2019-10-14 09:18:00 -04:00
|
|
|
async function _addIdentifier(
|
|
|
|
userId,
|
|
|
|
externalUserId,
|
|
|
|
providerId,
|
|
|
|
hasEntitlement,
|
|
|
|
institutionEmail
|
|
|
|
) {
|
|
|
|
// first check if institutionEmail linked to another account
|
|
|
|
// before adding the identifier for the email
|
|
|
|
const user = await UserGetter.promises.getUserByAnyEmail(institutionEmail)
|
|
|
|
if (user && user._id.toString() !== userId.toString()) {
|
2019-10-15 11:53:10 -04:00
|
|
|
const existingEmailData = user.emails.find(
|
|
|
|
emailData => emailData.email === institutionEmail
|
|
|
|
)
|
|
|
|
if (existingEmailData && existingEmailData.samlProviderId) {
|
|
|
|
// email exists and institution link.
|
|
|
|
// Return back to requesting page with error
|
|
|
|
throw new Errors.SAMLIdentityExistsError()
|
|
|
|
} else {
|
|
|
|
// Only email exists but not linked, so redirect to linking page
|
|
|
|
// which will tell this user to log out to link
|
|
|
|
throw new Errors.EmailExistsError()
|
|
|
|
}
|
2019-10-14 09:18:00 -04:00
|
|
|
}
|
2019-09-30 09:21:31 -04:00
|
|
|
providerId = providerId.toString()
|
2019-10-05 13:43:21 -04:00
|
|
|
hasEntitlement = !!hasEntitlement
|
2019-09-12 10:01:12 -04:00
|
|
|
const query = {
|
|
|
|
_id: userId,
|
|
|
|
'samlIdentifiers.providerId': {
|
|
|
|
$ne: providerId
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const update = {
|
|
|
|
$push: {
|
|
|
|
samlIdentifiers: {
|
2019-10-05 13:43:21 -04:00
|
|
|
hasEntitlement,
|
2019-09-12 10:01:12 -04:00
|
|
|
externalUserId,
|
|
|
|
providerId
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
try {
|
2019-10-21 08:33:53 -04:00
|
|
|
// update v2 user record
|
|
|
|
const updatedUser = User.findOneAndUpdate(query, update, {
|
|
|
|
new: true
|
|
|
|
}).exec()
|
|
|
|
// update v1 affiliations record
|
|
|
|
if (hasEntitlement) {
|
|
|
|
await InstitutionsAPI.promises.addEntitlement(userId, institutionEmail)
|
|
|
|
}
|
|
|
|
return updatedUser
|
2019-09-12 10:01:12 -04:00
|
|
|
} catch (err) {
|
2019-10-21 08:33:53 -04:00
|
|
|
if (err.code === 11000) {
|
2019-09-12 10:01:12 -04:00
|
|
|
throw new Errors.SAMLIdentityExistsError()
|
2019-10-21 08:33:53 -04:00
|
|
|
} else {
|
2019-09-12 10:01:12 -04:00
|
|
|
throw new OError(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-30 09:21:31 -04:00
|
|
|
function _getUserQuery(providerId, externalUserId) {
|
|
|
|
externalUserId = externalUserId.toString()
|
|
|
|
providerId = providerId.toString()
|
|
|
|
const query = {
|
|
|
|
'samlIdentifiers.externalUserId': externalUserId,
|
|
|
|
'samlIdentifiers.providerId': providerId
|
|
|
|
}
|
|
|
|
return query
|
|
|
|
}
|
|
|
|
|
|
|
|
async function _addInstitutionEmail(userId, email, providerId) {
|
2019-09-12 10:01:12 -04:00
|
|
|
const user = await UserGetter.promises.getUser(userId)
|
2019-09-30 09:21:31 -04:00
|
|
|
const query = {
|
|
|
|
_id: userId,
|
|
|
|
'emails.email': email
|
|
|
|
}
|
|
|
|
const update = {
|
|
|
|
$set: {
|
|
|
|
'emails.$.samlProviderId': providerId.toString()
|
|
|
|
}
|
|
|
|
}
|
2019-09-12 10:01:12 -04:00
|
|
|
if (user == null) {
|
|
|
|
logger.log(userId, 'could not find user for institution SAML linking')
|
|
|
|
throw new Errors.NotFoundError('user not found')
|
|
|
|
}
|
|
|
|
const emailAlreadyAssociated = user.emails.find(e => e.email === email)
|
|
|
|
if (emailAlreadyAssociated && emailAlreadyAssociated.confirmedAt) {
|
2019-09-30 09:21:31 -04:00
|
|
|
await UserUpdater.promises.updateUser(query, update)
|
2019-09-12 10:01:12 -04:00
|
|
|
} else if (emailAlreadyAssociated) {
|
|
|
|
// add and confirm email
|
2019-09-30 09:21:31 -04:00
|
|
|
await UserUpdater.promises.confirmEmail(user._id, email)
|
|
|
|
await UserUpdater.promises.updateUser(query, update)
|
2019-09-12 10:01:12 -04:00
|
|
|
} else {
|
|
|
|
// add and confirm email
|
2019-09-30 09:21:31 -04:00
|
|
|
await UserUpdater.promises.addEmailAddress(user._id, email)
|
|
|
|
await UserUpdater.promises.confirmEmail(user._id, email)
|
|
|
|
await UserUpdater.promises.updateUser(query, update)
|
2019-09-12 10:01:12 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-03 10:10:22 -04:00
|
|
|
async function _sendLinkedEmail(userId, providerName) {
|
|
|
|
const user = await UserGetter.promises.getUser(userId, { email: 1 })
|
|
|
|
const emailOptions = {
|
|
|
|
to: user.email,
|
|
|
|
provider: providerName
|
|
|
|
}
|
|
|
|
EmailHandler.sendEmail(
|
|
|
|
'emailThirdPartyIdentifierLinked',
|
|
|
|
emailOptions,
|
|
|
|
error => {
|
|
|
|
if (error != null) {
|
|
|
|
logger.warn(error)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
function _sendUnlinkedEmail(primaryEmail, providerName) {
|
|
|
|
const emailOptions = {
|
|
|
|
to: primaryEmail,
|
|
|
|
provider: providerName
|
|
|
|
}
|
|
|
|
EmailHandler.sendEmail(
|
|
|
|
'emailThirdPartyIdentifierUnlinked',
|
|
|
|
emailOptions,
|
|
|
|
error => {
|
|
|
|
if (error != null) {
|
|
|
|
logger.warn(error)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2019-08-27 16:48:00 -04:00
|
|
|
async function getUser(providerId, externalUserId) {
|
|
|
|
if (providerId == null || externalUserId == null) {
|
2019-10-05 13:43:21 -04:00
|
|
|
throw new Error(
|
|
|
|
`invalid arguments: providerId: ${providerId}, externalUserId: ${externalUserId}`
|
|
|
|
)
|
2019-08-27 16:48:00 -04:00
|
|
|
}
|
2019-10-05 13:43:21 -04:00
|
|
|
providerId = providerId.toString()
|
|
|
|
externalUserId = externalUserId.toString()
|
2019-10-03 10:10:22 -04:00
|
|
|
const query = _getUserQuery(providerId, externalUserId)
|
|
|
|
let user = await User.findOne(query).exec()
|
|
|
|
if (!user) {
|
|
|
|
throw new Errors.SAMLUserNotFoundError()
|
2019-08-27 16:48:00 -04:00
|
|
|
}
|
2019-10-03 10:10:22 -04:00
|
|
|
return user
|
2019-08-27 16:48:00 -04:00
|
|
|
}
|
|
|
|
|
2019-09-12 10:01:12 -04:00
|
|
|
async function linkAccounts(
|
|
|
|
userId,
|
|
|
|
externalUserId,
|
|
|
|
institutionEmail,
|
2019-10-03 10:10:22 -04:00
|
|
|
providerId,
|
2019-10-05 13:43:21 -04:00
|
|
|
providerName,
|
|
|
|
hasEntitlement
|
2019-09-12 10:01:12 -04:00
|
|
|
) {
|
2019-10-14 09:18:00 -04:00
|
|
|
await _addIdentifier(
|
|
|
|
userId,
|
|
|
|
externalUserId,
|
|
|
|
providerId,
|
|
|
|
hasEntitlement,
|
|
|
|
institutionEmail
|
|
|
|
)
|
2019-10-03 10:10:22 -04:00
|
|
|
await _addInstitutionEmail(userId, institutionEmail, providerId)
|
|
|
|
await _sendLinkedEmail(userId, providerName)
|
|
|
|
}
|
|
|
|
|
2019-10-21 08:33:53 -04:00
|
|
|
async function unlinkAccounts(
|
|
|
|
userId,
|
|
|
|
institutionEmail,
|
|
|
|
primaryEmail,
|
|
|
|
providerId,
|
|
|
|
providerName
|
|
|
|
) {
|
2019-10-05 13:43:21 -04:00
|
|
|
providerId = providerId.toString()
|
2019-10-03 10:10:22 -04:00
|
|
|
const query = {
|
|
|
|
_id: userId
|
|
|
|
}
|
|
|
|
const update = {
|
|
|
|
$pull: {
|
|
|
|
samlIdentifiers: {
|
|
|
|
providerId
|
|
|
|
}
|
|
|
|
}
|
2019-09-30 09:21:31 -04:00
|
|
|
}
|
2019-10-21 08:33:53 -04:00
|
|
|
// update v2 user
|
2019-10-03 10:10:22 -04:00
|
|
|
await User.update(query, update).exec()
|
2019-10-21 08:33:53 -04:00
|
|
|
// update v1 affiliations record
|
|
|
|
await InstitutionsAPI.promises.removeEntitlement(userId, institutionEmail)
|
|
|
|
// send email
|
2019-10-03 10:10:22 -04:00
|
|
|
_sendUnlinkedEmail(primaryEmail, providerName)
|
2019-09-12 10:01:12 -04:00
|
|
|
}
|
|
|
|
|
2019-10-21 08:33:53 -04:00
|
|
|
async function updateEntitlement(
|
|
|
|
userId,
|
|
|
|
institutionEmail,
|
|
|
|
providerId,
|
|
|
|
hasEntitlement
|
|
|
|
) {
|
2019-10-05 13:43:21 -04:00
|
|
|
providerId = providerId.toString()
|
|
|
|
hasEntitlement = !!hasEntitlement
|
|
|
|
const query = {
|
|
|
|
_id: userId,
|
|
|
|
'samlIdentifiers.providerId': providerId.toString()
|
|
|
|
}
|
|
|
|
const update = {
|
|
|
|
$set: {
|
|
|
|
'samlIdentifiers.$.hasEntitlement': hasEntitlement
|
|
|
|
}
|
|
|
|
}
|
2019-10-21 08:33:53 -04:00
|
|
|
// update v2 user
|
2019-10-05 13:43:21 -04:00
|
|
|
await User.update(query, update).exec()
|
2019-10-21 08:33:53 -04:00
|
|
|
// update v1 affiliations record
|
|
|
|
if (hasEntitlement) {
|
|
|
|
await InstitutionsAPI.promises.addEntitlement(userId, institutionEmail)
|
|
|
|
} else {
|
|
|
|
await InstitutionsAPI.promises.removeEntitlement(userId, institutionEmail)
|
|
|
|
}
|
2019-10-05 13:43:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
function entitlementAttributeMatches(entitlementAttribute, entitlementMatcher) {
|
|
|
|
if (
|
|
|
|
typeof entitlementAttribute !== 'string' ||
|
|
|
|
typeof entitlementMatcher !== 'string'
|
|
|
|
) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
const entitlementRegExp = new RegExp(entitlementMatcher)
|
|
|
|
return !!entitlementAttribute.match(entitlementRegExp)
|
|
|
|
}
|
|
|
|
|
|
|
|
function userHasEntitlement(user, providerId) {
|
|
|
|
providerId = providerId.toString()
|
|
|
|
if (!user || !Array.isArray(user.samlIdentifiers)) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for (const samlIdentifier of user.samlIdentifiers) {
|
|
|
|
if (providerId && samlIdentifier.providerId !== providerId) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if (samlIdentifier.hasEntitlement) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2019-08-27 16:48:00 -04:00
|
|
|
const SAMLIdentityManager = {
|
2019-10-05 13:43:21 -04:00
|
|
|
entitlementAttributeMatches,
|
2019-09-12 10:01:12 -04:00
|
|
|
getUser,
|
2019-10-03 10:10:22 -04:00
|
|
|
linkAccounts,
|
2019-10-05 13:43:21 -04:00
|
|
|
unlinkAccounts,
|
|
|
|
updateEntitlement,
|
|
|
|
userHasEntitlement
|
2019-08-27 16:48:00 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = SAMLIdentityManager
|