Files
overleaf-cep/services/web/app/src/Features/User/ThirdPartyIdentityManager.js
T
Eric Mc Sween 01e2a29766 Merge pull request #18225 from overleaf/em-typescript-eslint
Add typescript-eslint rule: no-floating-promises

GitOrigin-RevId: 8c3decdff537c885f5bfeb5250b7805480bc6602
2024-05-27 10:22:20 +00:00

218 lines
5.2 KiB
JavaScript

const UserAuditLogHandler = require('../../../../app/src/Features/User/UserAuditLogHandler')
const EmailHandler = require('../../../../app/src/Features/Email/EmailHandler')
const EmailOptionsHelper = require('../../../../app/src/Features/Email/EmailOptionsHelper')
const Errors = require('../Errors/Errors')
const _ = require('lodash')
const logger = require('@overleaf/logger')
const settings = require('@overleaf/settings')
const { User } = require('../../../../app/src/models/User')
const { callbackify } = require('@overleaf/promise-utils')
const OError = require('@overleaf/o-error')
const oauthProviders = settings.oauthProviders || {}
async function getUser(providerId, externalUserId) {
if (providerId == null || externalUserId == null) {
throw new OError('invalid SSO arguments', {
externalUserId,
providerId,
})
}
const query = _getUserQuery(providerId, externalUserId)
const user = await User.findOne(query).exec()
if (!user) {
throw new Errors.ThirdPartyUserNotFoundError()
}
return user
}
async function login(providerId, externalUserId, externalData) {
const user = await ThirdPartyIdentityManager.promises.getUser(
providerId,
externalUserId
)
if (!externalData) {
return user
}
const query = _getUserQuery(providerId, externalUserId)
const update = _thirdPartyIdentifierUpdate(
user,
providerId,
externalUserId,
externalData
)
return await User.findOneAndUpdate(query, update, { new: true }).exec()
}
async function link(
userId,
providerId,
externalUserId,
externalData,
auditLog,
retry
) {
const accountLinked = true
if (!oauthProviders[providerId]) {
throw new Error('Not a valid provider')
}
await UserAuditLogHandler.promises.addEntry(
userId,
'link-sso',
auditLog.initiatorId,
auditLog.ipAddress,
{
providerId,
}
)
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
let res
try {
res = await User.findOneAndUpdate(query, update, { new: 1 }).exec()
} catch (err) {
if (err.code === 11000) {
throw new Errors.ThirdPartyIdentityExistsError({
info: { externalUserId },
})
}
throw err
}
if (res) {
_sendSecurityAlert(accountLinked, providerId, res, userId)
return res
}
if (retry) {
// if already retried then throw error
throw new Error('update failed')
}
// attempt to clear existing entry then retry
await ThirdPartyIdentityManager.promises.unlink(userId, providerId, auditLog)
return await ThirdPartyIdentityManager.promises.link(
userId,
providerId,
externalUserId,
externalData,
auditLog,
true
)
}
async function unlink(userId, providerId, auditLog) {
const accountLinked = false
if (!oauthProviders[providerId]) {
throw new Error('Not a valid provider')
}
await UserAuditLogHandler.promises.addEntry(
userId,
'unlink-sso',
auditLog.initiatorId,
auditLog.ipAddress,
{
...(auditLog.extraInfo || {}),
providerId,
}
)
const query = {
_id: userId,
}
const update = {
$pull: {
thirdPartyIdentifiers: {
providerId,
},
},
}
// projection includes thirdPartyIdentifiers for tests
const res = await User.findOneAndUpdate(query, update, { new: 1 })
if (!res) {
throw new Error('update failed')
}
_sendSecurityAlert(accountLinked, providerId, res, userId)
return res
}
function _getUserQuery(providerId, externalUserId) {
externalUserId = externalUserId.toString()
providerId = providerId.toString()
const query = {
'thirdPartyIdentifiers.externalUserId': externalUserId,
'thirdPartyIdentifiers.providerId': providerId,
}
return query
}
function _sendSecurityAlert(accountLinked, providerId, user, userId) {
const providerName = oauthProviders[providerId].name
const emailOptions = EmailOptionsHelper.linkOrUnlink(
accountLinked,
providerName,
user.email
)
EmailHandler.promises
.sendEmail('securityAlert', emailOptions)
.catch(error => {
logger.error(
{ err: error, userId },
`could not send security alert email when ${emailOptions.action.toLowerCase()}`
)
})
}
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: callbackify(getUser),
login: callbackify(login),
link: callbackify(link),
unlink: callbackify(unlink),
}
ThirdPartyIdentityManager.promises = {
getUser,
login,
link,
unlink,
}
module.exports = ThirdPartyIdentityManager