sync-user-entitlements.js 5.9 KB

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