Skip to content

Commit 5a60026

Browse files
committed
[provision group] Check emails against users
The provision-users command requires us to provide both a username and email for each user we want to add. A common use case is being given a (long) list of emails and wanting to check if a user already exists for each email, which is exactly what this script does.
1 parent d57a1a1 commit 5a60026

File tree

1 file changed

+130
-0
lines changed

1 file changed

+130
-0
lines changed
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#!/usr/bin/env node
2+
import { ArgumentParser } from 'argparse';
3+
import {
4+
CognitoIdentityProviderClient,
5+
ListUsersCommand,
6+
} from '@aws-sdk/client-cognito-identity-provider';
7+
import fs from 'fs';
8+
import yaml from 'js-yaml';
9+
import process from 'process';
10+
import { COGNITO_USER_POOL_ID } from '../src/config.js';
11+
import { reportUnhandledRejectionsAtExit } from '../src/utils/scripts.js';
12+
13+
const REGION = COGNITO_USER_POOL_ID.split("_")[0];
14+
const cognito = new CognitoIdentityProviderClient({ region: REGION });
15+
16+
function parseArgs() {
17+
const argparser = new ArgumentParser({
18+
description: `
19+
A helper script to check provided emails against existing cognito users.
20+
If a username is provided that is also checked against existing users.
21+
Messages are printed to STDOUT.
22+
Set 'CONFIG_FILE=env/production/config.json' to use the production cognito
23+
user pool (default: pool defined in env/testing/config.json)
24+
`,
25+
});
26+
argparser.addArgument("--members", {
27+
dest: "membersFile",
28+
metavar: "<file.yaml>",
29+
required: true,
30+
help: `
31+
A YAML file intended for 'provision-group.js'. Each entry must contain a 'email'
32+
key and optionally a 'username' key.
33+
`,
34+
});
35+
36+
return argparser.parseArgs();
37+
}
38+
39+
40+
async function main({membersFile}) {
41+
const members = readMembersFile(membersFile)
42+
const {usersByEmail, usersByUsername} = await cognitoUsers()
43+
queryUsernames(members, usersByEmail, usersByUsername)
44+
}
45+
46+
47+
function readMembersFile(file) {
48+
const members = yaml.load(fs.readFileSync(file));
49+
50+
const validationErrors = !Array.isArray(members)
51+
? ["Not an array"]
52+
: members.filter(m => !m.email)
53+
.map(m => `Email missing for member ${JSON.stringify(m)}`)
54+
;
55+
56+
if (validationErrors.length) {
57+
const msg = validationErrors.map((err, i) => ` ${i+1}. ${err}`).join("\n");
58+
const s = validationErrors.length === 1 ? "" : "s";
59+
throw new Error(`Members file contains ${validationErrors.length} error${s}:\n${msg}`);
60+
}
61+
62+
return members;
63+
}
64+
65+
function getEmailFromUser(user) {
66+
return user.Attributes.filter(({Name}) => Name==='email')[0].Value;
67+
}
68+
69+
async function cognitoUsers() {
70+
// https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/cognito-identity-provider/command/ListUsersCommand/
71+
const params = {
72+
AttributesToGet: ['email'], // all users must have this else cmd fails
73+
UserPoolId: COGNITO_USER_POOL_ID,
74+
// Limit: 60, // default: 60
75+
}
76+
let {Users, PaginationToken} = await cognito.send(new ListUsersCommand(params));
77+
while (PaginationToken) {
78+
const data = await cognito.send(new ListUsersCommand({...params, PaginationToken}));
79+
Users = [...Users, ...data.Users]
80+
PaginationToken = data.PaginationToken
81+
}
82+
// there may be duplicate emails (different users)
83+
const [usersByEmail, usersByUsername] = [{}, {}]
84+
for (const user of Users) {
85+
const email = getEmailFromUser(user);
86+
Object.hasOwn(usersByEmail, email) ? usersByEmail[email].push(user) : (usersByEmail[email]=[user])
87+
usersByUsername[user.Username] = user; // Username is unique (within a user pool)
88+
}
89+
90+
return {usersByEmail, usersByUsername};
91+
}
92+
93+
94+
function queryUsernames(members, usersByEmail, usersByUsername) {
95+
for (const member of members) {
96+
if (Object.hasOwn(usersByEmail, member.email)) {
97+
const existingMsg = `Email ${member.email} exists with username(s) ${usersByEmail[member.email].map((u) => `'${u.Username}'`).join(', ')}`
98+
if (member.username) {
99+
const usernameAssociatedWithEmail = usersByEmail[member.email].filter((u) => u.Username===member.username).length>0
100+
if (usernameAssociatedWithEmail) {
101+
console.log(`${existingMsg} ALL GOOD.`)
102+
} else if (Object.hasOwn(usersByUsername, member.username)) {
103+
console.log(`${existingMsg} but the username '${member.username}' is already associated with '${getEmailFromUser(usersByUsername[member.username])}'`)
104+
} else {
105+
console.log(`${existingMsg} and you are asking for the new username '${member.username}' to be created. ALL GOOD.`)
106+
}
107+
} else {
108+
console.log(existingMsg)
109+
}
110+
} else { // new email (not associated with any cognito user)
111+
const existingMsg = `Email ${member.email} doesn't yet exist`
112+
if (member.username) {
113+
if (Object.hasOwn(usersByUsername, member.username)) {
114+
console.log(`${existingMsg} but the username '${member.username}' is already associated with '${getEmailFromUser(usersByUsername[member.username])}'`)
115+
} else {
116+
console.log(`${existingMsg} and neither does the username '${member.username}. ALL GOOD.'`)
117+
}
118+
} else {
119+
console.log(`${existingMsg} (and you haven't specified a username)`)
120+
}
121+
}
122+
}
123+
}
124+
125+
reportUnhandledRejectionsAtExit();
126+
main(parseArgs())
127+
.catch((error) => {
128+
process.exitCode = 1;
129+
console.error(error)
130+
});

0 commit comments

Comments
 (0)