Browse Source

feat: introduce new env and password validations

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/2335/head
Pranav C 2 years ago
parent
commit
95aca2b666
  1. 98
      packages/nc-gui/pages/user/authentication/passwordValidateMixin.js
  2. 5
      packages/nc-gui/pages/user/authentication/signin.vue
  3. 13
      packages/noco-docs/content/en/getting-started/installation.md
  4. 4
      packages/noco-docs/content/en/setup-and-usages/dashboard.md
  5. 5
      packages/nocodb-sdk/src/index.ts
  6. 41
      packages/nocodb-sdk/src/lib/passwordHelpers.ts
  7. 3
      packages/nocodb/src/lib/Noco.ts
  8. 209
      packages/nocodb/src/lib/meta/api/userApi/initAdminFromEnv.ts
  9. 21
      packages/nocodb/src/lib/meta/api/userApi/userApis.ts
  10. 66
      packages/nocodb/src/lib/meta/helpers/NcPluginMgrv2.ts
  11. 4
      packages/nocodb/src/lib/models/Plugin.ts

98
packages/nc-gui/pages/user/authentication/passwordValidateMixin.js

@ -1,3 +1,5 @@
import { validatePassword } from 'nocodb-sdk'
export default { export default {
data: () => ({ data: () => ({
passwordProgress: 0, passwordProgress: 0,
@ -20,54 +22,62 @@ export default {
return this.formUtil.progressColorValue return this.formUtil.progressColorValue
}, },
PasswordValidate(p) { PasswordValidate(p) {
if (!p) { const { error, progress, valid } = validatePassword(p)
this.passwordProgress = 0 if (valid) { return true }
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 this.formUtil.passwordProgress = progress
// console.log('progress', progress);
// console.log('color', this.progressColor(this.formUtil.passwordProgress));
this.progressColorValue = this.progressColor(this.formUtil.passwordProgress) this.progressColorValue = this.progressColor(this.formUtil.passwordProgress)
this.formUtil.passwordValidateMsg = msg this.formUtil.passwordValidateMsg = error
return error
// console.log('msg', msg, validation); // if (!p) {
// this.passwordProgress = 0
return validation // 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
} }
} }

5
packages/nc-gui/pages/user/authentication/signin.vue

@ -244,10 +244,7 @@ export default {
], ],
password: [ password: [
// Password is required // Password is required
v => !!v || this.$t('msg.error.signUpRules.passwdRequired'), 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')
] ]
}, },
formUtil: { formUtil: {

13
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_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 | 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 | | | 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 <code>$&+,:;=?@#&#124;'.^*()%!_-"</code> ) | | |
### AWS ECS (Fargate) ### AWS ECS (Fargate)

4
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. Enter your work email and your password.
<alert> <alert id="password-conditions">
Your password has at least 8 letters with one uppercase, one number and one special letter Your password has at least 8 letters with one uppercase, one number and one special letter
</alert> </alert>
@ -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. 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) ![image](https://user-images.githubusercontent.com/35857179/163136039-ad521d74-6996-4173-84ba-cfc55392c3b7.png)

5
packages/nocodb-sdk/src/index.ts

@ -5,5 +5,6 @@ export * from './lib/sqlUi';
export * from './lib/globals'; export * from './lib/globals';
export * from './lib/helperFunctions'; export * from './lib/helperFunctions';
export * from './lib/formulaHelpers'; export * from './lib/formulaHelpers';
export {default as UITypes, isVirtualCol} from './lib/UITypes'; export * from './lib/passwordHelpers';
export {default as CustomAPI} from './lib/CustomAPI'; export { default as UITypes, isVirtualCol } from './lib/UITypes';
export { default as CustomAPI } from './lib/CustomAPI';

41
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 };
}

3
packages/nocodb/src/lib/Noco.ts

@ -42,6 +42,7 @@ import { Tele } from 'nc-help';
import * as http from 'http'; import * as http from 'http';
import weAreHiring from './utils/weAreHiring'; import weAreHiring from './utils/weAreHiring';
import getInstance from './utils/getInstance'; import getInstance from './utils/getInstance';
import initAdminFromEnv from './meta/api/userApi/initAdminFromEnv';
const log = debug('nc:app'); const log = debug('nc:app');
require('dotenv').config(); require('dotenv').config();
@ -186,8 +187,8 @@ export default class Noco {
} }
await Noco._ncMeta.metaInit(); await Noco._ncMeta.metaInit();
await this.readOrGenJwtSecret(); await this.readOrGenJwtSecret();
await initAdminFromEnv();
await NcUpgrader.upgrade({ ncMeta: Noco._ncMeta }); await NcUpgrader.upgrade({ ncMeta: Noco._ncMeta });

209
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);
}
}
}

21
packages/nocodb/src/lib/meta/api/userApi/userApis.ts

@ -1,5 +1,5 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { TableType } from 'nocodb-sdk'; import { TableType, validatePassword } from 'nocodb-sdk';
import catchError, { NcError } from '../../helpers/catchError'; import catchError, { NcError } from '../../helpers/catchError';
const { isEmail } = require('validator'); const { isEmail } = require('validator');
import * as ejs from 'ejs'; import * as ejs from 'ejs';
@ -31,6 +31,12 @@ export async function signup(req: Request, res: Response<TableType>) {
} = req.body; } = req.body;
let { password } = 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)) { if (!isEmail(_email)) {
NcError.badRequest(`Invalid email`); NcError.badRequest(`Invalid email`);
} }
@ -262,6 +268,13 @@ async function passwordChange(req: Request<any, any>, res): Promise<any> {
if (!currentPassword || !newPassword) { if (!currentPassword || !newPassword) {
return NcError.badRequest('Missing new/old password'); 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 user = await User.getByEmail((req as any).user.email);
const hashedPassword = await promisify(bcrypt.hash)( const hashedPassword = await promisify(bcrypt.hash)(
currentPassword, currentPassword,
@ -381,6 +394,12 @@ async function passwordReset(req, res): Promise<any> {
NcError.badRequest('Email registered via social account'); 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 salt = await promisify(bcrypt.genSalt)(10);
const password = await promisify(bcrypt.hash)(req.body.password, salt); const password = await promisify(bcrypt.hash)(req.body.password, salt);

66
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 Local from '../../v1-legacy/plugins/adapters/storage/Local';
import { MetaTable } from '../../utils/globals'; import { MetaTable } from '../../utils/globals';
import { PluginCategory } from 'nocodb-sdk'; import { PluginCategory } from 'nocodb-sdk';
import Plugin from '../../models/Plugin';
const defaultPlugins = [ const defaultPlugins = [
SlackPluginConfig, SlackPluginConfig,
@ -97,25 +98,54 @@ class NcPluginMgrv2 {
pluginConfig.id pluginConfig.id
); );
} }
}
await this.initPluginsFromEnv();
}
/* init only the active plugins */ private static async initPluginsFromEnv() {
// if (pluginConfig?.active) { /*
// const tempPlugin = new plugin.builder(this.app, plugin); * NC_S3_BUCKET_NAME
// * NC_S3_REGION
// this.activePlugins.push(tempPlugin); * NC_S3_ACCESS_KEY
// * NC_S3_ACCESS_SECRET
// if (pluginConfig?.input) { * */
// pluginConfig.input = JSON.parse(pluginConfig.input);
// } if (
// process.env.NC_S3_BUCKET_NAME &&
// try { process.env.NC_S3_REGION &&
// await tempPlugin.init(pluginConfig?.input); process.env.NC_S3_ACCESS_KEY &&
// } catch (e) { process.env.NC_S3_ACCESS_SECRET
// console.log( ) {
// `Plugin(${plugin?.title}) initialization failed : ${e.message}` 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
})
});
} }
} }

4
packages/nocodb/src/lib/models/Plugin.ts

@ -91,7 +91,7 @@ export default class Plugin implements PluginType {
/** /**
* get plugin by title * get plugin by title
*/ */
public static async getPluginByTitle(title: string) { public static async getPluginByTitle(title: string, ncMeta = Noco.ncMeta) {
let plugin = let plugin =
title && title &&
(await NocoCache.get( (await NocoCache.get(
@ -99,7 +99,7 @@ export default class Plugin implements PluginType {
CacheGetType.TYPE_OBJECT CacheGetType.TYPE_OBJECT
)); ));
if (!plugin) { if (!plugin) {
plugin = await Noco.ncMeta.metaGet2(null, null, MetaTable.PLUGIN, { plugin = await ncMeta.metaGet2(null, null, MetaTable.PLUGIN, {
title title
}); });
await NocoCache.set(`${CacheScope.PLUGIN}:${title}`, plugin); await NocoCache.set(`${CacheScope.PLUGIN}:${title}`, plugin);

Loading…
Cancel
Save