overleaf/services/web/scripts/sync-user-entitlements/sync-user-entitlements.mjs
Liangjun Song 14cd8f5479 Merge pull request #21282 from overleaf/ls-scripts-to-esm-5
Migrate rest of the scripts to esm

GitOrigin-RevId: 421f3ccd15342d34113be8d22e343d08533177ea
2024-10-25 08:05:41 +00:00

196 lines
6 KiB
JavaScript

import fs from 'fs'
import minimist from 'minimist'
import InstitutionsAPIModule from '../../app/src/Features/Institutions/InstitutionsAPI.js'
const { promises: InstitutionsAPI } = InstitutionsAPIModule
const argv = minimist(process.argv.slice(2))
const commit = argv.commit !== undefined
const ignoreNulls = !!argv['ignore-nulls']
if (!commit) {
console.log('DOING DRY RUN. TO SAVE CHANGES PASS --commit')
}
const userEntitlements = loadUserEntitlements(argv['user-entitlements'])
const cachedEntitlements = loadCachedEntitlements(argv['cached-entitlements'])
async function syncUserEntitlements(userEntitlements, cachedEntitlements) {
// check for user entitlements in mongo but not in postgres
for (const key of Object.keys(userEntitlements)) {
const userEntitlement = userEntitlements[key]
if (!userEntitlement) {
continue
}
// find any email(s) that are linked through sso
for (const email of userEntitlement.emails) {
if (!email.samlProviderId) {
continue
}
// get samlIdentifiers entry for email
const samlIdentifier = userEntitlement.samlIdentifiers.find(
samlIdentifier => samlIdentifier.providerId === email.samlProviderId
)
// validate that entitlement is cached
if (samlIdentifier) {
const cachedEntitlment = cachedEntitlements[email.email]
// validate that record is correct
if (cachedEntitlment) {
if (
cachedEntitlment.hasEntitlement !== samlIdentifier.hasEntitlement
) {
console.log(
`cached entitlement mismatch for user ${userEntitlement.userId} mongo(${samlIdentifier.hasEntitlement}) postgres(${cachedEntitlment.hasEntitlement})`
)
await syncUserEntitlement(
userEntitlement.userId,
email.email,
samlIdentifier.hasEntitlement
)
}
}
// there is not record in postgres at all
else {
console.log(
`missing cached entitlement for user ${userEntitlement.userId}`
)
await syncUserEntitlement(
userEntitlement.userId,
email.email,
samlIdentifier.hasEntitlement
)
}
}
// if identifier is missing for email this is internal inconsistency in mongo
else {
console.log(`missing samlIdentifier for user ${userEntitlement.userId}`)
}
}
// find any samlIdentifier records missing email entry
for (const samlIdentifier of userEntitlement.samlIdentifiers) {
const email = userEntitlement.emails.find(
email => email.samlProviderId === samlIdentifier.providerId
)
if (!email) {
console.log(
`missing email entry for samlIdentifier for user ${userEntitlement.userId}`
)
}
}
}
// check for user entitlements in postgres but not in mongo
for (const key of Object.keys(cachedEntitlements)) {
const cachedEntitlment = cachedEntitlements[key]
if (!cachedEntitlment) {
continue
}
if (!cachedEntitlment.hasEntitlement) {
continue
}
const userEntitlement = userEntitlements[cachedEntitlment.userId]
// validate that mongo has correct entitlement
if (userEntitlement) {
// find samlIdentifier for provider
const samlIdentifier = userEntitlement.samlIdentifiers.find(
samlIdentifier =>
samlIdentifier.providerId === cachedEntitlment.providerId
)
if (!samlIdentifier || !samlIdentifier.hasEntitlement) {
console.log(
`cached entitlement mismatch for user ${userEntitlement.userId} mongo(false) postgres(true)`
)
await syncUserEntitlement(
userEntitlement.userId,
cachedEntitlment.email,
false
)
}
}
// if the record does not exist it is probably because users without
// entitlements were not exported
else {
console.log(
`missing cached entitlement in mongo for user ${cachedEntitlment.userId}`
)
}
}
}
async function syncUserEntitlement(userId, email, hasEntitlement) {
if (!commit) {
return
}
try {
if (hasEntitlement) {
await InstitutionsAPI.addEntitlement(userId, email)
} else {
await InstitutionsAPI.removeEntitlement(userId, email)
}
} catch (err) {
console.error(
`error setting entitlement: ${userId}, ${email}, ${hasEntitlement} - ${err.message}`
)
}
}
function loadUserEntitlements(userEntitlementsFilename) {
const userEntitlementsData = fs
.readFileSync(userEntitlementsFilename, {
encoding: 'utf8',
})
.split('\n')
const userEntitlements = {}
for (const userEntitlementLine of userEntitlementsData) {
if (!userEntitlementLine) {
continue
}
const userEntitlementExport = JSON.parse(userEntitlementLine)
const userId = userEntitlementExport._id.$oid
delete userEntitlementExport._id
userEntitlementExport.userId = userId
userEntitlements[userId] = userEntitlementExport
}
return userEntitlements
}
function loadCachedEntitlements(cachedEntitlementsFilename) {
const cachedEntitlementsData = fs
.readFileSync(cachedEntitlementsFilename, {
encoding: 'utf8',
})
.split('\n')
const cachedEntitlements = {}
for (const cachedEntitlementLine of cachedEntitlementsData) {
// this is safe because comma is not an allowed value for any column
const [userId, email, hasEntitlement, providerId] =
cachedEntitlementLine.split(',')
let hasEntitlementBoolean
if (ignoreNulls) {
hasEntitlementBoolean = hasEntitlement === 't'
} else {
hasEntitlementBoolean =
hasEntitlement === 't' ? true : hasEntitlement === 'f' ? false : null
}
cachedEntitlements[email] = {
email,
hasEntitlement: hasEntitlementBoolean,
providerId,
userId,
}
}
return cachedEntitlements
}
try {
await syncUserEntitlements(userEntitlements, cachedEntitlements)
} catch (error) {
console.error(error.stack)
}
process.exit()