import fs from 'node: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()