Skip to content

Commit dfa4982

Browse files
committed
[provision group] case insensitive email matching
Usernames and emails in our Cognito user pool are case sensitive (at least the way we have them set up) but we want to avoid the situation where we add a "new" email when there's an existing email which is the same except for casing. Resolves <#1258>
1 parent 776022f commit dfa4982

File tree

1 file changed

+64
-3
lines changed

1 file changed

+64
-3
lines changed

scripts/provision-group-check-users.js

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,49 @@ async function getUserRoles(group, username) {
163163
return existingRoles;
164164
}
165165

166+
/**
167+
* The cognito pools we use are case-sensitive for emails, but we want to avoid
168+
* provisioning multiple emails with different casings. Here we create a lookup
169+
* dictionary by uppercase email addresses, and for good measure spit out warnings
170+
* if there are multiple emails with different casings.
171+
*/
172+
function summariseEmails(usersByEmail) {
173+
const db = {}
174+
for (const email of Object.keys(usersByEmail)) {
175+
const key = email.toUpperCase();
176+
Object.hasOwn(db, key) ? db[key].push(email) : (db[key]=[email]);
177+
}
178+
// eslint-disable-next-line no-unused-vars
179+
for (const [_key, emails] of Object.entries(db)) {
180+
if (emails.length>1) {
181+
console.log(`WARNING: Multiple emails with different casings present in pool: ${emails.join(', ')}`)
182+
}
183+
}
184+
return db;
185+
}
186+
187+
/**
188+
* Match a provided case-sensitive *email* against the *emailDb* which is
189+
* indexed by all-caps emails. This is necessary because cognito (at least the
190+
* way we have it) is case-sensitive, but emails are generally case-insensitive.
191+
* Ultimately we want to avoid situations where we provision multiple emails
192+
* which differ only in their casing.
193+
* @param {object} emailDb
194+
* @param {string} email
195+
* @returns {object} exact: boolean (whether the email is an exact match),
196+
* candidates: string[] cognito URLs which are identical to the provided email
197+
* except for potential different casing
198+
*/
199+
function searchEmails(emailDb, email) {
200+
const candidates = emailDb[email.toUpperCase()] || [];
201+
const exact = candidates.includes(email);
202+
if (candidates.length===0) return false
203+
return {candidates, exact}
204+
}
166205

167206
async function queryUsernames(group, members, usersByEmail, usersByUsername) {
207+
const emailDb = summariseEmails(usersByEmail); // uppercase emails for searching purposes
208+
168209
for (const member of members) {
169210
console.log()
170211
console.log(`Membership request for user "${member.username}" (${member.email}), role: ${member.role}`)
@@ -175,7 +216,29 @@ async function queryUsernames(group, members, usersByEmail, usersByUsername) {
175216
process.exit(2)
176217
}
177218

178-
if (Object.hasOwn(usersByEmail, member.email)) {
219+
/* match emails because cognito is case-sensitive and emails generally aren't */
220+
const emailMatch = searchEmails(emailDb, member.email);
221+
222+
if (emailMatch) {
223+
224+
/* If the email's not an exact match, then print out a warning and go to the next member */
225+
if (!emailMatch.exact) {
226+
if (emailMatch.candidates.length===1) {
227+
console.log(`\tWARNING: The provided email ${member.email} wasn't an exact match - you probably meant "${emailMatch.candidates[0]}"`)
228+
console.log(`\tACTION: try again with the case-corrected email`)
229+
} else {
230+
console.log(`\tWARNING: The provided email ${member.email} wasn't an exact match but there are` +
231+
`multiple different (case-sensitive) matches: ${emailMatch.candidates.join(", ")}`)
232+
console.log(`\tACTION: try again with a case-corrected email or (better) unify these various emails before proceeding`)
233+
}
234+
continue
235+
}
236+
/* If it's an exact match but there are also other (case-sensitive) alternatives, proceed with a loud warning */
237+
if (emailMatch.candidates.length>1) {
238+
console.log(`\tWARNING: while this exact email already exists, other emails with different casing also exist (see earlier warnings). ` +
239+
`We recommend you unify these emails (and associated usernames) before proceeding`)
240+
}
241+
179242
// Pay the cost upfront to get all the roles currently set (in the context of the chosen
180243
// Nextstrain group) for all cognito users associated with this email address
181244
console.log(`\tThis email address is currently associated with ${usersByEmail[member.email].length} user(s)`)
@@ -187,8 +250,6 @@ async function queryUsernames(group, members, usersByEmail, usersByUsername) {
187250
}
188251
const numAssociatedUsers = usersByEmail[member.email].length;
189252

190-
// TODO XXX - check case of emails...
191-
192253
if (member.username) {
193254
// Simplest case: the requested username is associated with the requested email
194255
if (Object.hasOwn(userRoles, member.username)) {

0 commit comments

Comments
 (0)