diff --git a/README.md b/README.md index 766f5b1..a6a9e6a 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,19 @@ MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY = default: 100; # Once a successful login is attempted, it resets MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP = default: 10; +# Name of the admin user that will be created on startup if not exists already +# Default is `secretuser` +ADMIN_USERNAME=secretuser + +# Temporary password for the ADMIN_USERNAME, which is in place until the first login +# Default is `secretpassword` +ADMIN_PASSWORD_INITIAL=secretpassword + +# Specify whether app has to reset the ADMIN_USERNAME's password or not +# Default is NO. Possible options are YES and NO +# If ADMIN_PASSWORD_RESET is YES then the ADMIN_USERNAME will be prompted to change the password from ADMIN_PASSWORD_INITIAL on their next login. This will repeat on every server restart, unless the option is removed / set to NO. +ADMIN_PASSWORD_RESET=NO + # LOG_FORMAT_MORGAN options: [combined|common|dev|short|tiny] default: `common` # Docs: https://www.npmjs.com/package/morgan#predefined-formats LOG_FORMAT_MORGAN= diff --git a/api/.env.example b/api/.env.example index 750f5d8..d59f43e 100644 --- a/api/.env.example +++ b/api/.env.example @@ -30,6 +30,10 @@ MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY=100 #default value is 10 MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP=10 +ADMIN_USERNAME=secretuser +ADMIN_PASSWORD_INITIAL=secretpassword +ADMIN_PASSWORD_RESET=NO + RUN_TIMES=[sas,js,py | js,py | sas | sas,js] default considered as sas SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node diff --git a/api/src/utils/seedDB.ts b/api/src/utils/seedDB.ts index c4fc6ad..6a37b79 100644 --- a/api/src/utils/seedDB.ts +++ b/api/src/utils/seedDB.ts @@ -1,7 +1,9 @@ +import bcrypt from 'bcryptjs' import Client from '../model/Client' import Group, { PUBLIC_GROUP_NAME } from '../model/Group' -import User from '../model/User' +import User, { IUser } from '../model/User' import Configuration, { ConfigurationType } from '../model/Configuration' +import { ResetAdminPasswordType } from './verifyEnvVariables' import { randomBytes } from 'crypto' @@ -40,9 +42,13 @@ export const seedDB = async (): Promise => { process.logger.success(`DB Seed - Group created: ${PUBLIC_GROUP.name}`) } + const ADMIN_USER = getAdminUser() + // Checking if user is already in the database let usernameExist = await User.findOne({ username: ADMIN_USER.username }) - if (!usernameExist) { + if (usernameExist) { + usernameExist = await resetAdminPassword(usernameExist, ADMIN_USER.password) + } else { const user = new User(ADMIN_USER) usernameExist = await user.save() @@ -51,7 +57,7 @@ export const seedDB = async (): Promise => { ) } - if (!groupExist.hasUser(usernameExist)) { + if (usernameExist.isAdmin && !groupExist.hasUser(usernameExist)) { groupExist.addUser(usernameExist) process.logger.success( `DB Seed - admin account '${ADMIN_USER.username}' added to Group '${ALL_USERS_GROUP.name}'` @@ -90,11 +96,52 @@ const CLIENT = { clientId: 'clientID1', clientSecret: 'clientSecret' } -const ADMIN_USER = { - id: 1, - displayName: 'Super Admin', - username: 'secretuser', - password: '$2a$10$hKvcVEZdhEQZCcxt6npazO6mY4jJkrzWvfQ5stdBZi8VTTwVMCVXO', - isAdmin: true, - isActive: true + +const getAdminUser = () => { + const { ADMIN_USERNAME, ADMIN_PASSWORD_INITIAL } = process.env + + const salt = bcrypt.genSaltSync(10) + const hashedPassword = bcrypt.hashSync(ADMIN_PASSWORD_INITIAL as string, salt) + + return { + displayName: 'Super Admin', + username: ADMIN_USERNAME, + password: hashedPassword, + isAdmin: true, + isActive: true + } +} + +const resetAdminPassword = async (user: IUser, password: string) => { + const { ADMIN_PASSWORD_RESET } = process.env + + if (ADMIN_PASSWORD_RESET === ResetAdminPasswordType.YES) { + if (!user.isAdmin) { + process.logger.error( + `Can not reset the password of non-admin user (${user.username}) on startup.` + ) + + return user + } + + if (user.authProvider) { + process.logger.error( + `Can not reset the password of admin (${user.username}) with ${user.authProvider} as authentication mechanism.` + ) + + return user + } + + process.logger.info( + `DB Seed - resetting password for admin user: ${user.username}` + ) + + user.password = password + user.needsToUpdatePassword = true + user = await user.save() + + process.logger.success(`DB Seed - successfully reset the password`) + } + + return user } diff --git a/api/src/utils/verifyEnvVariables.ts b/api/src/utils/verifyEnvVariables.ts index d6ec69b..9c551b2 100644 --- a/api/src/utils/verifyEnvVariables.ts +++ b/api/src/utils/verifyEnvVariables.ts @@ -52,6 +52,11 @@ export enum DatabaseType { COSMOS_MONGODB = 'cosmos_mongodb' } +export enum ResetAdminPasswordType { + YES = 'YES', + NO = 'NO' +} + export const verifyEnvVariables = (): ReturnCode => { const errors: string[] = [] @@ -79,6 +84,8 @@ export const verifyEnvVariables = (): ReturnCode => { errors.push(...verifyRateLimiter()) + errors.push(...verifyAdminUserConfig()) + if (errors.length) { process.logger?.error( `Invalid environment variable(s) provided: \n${errors.join('\n')}` @@ -409,6 +416,38 @@ const verifyRateLimiter = () => { return errors } +const verifyAdminUserConfig = () => { + const errors: string[] = [] + const { MODE, ADMIN_USERNAME, ADMIN_PASSWORD_INITIAL, ADMIN_PASSWORD_RESET } = + process.env + if (MODE === ModeType.Server) { + if (ADMIN_USERNAME) { + process.env.ADMIN_USERNAME = ADMIN_USERNAME.toLowerCase() + } else { + process.env.ADMIN_USERNAME = DEFAULTS.ADMIN_USERNAME + } + + if (!ADMIN_PASSWORD_INITIAL) + process.env.ADMIN_PASSWORD_INITIAL = DEFAULTS.ADMIN_PASSWORD_INITIAL + + if (ADMIN_PASSWORD_RESET) { + const resetPasswordTypes = Object.values(ResetAdminPasswordType) + if ( + !resetPasswordTypes.includes( + ADMIN_PASSWORD_RESET as ResetAdminPasswordType + ) + ) + errors.push( + `- ADMIN_PASSWORD_RESET '${ADMIN_PASSWORD_RESET}'\n - valid options ${resetPasswordTypes}` + ) + } else { + process.env.ADMIN_PASSWORD_RESET = DEFAULTS.ADMIN_PASSWORD_RESET + } + } + + return errors +} + const isNumeric = (val: string): boolean => { return !isNaN(Number(val)) } @@ -422,5 +461,8 @@ const DEFAULTS = { RUN_TIMES: RunTimeType.SAS, DB_TYPE: DatabaseType.MONGO, MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY: '100', - MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP: '10' + MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP: '10', + ADMIN_USERNAME: 'secretuser', + ADMIN_PASSWORD_INITIAL: 'secretpassword', + ADMIN_PASSWORD_RESET: ResetAdminPasswordType.NO }