2019-11-01 10:19:56 -04:00
|
|
|
const APP_ROOT = '../../../../app/src'
|
2020-10-06 09:50:07 -04:00
|
|
|
const UserAuditLogHandler = require(`${APP_ROOT}/Features/User/UserAuditLogHandler`)
|
2019-11-01 10:19:56 -04:00
|
|
|
const EmailHandler = require(`${APP_ROOT}/Features/Email/EmailHandler`)
|
2019-05-29 05:21:06 -04:00
|
|
|
const Errors = require('../Errors/Errors')
|
2019-11-01 10:19:56 -04:00
|
|
|
const _ = require('lodash')
|
2019-07-11 11:22:25 -04:00
|
|
|
const logger = require('logger-sharelatex')
|
|
|
|
const settings = require('settings-sharelatex')
|
2019-11-01 10:19:56 -04:00
|
|
|
const { User } = require(`${APP_ROOT}/models/User`)
|
|
|
|
const { promisifyAll } = require(`${APP_ROOT}/util/promises`)
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2019-07-11 11:22:25 -04:00
|
|
|
const oauthProviders = settings.oauthProviders || {}
|
|
|
|
|
2020-07-21 10:28:52 -04:00
|
|
|
function _getIndefiniteArticle(providerName) {
|
|
|
|
const vowels = ['a', 'e', 'i', 'o', 'u']
|
|
|
|
if (vowels.includes(providerName.charAt(0).toLowerCase())) return 'an'
|
|
|
|
return 'a'
|
|
|
|
}
|
|
|
|
|
2019-11-01 10:19:56 -04:00
|
|
|
function getUser(providerId, externalUserId, callback) {
|
|
|
|
if (providerId == null || externalUserId == null) {
|
|
|
|
return callback(new Error('invalid arguments'))
|
|
|
|
}
|
|
|
|
const query = _getUserQuery(providerId, externalUserId)
|
|
|
|
User.findOne(query, function(err, user) {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(err)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-11-01 10:19:56 -04:00
|
|
|
if (!user) {
|
|
|
|
return callback(new Errors.ThirdPartyUserNotFoundError())
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-11-01 10:19:56 -04:00
|
|
|
callback(null, user)
|
|
|
|
})
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2019-11-01 10:19:56 -04:00
|
|
|
function login(providerId, externalUserId, externalData, callback) {
|
|
|
|
ThirdPartyIdentityManager.getUser(providerId, externalUserId, function(
|
|
|
|
err,
|
|
|
|
user
|
|
|
|
) {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
if (!externalData) {
|
|
|
|
return callback(null, user)
|
|
|
|
}
|
|
|
|
const query = _getUserQuery(providerId, externalUserId)
|
|
|
|
const update = _thirdPartyIdentifierUpdate(
|
|
|
|
user,
|
|
|
|
providerId,
|
|
|
|
externalUserId,
|
|
|
|
externalData
|
2019-05-29 05:21:06 -04:00
|
|
|
)
|
2019-11-01 10:19:56 -04:00
|
|
|
User.findOneAndUpdate(query, update, { new: true }, callback)
|
|
|
|
})
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2019-11-01 10:19:56 -04:00
|
|
|
function link(
|
|
|
|
userId,
|
|
|
|
providerId,
|
|
|
|
externalUserId,
|
|
|
|
externalData,
|
2020-10-06 09:50:07 -04:00
|
|
|
auditLog,
|
2019-11-01 10:19:56 -04:00
|
|
|
callback,
|
|
|
|
retry
|
|
|
|
) {
|
2020-09-29 10:05:12 -04:00
|
|
|
const accountLinked = true
|
2019-11-01 10:19:56 -04:00
|
|
|
if (!oauthProviders[providerId]) {
|
|
|
|
return callback(new Error('Not a valid provider'))
|
|
|
|
}
|
2020-10-06 09:50:07 -04:00
|
|
|
|
|
|
|
UserAuditLogHandler.addEntry(
|
|
|
|
userId,
|
|
|
|
'link-sso',
|
|
|
|
auditLog.initiatorId,
|
|
|
|
auditLog.ipAddress,
|
|
|
|
{
|
|
|
|
providerId
|
|
|
|
},
|
|
|
|
error => {
|
|
|
|
if (error) {
|
|
|
|
return callback(error)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2020-10-06 09:50:07 -04:00
|
|
|
const query = {
|
|
|
|
_id: userId,
|
|
|
|
'thirdPartyIdentifiers.providerId': {
|
|
|
|
$ne: providerId
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const update = {
|
|
|
|
$push: {
|
|
|
|
thirdPartyIdentifiers: {
|
|
|
|
externalUserId,
|
|
|
|
externalData,
|
|
|
|
providerId
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// add new tpi only if an entry for the provider does not exist
|
|
|
|
// projection includes thirdPartyIdentifiers for tests
|
|
|
|
User.findOneAndUpdate(query, update, { new: 1 }, (err, res) => {
|
|
|
|
if (err && err.code === 11000) {
|
|
|
|
callback(new Errors.ThirdPartyIdentityExistsError())
|
|
|
|
} else if (err != null) {
|
|
|
|
callback(err)
|
|
|
|
} else if (res) {
|
|
|
|
_sendSecurityAlert(accountLinked, providerId, res, userId)
|
|
|
|
callback(null, res)
|
|
|
|
} else if (retry) {
|
|
|
|
// if already retried then throw error
|
|
|
|
callback(new Error('update failed'))
|
|
|
|
} else {
|
|
|
|
// attempt to clear existing entry then retry
|
|
|
|
ThirdPartyIdentityManager.unlink(
|
|
|
|
userId,
|
|
|
|
providerId,
|
|
|
|
auditLog,
|
|
|
|
function(err) {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
ThirdPartyIdentityManager.link(
|
|
|
|
userId,
|
|
|
|
providerId,
|
|
|
|
externalUserId,
|
|
|
|
externalData,
|
|
|
|
auditLog,
|
|
|
|
callback,
|
|
|
|
true
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
2019-11-01 10:19:56 -04:00
|
|
|
}
|
|
|
|
})
|
2019-07-11 11:22:25 -04:00
|
|
|
}
|
2020-10-06 09:50:07 -04:00
|
|
|
)
|
2019-11-01 10:19:56 -04:00
|
|
|
}
|
|
|
|
|
2020-10-06 09:50:07 -04:00
|
|
|
function unlink(userId, providerId, auditLog, callback) {
|
2020-09-29 10:05:12 -04:00
|
|
|
const accountLinked = false
|
2019-11-01 10:19:56 -04:00
|
|
|
if (!oauthProviders[providerId]) {
|
|
|
|
return callback(new Error('Not a valid provider'))
|
|
|
|
}
|
2020-10-06 09:50:07 -04:00
|
|
|
UserAuditLogHandler.addEntry(
|
|
|
|
userId,
|
|
|
|
'unlink-sso',
|
|
|
|
auditLog.initiatorId,
|
|
|
|
auditLog.ipAddress,
|
|
|
|
{
|
|
|
|
providerId
|
|
|
|
},
|
|
|
|
error => {
|
|
|
|
if (error) {
|
|
|
|
return callback(error)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2020-10-06 09:50:07 -04:00
|
|
|
const query = {
|
|
|
|
_id: userId
|
|
|
|
}
|
|
|
|
const update = {
|
|
|
|
$pull: {
|
|
|
|
thirdPartyIdentifiers: {
|
|
|
|
providerId
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// projection includes thirdPartyIdentifiers for tests
|
|
|
|
User.findOneAndUpdate(query, update, { new: 1 }, (err, res) => {
|
|
|
|
if (err != null) {
|
|
|
|
callback(err)
|
|
|
|
} else if (!res) {
|
|
|
|
callback(new Error('update failed'))
|
|
|
|
} else {
|
|
|
|
// no need to wait, errors are logged and not passed back
|
|
|
|
_sendSecurityAlert(accountLinked, providerId, res, userId)
|
|
|
|
callback(null, res)
|
|
|
|
}
|
|
|
|
})
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2020-10-06 09:50:07 -04:00
|
|
|
)
|
2019-11-01 10:19:56 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
function _getUserQuery(providerId, externalUserId) {
|
|
|
|
externalUserId = externalUserId.toString()
|
|
|
|
providerId = providerId.toString()
|
|
|
|
const query = {
|
|
|
|
'thirdPartyIdentifiers.externalUserId': externalUserId,
|
|
|
|
'thirdPartyIdentifiers.providerId': providerId
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-11-01 10:19:56 -04:00
|
|
|
return query
|
|
|
|
}
|
|
|
|
|
2020-09-29 10:05:12 -04:00
|
|
|
function _sendSecurityAlert(accountLinked, providerId, user, userId) {
|
|
|
|
const operation = accountLinked ? 'linked' : 'no longer linked'
|
|
|
|
const tense = accountLinked ? 'was' : 'is'
|
|
|
|
const providerName = oauthProviders[providerId].name
|
|
|
|
const indefiniteArticle = _getIndefiniteArticle(providerName)
|
|
|
|
const emailOptions = {
|
|
|
|
to: user.email,
|
|
|
|
action: `${providerName} account ${operation}`,
|
|
|
|
actionDescribed: `${indefiniteArticle} ${providerName} account ${tense} ${operation} to your account ${
|
|
|
|
user.email
|
|
|
|
}`
|
|
|
|
}
|
|
|
|
EmailHandler.sendEmail('securityAlert', emailOptions, error => {
|
|
|
|
if (error) {
|
|
|
|
logger.error(
|
|
|
|
{ error, userId },
|
|
|
|
`could not send security alert email when ${providerName} ${operation}`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-11-01 10:19:56 -04:00
|
|
|
function _thirdPartyIdentifierUpdate(
|
|
|
|
user,
|
|
|
|
providerId,
|
|
|
|
externalUserId,
|
|
|
|
externalData
|
|
|
|
) {
|
|
|
|
providerId = providerId.toString()
|
|
|
|
// get third party identifier object from array
|
|
|
|
const thirdPartyIdentifier = user.thirdPartyIdentifiers.find(
|
|
|
|
tpi =>
|
|
|
|
tpi.externalUserId === externalUserId && tpi.providerId === providerId
|
|
|
|
)
|
|
|
|
// do recursive merge of new data over existing data
|
|
|
|
_.merge(thirdPartyIdentifier.externalData, externalData)
|
|
|
|
const update = { 'thirdPartyIdentifiers.$': thirdPartyIdentifier }
|
|
|
|
return update
|
|
|
|
}
|
|
|
|
|
|
|
|
const ThirdPartyIdentityManager = {
|
|
|
|
getUser,
|
|
|
|
login,
|
|
|
|
link,
|
|
|
|
unlink
|
|
|
|
}
|
|
|
|
|
|
|
|
ThirdPartyIdentityManager.promises = promisifyAll(ThirdPartyIdentityManager)
|
|
|
|
|
|
|
|
module.exports = ThirdPartyIdentityManager
|