Browse Source

feat: add middlewares ( interceptors ) - WIP

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/5444/head
Pranav C 2 years ago
parent
commit
ccee5400cb
  1. BIN
      packages/nocodb-nest/noco.db
  2. 2
      packages/nocodb-nest/src/Noco.ts
  3. 3
      packages/nocodb-nest/src/app.module.ts
  4. 2
      packages/nocodb-nest/src/auth/auth.module.ts
  5. 4
      packages/nocodb-nest/src/auth/auth.service.ts
  6. 2
      packages/nocodb-nest/src/cache/RedisMockCacheMgr.ts
  7. 13
      packages/nocodb-nest/src/connection/connection.ts
  8. 2
      packages/nocodb-nest/src/db/util/emit.ts
  9. 1
      packages/nocodb-nest/src/helpers/extractProjectIdAndAuthenticate.ts
  10. 2
      packages/nocodb-nest/src/main.ts
  11. 1
      packages/nocodb-nest/src/meta/meta.service.ts
  12. 471
      packages/nocodb-nest/src/middlewares/catchError.ts
  13. 306
      packages/nocodb-nest/src/middlewares/extract-project-id/extract-project-id.middleware.ts
  14. 152
      packages/nocodb-nest/src/middlewares/extractProjectIdAndAuthenticate.ts
  15. 96
      packages/nocodb-nest/src/middlewares/ncMetaAclMw.ts
  16. 6
      packages/nocodb-nest/src/models/Base.ts
  17. 2
      packages/nocodb-nest/src/nocobuild.ts
  18. 17
      packages/nocodb-nest/src/projects/projects.controller.ts
  19. 3
      packages/nocodb-nest/src/projects/projects.module.ts
  20. 2
      packages/nocodb-nest/src/projects/projects.service.ts
  21. 7
      packages/nocodb-nest/src/strategies/jwt.strategy.ts
  22. 10
      packages/nocodb-nest/src/users/users.controller.ts
  23. 10
      packages/nocodb-nest/src/utils/NcConfigFactory.ts
  24. 3
      packages/nocodb-nest/tsconfig.json

BIN
packages/nocodb-nest/noco.db

Binary file not shown.

2
packages/nocodb-nest/src/Noco.ts

@ -39,7 +39,7 @@ export default class Noco {
return Noco._this.init(args, server, app);
}*/
private static config: any;
public static config: any;
public readonly router: express.Router;
public readonly projectRouter: express.Router;
public static _ncMeta: any;

3
packages/nocodb-nest/src/app.module.ts

@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { Connection } from './connection/connection';
import { AuthModule } from './auth/auth.module';
import { ExtractProjectIdMiddleware } from './middlewares/extract-project-id/extract-project-id.middleware'
import { UsersModule } from './users/users.module';
import { MetaService } from './meta/meta.service';
import { LocalStrategy } from './strategies/local.strategy';
@ -11,7 +12,7 @@ import { JwtStrategy } from './strategies/jwt.strategy';
@Module({
imports: [AuthModule, UsersModule, UtilsModule, ProjectsModule],
controllers: [],
providers: [Connection, MetaService, JwtStrategy],
providers: [Connection, MetaService, JwtStrategy, ExtractProjectIdMiddleware],
exports: [Connection, MetaService],
})
export class AppModule {}

2
packages/nocodb-nest/src/auth/auth.module.ts

@ -15,7 +15,7 @@ import { jwtConstants } from './constants';
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
signOptions: { expiresIn: '10h' },
}),
],
providers: [AuthService, LocalStrategy],

4
packages/nocodb-nest/src/auth/auth.service.ts

@ -31,7 +31,9 @@ export class AuthService {
}
async login(user: any) {
const payload = { username: user.username, sub: user.userId };
delete user.password;
delete user.salt;
const payload = user;
return {
token: this.jwtService.sign(payload),
};

2
packages/nocodb-nest/src/cache/RedisMockCacheMgr.ts vendored

@ -1,5 +1,5 @@
import debug from 'debug';
import * as Redis from 'ioredis-mock';
import Redis from 'ioredis-mock';
import { CacheDelDirection, CacheGetType, CacheScope } from '../utils/globals';
import CacheMgr from './CacheMgr';
const log = debug('nc:cache');

13
packages/nocodb-nest/src/connection/connection.ts

@ -7,15 +7,22 @@ import NcConfigFactory from '../utils/NcConfigFactory';
@Injectable()
export class Connection implements OnModuleInit {
private knex: knex.Knex;
private dbConfig: any;
private _config: any;
get knexInstance(): knex.Knex {
return this.knex;
}
get config(): knex.Knex {
return this._config;
}
// init metadb connection
async onModuleInit(): Promise<void> {
this.dbConfig = await NcConfigFactory.make();
this.knex = knex.default({ ...this.dbConfig.meta.db, useNullAsDefault: true });
this._config = await NcConfigFactory.make();
this.knex = knex.default({
...this._config.meta.db,
useNullAsDefault: true,
});
}
}

2
packages/nocodb-nest/src/db/util/emit.ts

@ -1,4 +1,4 @@
import * as Emittery from 'emittery';
import Emittery from 'emittery';
let emitSingleton = null;

1
packages/nocodb-nest/src/helpers/extractProjectIdAndAuthenticate.ts

@ -1,3 +1,4 @@
import { promisify } from 'util';
import passport from 'passport';

2
packages/nocodb-nest/src/main.ts

@ -1,6 +1,6 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as cors from 'cors';
import cors from 'cors';
async function bootstrap() {
const app = await NestFactory.create(AppModule);

1
packages/nocodb-nest/src/meta/meta.service.ts

@ -352,6 +352,7 @@ export class MetaService implements OnApplicationBootstrap {
// todo: tobe fixed - temporary workaround
Noco._ncMeta = this;
Noco.config = this.metaConnection.config;
}

471
packages/nocodb-nest/src/middlewares/catchError.ts

@ -0,0 +1,471 @@
import type { ErrorObject } from 'ajv';
enum DBError {
TABLE_EXIST = 'TABLE_EXIST',
TABLE_NOT_EXIST = 'TABLE_NOT_EXIST',
COLUMN_EXIST = 'COLUMN_EXIST',
COLUMN_NOT_EXIST = 'COLUMN_NOT_EXIST',
CONSTRAINT_EXIST = 'CONSTRAINT_EXIST',
CONSTRAINT_NOT_EXIST = 'CONSTRAINT_NOT_EXIST',
COLUMN_NOT_NULL = 'COLUMN_NOT_NULL',
}
// extract db errors using database error code
function extractDBError(error): {
type: DBError;
message: string;
info: any;
extra?: Record<string, any>;
} | void {
if (!error.code) return;
let message: string;
let extra: Record<string, any>;
let type: DBError;
// todo: handle not null constraint error for all databases
switch (error.code) {
// sqlite errors
case 'SQLITE_BUSY':
message = 'The database is locked by another process or transaction.';
break;
case 'SQLITE_CONSTRAINT':
{
const constraint = /FOREIGN KEY|UNIQUE/.test(error.message)
? error.message.match(/FOREIGN KEY|UNIQUE/gi)?.join(' ')
: 'constraint';
message = `A ${constraint} constraint was violated: ${error.message}`;
extra = {
constraint,
};
}
break;
case 'SQLITE_CORRUPT':
message = 'The database file is corrupt.';
break;
case 'SQLITE_ERROR':
message = 'A SQL error occurred.';
if (error.message) {
const noSuchTableMatch = error.message.match(/no such table: (\w+)/);
const tableAlreadyExistsMatch = error.message.match(
/SQLITE_ERROR: table `?(\w+)`? already exists/
);
const duplicateColumnExistsMatch = error.message.match(
/SQLITE_ERROR: duplicate column name: (\w+)/
);
const unrecognizedTokenMatch = error.message.match(
/SQLITE_ERROR: unrecognized token: "(\w+)"/
);
const columnDoesNotExistMatch = error.message.match(
/SQLITE_ERROR: no such column: (\w+)/
);
const constraintFailedMatch = error.message.match(
/SQLITE_ERROR: constraint failed: (\w+)/
);
if (noSuchTableMatch && noSuchTableMatch[1]) {
message = `The table '${noSuchTableMatch[1]}' does not exist.`;
type = DBError.TABLE_NOT_EXIST;
extra = {
table: noSuchTableMatch[1],
};
} else if (tableAlreadyExistsMatch && tableAlreadyExistsMatch[1]) {
message = `The table '${tableAlreadyExistsMatch[1]}' already exists.`;
type = DBError.TABLE_EXIST;
extra = {
table: tableAlreadyExistsMatch[1],
};
} else if (unrecognizedTokenMatch && unrecognizedTokenMatch[1]) {
message = `Unrecognized token: ${unrecognizedTokenMatch[1]}`;
extra = {
token: unrecognizedTokenMatch[1],
};
} else if (columnDoesNotExistMatch && columnDoesNotExistMatch[1]) {
message = `The column ${columnDoesNotExistMatch[1]} does not exist.`;
type = DBError.COLUMN_NOT_EXIST;
extra = {
column: columnDoesNotExistMatch[1],
};
} else if (constraintFailedMatch && constraintFailedMatch[1]) {
message = `A constraint failed: ${constraintFailedMatch[1]}`;
} else if (
duplicateColumnExistsMatch &&
duplicateColumnExistsMatch[1]
) {
message = `The column '${duplicateColumnExistsMatch[1]}' already exists.`;
type = DBError.COLUMN_EXIST;
extra = {
column: duplicateColumnExistsMatch[1],
};
} else {
const match = error.message.match(/SQLITE_ERROR:\s*(\w+)/);
if (match && match[1]) {
message = match[1];
}
}
}
break;
case 'SQLITE_RANGE':
message = 'A column index is out of range.';
break;
case 'SQLITE_SCHEMA':
message = 'The database schema has changed.';
break;
// mysql errors
case 'ER_TABLE_EXISTS_ERROR':
message = 'The table already exists.';
if (error.message) {
const extractTableNameMatch = error.message.match(
/ Table '?(\w+)'? already exists/i
);
if (extractTableNameMatch && extractTableNameMatch[1]) {
message = `The table '${extractTableNameMatch[1]}' already exists.`;
type = DBError.TABLE_EXIST;
extra = {
table: extractTableNameMatch[1],
};
}
}
break;
case 'ER_DUP_FIELDNAME':
message = 'The column already exists.';
if (error.message) {
const extractColumnNameMatch = error.message.match(
/ Duplicate column name '(\w+)'/i
);
if (extractColumnNameMatch && extractColumnNameMatch[1]) {
message = `The column '${extractColumnNameMatch[1]}' already exists.`;
type = DBError.COLUMN_EXIST;
extra = {
column: extractColumnNameMatch[1],
};
}
}
break;
case 'ER_NO_SUCH_TABLE':
message = 'The table does not exist.';
if (error.message) {
const missingTableMatch = error.message.match(
/ Table '(?:\w+\.)?(\w+)' doesn't exist/i
);
if (missingTableMatch && missingTableMatch[1]) {
message = `The table '${missingTableMatch[1]}' does not exist`;
type = DBError.TABLE_NOT_EXIST;
extra = {
table: missingTableMatch[1],
};
}
}
break;
case 'ER_DUP_ENTRY':
message = 'This record already exists.';
break;
case 'ER_PARSE_ERROR':
message = 'There was a syntax error in your SQL query.';
break;
case 'ER_NO_DEFAULT_FOR_FIELD':
message = 'A value is required for this field.';
break;
case 'ER_BAD_NULL_ERROR':
message = 'A null value is not allowed for this field.';
{
const extractColNameMatch = error.message.match(
/Column '(\w+)' cannot be null/i
);
if (extractColNameMatch && extractColNameMatch[1]) {
message = `The column '${extractColNameMatch[1]}' cannot be null.`;
type = DBError.COLUMN_NOT_NULL;
extra = {
column: extractColNameMatch[1],
};
}
}
break;
case 'ER_DATA_TOO_LONG':
message = 'The data entered is too long for this field.';
break;
case 'ER_BAD_FIELD_ERROR':
{
message = 'The field you are trying to access does not exist.';
const extractColNameMatch = error.message.match(
/ Unknown column '(\w+)' in 'field list'/i
);
if (extractColNameMatch && extractColNameMatch[1]) {
message = `The column '${extractColNameMatch[1]}' does not exist.`;
type = DBError.COLUMN_NOT_EXIST;
extra = {
column: extractColNameMatch[1],
};
}
}
break;
case 'ER_ACCESS_DENIED_ERROR':
message = 'You do not have permission to perform this action.';
break;
case 'ER_LOCK_WAIT_TIMEOUT':
message = 'A timeout occurred while waiting for a table lock.';
break;
case 'ER_NO_REFERENCED_ROW':
message = 'The referenced row does not exist.';
break;
case 'ER_ROW_IS_REFERENCED':
message = 'This record is being referenced by other records.';
break;
// postgres errors
case '23505':
message = 'This record already exists.';
break;
case '42601':
message = 'There was a syntax error in your SQL query.';
break;
case '23502':
message = 'A value is required for this field.';
break;
case '23503':
message = 'The referenced row does not exist.';
break;
case '23514':
message = 'A null value is not allowed for this field.';
break;
case '22001':
message = 'The data entered is too long for this field.';
break;
case '28000':
message = 'You do not have permission to perform this action.';
break;
case '40P01':
message = 'A timeout occurred while waiting for a table lock.';
break;
case '23506':
message = 'This record is being referenced by other records.';
break;
case '42P07':
message = 'The table already exists.';
if (error.message) {
const extractTableNameMatch = error.message.match(
/ relation "?(\w+)"? already exists/i
);
if (extractTableNameMatch && extractTableNameMatch[1]) {
message = `The table '${extractTableNameMatch[1]}' already exists.`;
type = DBError.TABLE_EXIST;
extra = {
table: extractTableNameMatch[1],
};
}
}
break;
case '42701':
message = 'The column already exists.';
if (error.message) {
const extractTableNameMatch = error.message.match(
/ column "(\w+)" of relation "(\w+)" already exists/i
);
if (extractTableNameMatch && extractTableNameMatch[1]) {
message = `The column '${extractTableNameMatch[1]}' already exists.`;
type = DBError.COLUMN_EXIST;
extra = {
column: extractTableNameMatch[1],
};
}
}
break;
case '42P01':
message = 'The table does not exist.';
if (error.message) {
const extractTableNameMatch = error.message.match(
/ relation "(\w+)" does not exist/i
);
if (extractTableNameMatch && extractTableNameMatch[1]) {
message = `The table '${extractTableNameMatch[1]}' does not exist.`;
type = DBError.TABLE_NOT_EXIST;
extra = {
table: extractTableNameMatch[1],
};
}
}
break;
case '42703':
message = 'The column does not exist.';
if (error.message) {
const extractTableNameMatch = error.message.match(
/ column "(\w+)" does not exist/i
);
if (extractTableNameMatch && extractTableNameMatch[1]) {
message = `The column '${extractTableNameMatch[1]}' does not exist.`;
type = DBError.COLUMN_NOT_EXIST;
extra = {
column: extractTableNameMatch[1],
};
}
}
break;
// mssql errors
case 'EREQUEST':
message = 'There was a syntax error in your SQL query.';
if (error.message) {
const extractTableNameMatch = error.message.match(
/ There is already an object named '(\w+)' in the database/i
);
const extractDupColMatch = error.message.match(
/ Column name '(\w+)' in table '(\w+)' is specified more than once/i
);
const extractMissingTableMatch = error.message.match(
/ Invalid object name '(\w+)'./i
);
const extractMissingColMatch = error.message.match(
/ Invalid column name '(\w+)'./i
);
if (extractTableNameMatch && extractTableNameMatch[1]) {
message = `The table '${extractTableNameMatch[1]}' already exists.`;
type = DBError.TABLE_EXIST;
extra = {
table: extractTableNameMatch[1],
};
} else if (extractDupColMatch && extractDupColMatch[1]) {
message = `The column '${extractDupColMatch[1]}' already exists.`;
type = DBError.COLUMN_EXIST;
extra = {
column: extractDupColMatch[1],
};
} else if (extractMissingTableMatch && extractMissingTableMatch[1]) {
message = `The table '${extractMissingTableMatch[1]}' does not exist`;
type = DBError.TABLE_NOT_EXIST;
extra = {
table: extractMissingTableMatch[1],
};
} else if (extractMissingColMatch && extractMissingColMatch[1]) {
message = `The column '${extractMissingColMatch[1]}' does not exist`;
type = DBError.COLUMN_NOT_EXIST;
extra = {
column: extractMissingColMatch[1],
};
}
}
break;
case 'ELOGIN':
message = 'You do not have permission to perform this action.';
break;
case 'ETIMEOUT':
message = 'A timeout occurred while waiting for a table lock.';
break;
case 'ECONNRESET':
message = 'The connection was reset.';
break;
case 'ECONNREFUSED':
message = 'The connection was refused.';
break;
case 'EHOSTUNREACH':
message = 'The host is unreachable.';
break;
case 'EHOSTDOWN':
message = 'The host is down.';
break;
}
if (message) {
return {
message,
type,
extra,
info: { message: error.message, code: error.code },
};
}
}
export default function (
requestHandler: (req: any, res: any, next?: any) => any
) {
return async function (req: any, res: any, next: any) {
try {
return await requestHandler(req, res, next);
} catch (e) {
// todo: error log
console.log(requestHandler.name ? `${requestHandler.name} ::` : '', e);
const dbError = extractDBError(e);
if (dbError) {
return res.status(400).json(dbError);
}
if (e instanceof BadRequest) {
return res.status(400).json({ msg: e.message });
} else if (e instanceof Unauthorized) {
return res.status(401).json({ msg: e.message });
} else if (e instanceof Forbidden) {
return res.status(403).json({ msg: e.message });
} else if (e instanceof NotFound) {
return res.status(404).json({ msg: e.message });
} else if (e instanceof InternalServerError) {
return res.status(500).json({ msg: e.message });
} else if (e instanceof NotImplemented) {
return res.status(501).json({ msg: e.message });
} else if (e instanceof AjvError) {
return res.status(400).json({ msg: e.message, errors: e.errors });
}
next(e);
}
};
}
class BadRequest extends Error {}
class Unauthorized extends Error {}
class Forbidden extends Error {}
class NotFound extends Error {}
class InternalServerError extends Error {}
class NotImplemented extends Error {}
class AjvError extends Error {
constructor(param: { message: string; errors: ErrorObject[] }) {
super(param.message);
this.errors = param.errors;
}
errors: ErrorObject[];
}
export class NcError {
static notFound(message = 'Not found') {
throw new NotFound(message);
}
static badRequest(message) {
throw new BadRequest(message);
}
static unauthorized(message) {
throw new Unauthorized(message);
}
static forbidden(message) {
throw new Forbidden(message);
}
static internalServerError(message = 'Internal server error') {
throw new InternalServerError(message);
}
static notImplemented(message = 'Not implemented') {
throw new NotImplemented(message);
}
static ajvValidationError(param: { message: string; errors: ErrorObject[] }) {
throw new AjvError(param);
}
}

306
packages/nocodb-nest/src/middlewares/extract-project-id/extract-project-id.middleware.ts

@ -0,0 +1,306 @@
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
NestMiddleware,
SetMetadata,
UseInterceptors,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { NextFunction, Request, Response } from 'express';
import { OrgUserRoles } from 'nocodb-sdk';
import passport from 'passport';
import { map, Observable, throwError } from 'rxjs';
import { promisify } from 'util';
import {
Column,
Filter,
FormViewColumn,
GalleryViewColumn,
GridViewColumn,
Hook,
Model,
Project,
Sort,
View,
} from '../../models';
import projectAcl from '../../utils/projectAcl';
import catchError, { NcError } from '../catchError';
import extractProjectIdAndAuthenticate from '../extractProjectIdAndAuthenticate';
@Injectable()
export class ExtractProjectIdMiddleware implements NestInterceptor {
constructor(private reflector: Reflector) {}
async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
const req = context.switchToHttp().getRequest();
const res = context.switchToHttp().getResponse();
req.customProperty = 'This is a custom property';
try {
const { params } = req;
// extract project id based on request path params
if (params.projectName) {
const project = await Project.getByTitleOrId(params.projectName);
req.ncProjectId = project.id;
res.locals.project = project;
}
if (params.projectId) {
req.ncProjectId = params.projectId;
} else if (req.query.project_id) {
req.ncProjectId = req.query.project_id;
} else if (
params.tableId ||
req.query.fk_model_id ||
req.body?.fk_model_id
) {
const model = await Model.getByIdOrName({
id: params.tableId || req.query?.fk_model_id || req.body?.fk_model_id,
});
req.ncProjectId = model?.project_id;
} else if (params.viewId) {
const view =
(await View.get(params.viewId)) || (await Model.get(params.viewId));
req.ncProjectId = view?.project_id;
} else if (
params.formViewId ||
params.gridViewId ||
params.kanbanViewId ||
params.galleryViewId
) {
const view = await View.get(
params.formViewId ||
params.gridViewId ||
params.kanbanViewId ||
params.galleryViewId,
);
req.ncProjectId = view?.project_id;
} else if (params.publicDataUuid) {
const view = await View.getByUUID(req.params.publicDataUuid);
req.ncProjectId = view?.project_id;
} else if (params.hookId) {
const hook = await Hook.get(params.hookId);
req.ncProjectId = hook?.project_id;
} else if (params.gridViewColumnId) {
const gridViewColumn = await GridViewColumn.get(
params.gridViewColumnId,
);
req.ncProjectId = gridViewColumn?.project_id;
} else if (params.formViewColumnId) {
const formViewColumn = await FormViewColumn.get(
params.formViewColumnId,
);
req.ncProjectId = formViewColumn?.project_id;
} else if (params.galleryViewColumnId) {
const galleryViewColumn = await GalleryViewColumn.get(
params.galleryViewColumnId,
);
req.ncProjectId = galleryViewColumn?.project_id;
} else if (params.columnId) {
const column = await Column.get({ colId: params.columnId });
req.ncProjectId = column?.project_id;
} else if (params.filterId) {
const filter = await Filter.get(params.filterId);
req.ncProjectId = filter?.project_id;
} else if (params.filterParentId) {
const filter = await Filter.get(params.filterParentId);
req.ncProjectId = filter?.project_id;
} else if (params.sortId) {
const sort = await Sort.get(params.sortId);
req.ncProjectId = sort?.project_id;
}
// const user = await new Promise((resolve, _reject) => {
// passport.authenticate(
// 'jwt',
// { session: false },
// (_err, user, _info) => {
// if (user && !req.headers['xc-shared-base-id']) {
// if (
// req.path.indexOf('/user/me') === -1 &&
// req.header('xc-preview') &&
// /(?:^|,)(?:owner|creator)(?:$|,)/.test(user.roles)
// ) {
// return resolve({
// ...user,
// isAuthorized: true,
// roles: req.header('xc-preview'),
// });
// }
//
// return resolve({ ...user, isAuthorized: true });
// }
//
// if (req.headers['xc-token']) {
// passport.authenticate(
// 'authtoken',
// {
// session: false,
// optional: false,
// } as any,
// (_err, user, _info) => {
// // if (_err) return reject(_err);
// if (user) {
// return resolve({
// ...user,
// isAuthorized: true,
// roles:
// user.roles === 'owner' ? 'owner,creator' : user.roles,
// });
// } else {
// resolve({ roles: 'guest' });
// }
// },
// )(req, res, next);
// } else if (req.headers['xc-shared-base-id']) {
// passport.authenticate('baseView', {}, (_err, user, _info) => {
// // if (_err) return reject(_err);
// if (user) {
// return resolve({
// ...user,
// isAuthorized: true,
// isPublicBase: true,
// });
// } else {
// resolve({ roles: 'guest' });
// }
// })(req, res, next);
// } else {
// resolve({ roles: 'guest' });
// }
// },
// )(req, res, next);
// });
//
// await promisify((req as any).login.bind(req))(user);
} catch (e) {
console.log(e);
return throwError(new Error('Internal error'));
}
return next.handle().pipe(
map((data) => {
return data;
}),
);
}
// async use(req: any, res: any, next: () => void) {
// const customValue = this.reflector.get<string>(
// 'customValue',
// req.route?.stack[0].handle,
// );
//
//
// }
}
@Injectable()
export class AclMiddleware implements NestInterceptor {
constructor(private reflector: Reflector) {}
async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
const permissionName = this.reflector.get<string>(
'permission',
context.getHandler(),
);
const allowedRoles = this.reflector.get<(OrgUserRoles | string)[]>(
'allowedRoles',
context.getHandler(),
);
const blockApiTokenAccess = this.reflector.get<boolean>(
'blockApiTokenAccess',
context.getHandler(),
);
const req = context.switchToHttp().getRequest();
const res = context.switchToHttp().getResponse();
req.customProperty = 'This is a custom property';
const roles = req.user.roles.split(',').reduce((acc, role) => {
acc[role] = true;
return acc;
}, {});
if (req?.session?.passport?.user?.is_api_token && blockApiTokenAccess) {
NcError.forbidden('Not allowed with API token');
}
if (
(!allowedRoles || allowedRoles.some((role) => roles?.[role])) &&
!(
roles?.creator ||
roles?.owner ||
roles?.editor ||
roles?.viewer ||
roles?.commenter ||
roles?.[OrgUserRoles.SUPER_ADMIN] ||
roles?.[OrgUserRoles.CREATOR] ||
roles?.[OrgUserRoles.VIEWER]
)
) {
NcError.unauthorized('Unauthorized access');
}
// todo : verify user have access to project or not
const isAllowed =
roles &&
Object.entries(roles).some(([name, hasRole]) => {
return (
hasRole &&
projectAcl[name] &&
(projectAcl[name] === '*' ||
(projectAcl[name].exclude &&
!projectAcl[name].exclude[permissionName]) ||
(projectAcl[name].include &&
projectAcl[name].include[permissionName]))
);
});
if (!isAllowed) {
NcError.forbidden(
`${permissionName} - ${Object.keys(roles).filter(
(k) => roles[k],
)} : Not allowed`,
);
}
return next.handle().pipe(
map((data) => {
return data;
}),
);
}
}
export const UseProjectIdMiddleware = () => (target: any, key?: string, descriptor?: PropertyDescriptor) => {
UseInterceptors(ExtractProjectIdMiddleware)(target, key, descriptor);
};
export const UseAclMiddleware =
({
permissionName,
allowedRoles,
blockApiTokenAccess,
}: {
permissionName: string;
allowedRoles?: (OrgUserRoles | string)[];
blockApiTokenAccess?: boolean;
}) =>
(target: any, key?: string, descriptor?: PropertyDescriptor) => {
SetMetadata('permission', permissionName)(target, key, descriptor);
SetMetadata('allowedRoles', allowedRoles)(target, key, descriptor);
SetMetadata('blockApiTokenAccess', blockApiTokenAccess)(
target,
key,
descriptor,
);
UseInterceptors(ExtractProjectIdMiddleware)(target, key, descriptor);
UseInterceptors(AclMiddleware)(target, key, descriptor);
};

152
packages/nocodb-nest/src/middlewares/extractProjectIdAndAuthenticate.ts

@ -0,0 +1,152 @@
import { promisify } from 'util';
import passport from 'passport';
import {
Model,
View,
Hook,
GridViewColumn,
FormViewColumn,
GalleryViewColumn,
Project,
Column,
Filter,
Sort,
} from '../models';
export default async (req, res, next) => {
try {
const { params } = req;
// extract project id based on request path params
if (params.projectName) {
const project = await Project.getByTitleOrId(params.projectName);
req.ncProjectId = project.id;
res.locals.project = project;
}
if (params.projectId) {
req.ncProjectId = params.projectId;
} else if (req.query.project_id) {
req.ncProjectId = req.query.project_id;
} else if (
params.tableId ||
req.query.fk_model_id ||
req.body?.fk_model_id
) {
const model = await Model.getByIdOrName({
id: params.tableId || req.query?.fk_model_id || req.body?.fk_model_id,
});
req.ncProjectId = model?.project_id;
} else if (params.viewId) {
const view =
(await View.get(params.viewId)) || (await Model.get(params.viewId));
req.ncProjectId = view?.project_id;
} else if (
params.formViewId ||
params.gridViewId ||
params.kanbanViewId ||
params.galleryViewId
) {
const view = await View.get(
params.formViewId ||
params.gridViewId ||
params.kanbanViewId ||
params.galleryViewId,
);
req.ncProjectId = view?.project_id;
} else if (params.publicDataUuid) {
const view = await View.getByUUID(req.params.publicDataUuid);
req.ncProjectId = view?.project_id;
} else if (params.hookId) {
const hook = await Hook.get(params.hookId);
req.ncProjectId = hook?.project_id;
} else if (params.gridViewColumnId) {
const gridViewColumn = await GridViewColumn.get(params.gridViewColumnId);
req.ncProjectId = gridViewColumn?.project_id;
} else if (params.formViewColumnId) {
const formViewColumn = await FormViewColumn.get(params.formViewColumnId);
req.ncProjectId = formViewColumn?.project_id;
} else if (params.galleryViewColumnId) {
const galleryViewColumn = await GalleryViewColumn.get(
params.galleryViewColumnId,
);
req.ncProjectId = galleryViewColumn?.project_id;
} else if (params.columnId) {
const column = await Column.get({ colId: params.columnId });
req.ncProjectId = column?.project_id;
} else if (params.filterId) {
const filter = await Filter.get(params.filterId);
req.ncProjectId = filter?.project_id;
} else if (params.filterParentId) {
const filter = await Filter.get(params.filterParentId);
req.ncProjectId = filter?.project_id;
} else if (params.sortId) {
const sort = await Sort.get(params.sortId);
req.ncProjectId = sort?.project_id;
}
const user = await new Promise((resolve, _reject) => {
passport.authenticate('jwt', { session: false }, (_err, user, _info) => {
if (user && !req.headers['xc-shared-base-id']) {
if (
req.path.indexOf('/user/me') === -1 &&
req.header('xc-preview') &&
/(?:^|,)(?:owner|creator)(?:$|,)/.test(user.roles)
) {
return resolve({
...user,
isAuthorized: true,
roles: req.header('xc-preview'),
});
}
return resolve({ ...user, isAuthorized: true });
}
if (req.headers['xc-token']) {
passport.authenticate(
'authtoken',
{
session: false,
optional: false,
} as any,
(_err, user, _info) => {
// if (_err) return reject(_err);
if (user) {
return resolve({
...user,
isAuthorized: true,
roles: user.roles === 'owner' ? 'owner,creator' : user.roles,
});
} else {
resolve({ roles: 'guest' });
}
},
)(req, res, next);
} else if (req.headers['xc-shared-base-id']) {
passport.authenticate('baseView', {}, (_err, user, _info) => {
// if (_err) return reject(_err);
if (user) {
return resolve({
...user,
isAuthorized: true,
isPublicBase: true,
});
} else {
resolve({ roles: 'guest' });
}
})(req, res, next);
} else {
resolve({ roles: 'guest' });
}
})(req, res, next);
});
await promisify((req as any).login.bind(req))(user);
next();
} catch (e) {
console.log(e);
next(new Error('Internal error'));
}
};

96
packages/nocodb-nest/src/middlewares/ncMetaAclMw.ts

@ -0,0 +1,96 @@
import { OrgUserRoles } from 'nocodb-sdk';
import projectAcl from '../utils/projectAcl';
import catchError, { NcError } from './catchError';
import extractProjectIdAndAuthenticate from './extractProjectIdAndAuthenticate';
import type { NextFunction, Request, Response } from 'express';
export default function (
handlerFn,
permissionName,
{
allowedRoles,
blockApiTokenAccess,
}: {
allowedRoles?: (OrgUserRoles | string)[];
blockApiTokenAccess?: boolean;
} = {}
) {
return [
extractProjectIdAndAuthenticate,
catchError(async function authMiddleware(req, _res, next) {
const roles = req?.session?.passport?.user?.roles;
if (req?.session?.passport?.user?.is_api_token && blockApiTokenAccess) {
NcError.forbidden('Not allowed with API token');
}
if (
(!allowedRoles || allowedRoles.some((role) => roles?.[role])) &&
!(
roles?.creator ||
roles?.owner ||
roles?.editor ||
roles?.viewer ||
roles?.commenter ||
roles?.[OrgUserRoles.SUPER_ADMIN] ||
roles?.[OrgUserRoles.CREATOR] ||
roles?.[OrgUserRoles.VIEWER]
)
) {
NcError.unauthorized('Unauthorized access');
}
next();
}),
// @ts-ignore
catchError(async function projectAclMiddleware(
req: Request<any, any, any, any, any>,
_res: Response,
next: NextFunction
) {
// if (req['files'] && req.body.json) {
// req.body = JSON.parse(req.body.json);
// }
// if (req['session']?.passport?.user?.isAuthorized) {
// if (
// req?.body?.project_id &&
// !req['session']?.passport?.user?.isPublicBase &&
// !(await this.xcMeta.isUserHaveAccessToProject(
// req?.body?.project_id,
// req['session']?.passport?.user?.id
// ))
// ) {
// return res
// .status(403)
// .json({ msg: "User doesn't have project access" });
// }
//
// if (req?.body?.api) {
// todo : verify user have access to project or not
const roles = req['session']?.passport?.user?.roles;
const isAllowed =
roles &&
Object.entries(roles).some(([name, hasRole]) => {
return (
hasRole &&
projectAcl[name] &&
(projectAcl[name] === '*' ||
(projectAcl[name].exclude &&
!projectAcl[name].exclude[permissionName]) ||
(projectAcl[name].include &&
projectAcl[name].include[permissionName]))
);
});
if (!isAllowed) {
NcError.forbidden(
`${permissionName} - ${Object.keys(roles).filter(
(k) => roles[k]
)} : Not allowed`
);
}
// }
// }
next();
}),
catchError(handlerFn),
];
}

6
packages/nocodb-nest/src/models/Base.ts

@ -57,9 +57,13 @@ export default class Base implements BaseType {
'order',
'enabled',
]);
const secret =
Noco.getConfig()?.auth?.jwt?.secret;
insertObj.config = CryptoJS.AES.encrypt(
JSON.stringify(base.config),
Noco.getConfig()?.auth?.jwt?.secret
secret
).toString();
const { id } = await ncMeta.metaInsert2(

2
packages/nocodb-nest/src/nocobuild.ts

@ -1,6 +1,6 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as express from 'express';
import express from 'express';
export default async function(app = express()) {
const nestApp = await NestFactory.create(AppModule);

17
packages/nocodb-nest/src/projects/projects.controller.ts

@ -13,6 +13,12 @@ import {
import { AuthGuard } from '@nestjs/passport';
import isDocker from 'is-docker';
import { ProjectReqType } from 'nocodb-sdk';
import { PagedResponseImpl } from 'src/helpers/PagedResponse';
import { ProjectType } from '../../../nocodb-sdk'
import {
UseAclMiddleware,
UseProjectIdMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware'
import Noco from '../Noco';
import { packageVersion } from '../utils/packageVersion';
import { ProjectsService } from './projects.service';
@ -22,12 +28,19 @@ import { ProjectsService } from './projects.service';
export class ProjectsController {
constructor(private readonly projectsService: ProjectsService) {}
@UseAclMiddleware({
permissionName: 'projectList'
})
@Get('/api/v1/db/meta/projects/')
list(@Query() queryParams: Record<string, any>, @Request() req) {
return this.projectsService.list({
async list(@Query() queryParams: Record<string, any>, @Request() req) {
const projects = await this.projectsService.list({
user: req.user,
query: queryParams,
});
return new PagedResponseImpl(projects as ProjectType[], {
count: projects.length,
limit: projects.length,
})
}
@Get('/api/v1/db/meta/projects/:projectId/info')

3
packages/nocodb-nest/src/projects/projects.module.ts

@ -1,9 +1,10 @@
import { Module } from '@nestjs/common';
import { ExtractProjectIdMiddleware } from '../middlewares/extract-project-id/extract-project-id.middleware'
import { ProjectsService } from './projects.service';
import { ProjectsController } from './projects.controller';
@Module({
controllers: [ProjectsController],
providers: [ProjectsService]
providers: [ProjectsService, ExtractProjectIdMiddleware]
})
export class ProjectsModule {}

2
packages/nocodb-nest/src/projects/projects.service.ts

@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import DOMPurify from 'isomorphic-dompurify';
import * as DOMPurify from 'isomorphic-dompurify';
import { customAlphabet } from 'nanoid';
import { ProjectReqType } from 'nocodb-sdk';
import { promisify } from 'util';

7
packages/nocodb-nest/src/strategies/jwt.strategy.ts

@ -1,15 +1,18 @@
import { Injectable, UnauthorizedException } from '@nestjs/common'
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';
import { jwtConstants } from '../auth/constants'
import { UsersService } from '../users/users.service'
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private userService: UsersService) {
super({
ignoreExpiration: false,
// ignoreExpiration: false,
jwtFromRequest: ExtractJwt.fromHeader('xc-auth'),
});
secretOrKey: jwtConstants.secret,
expiresIn: '10h'
})
}
async validate(payload: any) {

10
packages/nocodb-nest/src/users/users.controller.ts

@ -1,7 +1,13 @@
import { Controller } from '@nestjs/common';
import { Controller, Get } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('users')
@Controller()
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get('/api/v1/auth/user/me')
async me() {
// return this.usersService.me();
}
}

10
packages/nocodb-nest/src/utils/NcConfigFactory.ts

@ -100,7 +100,7 @@ export default class NcConfigFactory {
ncConfig.auth = {
jwt: {
secret: process.env.NC_AUTH_JWT_SECRET,
secret: process.env.NC_AUTH_JWT_SECRET ?? 'temporary-key',
},
};
@ -421,7 +421,7 @@ export default class NcConfigFactory {
if (process.env.NC_AUTH_ADMIN_SECRET) {
config.auth = {
masterKey: {
secret: process.env.NC_AUTH_ADMIN_SECRET,
secret: process.env.NC_AUTH_ADMIN_SECRET ?? 'temporary-key',
},
};
} else if (process.env.NC_NO_AUTH) {
@ -436,7 +436,7 @@ export default class NcConfigFactory {
dbAlias:
process.env.NC_AUTH_JWT_DB_ALIAS ||
config.envs['_noco'].db[0].meta.dbAlias,
secret: process.env.NC_AUTH_JWT_SECRET,
secret: process.env.NC_AUTH_JWT_SECRET ?? 'temporary-key',
},
};
}
@ -536,7 +536,7 @@ export default class NcConfigFactory {
if (process.env.NC_AUTH_ADMIN_SECRET) {
config.auth = {
masterKey: {
secret: process.env.NC_AUTH_ADMIN_SECRET,
secret: process.env.NC_AUTH_ADMIN_SECRET ?? 'temporary-key',
},
};
} else if (process.env.NC_NO_AUTH) {
@ -551,7 +551,7 @@ export default class NcConfigFactory {
dbAlias:
process.env.NC_AUTH_JWT_DB_ALIAS ||
config.envs['_noco'].db[0].meta.dbAlias,
secret: process.env.NC_AUTH_JWT_SECRET,
secret: process.env.NC_AUTH_JWT_SECRET ?? 'temporary-key',
},
};
}

3
packages/nocodb-nest/tsconfig.json

@ -17,6 +17,7 @@
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false,
"resolveJsonModule": true
"resolveJsonModule": true,
"esModuleInterop": true
}
}

Loading…
Cancel
Save