mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #6509 from overleaf/jel-migrate-sso-id
[v1 and web] Migrate institution SSO external user ID GitOrigin-RevId: f31cd50fbada9a2704df1c837d695f2ff547420d
This commit is contained in:
parent
cb657d1f1c
commit
8c816b3b23
8 changed files with 408 additions and 85 deletions
2
package-lock.json
generated
2
package-lock.json
generated
|
@ -35052,6 +35052,7 @@
|
||||||
"multer": "https://github.com/overleaf/multer/archive/7a2928d7ea2da02dd92888ea1c9ba5704e07aeeb.tar.gz",
|
"multer": "https://github.com/overleaf/multer/archive/7a2928d7ea2da02dd92888ea1c9ba5704e07aeeb.tar.gz",
|
||||||
"nocache": "^2.1.0",
|
"nocache": "^2.1.0",
|
||||||
"nock": "^13.1.3",
|
"nock": "^13.1.3",
|
||||||
|
"node-fetch": "^2.6.7",
|
||||||
"nodemailer": "^6.7.0",
|
"nodemailer": "^6.7.0",
|
||||||
"nodemailer-mandrill-transport": "^1.2.0",
|
"nodemailer-mandrill-transport": "^1.2.0",
|
||||||
"nodemailer-ses-transport": "^1.5.1",
|
"nodemailer-ses-transport": "^1.5.1",
|
||||||
|
@ -35176,7 +35177,6 @@
|
||||||
"mocha": "^8.4.0",
|
"mocha": "^8.4.0",
|
||||||
"mock-fs": "^5.1.2",
|
"mock-fs": "^5.1.2",
|
||||||
"nock": "^13.1.1",
|
"nock": "^13.1.1",
|
||||||
"node-fetch": "^2.6.7",
|
|
||||||
"pirates": "^4.0.1",
|
"pirates": "^4.0.1",
|
||||||
"postcss-loader": "^6.2.1",
|
"postcss-loader": "^6.2.1",
|
||||||
"requirejs": "^2.3.6",
|
"requirejs": "^2.3.6",
|
||||||
|
|
|
@ -10,26 +10,15 @@ const UserGetter = require('../User/UserGetter')
|
||||||
const UserUpdater = require('../User/UserUpdater')
|
const UserUpdater = require('../User/UserUpdater')
|
||||||
const logger = require('@overleaf/logger')
|
const logger = require('@overleaf/logger')
|
||||||
const { User } = require('../../models/User')
|
const { User } = require('../../models/User')
|
||||||
|
const { promiseMapWithLimit } = require('../../util/promises')
|
||||||
|
|
||||||
async function _addAuditLogEntry(
|
async function _addAuditLogEntry(operation, userId, auditLog, extraInfo) {
|
||||||
link,
|
|
||||||
userId,
|
|
||||||
auditLog,
|
|
||||||
institutionEmail,
|
|
||||||
providerId,
|
|
||||||
providerName
|
|
||||||
) {
|
|
||||||
const operation = link ? 'link-institution-sso' : 'unlink-institution-sso'
|
|
||||||
await UserAuditLogHandler.promises.addEntry(
|
await UserAuditLogHandler.promises.addEntry(
|
||||||
userId,
|
userId,
|
||||||
operation,
|
operation,
|
||||||
auditLog.initiatorId,
|
auditLog.initiatorId,
|
||||||
auditLog.ipAddress,
|
auditLog.ipAddress,
|
||||||
{
|
extraInfo
|
||||||
institutionEmail,
|
|
||||||
providerId,
|
|
||||||
providerName,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,19 +84,25 @@ async function _addIdentifier(
|
||||||
hasEntitlement,
|
hasEntitlement,
|
||||||
institutionEmail,
|
institutionEmail,
|
||||||
providerName,
|
providerName,
|
||||||
auditLog
|
auditLog,
|
||||||
|
userIdAttribute
|
||||||
) {
|
) {
|
||||||
providerId = providerId.toString()
|
providerId = providerId.toString()
|
||||||
|
|
||||||
await _ensureCanAddIdentifier(userId, institutionEmail, providerId)
|
await _ensureCanAddIdentifier(userId, institutionEmail, providerId)
|
||||||
|
|
||||||
await _addAuditLogEntry(
|
const auditLogInfo = {
|
||||||
true,
|
|
||||||
userId,
|
|
||||||
auditLog,
|
|
||||||
institutionEmail,
|
institutionEmail,
|
||||||
providerId,
|
providerId,
|
||||||
providerName
|
providerName,
|
||||||
|
userIdAttribute,
|
||||||
|
}
|
||||||
|
|
||||||
|
await _addAuditLogEntry(
|
||||||
|
'link-institution-sso',
|
||||||
|
userId,
|
||||||
|
auditLog,
|
||||||
|
auditLogInfo
|
||||||
)
|
)
|
||||||
|
|
||||||
hasEntitlement = !!hasEntitlement
|
hasEntitlement = !!hasEntitlement
|
||||||
|
@ -117,12 +112,14 @@ async function _addIdentifier(
|
||||||
$ne: providerId,
|
$ne: providerId,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const update = {
|
const update = {
|
||||||
$push: {
|
$push: {
|
||||||
samlIdentifiers: {
|
samlIdentifiers: {
|
||||||
hasEntitlement,
|
hasEntitlement,
|
||||||
externalUserId,
|
externalUserId,
|
||||||
providerId,
|
providerId,
|
||||||
|
userIdAttribute,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -239,15 +236,16 @@ async function redundantSubscription(userId, providerId, providerName) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function linkAccounts(
|
async function linkAccounts(userId, samlData, auditLog) {
|
||||||
userId,
|
const {
|
||||||
externalUserId,
|
externalUserId,
|
||||||
institutionEmail,
|
institutionEmail,
|
||||||
providerId,
|
universityId: providerId,
|
||||||
providerName,
|
universityName: providerName,
|
||||||
hasEntitlement,
|
hasEntitlement,
|
||||||
auditLog
|
userIdAttribute,
|
||||||
) {
|
} = samlData
|
||||||
|
|
||||||
await _addIdentifier(
|
await _addIdentifier(
|
||||||
userId,
|
userId,
|
||||||
externalUserId,
|
externalUserId,
|
||||||
|
@ -255,7 +253,8 @@ async function linkAccounts(
|
||||||
hasEntitlement,
|
hasEntitlement,
|
||||||
institutionEmail,
|
institutionEmail,
|
||||||
providerName,
|
providerName,
|
||||||
auditLog
|
auditLog,
|
||||||
|
userIdAttribute
|
||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
await _addInstitutionEmail(userId, institutionEmail, providerId, auditLog)
|
await _addInstitutionEmail(userId, institutionEmail, providerId, auditLog)
|
||||||
|
@ -288,14 +287,11 @@ async function unlinkAccounts(
|
||||||
) {
|
) {
|
||||||
providerId = providerId.toString()
|
providerId = providerId.toString()
|
||||||
|
|
||||||
await _addAuditLogEntry(
|
await _addAuditLogEntry('unlink-institution-sso', userId, auditLog, {
|
||||||
false,
|
|
||||||
userId,
|
|
||||||
auditLog,
|
|
||||||
institutionEmail,
|
institutionEmail,
|
||||||
providerId,
|
providerId,
|
||||||
providerName
|
providerName,
|
||||||
)
|
})
|
||||||
// update v2 user
|
// update v2 user
|
||||||
await _removeIdentifier(userId, providerId)
|
await _removeIdentifier(userId, providerId)
|
||||||
// update v1 affiliations record
|
// update v1 affiliations record
|
||||||
|
@ -379,12 +375,96 @@ function userHasEntitlement(user, providerId) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function migrateIdentifier(
|
||||||
|
userId,
|
||||||
|
externalUserId,
|
||||||
|
providerId,
|
||||||
|
hasEntitlement,
|
||||||
|
institutionEmail,
|
||||||
|
providerName,
|
||||||
|
auditLog,
|
||||||
|
userIdAttribute
|
||||||
|
) {
|
||||||
|
providerId = providerId.toString()
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
_id: userId,
|
||||||
|
'samlIdentifiers.providerId': providerId,
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = {
|
||||||
|
$set: {
|
||||||
|
'samlIdentifiers.$.externalUserId': externalUserId,
|
||||||
|
'samlIdentifiers.$.userIdAttribute': userIdAttribute,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await User.updateOne(query, update).exec()
|
||||||
|
|
||||||
|
const auditLogInfo = {
|
||||||
|
institutionEmail,
|
||||||
|
providerId,
|
||||||
|
providerName,
|
||||||
|
userIdAttribute,
|
||||||
|
}
|
||||||
|
|
||||||
|
await _addAuditLogEntry(
|
||||||
|
'migrate-institution-sso-id',
|
||||||
|
userId,
|
||||||
|
auditLog,
|
||||||
|
auditLogInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unlinkNotMigrated(userId, providerId, providerName, auditLog) {
|
||||||
|
providerId = providerId.toString()
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
_id: userId,
|
||||||
|
'emails.samlProviderId': providerId,
|
||||||
|
}
|
||||||
|
const update = {
|
||||||
|
$pull: {
|
||||||
|
samlIdentifiers: {
|
||||||
|
providerId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
$unset: {
|
||||||
|
'emails.$.samlProviderId': 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalDoc = await User.findOneAndUpdate(query, update).exec()
|
||||||
|
|
||||||
|
// should only be 1
|
||||||
|
const linkedEmails = originalDoc.emails.filter(email => {
|
||||||
|
return email.samlProviderId === providerId
|
||||||
|
})
|
||||||
|
|
||||||
|
const auditLogInfo = {
|
||||||
|
providerId,
|
||||||
|
providerName,
|
||||||
|
}
|
||||||
|
|
||||||
|
await _addAuditLogEntry(
|
||||||
|
'unlink-institution-sso-not-migrated',
|
||||||
|
userId,
|
||||||
|
auditLog,
|
||||||
|
auditLogInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
await promiseMapWithLimit(10, linkedEmails, async emailData => {
|
||||||
|
await InstitutionsAPI.promises.removeEntitlement(userId, emailData.email)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const SAMLIdentityManager = {
|
const SAMLIdentityManager = {
|
||||||
entitlementAttributeMatches,
|
entitlementAttributeMatches,
|
||||||
getUser,
|
getUser,
|
||||||
linkAccounts,
|
linkAccounts,
|
||||||
|
migrateIdentifier,
|
||||||
redundantSubscription,
|
redundantSubscription,
|
||||||
unlinkAccounts,
|
unlinkAccounts,
|
||||||
|
unlinkNotMigrated,
|
||||||
updateEntitlement,
|
updateEntitlement,
|
||||||
userHasEntitlement,
|
userHasEntitlement,
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ function _canHaveNoInitiatorId(operation, info) {
|
||||||
if (operation === 'reset-password') return true
|
if (operation === 'reset-password') return true
|
||||||
if (operation === 'unlink-sso' && info.providerId === 'collabratec')
|
if (operation === 'unlink-sso' && info.providerId === 'collabratec')
|
||||||
return true
|
return true
|
||||||
|
if (operation === 'unlink-institution-sso-not-migrated') return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -145,6 +145,7 @@
|
||||||
"multer": "https://github.com/overleaf/multer/archive/7a2928d7ea2da02dd92888ea1c9ba5704e07aeeb.tar.gz",
|
"multer": "https://github.com/overleaf/multer/archive/7a2928d7ea2da02dd92888ea1c9ba5704e07aeeb.tar.gz",
|
||||||
"nocache": "^2.1.0",
|
"nocache": "^2.1.0",
|
||||||
"nock": "^13.1.3",
|
"nock": "^13.1.3",
|
||||||
|
"node-fetch": "^2.6.7",
|
||||||
"nodemailer": "^6.7.0",
|
"nodemailer": "^6.7.0",
|
||||||
"nodemailer-mandrill-transport": "^1.2.0",
|
"nodemailer-mandrill-transport": "^1.2.0",
|
||||||
"nodemailer-ses-transport": "^1.5.1",
|
"nodemailer-ses-transport": "^1.5.1",
|
||||||
|
@ -269,7 +270,6 @@
|
||||||
"mocha": "^8.4.0",
|
"mocha": "^8.4.0",
|
||||||
"mock-fs": "^5.1.2",
|
"mock-fs": "^5.1.2",
|
||||||
"nock": "^13.1.1",
|
"nock": "^13.1.1",
|
||||||
"node-fetch": "^2.6.7",
|
|
||||||
"pirates": "^4.0.1",
|
"pirates": "^4.0.1",
|
||||||
"postcss-loader": "^6.2.1",
|
"postcss-loader": "^6.2.1",
|
||||||
"requirejs": "^2.3.6",
|
"requirejs": "^2.3.6",
|
||||||
|
|
38
services/web/scripts/backfill_users_sso_attribute.js
Normal file
38
services/web/scripts/backfill_users_sso_attribute.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
const SAMLUserIdAttributeBatchHandler = require('../modules/overleaf-integration/app/src/SAML/SAMLUserIdAttributeBatchHandler')
|
||||||
|
|
||||||
|
const COMMIT = process.argv.includes('--commit')
|
||||||
|
const startInstitutionId = parseInt(process.argv[2])
|
||||||
|
const endInstitutionId = parseInt(process.argv[3])
|
||||||
|
|
||||||
|
process.env.LOG_LEVEL = 'info'
|
||||||
|
|
||||||
|
let method = 'check'
|
||||||
|
if (COMMIT) {
|
||||||
|
method = 'run'
|
||||||
|
console.log('Setting attribute for linked users')
|
||||||
|
} else {
|
||||||
|
process.env.MONGO_CONNECTION_STRING =
|
||||||
|
process.env.READ_ONLY_MONGO_CONNECTION_STRING
|
||||||
|
console.log('Doing a dry run without --commit')
|
||||||
|
console.log('Checking users at institutions')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'Start institution ID:',
|
||||||
|
startInstitutionId ||
|
||||||
|
'none provided, will start at beginning of ordered list.'
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
'End institution ID:',
|
||||||
|
endInstitutionId || 'none provided, will go to end of ordered list.'
|
||||||
|
)
|
||||||
|
|
||||||
|
SAMLUserIdAttributeBatchHandler[method](startInstitutionId, endInstitutionId)
|
||||||
|
.then(result => {
|
||||||
|
console.log(result)
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
36
services/web/scripts/sso_id_migration_check.js
Normal file
36
services/web/scripts/sso_id_migration_check.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
process.env.MONGO_CONNECTION_STRING =
|
||||||
|
process.env.READ_ONLY_MONGO_CONNECTION_STRING
|
||||||
|
|
||||||
|
const { waitForDb } = require('../app/src/infrastructure/mongodb')
|
||||||
|
const SAMLUserIdMigrationHandler = require('../modules/overleaf-integration/app/src/SAML/SAMLUserIdMigrationHandler')
|
||||||
|
|
||||||
|
const institutionId = parseInt(process.argv[2])
|
||||||
|
if (isNaN(institutionId)) throw new Error('No institution id')
|
||||||
|
const emitUsers = process.argv.includes('--emit-users')
|
||||||
|
|
||||||
|
console.log('Checking SSO user ID migration for institution:', institutionId)
|
||||||
|
|
||||||
|
waitForDb()
|
||||||
|
.then(main)
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const result = await SAMLUserIdMigrationHandler.promises.checkMigration(
|
||||||
|
institutionId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (emitUsers) {
|
||||||
|
console.log(
|
||||||
|
`\nMigrated: ${result.migrated}\nNot migrated: ${result.notMigrated}\nMultiple Identifers: ${result.multipleIdentifiers}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`\nMigrated: ${result.migrated.length}\nNot migrated: ${result.notMigrated.length}\nMultiple Identifers: ${result.multipleIdentifiers.length}`
|
||||||
|
)
|
||||||
|
|
||||||
|
process.exit()
|
||||||
|
}
|
36
services/web/scripts/sso_id_remove_not_migrated.js
Normal file
36
services/web/scripts/sso_id_remove_not_migrated.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
const { waitForDb } = require('../app/src/infrastructure/mongodb')
|
||||||
|
const SAMLUserIdMigrationHandler = require('../modules/overleaf-integration/app/src/SAML/SAMLUserIdMigrationHandler')
|
||||||
|
|
||||||
|
const institutionId = parseInt(process.argv[2])
|
||||||
|
if (isNaN(institutionId)) throw new Error('No institution id')
|
||||||
|
const emitUsers = process.argv.includes('--emit-users')
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'Remove SSO linking for users not migrated at institution:',
|
||||||
|
institutionId
|
||||||
|
)
|
||||||
|
|
||||||
|
waitForDb()
|
||||||
|
.then(main)
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const result = await SAMLUserIdMigrationHandler.promises.removeNotMigrated(
|
||||||
|
institutionId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (emitUsers) {
|
||||||
|
console.log(
|
||||||
|
`\nRemoved: ${result.success}\nFailed to remove: ${result.failed}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`\nRemoved: ${result.success.length}\nFailed to remove: ${result.failed.length}`
|
||||||
|
)
|
||||||
|
|
||||||
|
process.exit()
|
||||||
|
}
|
|
@ -121,13 +121,7 @@ describe('SAMLIdentityManager', function () {
|
||||||
it('should throw an error if missing data', async function () {
|
it('should throw an error if missing data', async function () {
|
||||||
let error
|
let error
|
||||||
try {
|
try {
|
||||||
await this.SAMLIdentityManager.linkAccounts(
|
await this.SAMLIdentityManager.linkAccounts(null, null, null)
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e
|
error = e
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -144,14 +138,17 @@ describe('SAMLIdentityManager', function () {
|
||||||
|
|
||||||
it('should throw an EmailExistsError error', async function () {
|
it('should throw an EmailExistsError error', async function () {
|
||||||
let error
|
let error
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.SAMLIdentityManager.linkAccounts(
|
await this.SAMLIdentityManager.linkAccounts(
|
||||||
'6005c75b12cbcaf771f4a105',
|
'6005c75b12cbcaf771f4a105',
|
||||||
'not-linked-id',
|
{
|
||||||
'exists@overleaf.com',
|
externalUserId: 'not-linked-id',
|
||||||
'provider-id',
|
institutionEmail: 'exists@overleaf.com',
|
||||||
'provider-name',
|
universityId: 'provider-id',
|
||||||
true,
|
universityName: 'provider-name',
|
||||||
|
hasEntitlement: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
intiatorId: '6005c75b12cbcaf771f4a105',
|
intiatorId: '6005c75b12cbcaf771f4a105',
|
||||||
ip: '0:0:0:0',
|
ip: '0:0:0:0',
|
||||||
|
@ -181,11 +178,13 @@ describe('SAMLIdentityManager', function () {
|
||||||
try {
|
try {
|
||||||
await this.SAMLIdentityManager.linkAccounts(
|
await this.SAMLIdentityManager.linkAccounts(
|
||||||
'6005c75b12cbcaf771f4a105',
|
'6005c75b12cbcaf771f4a105',
|
||||||
'not-linked-id',
|
{
|
||||||
'not-affiliated@overleaf.com',
|
externalUserId: 'not-linked-id',
|
||||||
'provider-id',
|
institutionEmail: 'not-affiliated@overleaf.com',
|
||||||
'provider-name',
|
universityId: 'provider-id',
|
||||||
true,
|
universityName: 'provider-name',
|
||||||
|
hasEntitlement: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
intiatorId: 'user-id-1',
|
intiatorId: 'user-id-1',
|
||||||
ip: '0:0:0:0',
|
ip: '0:0:0:0',
|
||||||
|
@ -216,11 +215,13 @@ describe('SAMLIdentityManager', function () {
|
||||||
try {
|
try {
|
||||||
await this.SAMLIdentityManager.linkAccounts(
|
await this.SAMLIdentityManager.linkAccounts(
|
||||||
'6005c75b12cbcaf771f4a105',
|
'6005c75b12cbcaf771f4a105',
|
||||||
'not-linked-id',
|
{
|
||||||
'affiliated@overleaf.com',
|
externalUserId: 'not-linked-id',
|
||||||
'provider-id',
|
institutionEmail: 'affiliated@overleaf.com',
|
||||||
'provider-name',
|
universityId: 'provider-id',
|
||||||
true,
|
universityName: 'provider-name',
|
||||||
|
hasEntitlement: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
intiatorId: 'user-id-1',
|
intiatorId: 'user-id-1',
|
||||||
ip: '0:0:0:0',
|
ip: '0:0:0:0',
|
||||||
|
@ -249,11 +250,13 @@ describe('SAMLIdentityManager', function () {
|
||||||
try {
|
try {
|
||||||
await this.SAMLIdentityManager.linkAccounts(
|
await this.SAMLIdentityManager.linkAccounts(
|
||||||
'6005c75b12cbcaf771f4a105',
|
'6005c75b12cbcaf771f4a105',
|
||||||
'already-linked-id',
|
{
|
||||||
'linked@overleaf.com',
|
externalUserId: 'already-linked-id',
|
||||||
'provider-id',
|
institutionEmail: 'linked@overleaf.com',
|
||||||
'provider-name',
|
universityId: 'provider-id',
|
||||||
true,
|
universityName: 'provider-name',
|
||||||
|
hasEntitlement: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
intiatorId: '6005c75b12cbcaf771f4a105',
|
intiatorId: '6005c75b12cbcaf771f4a105',
|
||||||
ip: '0:0:0:0',
|
ip: '0:0:0:0',
|
||||||
|
@ -279,11 +282,13 @@ describe('SAMLIdentityManager', function () {
|
||||||
try {
|
try {
|
||||||
await this.SAMLIdentityManager.linkAccounts(
|
await this.SAMLIdentityManager.linkAccounts(
|
||||||
'6005c75b12cbcaf771f4a105',
|
'6005c75b12cbcaf771f4a105',
|
||||||
'already-linked-id',
|
{
|
||||||
'linked@overleaf.com',
|
externalUserId: 'already-linked-id',
|
||||||
123456,
|
institutionEmail: 'linked@overleaf.com',
|
||||||
'provider-name',
|
universityId: 123456,
|
||||||
true,
|
universityName: 'provider-name',
|
||||||
|
hasEntitlement: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
intiatorId: '6005c75b12cbcaf771f4a105',
|
intiatorId: '6005c75b12cbcaf771f4a105',
|
||||||
ip: '0:0:0:0',
|
ip: '0:0:0:0',
|
||||||
|
@ -311,11 +316,13 @@ describe('SAMLIdentityManager', function () {
|
||||||
try {
|
try {
|
||||||
await this.SAMLIdentityManager.linkAccounts(
|
await this.SAMLIdentityManager.linkAccounts(
|
||||||
this.user._id,
|
this.user._id,
|
||||||
'externalUserId',
|
{
|
||||||
this.user.email,
|
externalUserId: 'externalUserId',
|
||||||
'1',
|
institutionEmail: this.user.email,
|
||||||
'Overleaf University',
|
universityId: '1',
|
||||||
undefined,
|
universityName: 'Overleaf University',
|
||||||
|
hasEntitlement: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
intiatorId: '6005c75b12cbcaf771f4a105',
|
intiatorId: '6005c75b12cbcaf771f4a105',
|
||||||
ipAddress: '0:0:0:0',
|
ipAddress: '0:0:0:0',
|
||||||
|
@ -346,11 +353,14 @@ describe('SAMLIdentityManager', function () {
|
||||||
}
|
}
|
||||||
await this.SAMLIdentityManager.linkAccounts(
|
await this.SAMLIdentityManager.linkAccounts(
|
||||||
this.user._id,
|
this.user._id,
|
||||||
'externalUserId',
|
{
|
||||||
this.user.email,
|
externalUserId: 'externalUserId',
|
||||||
'1',
|
institutionEmail: this.user.email,
|
||||||
'Overleaf University',
|
universityId: '1',
|
||||||
undefined,
|
universityName: 'Overleaf University',
|
||||||
|
hasEntitlement: false,
|
||||||
|
userIdAttribute: 'uniqueId',
|
||||||
|
},
|
||||||
auditLog
|
auditLog
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -365,6 +375,7 @@ describe('SAMLIdentityManager', function () {
|
||||||
institutionEmail: this.user.email,
|
institutionEmail: this.user.email,
|
||||||
providerId: '1',
|
providerId: '1',
|
||||||
providerName: 'Overleaf University',
|
providerName: 'Overleaf University',
|
||||||
|
userIdAttribute: 'uniqueId',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -372,11 +383,13 @@ describe('SAMLIdentityManager', function () {
|
||||||
it('should send an email notification', async function () {
|
it('should send an email notification', async function () {
|
||||||
await this.SAMLIdentityManager.linkAccounts(
|
await this.SAMLIdentityManager.linkAccounts(
|
||||||
this.user._id,
|
this.user._id,
|
||||||
'externalUserId',
|
{
|
||||||
this.user.email,
|
externalUserId: 'externalUserId',
|
||||||
'1',
|
institutionEmail: this.user.email,
|
||||||
'Overleaf University',
|
universityId: '1',
|
||||||
undefined,
|
universityName: 'Overleaf University',
|
||||||
|
hasEntitlement: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
intiatorId: '6005c75b12cbcaf771f4a105',
|
intiatorId: '6005c75b12cbcaf771f4a105',
|
||||||
ipAddress: '0:0:0:0',
|
ipAddress: '0:0:0:0',
|
||||||
|
@ -602,4 +615,123 @@ describe('SAMLIdentityManager', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('migrateIdentifier', function () {
|
||||||
|
const userId = '5efb8b6e9b647b0027e4c0b0'
|
||||||
|
const externalUserId = '987zyx'
|
||||||
|
const providerId = 123
|
||||||
|
const hasEntitlement = false
|
||||||
|
const institutionEmail = 'someone@email.com'
|
||||||
|
const providerName = 'Example University'
|
||||||
|
const auditLog = {
|
||||||
|
initiatorId: userId,
|
||||||
|
ipAddress: '0.0.0.0',
|
||||||
|
migration: {
|
||||||
|
from: 'uniqueId',
|
||||||
|
to: 'newUniqueId',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const userIdAttribute = 'newUniqueId'
|
||||||
|
|
||||||
|
it('should remove the old identifier and add the new identifier', async function () {
|
||||||
|
this.UserGetter.promises.getUser.resolves()
|
||||||
|
this.UserGetter.promises.getUserByAnyEmail
|
||||||
|
.withArgs(institutionEmail)
|
||||||
|
.resolves({ _id: userId, emails: [{ email: institutionEmail }] })
|
||||||
|
this.UserGetter.promises.getUserFullEmails.withArgs(userId).resolves([
|
||||||
|
{
|
||||||
|
email: institutionEmail,
|
||||||
|
affiliation: { institution: { id: providerId } },
|
||||||
|
},
|
||||||
|
])
|
||||||
|
await this.SAMLIdentityManager.migrateIdentifier(
|
||||||
|
userId,
|
||||||
|
externalUserId,
|
||||||
|
providerId,
|
||||||
|
hasEntitlement,
|
||||||
|
institutionEmail,
|
||||||
|
providerName,
|
||||||
|
auditLog,
|
||||||
|
userIdAttribute
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(this.User.updateOne).to.have.been.calledOnce
|
||||||
|
const query = {
|
||||||
|
_id: userId,
|
||||||
|
'samlIdentifiers.providerId': providerId.toString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = {
|
||||||
|
$set: {
|
||||||
|
'samlIdentifiers.$.externalUserId': externalUserId,
|
||||||
|
'samlIdentifiers.$.userIdAttribute': userIdAttribute,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(this.User.updateOne.lastCall.args).to.deep.equal([query, update])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('unlinkNotMigrated', function () {
|
||||||
|
const userId = '5efb8b6e9b647b0027e4c0b0'
|
||||||
|
const providerId = '123'
|
||||||
|
const institutionEmail = 'someone@email.com'
|
||||||
|
const providerName = 'Example University'
|
||||||
|
const auditLog = {
|
||||||
|
ipAddress: 'N/A',
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should remove the identifier om samlIdentifiers and samlProviderId on the email', async function () {
|
||||||
|
this.User.findOneAndUpdate = sinon.stub().returns({
|
||||||
|
exec: sinon.stub().resolves({
|
||||||
|
_id: userId,
|
||||||
|
emails: [{ email: institutionEmail, samlProviderId: providerId }],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.SAMLIdentityManager.unlinkNotMigrated(
|
||||||
|
userId,
|
||||||
|
providerId,
|
||||||
|
providerName,
|
||||||
|
auditLog
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(this.User.findOneAndUpdate).to.have.been.calledOnce
|
||||||
|
const query = {
|
||||||
|
_id: userId,
|
||||||
|
'emails.samlProviderId': providerId,
|
||||||
|
}
|
||||||
|
const update = {
|
||||||
|
$pull: {
|
||||||
|
samlIdentifiers: {
|
||||||
|
providerId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
$unset: {
|
||||||
|
'emails.$.samlProviderId': 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expect(this.User.findOneAndUpdate.lastCall.args).to.deep.equal([
|
||||||
|
query,
|
||||||
|
update,
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(this.UserAuditLogHandler.promises.addEntry).to.have.been.calledOnce
|
||||||
|
expect(
|
||||||
|
this.UserAuditLogHandler.promises.addEntry.lastCall.args
|
||||||
|
).to.deep.equal([
|
||||||
|
userId,
|
||||||
|
'unlink-institution-sso-not-migrated',
|
||||||
|
undefined,
|
||||||
|
'N/A',
|
||||||
|
{ providerId, providerName },
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(this.InstitutionsAPI.promises.removeEntitlement).to.have.been
|
||||||
|
.calledOnce
|
||||||
|
expect(
|
||||||
|
this.InstitutionsAPI.promises.removeEntitlement.lastCall.args
|
||||||
|
).to.deep.equal([userId, institutionEmail])
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue