From a03ad002e4a9f905f0d9c1051cb488c3bb364ed6 Mon Sep 17 00:00:00 2001 From: Ryan Ahearn Date: Tue, 19 Jan 2021 17:20:53 -0500 Subject: [PATCH 1/4] Add function to bootstrap an admin account --- package.json | 4 ++-- tools/bootstrapAdmin.js | 26 +++++++++++++++++++++++ tools/bootstrapAdmin.test.js | 40 ++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 tools/bootstrapAdmin.js create mode 100644 tools/bootstrapAdmin.test.js diff --git a/package.json b/package.json index 92d1e01b64..1f735c4310 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "server": "nodemon src/index.js --exec babel-node", "server:debug": "nodemon src/index.js --exec babel-node --inspect", "client": "yarn --cwd frontend start", - "test": "jest src", + "test": "jest src tools", "test:ci": "cross-env JEST_JUNIT_OUTPUT_DIR=reports JEST_JUNIT_OUTPUT_NAME=unit.xml POSTGRES_USERNAME=postgres POSTGRES_DB=ttasmarthub CURRENT_USER_ID=5 CI=true jest src tools --coverage --reporters=default --reporters=jest-junit", "test:all": "yarn test:ci && yarn --cwd frontend test:ci", "lint": "eslint src", @@ -86,7 +86,7 @@ ], "coverageThreshold": { "global": { - "branches": 70 + "branches": 75 } } }, diff --git a/tools/bootstrapAdmin.js b/tools/bootstrapAdmin.js new file mode 100644 index 0000000000..2e87f3a880 --- /dev/null +++ b/tools/bootstrapAdmin.js @@ -0,0 +1,26 @@ +import { User, Permission, sequelize } from '../src/models'; +import logger from '../src/logger'; +import SCOPES from '../src/middleware/scopeConstants'; + +const { ADMIN } = SCOPES; + +export const ADMIN_EMAIL = "ryan.ahearn@gsa.gov"; + +const bootstrapAdmin = async () => { + const user = await User.findOne({ where: { email: ADMIN_EMAIL } }); + if (user === null) { + throw new Error("User could not be found to bootstrap admin"); + } + + logger.warn(`SECURITY ALERT: Setting ${ADMIN_EMAIL} as an ADMIN`); + const [permission] = await Permission.findOrCreate({ + where: { + userId: user.id, + scopeId: ADMIN, + regionId: 14 + } + }); + return permission; +} + +export default bootstrapAdmin; diff --git a/tools/bootstrapAdmin.test.js b/tools/bootstrapAdmin.test.js new file mode 100644 index 0000000000..7fc170c324 --- /dev/null +++ b/tools/bootstrapAdmin.test.js @@ -0,0 +1,40 @@ +import bootstrapAdmin, { ADMIN_EMAIL } from './bootstrapAdmin' +import db, { User } from '../src/models'; + +describe('Bootstrap the first Admin user', () => { + afterAll(() => { + db.sequelize.close(); + }); + + describe('when user already exists', () => { + beforeAll(async () => { + await User.findOrCreate({ where: { email: ADMIN_EMAIL } }); + }); + afterAll(async () => { + await User.destroy({ where: { email: ADMIN_EMAIL } }); + }); + + it('should create an admin permission for the user', async () => { + let user = await User.findOne({ where: { email: ADMIN_EMAIL } }); + expect(user).toBeDefined(); + expect((await user.getPermissions()).length).toBe(0); + const newPermission = await bootstrapAdmin(); + expect(newPermission).toBeDefined(); + expect((await user.getPermissions()).length).toBe(1) + await newPermission.destroy(); + }); + + it('is idempotent', async () => { + const permission = await bootstrapAdmin(); + const secondRun = await bootstrapAdmin(); + expect(secondRun.id).toBe(permission.id); + await permission.destroy(); + }); + }); + + describe('when user does not exist', () => { + it('should loudly exit', async () => { + await expect(bootstrapAdmin()).rejects.toThrow(`User ${ADMIN_EMAIL} could not be found to bootstrap admin`); + }); + }); +}); From 64c95768dcfa311eb8cc2d876b34eb17bcea85d4 Mon Sep 17 00:00:00 2001 From: Ryan Ahearn Date: Wed, 20 Jan 2021 09:22:50 -0500 Subject: [PATCH 2/4] Add yarn script and CLI for running bootstrapAdmin --- package.json | 1 + tools/bootstrapAdmin.js | 8 +++++--- tools/bootstrapAdminCLI.js | 15 +++++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 tools/bootstrapAdminCLI.js diff --git a/package.json b/package.json index 1f735c4310..4356724822 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "docs:serve": "npx redoc-cli serve -p 5000 docs/openapi/index.yaml", "cucumber": "./node_modules/.bin/cucumber-js --publish ./cucumber/features/*.feature -f json:./reports/cucumber_report.json && node ./cucumber/index.js", "cucumber:ci": "cross-env TTA_SMART_HUB_URI=http://localhost:3000 yarn cucumber", + "db:bootstrap:admin": "./node_modules/.bin/babel-node ./tools/bootstrapAdminCLI.js", "db:migrate": "node_modules/.bin/sequelize db:migrate", "db:migrate:ci": "cross-env POSTGRES_USERNAME=postgres POSTGRES_DB=ttasmarthub node_modules/.bin/sequelize db:migrate", "db:migrate:prod": "node_modules/.bin/sequelize db:migrate --options-path .production.sequelizerc", diff --git a/tools/bootstrapAdmin.js b/tools/bootstrapAdmin.js index 2e87f3a880..bba3e9d6d8 100644 --- a/tools/bootstrapAdmin.js +++ b/tools/bootstrapAdmin.js @@ -9,17 +9,19 @@ export const ADMIN_EMAIL = "ryan.ahearn@gsa.gov"; const bootstrapAdmin = async () => { const user = await User.findOne({ where: { email: ADMIN_EMAIL } }); if (user === null) { - throw new Error("User could not be found to bootstrap admin"); + throw new Error(`User ${ADMIN_EMAIL} could not be found to bootstrap admin`); } - logger.warn(`SECURITY ALERT: Setting ${ADMIN_EMAIL} as an ADMIN`); - const [permission] = await Permission.findOrCreate({ + const [permission, created] = await Permission.findOrCreate({ where: { userId: user.id, scopeId: ADMIN, regionId: 14 } }); + if (created) { + logger.warn(`SECURITY ALERT: Setting ${ADMIN_EMAIL} as an ADMIN`); + } return permission; } diff --git a/tools/bootstrapAdminCLI.js b/tools/bootstrapAdminCLI.js new file mode 100644 index 0000000000..f4b3958bea --- /dev/null +++ b/tools/bootstrapAdminCLI.js @@ -0,0 +1,15 @@ +import bootstrapAdmin from './bootstrapAdmin'; + +/********* + * bootstrapAdminCLI is responsible for setting the first ADMIN for TTA Smart Hub. + * It should be run via `cf run-task tta-smarthub-prod "yarn db:bootstrap:admin"` + * + * All other admins and permissions should be set via the admin UI. + * + * To change the initial admin (for instance, after the previous one has left the project) + * Open a new issue and PR to update the ADMIN_EMAIL constant within bootstrapAdmin.js + */ +bootstrapAdmin().catch(e => { + console.log(e); + return process.exit(1); +}); From 1f9b6c7435084830662600f10c473b6d8a179837 Mon Sep 17 00:00:00 2001 From: Ryan Ahearn Date: Wed, 20 Jan 2021 11:36:22 -0500 Subject: [PATCH 3/4] Add tools directory to the eslint check --- package.json | 2 +- tools/bootstrapAdmin.js | 10 +++++----- tools/bootstrapAdmin.test.js | 6 +++--- tools/bootstrapAdminCLI.js | 5 +++-- tools/importPlanGoals.js | 5 +++-- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 4356724822..26155db109 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "test:ci": "cross-env JEST_JUNIT_OUTPUT_DIR=reports JEST_JUNIT_OUTPUT_NAME=unit.xml POSTGRES_USERNAME=postgres POSTGRES_DB=ttasmarthub CURRENT_USER_ID=5 CI=true jest src tools --coverage --reporters=default --reporters=jest-junit", "test:all": "yarn test:ci && yarn --cwd frontend test:ci", "lint": "eslint src", - "lint:ci": "eslint -f eslint-formatter-multiple src", + "lint:ci": "eslint -f eslint-formatter-multiple src tools", "lint:all": "yarn lint:ci && yarn --cwd frontend lint:ci", "clean": "rm -rf coverage reports frontend/coverage frontend/reports frontend/build", "docs:serve": "npx redoc-cli serve -p 5000 docs/openapi/index.yaml", diff --git a/tools/bootstrapAdmin.js b/tools/bootstrapAdmin.js index bba3e9d6d8..831087d1b1 100644 --- a/tools/bootstrapAdmin.js +++ b/tools/bootstrapAdmin.js @@ -1,10 +1,10 @@ -import { User, Permission, sequelize } from '../src/models'; +import { User, Permission } from '../src/models'; import logger from '../src/logger'; import SCOPES from '../src/middleware/scopeConstants'; const { ADMIN } = SCOPES; -export const ADMIN_EMAIL = "ryan.ahearn@gsa.gov"; +export const ADMIN_EMAIL = 'ryan.ahearn@gsa.gov'; const bootstrapAdmin = async () => { const user = await User.findOne({ where: { email: ADMIN_EMAIL } }); @@ -16,13 +16,13 @@ const bootstrapAdmin = async () => { where: { userId: user.id, scopeId: ADMIN, - regionId: 14 - } + regionId: 14, + }, }); if (created) { logger.warn(`SECURITY ALERT: Setting ${ADMIN_EMAIL} as an ADMIN`); } return permission; -} +}; export default bootstrapAdmin; diff --git a/tools/bootstrapAdmin.test.js b/tools/bootstrapAdmin.test.js index 7fc170c324..9d2b1b812a 100644 --- a/tools/bootstrapAdmin.test.js +++ b/tools/bootstrapAdmin.test.js @@ -1,4 +1,4 @@ -import bootstrapAdmin, { ADMIN_EMAIL } from './bootstrapAdmin' +import bootstrapAdmin, { ADMIN_EMAIL } from './bootstrapAdmin'; import db, { User } from '../src/models'; describe('Bootstrap the first Admin user', () => { @@ -15,12 +15,12 @@ describe('Bootstrap the first Admin user', () => { }); it('should create an admin permission for the user', async () => { - let user = await User.findOne({ where: { email: ADMIN_EMAIL } }); + const user = await User.findOne({ where: { email: ADMIN_EMAIL } }); expect(user).toBeDefined(); expect((await user.getPermissions()).length).toBe(0); const newPermission = await bootstrapAdmin(); expect(newPermission).toBeDefined(); - expect((await user.getPermissions()).length).toBe(1) + expect((await user.getPermissions()).length).toBe(1); await newPermission.destroy(); }); diff --git a/tools/bootstrapAdminCLI.js b/tools/bootstrapAdminCLI.js index f4b3958bea..080395fcfe 100644 --- a/tools/bootstrapAdminCLI.js +++ b/tools/bootstrapAdminCLI.js @@ -1,6 +1,6 @@ import bootstrapAdmin from './bootstrapAdmin'; -/********* +/** * bootstrapAdminCLI is responsible for setting the first ADMIN for TTA Smart Hub. * It should be run via `cf run-task tta-smarthub-prod "yarn db:bootstrap:admin"` * @@ -9,7 +9,8 @@ import bootstrapAdmin from './bootstrapAdmin'; * To change the initial admin (for instance, after the previous one has left the project) * Open a new issue and PR to update the ADMIN_EMAIL constant within bootstrapAdmin.js */ -bootstrapAdmin().catch(e => { +bootstrapAdmin().catch((e) => { + // eslint-disable-next-line no-console console.log(e); return process.exit(1); }); diff --git a/tools/importPlanGoals.js b/tools/importPlanGoals.js index 3621d3585d..bb2aa3f1e1 100644 --- a/tools/importPlanGoals.js +++ b/tools/importPlanGoals.js @@ -3,9 +3,8 @@ import { readFileSync } from 'fs'; import parse from 'csv-parse/lib/sync'; import { - Role, Topic, RoleTopic, Goal, TopicGoal, Grantee, Grant, GrantGoal, + Role, Topic, RoleTopic, Goal, TopicGoal, Grant, GrantGoal, } from '../src/models'; -import { exit } from 'process'; const hubRoles = [ { name: 'RPM', fullName: 'Regional Program Manager' }, @@ -161,6 +160,7 @@ export default async function importGoals(file, region) { const fullGrant = { number: grant.trim(), regionId }; const dbGrant = await Grant.findOne({ where: { ...fullGrant }, attributes: ['id', 'granteeId'] }); if (!dbGrant) { + // eslint-disable-next-line no-console console.log(`Couldn't find grant: ${fullGrant.number}. Exiting...`); process.exit(1); } @@ -190,6 +190,7 @@ export default async function importGoals(file, region) { ignoreDuplicates: true, }); } catch (err) { + // eslint-disable-next-line no-console console.log(err); } } From 8fd1f1bce1b9f3c16babfc5643888303a89a871f Mon Sep 17 00:00:00 2001 From: Ryan Ahearn Date: Wed, 20 Jan 2021 14:55:17 -0500 Subject: [PATCH 4/4] Move tools directory within src --- package.json | 14 ++++++++------ {tools => src/tools}/bootstrapAdmin.js | 6 +++--- {tools => src/tools}/bootstrapAdmin.test.js | 2 +- {tools => src/tools}/bootstrapAdminCLI.js | 0 {tools => src/tools}/importPlanGoals.js | 2 +- {tools => src/tools}/importPlanGoals.test.js | 4 ++-- {tools => src/tools}/importTTAPlanGoals.js | 0 7 files changed, 15 insertions(+), 13 deletions(-) rename {tools => src/tools}/bootstrapAdmin.js (80%) rename {tools => src/tools}/bootstrapAdmin.test.js (96%) rename {tools => src/tools}/bootstrapAdminCLI.js (100%) rename {tools => src/tools}/importPlanGoals.js (99%) rename {tools => src/tools}/importPlanGoals.test.js (99%) rename {tools => src/tools}/importTTAPlanGoals.js (100%) diff --git a/package.json b/package.json index 26155db109..1ec5878da6 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ }, "scripts": { "axe:ci": "axe --exit --load-delay 1000 `cat axe-urls` --dir ./reports/ --chromedriver-path=\"node_modules/chromedriver/bin/chromedriver\"", - "build": "./node_modules/.bin/babel src -d ./build/server && ./node_modules/.bin/babel config -d build/config", + "build": "./node_modules/.bin/babel src -d ./build/server && ./node_modules/.bin/babel config -d build/config", "deps:local": "yarn install && yarn --cwd frontend install", "deps": "yarn install --frozen-lockfile && yarn --cwd frontend install --frozen-lockfile", "start:local": "concurrently \"yarn server\" \"yarn client\"", @@ -17,17 +17,18 @@ "server": "nodemon src/index.js --exec babel-node", "server:debug": "nodemon src/index.js --exec babel-node --inspect", "client": "yarn --cwd frontend start", - "test": "jest src tools", - "test:ci": "cross-env JEST_JUNIT_OUTPUT_DIR=reports JEST_JUNIT_OUTPUT_NAME=unit.xml POSTGRES_USERNAME=postgres POSTGRES_DB=ttasmarthub CURRENT_USER_ID=5 CI=true jest src tools --coverage --reporters=default --reporters=jest-junit", + "test": "jest src", + "test:ci": "cross-env JEST_JUNIT_OUTPUT_DIR=reports JEST_JUNIT_OUTPUT_NAME=unit.xml POSTGRES_USERNAME=postgres POSTGRES_DB=ttasmarthub CURRENT_USER_ID=5 CI=true jest src --coverage --reporters=default --reporters=jest-junit", "test:all": "yarn test:ci && yarn --cwd frontend test:ci", "lint": "eslint src", - "lint:ci": "eslint -f eslint-formatter-multiple src tools", + "lint:ci": "eslint -f eslint-formatter-multiple src", "lint:all": "yarn lint:ci && yarn --cwd frontend lint:ci", "clean": "rm -rf coverage reports frontend/coverage frontend/reports frontend/build", "docs:serve": "npx redoc-cli serve -p 5000 docs/openapi/index.yaml", "cucumber": "./node_modules/.bin/cucumber-js --publish ./cucumber/features/*.feature -f json:./reports/cucumber_report.json && node ./cucumber/index.js", "cucumber:ci": "cross-env TTA_SMART_HUB_URI=http://localhost:3000 yarn cucumber", - "db:bootstrap:admin": "./node_modules/.bin/babel-node ./tools/bootstrapAdminCLI.js", + "db:bootstrap:admin:local": "./node_modules/.bin/babel-node ./src/tools/bootstrapAdminCLI.js", + "db:bootstrap:admin": "node ./build/server/tools/bootstrapAdminCLI.js", "db:migrate": "node_modules/.bin/sequelize db:migrate", "db:migrate:ci": "cross-env POSTGRES_USERNAME=postgres POSTGRES_DB=ttasmarthub node_modules/.bin/sequelize db:migrate", "db:migrate:prod": "node_modules/.bin/sequelize db:migrate --options-path .production.sequelizerc", @@ -50,7 +51,8 @@ "docker:db:migrate:undo": "docker-compose run --rm backend node_modules/.bin/sequelize db:migrate:undo", "docker:db:seed": "docker-compose run --rm backend yarn db:seed", "docker:db:seed:undo": "docker-compose run --rm backend yarn db:seed:undo", - "import:goals": "./node_modules/.bin/babel-node ./tools/importTTAPlanGoals.js" + "import:goals:local": "./node_modules/.bin/babel-node ./src/tools/importTTAPlanGoals.js", + "import:goals": "node ./build/server/tools/importTTAPlanGoals.js" }, "repository": { "type": "git", diff --git a/tools/bootstrapAdmin.js b/src/tools/bootstrapAdmin.js similarity index 80% rename from tools/bootstrapAdmin.js rename to src/tools/bootstrapAdmin.js index 831087d1b1..a6676445a0 100644 --- a/tools/bootstrapAdmin.js +++ b/src/tools/bootstrapAdmin.js @@ -1,6 +1,6 @@ -import { User, Permission } from '../src/models'; -import logger from '../src/logger'; -import SCOPES from '../src/middleware/scopeConstants'; +import { User, Permission } from '../models'; +import logger from '../logger'; +import SCOPES from '../middleware/scopeConstants'; const { ADMIN } = SCOPES; diff --git a/tools/bootstrapAdmin.test.js b/src/tools/bootstrapAdmin.test.js similarity index 96% rename from tools/bootstrapAdmin.test.js rename to src/tools/bootstrapAdmin.test.js index 9d2b1b812a..bd0cad70af 100644 --- a/tools/bootstrapAdmin.test.js +++ b/src/tools/bootstrapAdmin.test.js @@ -1,5 +1,5 @@ import bootstrapAdmin, { ADMIN_EMAIL } from './bootstrapAdmin'; -import db, { User } from '../src/models'; +import db, { User } from '../models'; describe('Bootstrap the first Admin user', () => { afterAll(() => { diff --git a/tools/bootstrapAdminCLI.js b/src/tools/bootstrapAdminCLI.js similarity index 100% rename from tools/bootstrapAdminCLI.js rename to src/tools/bootstrapAdminCLI.js diff --git a/tools/importPlanGoals.js b/src/tools/importPlanGoals.js similarity index 99% rename from tools/importPlanGoals.js rename to src/tools/importPlanGoals.js index bb2aa3f1e1..19ddd62a19 100644 --- a/tools/importPlanGoals.js +++ b/src/tools/importPlanGoals.js @@ -4,7 +4,7 @@ import { readFileSync } from 'fs'; import parse from 'csv-parse/lib/sync'; import { Role, Topic, RoleTopic, Goal, TopicGoal, Grant, GrantGoal, -} from '../src/models'; +} from '../models'; const hubRoles = [ { name: 'RPM', fullName: 'Regional Program Manager' }, diff --git a/tools/importPlanGoals.test.js b/src/tools/importPlanGoals.test.js similarity index 99% rename from tools/importPlanGoals.test.js rename to src/tools/importPlanGoals.test.js index 500b9a9510..3b450ce416 100644 --- a/tools/importPlanGoals.test.js +++ b/src/tools/importPlanGoals.test.js @@ -1,9 +1,9 @@ import { Op } from 'sequelize'; import importGoals from './importPlanGoals'; -import { processFiles } from '../src/lib/updateGrantsGrantees'; +import { processFiles } from '../lib/updateGrantsGrantees'; import db, { Role, Topic, RoleTopic, Goal, Grantee, Grant, -} from '../src/models'; +} from '../models'; describe('Import TTA plan goals', () => { afterAll(() => { diff --git a/tools/importTTAPlanGoals.js b/src/tools/importTTAPlanGoals.js similarity index 100% rename from tools/importTTAPlanGoals.js rename to src/tools/importTTAPlanGoals.js