sync-user-entitlements.mjs 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. import fs from 'node:fs'
  2. import minimist from 'minimist'
  3. import InstitutionsAPIModule from '../../app/src/Features/Institutions/InstitutionsAPI.mjs'
  4. const { promises: InstitutionsAPI } = InstitutionsAPIModule
  5. const argv = minimist(process.argv.slice(2))
  6. const commit = argv.commit !== undefined
  7. const ignoreNulls = !!argv['ignore-nulls']
  8. if (!commit) {
  9. console.log('DOING DRY RUN. TO SAVE CHANGES PASS --commit')
  10. }
  11. const userEntitlements = loadUserEntitlements(argv['user-entitlements'])
  12. const cachedEntitlements = loadCachedEntitlements(argv['cached-entitlements'])
  13. async function syncUserEntitlements(userEntitlements, cachedEntitlements) {
  14. // check for user entitlements in mongo but not in postgres
  15. for (const key of Object.keys(userEntitlements)) {
  16. const userEntitlement = userEntitlements[key]
  17. if (!userEntitlement) {
  18. continue
  19. }
  20. // find any email(s) that are linked through sso
  21. for (const email of userEntitlement.emails) {
  22. if (!email.samlProviderId) {
  23. continue
  24. }
  25. // get samlIdentifiers entry for email
  26. const samlIdentifier = userEntitlement.samlIdentifiers.find(
  27. samlIdentifier => samlIdentifier.providerId === email.samlProviderId
  28. )
  29. // validate that entitlement is cached
  30. if (samlIdentifier) {
  31. const cachedEntitlment = cachedEntitlements[email.email]
  32. // validate that record is correct
  33. if (cachedEntitlment) {
  34. if (
  35. cachedEntitlment.hasEntitlement !== samlIdentifier.hasEntitlement
  36. ) {
  37. console.log(
  38. `cached entitlement mismatch for user ${userEntitlement.userId} mongo(${samlIdentifier.hasEntitlement}) postgres(${cachedEntitlment.hasEntitlement})`
  39. )
  40. await syncUserEntitlement(
  41. userEntitlement.userId,
  42. email.email,
  43. samlIdentifier.hasEntitlement
  44. )
  45. }
  46. }
  47. // there is not record in postgres at all
  48. else {
  49. console.log(
  50. `missing cached entitlement for user ${userEntitlement.userId}`
  51. )
  52. await syncUserEntitlement(
  53. userEntitlement.userId,
  54. email.email,
  55. samlIdentifier.hasEntitlement
  56. )
  57. }
  58. }
  59. // if identifier is missing for email this is internal inconsistency in mongo
  60. else {
  61. console.log(`missing samlIdentifier for user ${userEntitlement.userId}`)
  62. }
  63. }
  64. // find any samlIdentifier records missing email entry
  65. for (const samlIdentifier of userEntitlement.samlIdentifiers) {
  66. const email = userEntitlement.emails.find(
  67. email => email.samlProviderId === samlIdentifier.providerId
  68. )
  69. if (!email) {
  70. console.log(
  71. `missing email entry for samlIdentifier for user ${userEntitlement.userId}`
  72. )
  73. }
  74. }
  75. }
  76. // check for user entitlements in postgres but not in mongo
  77. for (const key of Object.keys(cachedEntitlements)) {
  78. const cachedEntitlment = cachedEntitlements[key]
  79. if (!cachedEntitlment) {
  80. continue
  81. }
  82. if (!cachedEntitlment.hasEntitlement) {
  83. continue
  84. }
  85. const userEntitlement = userEntitlements[cachedEntitlment.userId]
  86. // validate that mongo has correct entitlement
  87. if (userEntitlement) {
  88. // find samlIdentifier for provider
  89. const samlIdentifier = userEntitlement.samlIdentifiers.find(
  90. samlIdentifier =>
  91. samlIdentifier.providerId === cachedEntitlment.providerId
  92. )
  93. if (!samlIdentifier || !samlIdentifier.hasEntitlement) {
  94. console.log(
  95. `cached entitlement mismatch for user ${userEntitlement.userId} mongo(false) postgres(true)`
  96. )
  97. await syncUserEntitlement(
  98. userEntitlement.userId,
  99. cachedEntitlment.email,
  100. false
  101. )
  102. }
  103. }
  104. // if the record does not exist it is probably because users without
  105. // entitlements were not exported
  106. else {
  107. console.log(
  108. `missing cached entitlement in mongo for user ${cachedEntitlment.userId}`
  109. )
  110. }
  111. }
  112. }
  113. async function syncUserEntitlement(userId, email, hasEntitlement) {
  114. if (!commit) {
  115. return
  116. }
  117. try {
  118. if (hasEntitlement) {
  119. await InstitutionsAPI.addEntitlement(userId, email)
  120. } else {
  121. await InstitutionsAPI.removeEntitlement(userId, email)
  122. }
  123. } catch (err) {
  124. console.error(
  125. `error setting entitlement: ${userId}, ${email}, ${hasEntitlement} - ${err.message}`
  126. )
  127. }
  128. }
  129. function loadUserEntitlements(userEntitlementsFilename) {
  130. const userEntitlementsData = fs
  131. .readFileSync(userEntitlementsFilename, {
  132. encoding: 'utf8',
  133. })
  134. .split('\n')
  135. const userEntitlements = {}
  136. for (const userEntitlementLine of userEntitlementsData) {
  137. if (!userEntitlementLine) {
  138. continue
  139. }
  140. const userEntitlementExport = JSON.parse(userEntitlementLine)
  141. const userId = userEntitlementExport._id.$oid
  142. delete userEntitlementExport._id
  143. userEntitlementExport.userId = userId
  144. userEntitlements[userId] = userEntitlementExport
  145. }
  146. return userEntitlements
  147. }
  148. function loadCachedEntitlements(cachedEntitlementsFilename) {
  149. const cachedEntitlementsData = fs
  150. .readFileSync(cachedEntitlementsFilename, {
  151. encoding: 'utf8',
  152. })
  153. .split('\n')
  154. const cachedEntitlements = {}
  155. for (const cachedEntitlementLine of cachedEntitlementsData) {
  156. // this is safe because comma is not an allowed value for any column
  157. const [userId, email, hasEntitlement, providerId] =
  158. cachedEntitlementLine.split(',')
  159. let hasEntitlementBoolean
  160. if (ignoreNulls) {
  161. hasEntitlementBoolean = hasEntitlement === 't'
  162. } else {
  163. hasEntitlementBoolean =
  164. hasEntitlement === 't' ? true : hasEntitlement === 'f' ? false : null
  165. }
  166. cachedEntitlements[email] = {
  167. email,
  168. hasEntitlement: hasEntitlementBoolean,
  169. providerId,
  170. userId,
  171. }
  172. }
  173. return cachedEntitlements
  174. }
  175. try {
  176. await syncUserEntitlements(userEntitlements, cachedEntitlements)
  177. } catch (error) {
  178. console.error(error.stack)
  179. }
  180. process.exit()