feat: allow bypass on v2 bases only

Signed-off-by: mertmit <mertmit99@gmail.com>
This commit is contained in:
mertmit
2025-09-22 14:19:31 +03:00
parent d59784f4bd
commit 2aa1fc4f11
6 changed files with 470 additions and 25 deletions

View File

@@ -5,7 +5,13 @@ import { v7 as uuidv7 } from 'uuid';
import type { Condition, Knex } from '~/db/CustomKnex';
import XcMigrationSourcev3 from '~/meta/migrations/XcMigrationSourcev3';
import { NcConfig } from '~/utils/nc-config';
import { MetaTable, RootScopes, RootScopeTables } from '~/utils/globals';
import {
BaseRelatedMetaTables,
BaseVersion,
MetaTable,
RootScopes,
RootScopeTables,
} from '~/utils/globals';
import { NcError } from '~/helpers/catchError';
import { isWorker } from '~/utils';
@@ -485,7 +491,15 @@ export class MetaService extends MetaServiceCE {
}
if (workspace_id === RootScopes.BYPASS && base_id === RootScopes.BYPASS) {
// bypass
// bypass is only allowed for v2 bases, so lets join the base table to ensure the base is v2
if (BaseRelatedMetaTables.includes(target as MetaTable)) {
query.whereExists(function () {
this.select(1)
.from(`${MetaTable.PROJECT} as p`)
.whereRaw('p.id = base_id')
.andWhere('p.version', BaseVersion.V2);
});
}
} else if (workspace_id === base_id) {
if (!Object.values(RootScopes).includes(workspace_id as RootScopes)) {
NcError.metaError({

View File

@@ -84,6 +84,7 @@ const up = async (knex: Knex) => {
[MetaTable.SYNC_SOURCE]: ['base_id', 'id'],
[MetaTable.VIEWS]: ['base_id', 'id'],
[MetaTable.WIDGETS]: ['base_id', 'id'],
[MetaTable.MODEL_STAT]: ['fk_workspace_id', 'base_id', 'fk_model_id'],
};
const customPkTitles = {
@@ -255,6 +256,7 @@ const down = async (knex: Knex) => {
[MetaTable.SYNC_SOURCE]: ['id'],
[MetaTable.VIEWS]: ['id'],
[MetaTable.WIDGETS]: ['id'],
[MetaTable.MODEL_STAT]: ['fk_workspace_id', 'fk_model_id'],
};
const customPkTitles = {

View File

@@ -116,6 +116,355 @@ const getApiVersionFromUrl = (url: string) => {
export class ExtractIdsMiddleware implements NestMiddleware, CanActivate {
async use(req, res, next): Promise<any> {
const { params } = req;
req.ncApiVersion = getApiVersionFromUrl(req.route.path);
req.ncSocketId = req.headers['xc-socket-id'];
const bypassContext = {
workspace_id: RootScopes.BYPASS,
base_id: RootScopes.BYPASS,
api_version: req.ncApiVersion,
socket_id: req.ncSocketId,
};
// this is a special route for ws operations we pass 'nc' as base id
const isInternalApi = !!req.path?.startsWith('/api/v2/internal');
const isInternalWorkspaceScope = isInternalApi && params.baseId === 'nc';
const baseId = params.baseId || params.baseName;
if (!isInternalWorkspaceScope && baseId) {
const base = await Base.get(bypassContext, baseId);
if (!base) {
NcError.get(bypassContext).baseNotFound(baseId);
}
const context = {
workspace_id: base.fk_workspace_id,
base_id: base.id,
api_version: req.ncApiVersion,
socket_id: req.ncSocketId,
};
req.ncBaseId = base.id;
req.ncWorkspaceId = base.fk_workspace_id;
const workspace = await Workspace.get(req.ncWorkspaceId);
if (!workspace) {
NcError.workspaceNotFound(req.ncWorkspaceId);
}
req.ncOrgId = workspace.fk_org_id;
req.context = {
org_id: req.ncOrgId,
workspace_id: req.ncWorkspaceId,
base_id: req.ncBaseId,
api_version: req.ncApiVersion,
socket_id: req.ncSocketId,
nc_site_url: req.ncSiteUrl,
};
if (params.mcpTokenId) {
const mcpToken = await MCPToken.get(context, params.mcpTokenId);
if (!mcpToken) {
NcError.get(context).genericNotFound('MCPToken', params.mcpTokenId);
}
} else if (params.integrationId) {
const integration = await Integration.get(
context,
params.integrationId,
);
if (!integration) {
NcError.get(context).integrationNotFound(params.integrationId);
}
} else if (params.tableId || params.modelId || params.tableName) {
const model = await Model.get(
context,
params.tableId || params.modelId || params.tableName,
);
if (!model) {
NcError.get(context).tableNotFound(
params.tableId || params.modelId || params.tableName,
);
}
req.ncSourceId = model.source_id;
} else if (params.viewId || params.viewName) {
const view =
(await View.get(context, params.viewId || params.viewName)) ||
(await Model.get(context, params.viewId || params.viewName));
if (!view) {
NcError.get(context).viewNotFound(params.viewId || params.viewName);
}
req.ncSourceId = view.source_id;
} else if (
params.formViewId ||
params.gridViewId ||
params.kanbanViewId ||
params.galleryViewId ||
params.calendarViewId
) {
const view = await View.get(
context,
params.formViewId ||
params.gridViewId ||
params.kanbanViewId ||
params.galleryViewId ||
params.calendarViewId,
);
if (!view) {
NcError.get(context).viewNotFound(
params.formViewId ||
params.gridViewId ||
params.kanbanViewId ||
params.galleryViewId ||
params.calendarViewId,
);
}
req.ncSourceId = view.source_id;
} else if (params.publicDataUuid) {
const view = await View.getByUUID(context, req.params.publicDataUuid);
if (!view) {
NcError.get(context).viewNotFound(req.params.publicDataUuid);
}
req.ncSourceId = view?.source_id;
} else if (params.sharedViewUuid) {
const view = await View.getByUUID(context, req.params.sharedViewUuid);
if (!view) {
NcError.get(context).viewNotFound(req.params.sharedViewUuid);
}
req.ncSourceId = view.source_id;
} else if (params.sharedBaseUuid) {
const base = await Base.getByUuid(context, req.params.sharedBaseUuid);
if (!base) {
NcError.get(context).baseNotFound(req.params.sharedBaseUuid);
}
} else if (params.sharedDashboardUuid) {
const dashboard = await Dashboard.getByUUID(
context,
req.params.sharedDashboardUuid,
);
if (!dashboard) {
NcError.dashboardNotFound(req.params.sharedDashboardUuid);
}
} else if (params.hookId) {
const hook = await Hook.get(context, params.hookId);
if (!hook) {
NcError.get(context).genericNotFound('Webhook', params.hookId);
}
req.ncSourceId = hook.source_id;
} else if (params.rowColorConditionId) {
const rowColorCondition = await RowColorCondition.getById(
context,
params.rowColorConditionId,
);
if (!rowColorCondition) {
NcError.get(context).genericNotFound(
'Row color condition',
params.rowColorConditionId,
);
}
const view = await View.get(context, rowColorCondition.fk_view_id);
if (!view) {
NcError.get(context).viewNotFound(rowColorCondition.fk_view_id);
}
req.ncSourceId = view.source_id;
} else if (params.gridViewColumnId) {
const gridViewColumn = await GridViewColumn.get(
context,
params.gridViewColumnId,
);
if (!gridViewColumn) {
NcError.get(context).fieldNotFound(params.gridViewColumnId);
}
req.ncSourceId = gridViewColumn?.source_id;
} else if (params.formViewColumnId) {
const formViewColumn = await FormViewColumn.get(
context,
params.formViewColumnId,
);
if (!formViewColumn) {
NcError.get(context).fieldNotFound(params.formViewColumnId);
}
req.ncSourceId = formViewColumn.source_id;
} else if (params.galleryViewColumnId) {
const galleryViewColumn = await GalleryViewColumn.get(
context,
params.galleryViewColumnId,
);
if (!galleryViewColumn) {
NcError.get(context).fieldNotFound(params.galleryViewColumnId);
}
req.ncSourceId = galleryViewColumn.source_id;
} else if (params.columnId) {
const column = await Column.get(context, { colId: params.columnId });
if (!column) {
NcError.get(context).fieldNotFound(params.columnId);
}
req.ncSourceId = column.source_id;
} else if (params.filterId) {
const filter = await Filter.get(context, params.filterId);
if (!filter) {
NcError.genericNotFound('Filter', params.filterId);
}
req.ncSourceId = filter.source_id;
} else if (params.filterParentId) {
const filter = await Filter.get(context, params.filterParentId);
if (!filter) {
NcError.genericNotFound('Filter', params.filterParentId);
}
req.ncSourceId = filter.source_id;
} else if (params.widgetId) {
const widget = await Widget.get(context, params.widgetId);
if (!widget) {
NcError.genericNotFound('Widget', params.widgetId);
}
} else if (params.sortId) {
const sort = await Sort.get(context, params.sortId);
if (!sort) {
NcError.genericNotFound('Sort', params.sortId);
}
req.ncSourceId = sort.source_id;
} else if (params.syncId) {
const syncSource = await SyncSource.get(context, req.params.syncId);
if (!syncSource) {
NcError.genericNotFound('Sync Source', req.params.syncId);
}
req.ncSourceId = syncSource.source_id;
} else if (params.extensionId) {
const extension = await Extension.get(context, req.params.extensionId);
if (!extension) {
NcError.genericNotFound('Extension', req.params.extensionId);
}
}
/*
TODO: migrate after comments api
// extract fk_model_id from query params only if it's audit post or comments post, get, patch, delete endpoint
else if (
['/api/v1/db/meta/comments', '/api/v2/meta/comments'].some(
(auditInsertOrUpdatePath) =>
req.route.path === auditInsertOrUpdatePath,
) &&
req.method === 'POST' &&
req.body?.fk_model_id
) {
const model = await Model.getByIdOrName(context, {
id: req.body.fk_model_id,
});
if (!model) {
NcError.get(context).tableNotFound(req.body.fk_model_id);
}
req.ncBaseId = model.base_id;
req.ncSourceId = model.source_id;
} else if (
[
'/api/v2/meta/comments/count',
'/api/v1/db/meta/comments/count',
'/api/v2/meta/comments',
'/api/v1/db/meta/comments',
].some((auditReadPath) => req.route.path === auditReadPath) &&
req.method === 'GET' &&
req.query.fk_model_id
) {
const model = await Model.getByIdOrName(context, {
id: req.query?.fk_model_id,
});
if (!model) {
NcError.get(context).tableNotFound(req.query?.fk_model_id);
}
req.ncBaseId = model.base_id;
req.ncSourceId = model.source_id;
} else if (
[
'/api/v1/db/meta/comment/:commentId',
'/api/v2/meta/comment/:commentId',
'/api/v1/db/meta/comment/:commentId/resolve',
'/api/v2/meta/comment/:commentId/resolve',
].some((auditPatchPath) => req.route.path === auditPatchPath) &&
(req.method === 'PATCH' ||
req.method === 'DELETE' ||
req.method === 'POST') &&
req.params.commentId
) {
const audit = await Comment.get(context, params.commentId);
if (!audit) {
NcError.genericNotFound('Comment', params.commentId);
}
req.ncBaseId = audit.base_id;
req.ncSourceId = audit.source_id;
}
*/
} else {
await this.legacyExtractIds(req);
}
await this.additionalValidation({ req, res, next });
next();
}
async canActivate(context: ExecutionContext): Promise<boolean> {
await this.use(
context.switchToHttp().getRequest(),
context.switchToHttp().getResponse(),
() => {},
);
return true;
}
// additional validation logic which can be overridden
protected async additionalValidation(_param: {
next: any;
res: any;
req: any;
}) {
// do nothing
}
protected async legacyExtractIds(req) {
const { params } = req;
let view;
const context = {
@@ -626,28 +975,6 @@ export class ExtractIdsMiddleware implements NestMiddleware, CanActivate {
if (req.ncBaseId && !isInternalWorkspaceScope) {
req.permissions = await Permission.list(req.context, req.ncBaseId);
}
await this.additionalValidation({ req, res, next });
next();
}
async canActivate(context: ExecutionContext): Promise<boolean> {
await this.use(
context.switchToHttp().getRequest(),
context.switchToHttp().getResponse(),
() => {},
);
return true;
}
// additional validation logic which can be overridden
protected async additionalValidation(_param: {
next: any;
res: any;
req: any;
}) {
// do nothing
}
}

View File

@@ -93,6 +93,56 @@ export enum MetaTable {
WIDGETS = 'nc_widgets_v2',
}
export const BaseRelatedMetaTables = [
MetaTable.CALENDAR_VIEW_COLUMNS,
MetaTable.CALENDAR_VIEW_RANGE,
MetaTable.CALENDAR_VIEW,
MetaTable.COL_BARCODE,
MetaTable.COL_BUTTON,
MetaTable.COL_FORMULA,
MetaTable.COL_LONG_TEXT,
MetaTable.COL_LOOKUP,
MetaTable.COL_QRCODE,
MetaTable.COL_RELATIONS,
MetaTable.COL_ROLLUP,
MetaTable.COL_SELECT_OPTIONS,
MetaTable.COLUMNS,
MetaTable.COMMENTS_REACTIONS,
MetaTable.COMMENTS,
MetaTable.CUSTOM_URLS,
MetaTable.DASHBOARDS,
MetaTable.MODEL_ROLE_VISIBILITY,
MetaTable.EXTENSIONS,
MetaTable.FILTER_EXP,
MetaTable.FORM_VIEW_COLUMNS,
MetaTable.FORM_VIEW,
MetaTable.GALLERY_VIEW_COLUMNS,
MetaTable.GALLERY_VIEW,
MetaTable.GRID_VIEW_COLUMNS,
MetaTable.GRID_VIEW,
MetaTable.HOOK_LOGS,
MetaTable.HOOKS,
MetaTable.KANBAN_VIEW_COLUMNS,
MetaTable.KANBAN_VIEW,
MetaTable.MAP_VIEW_COLUMNS,
MetaTable.MAP_VIEW,
MetaTable.MCP_TOKENS,
MetaTable.MODELS,
MetaTable.PERMISSIONS,
MetaTable.PERMISSION_SUBJECTS,
MetaTable.ROW_COLOR_CONDITIONS,
MetaTable.SCRIPTS,
MetaTable.SORT,
MetaTable.SOURCES,
MetaTable.SYNC_CONFIGS,
MetaTable.SYNC_LOGS,
MetaTable.SYNC_MAPPINGS,
MetaTable.SYNC_SOURCE,
MetaTable.VIEWS,
MetaTable.WIDGETS,
MetaTable.MODEL_STAT,
];
export const orderedMetaTables = [
MetaTable.MODEL_ROLE_VISIBILITY,
MetaTable.PLUGIN,

View File

@@ -1,6 +1,6 @@
import { Logger } from '@nestjs/common';
import type { BaseType, BoolType, MetaType } from 'nocodb-sdk';
import type { DB_TYPES } from '~/utils/globals';
import type { BaseVersion, DB_TYPES } from '~/utils/globals';
import type { NcContext } from '~/interface/config';
import {
BaseUser,
@@ -45,6 +45,7 @@ export default class Base implements BaseType {
public linked_db_projects?: Base[];
public default_role?: 'no-access';
public is_snapshot?: boolean;
public version?: BaseVersion;
// shared base props
uuid?: string;

View File

@@ -73,8 +73,59 @@ export enum MetaTable {
PERMISSION_SUBJECTS = 'nc_permission_subjects',
DASHBOARDS = 'nc_dashboards_v2',
WIDGETS = 'nc_widgets_v2',
MODEL_STAT = 'nc_model_stat',
}
export const BaseRelatedMetaTables = [
MetaTable.CALENDAR_VIEW_COLUMNS,
MetaTable.CALENDAR_VIEW_RANGE,
MetaTable.CALENDAR_VIEW,
MetaTable.COL_BARCODE,
MetaTable.COL_BUTTON,
MetaTable.COL_FORMULA,
MetaTable.COL_LONG_TEXT,
MetaTable.COL_LOOKUP,
MetaTable.COL_QRCODE,
MetaTable.COL_RELATIONS,
MetaTable.COL_ROLLUP,
MetaTable.COL_SELECT_OPTIONS,
MetaTable.COLUMNS,
MetaTable.COMMENTS_REACTIONS,
MetaTable.COMMENTS,
MetaTable.CUSTOM_URLS,
MetaTable.DASHBOARDS,
MetaTable.MODEL_ROLE_VISIBILITY,
MetaTable.EXTENSIONS,
MetaTable.FILTER_EXP,
MetaTable.FORM_VIEW_COLUMNS,
MetaTable.FORM_VIEW,
MetaTable.GALLERY_VIEW_COLUMNS,
MetaTable.GALLERY_VIEW,
MetaTable.GRID_VIEW_COLUMNS,
MetaTable.GRID_VIEW,
MetaTable.HOOK_LOGS,
MetaTable.HOOKS,
MetaTable.KANBAN_VIEW_COLUMNS,
MetaTable.KANBAN_VIEW,
MetaTable.MAP_VIEW_COLUMNS,
MetaTable.MAP_VIEW,
MetaTable.MCP_TOKENS,
MetaTable.MODELS,
MetaTable.PERMISSIONS,
MetaTable.PERMISSION_SUBJECTS,
MetaTable.ROW_COLOR_CONDITIONS,
MetaTable.SCRIPTS,
MetaTable.SORT,
MetaTable.SOURCES,
MetaTable.SYNC_CONFIGS,
MetaTable.SYNC_LOGS,
MetaTable.SYNC_MAPPINGS,
MetaTable.SYNC_SOURCE,
MetaTable.VIEWS,
MetaTable.WIDGETS,
MetaTable.MODEL_STAT,
];
export enum MetaTableOldV2 {
PROJECT = 'nc_projects_v2',
PROJECT_USERS = 'nc_project_users_v2',