From fb6d03bc0453eaa998a5042997b7cbc676970db9 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Wed, 15 Jun 2022 23:41:12 +0530 Subject: [PATCH] feat: introduce new env and password validations Signed-off-by: Pranav C --- .../authentication/passwordValidateMixin.js | 98 ++++---- .../pages/user/authentication/signin.vue | 5 +- .../en/getting-started/installation.md | 13 ++ .../content/en/setup-and-usages/dashboard.md | 4 +- packages/nocodb-sdk/src/index.ts | 5 +- .../nocodb-sdk/src/lib/passwordHelpers.ts | 41 ++++ packages/nocodb/src/lib/Noco.ts | 3 +- .../lib/meta/api/userApi/initAdminFromEnv.ts | 209 ++++++++++++++++++ .../src/lib/meta/api/userApi/userApis.ts | 21 +- .../src/lib/meta/helpers/NcPluginMgrv2.ts | 66 ++++-- packages/nocodb/src/lib/models/Plugin.ts | 4 +- 11 files changed, 395 insertions(+), 74 deletions(-) create mode 100644 packages/nocodb-sdk/src/lib/passwordHelpers.ts create mode 100644 packages/nocodb/src/lib/meta/api/userApi/initAdminFromEnv.ts diff --git a/packages/nc-gui/pages/user/authentication/passwordValidateMixin.js b/packages/nc-gui/pages/user/authentication/passwordValidateMixin.js index 71ea5c406c..6422c4920c 100644 --- a/packages/nc-gui/pages/user/authentication/passwordValidateMixin.js +++ b/packages/nc-gui/pages/user/authentication/passwordValidateMixin.js @@ -1,3 +1,5 @@ +import { validatePassword } from 'nocodb-sdk' + export default { data: () => ({ passwordProgress: 0, @@ -20,54 +22,62 @@ export default { return this.formUtil.progressColorValue }, PasswordValidate(p) { - if (!p) { - this.passwordProgress = 0 - this.passwordValidateMsg = 'At least 8 letters with one Uppercase, one number and one special letter' - return false - } - - let msg = '' - let validation = true - let progress = 0 - - if (!(p.length >= 8)) { - msg += 'Atleast 8 letters. ' - validation = validation && false - } else { - progress = Math.min(100, progress + 25) - } - - if (!(p.match(/.*[A-Z].*/))) { - msg += 'One Uppercase Letter. ' - validation = validation && false - } else { - progress = Math.min(100, progress + 25) - } - - if (!(p.match(/.*[0-9].*/))) { - msg += 'One Number. ' - validation = validation && false - } else { - progress = Math.min(100, progress + 25) - } - - if (!(p.match(/[$&+,:;=?@#|'<>.^*()%!_-]/))) { - msg += 'One special letter. ' - validation = validation && false - } else { - progress = Math.min(100, progress + 25) - } + const { error, progress, valid } = validatePassword(p) + if (valid) { return true } this.formUtil.passwordProgress = progress - // console.log('progress', progress); - // console.log('color', this.progressColor(this.formUtil.passwordProgress)); this.progressColorValue = this.progressColor(this.formUtil.passwordProgress) - this.formUtil.passwordValidateMsg = msg - - // console.log('msg', msg, validation); - - return validation + this.formUtil.passwordValidateMsg = error + return error + // if (!p) { + // this.passwordProgress = 0 + // this.passwordValidateMsg = 'At least 8 letters with one Uppercase, one number and one special letter' + // return false + // } + // + // let msg = '' + // let validation = true + // let progress = 0 + // + // if (!(p.length >= 8)) { + // msg += 'Atleast 8 letters. ' + // validation = validation && false + // } else { + // progress = Math.min(100, progress + 25) + // } + // + // if (!(p.match(/.*[A-Z].*/))) { + // msg += 'One Uppercase Letter. ' + // validation = validation && false + // } else { + // progress = Math.min(100, progress + 25) + // } + // + // if (!(p.match(/.*[0-9].*/))) { + // msg += 'One Number. ' + // validation = validation && false + // } else { + // progress = Math.min(100, progress + 25) + // } + // + // if (!(p.match(/[$&+,:;=?@#|'<>.^*()%!_-]/))) { + // msg += 'One special letter. ' + // validation = validation && false + // } else { + // progress = Math.min(100, progress + 25) + // } + // + // this.formUtil.passwordProgress = progress + // // console.log('progress', progress); + // // console.log('color', this.progressColor(this.formUtil.passwordProgress)); + // this.progressColorValue = this.progressColor(this.formUtil.passwordProgress) + // + // this.formUtil.passwordValidateMsg = msg + // + // // console.log('msg', msg, validation); + // + // return validation } } diff --git a/packages/nc-gui/pages/user/authentication/signin.vue b/packages/nc-gui/pages/user/authentication/signin.vue index ee0583675a..fb4688d27a 100644 --- a/packages/nc-gui/pages/user/authentication/signin.vue +++ b/packages/nc-gui/pages/user/authentication/signin.vue @@ -244,10 +244,7 @@ export default { ], password: [ // Password is required - v => !!v || this.$t('msg.error.signUpRules.passwdRequired'), - // You password must be atleast 8 characters - v => - (v && v.length >= 8) || this.$t('msg.error.signUpRules.passwdLength') + v => !!v || this.$t('msg.error.signUpRules.passwdRequired') ] }, formUtil: { diff --git a/packages/noco-docs/content/en/getting-started/installation.md b/packages/noco-docs/content/en/getting-started/installation.md index 9fd3b52f7f..ccdac4ac4f 100644 --- a/packages/noco-docs/content/en/getting-started/installation.md +++ b/packages/noco-docs/content/en/getting-started/installation.md @@ -206,6 +206,19 @@ It is mandatory to configure `NC_DB` environment variables for production usecas | AWS_SECRET_ACCESS_KEY | No | For Litestream - S3 secret access key | If Litestream is configured and NC_DB is not present. SQLite gets backed up to S3 | | | AWS_BUCKET | No | For Litestream - S3 bucket | If Litestream is configured and NC_DB is not present. SQLite gets backed up to S3 | | | AWS_BUCKET_PATH | No | For Litestream - S3 bucket path (like folder within S3 bucket) | If Litestream is configured and NC_DB is not present. SQLite gets backed up to S3 | | +| NC_SMTP_FROM | No | For SMTP plugin - Email sender address | | | +| NC_SMTP_HOST | No | For SMTP plugin - SMTP host value | | | +| NC_SMTP_PORT | No | For SMTP plugin - SMTP port value | | | +| NC_SMTP_USERNAME | No | For SMTP plugin (Optional) - SMTP username value for authentication | | | +| NC_SMTP_PASSWORD | No | For SMTP plugin (Optional) - SMTP password value for authentication | | | +| NC_SMTP_SECURE | No | For SMTP plugin (Optional) - To enable secure set value as `true` any other value treated as false | | | +| NC_SMTP_IGNORE_TLS | No | For SMTP plugin (Optional) - To ignore tls set value as `true` any other value treated as false. For more info visit https://nodemailer.com/smtp/ | | | +| NC_S3_BUCKET_NAME | No | For S3 storage plugin - AWS S3 bucket name | | | +| NC_S3_REGION | No | For S3 storage plugin - AWS S3 region | | | +| NC_S3_ACCESS_KEY | No | For S3 storage plugin - AWS access key credential for accessing resource | | | +| NC_S3_ACCESS_SECRET | No | For S3 storage plugin - AWS access secret credential for accessing resource | | | +| NC_ADMIN_EMAIL | No | For updating/creating super admin with provided email and password | | | +| NC_ADMIN_PASSWORD | No | For updating/creating super admin with provided email and password. Your password should have at least 8 letters with one uppercase, one number and one special letter(Allowed special chars $&+,:;=?@#|'.^*()%!_-" ) | | | ### AWS ECS (Fargate) diff --git a/packages/noco-docs/content/en/setup-and-usages/dashboard.md b/packages/noco-docs/content/en/setup-and-usages/dashboard.md index 34ed955cac..6a77d03419 100644 --- a/packages/noco-docs/content/en/setup-and-usages/dashboard.md +++ b/packages/noco-docs/content/en/setup-and-usages/dashboard.md @@ -16,7 +16,7 @@ Click `Let's Begin` button to sign up. Enter your work email and your password. - + Your password has at least 8 letters with one uppercase, one number and one special letter @@ -98,4 +98,4 @@ Tip 3: You can click Edit Connection JSON and specify the schema you want to use Click `Test Database Connection` to see if the connection can be established or not. NocoDB creates a new **empty database** with specified parameters if the database doesn't exist. -![image](https://user-images.githubusercontent.com/35857179/163136039-ad521d74-6996-4173-84ba-cfc55392c3b7.png) \ No newline at end of file +![image](https://user-images.githubusercontent.com/35857179/163136039-ad521d74-6996-4173-84ba-cfc55392c3b7.png) diff --git a/packages/nocodb-sdk/src/index.ts b/packages/nocodb-sdk/src/index.ts index 2d808fcc03..805152d812 100644 --- a/packages/nocodb-sdk/src/index.ts +++ b/packages/nocodb-sdk/src/index.ts @@ -5,5 +5,6 @@ export * from './lib/sqlUi'; export * from './lib/globals'; export * from './lib/helperFunctions'; export * from './lib/formulaHelpers'; -export {default as UITypes, isVirtualCol} from './lib/UITypes'; -export {default as CustomAPI} from './lib/CustomAPI'; +export * from './lib/passwordHelpers'; +export { default as UITypes, isVirtualCol } from './lib/UITypes'; +export { default as CustomAPI } from './lib/CustomAPI'; diff --git a/packages/nocodb-sdk/src/lib/passwordHelpers.ts b/packages/nocodb-sdk/src/lib/passwordHelpers.ts new file mode 100644 index 0000000000..2136a54809 --- /dev/null +++ b/packages/nocodb-sdk/src/lib/passwordHelpers.ts @@ -0,0 +1,41 @@ +export function validatePassword(p) { + let error = ''; + let progress = 0; + let hint = null; + let valid = true; + if (!p) { + error = + 'At least 8 letters with one Uppercase, one number and one special letter'; + valid = false; + } else { + if (!(p.length >= 8)) { + error += 'Atleast 8 letters. '; + valid = false; + } else { + progress = Math.min(100, progress + 25); + } + + if (!p.match(/.*[A-Z].*/)) { + error += 'One Uppercase Letter. '; + valid = false; + } else { + progress = Math.min(100, progress + 25); + } + + if (!p.match(/.*[0-9].*/)) { + error += 'One Number. '; + valid = false; + } else { + progress = Math.min(100, progress + 25); + } + + if (!p.match(/[$&+,:;=?@#|'<>.^*()%!_-]/)) { + error += 'One special letter. '; + hint = "Allowed special character list : $&+,:;=?@#|'<>.^*()%!_-"; + valid = false; + } else { + progress = Math.min(100, progress + 25); + } + } + return { error, valid, progress, hint }; +} diff --git a/packages/nocodb/src/lib/Noco.ts b/packages/nocodb/src/lib/Noco.ts index 2807cfddb4..c0439bfb75 100644 --- a/packages/nocodb/src/lib/Noco.ts +++ b/packages/nocodb/src/lib/Noco.ts @@ -42,6 +42,7 @@ import { Tele } from 'nc-help'; import * as http from 'http'; import weAreHiring from './utils/weAreHiring'; import getInstance from './utils/getInstance'; +import initAdminFromEnv from './meta/api/userApi/initAdminFromEnv'; const log = debug('nc:app'); require('dotenv').config(); @@ -186,8 +187,8 @@ export default class Noco { } await Noco._ncMeta.metaInit(); - await this.readOrGenJwtSecret(); + await initAdminFromEnv(); await NcUpgrader.upgrade({ ncMeta: Noco._ncMeta }); diff --git a/packages/nocodb/src/lib/meta/api/userApi/initAdminFromEnv.ts b/packages/nocodb/src/lib/meta/api/userApi/initAdminFromEnv.ts new file mode 100644 index 0000000000..81885ed12d --- /dev/null +++ b/packages/nocodb/src/lib/meta/api/userApi/initAdminFromEnv.ts @@ -0,0 +1,209 @@ +import User from '../../../models/User'; +import { v4 as uuidv4 } from 'uuid'; +import { promisify } from 'util'; +import { Tele } from 'nc-help'; + +import bcrypt from 'bcryptjs'; +import Noco from '../../../Noco'; +import { MetaTable } from '../../../utils/globals'; +import ProjectUser from '../../../models/ProjectUser'; +import { validatePassword } from 'nocodb-sdk'; +import boxen from 'boxen'; + +const { isEmail } = require('validator'); +const rolesLevel = { owner: 0, creator: 1, editor: 2, commenter: 3, viewer: 4 }; + +export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) { + if (process.env.NC_ADMIN_EMAIL && process.env.NC_ADMIN_PASSWORD) { + if (!isEmail(process.env.NC_ADMIN_EMAIL?.trim())) { + console.log( + '\n', + boxen( + `Provided admin email '${process.env.NC_ADMIN_EMAIL}' is not valid`, + { + title: 'Invalid admin email', + padding: 1, + borderStyle: 'double', + titleAlignment: 'center', + borderColor: 'red' + } + ), + '\n' + ); + process.exit(1); + } + + const { valid, error, hint } = validatePassword( + process.env.NC_ADMIN_PASSWORD + ); + if (!valid) { + console.log( + '\n', + boxen(`${error}${hint ? `\n\n${hint}` : ''}`, { + title: 'Invalid admin password', + padding: 1, + borderStyle: 'double', + titleAlignment: 'center', + borderColor: 'red' + }), + '\n' + ); + process.exit(1); + } + + let ncMeta; + try { + ncMeta = await _ncMeta.startTransaction(); + const email = process.env.NC_ADMIN_EMAIL.toLowerCase().trim(); + + const salt = await promisify(bcrypt.genSalt)(10); + const password = await promisify(bcrypt.hash)( + process.env.NC_ADMIN_PASSWORD, + salt + ); + const email_verification_token = uuidv4(); + + // if super admin not present + if (await User.isFirst(ncMeta)) { + const roles = 'user,super'; + + // roles = 'owner,creator,editor' + Tele.emit('evt', { + evt_type: 'project:invite', + count: 1 + }); + + await User.insert( + { + firstname: '', + lastname: '', + email, + salt, + password, + email_verification_token, + roles + }, + ncMeta + ); + } else { + const salt = await promisify(bcrypt.genSalt)(10); + const password = await promisify(bcrypt.hash)( + process.env.NC_ADMIN_PASSWORD, + salt + ); + const email_verification_token = uuidv4(); + const superUser = await ncMeta.metaGet2(null, null, MetaTable.USERS, { + roles: 'user,super' + }); + + if (email !== superUser.email) { + // update admin email and password and migrate projects + // if user already present and associated with some project + + // check user account already present with the new admin email + const existingUserWithNewEmail = await User.getByEmail(email, ncMeta); + + if (existingUserWithNewEmail) { + // get all project access belongs to the existing account + // and migrate to the admin account + const existingUserProjects = await ncMeta.metaList2( + null, + null, + MetaTable.PROJECT_USERS, + { + condition: { fk_user_id: existingUserWithNewEmail.id } + } + ); + + for (const existingUserProject of existingUserProjects) { + const userProject = await ProjectUser.get( + existingUserProject.project_id, + superUser.id, + ncMeta + ); + + // if admin user already have access to the project + // then update role based on the highest access level + if (userProject) { + if ( + rolesLevel[userProject.roles] > + rolesLevel[existingUserProject.roles] + ) { + await ProjectUser.update( + userProject.project_id, + superUser.id, + existingUserProject.roles, + ncMeta + ); + } + } else { + // if super doesn't have access then add the access + await ProjectUser.insert( + { + ...existingUserProject, + fk_user_id: superUser.id + }, + ncMeta + ); + } + // delete the old project access entry from DB + await ProjectUser.delete( + existingUserProject.project_id, + existingUserProject.fk_user_id, + ncMeta + ); + } + + // delete existing user + ncMeta.metaDelete( + null, + null, + MetaTable.USERS, + existingUserWithNewEmail.id + ); + + // Update email and password of super admin account + await User.update( + superUser.id, + { + salt, + email, + password, + email_verification_token + }, + ncMeta + ); + } else { + // if email's are not different update the password and hash + await User.update( + superUser.id, + { + salt, + email, + password, + email_verification_token + }, + ncMeta + ); + } + } else { + // if email's are not different update the password and hash + await User.update( + superUser.id, + { + salt, + password, + email_verification_token + }, + ncMeta + ); + } + } + await ncMeta.commit(); + } catch (e) { + console.log('Error occurred while updating/creating admin user'); + console.log(e); + await ncMeta.rollback(e); + } + } +} diff --git a/packages/nocodb/src/lib/meta/api/userApi/userApis.ts b/packages/nocodb/src/lib/meta/api/userApi/userApis.ts index 7a6fa31a3d..18243d9455 100644 --- a/packages/nocodb/src/lib/meta/api/userApi/userApis.ts +++ b/packages/nocodb/src/lib/meta/api/userApi/userApis.ts @@ -1,5 +1,5 @@ import { Request, Response } from 'express'; -import { TableType } from 'nocodb-sdk'; +import { TableType, validatePassword } from 'nocodb-sdk'; import catchError, { NcError } from '../../helpers/catchError'; const { isEmail } = require('validator'); import * as ejs from 'ejs'; @@ -31,6 +31,12 @@ export async function signup(req: Request, res: Response) { } = req.body; let { password } = req.body; + // validate password and throw error if password is satisfying the conditions + const { valid, error } = validatePassword(password); + if (!valid) { + NcError.badRequest(`Password : ${error}`); + } + if (!isEmail(_email)) { NcError.badRequest(`Invalid email`); } @@ -262,6 +268,13 @@ async function passwordChange(req: Request, res): Promise { if (!currentPassword || !newPassword) { return NcError.badRequest('Missing new/old password'); } + + // validate password and throw error if password is satisfying the conditions + const { valid, error } = validatePassword(newPassword); + if (!valid) { + NcError.badRequest(`Password : ${error}`); + } + const user = await User.getByEmail((req as any).user.email); const hashedPassword = await promisify(bcrypt.hash)( currentPassword, @@ -381,6 +394,12 @@ async function passwordReset(req, res): Promise { NcError.badRequest('Email registered via social account'); } + // validate password and throw error if password is satisfying the conditions + const { valid, error } = validatePassword(req.body.password); + if (!valid) { + NcError.badRequest(`Password : ${error}`); + } + const salt = await promisify(bcrypt.genSalt)(10); const password = await promisify(bcrypt.hash)(req.body.password, salt); diff --git a/packages/nocodb/src/lib/meta/helpers/NcPluginMgrv2.ts b/packages/nocodb/src/lib/meta/helpers/NcPluginMgrv2.ts index 0e5057887d..4f04838001 100644 --- a/packages/nocodb/src/lib/meta/helpers/NcPluginMgrv2.ts +++ b/packages/nocodb/src/lib/meta/helpers/NcPluginMgrv2.ts @@ -31,6 +31,7 @@ import Noco from '../../Noco'; import Local from '../../v1-legacy/plugins/adapters/storage/Local'; import { MetaTable } from '../../utils/globals'; import { PluginCategory } from 'nocodb-sdk'; +import Plugin from '../../models/Plugin'; const defaultPlugins = [ SlackPluginConfig, @@ -97,25 +98,54 @@ class NcPluginMgrv2 { pluginConfig.id ); } + } + await this.initPluginsFromEnv(); + } - /* init only the active plugins */ - // if (pluginConfig?.active) { - // const tempPlugin = new plugin.builder(this.app, plugin); - // - // this.activePlugins.push(tempPlugin); - // - // if (pluginConfig?.input) { - // pluginConfig.input = JSON.parse(pluginConfig.input); - // } - // - // try { - // await tempPlugin.init(pluginConfig?.input); - // } catch (e) { - // console.log( - // `Plugin(${plugin?.title}) initialization failed : ${e.message}` - // ); - // } - // } + private static async initPluginsFromEnv() { + /* + * NC_S3_BUCKET_NAME + * NC_S3_REGION + * NC_S3_ACCESS_KEY + * NC_S3_ACCESS_SECRET + * */ + + if ( + process.env.NC_S3_BUCKET_NAME && + process.env.NC_S3_REGION && + process.env.NC_S3_ACCESS_KEY && + process.env.NC_S3_ACCESS_SECRET + ) { + const s3Plugin = await Plugin.getPluginByTitle(S3PluginConfig.title); + await Plugin.update(s3Plugin.id, { + active: true, + input: JSON.stringify({ + bucket: process.env.NC_S3_BUCKET_NAME, + region: process.env.NC_S3_REGION, + access_key: process.env.NC_S3_ACCESS_KEY, + access_secret: process.env.NC_S3_ACCESS_SECRET + }) + }); + } + + if ( + process.env.NC_SMTP_FROM && + process.env.NC_SMTP_HOST && + process.env.NC_SMTP_PORT + ) { + const smtpPlugin = await Plugin.getPluginByTitle(SMTPPluginConfig.title); + await Plugin.update(smtpPlugin.id, { + active: true, + input: JSON.stringify({ + from: process.env.NC_SMTP_FROM, + host: process.env.NC_SMTP_HOST, + port: process.env.NC_SMTP_PORT, + username: process.env.NC_SMTP_USERNAME, + password: process.env.NC_SMTP_PASSWORD, + secure: process.env.NC_SMTP_SECURE, + ignoreTLS: process.env.NC_SMTP_IGNORE_TLS + }) + }); } } diff --git a/packages/nocodb/src/lib/models/Plugin.ts b/packages/nocodb/src/lib/models/Plugin.ts index ee08013651..bd952e4af4 100644 --- a/packages/nocodb/src/lib/models/Plugin.ts +++ b/packages/nocodb/src/lib/models/Plugin.ts @@ -91,7 +91,7 @@ export default class Plugin implements PluginType { /** * get plugin by title */ - public static async getPluginByTitle(title: string) { + public static async getPluginByTitle(title: string, ncMeta = Noco.ncMeta) { let plugin = title && (await NocoCache.get( @@ -99,7 +99,7 @@ export default class Plugin implements PluginType { CacheGetType.TYPE_OBJECT )); if (!plugin) { - plugin = await Noco.ncMeta.metaGet2(null, null, MetaTable.PLUGIN, { + plugin = await ncMeta.metaGet2(null, null, MetaTable.PLUGIN, { title }); await NocoCache.set(`${CacheScope.PLUGIN}:${title}`, plugin);