refactor(core): Store projectId on additionalData independent of dataStore context (no-changelog) (#19093)

This commit is contained in:
Charlie Kolb
2025-09-11 09:21:40 +02:00
committed by GitHub
parent a0d92a72b9
commit 6dd7797c39
6 changed files with 43 additions and 43 deletions

View File

@@ -10,7 +10,6 @@ import type { INodePropertyOptions, NodeParameterValueType } from 'n8n-workflow'
import { DynamicNodeParametersService } from '@/services/dynamic-node-parameters.service'; import { DynamicNodeParametersService } from '@/services/dynamic-node-parameters.service';
import { getBase } from '@/workflow-execute-additional-data'; import { getBase } from '@/workflow-execute-additional-data';
import { userHasScopes } from '@/permissions.ee/check-access';
@RestController('/dynamic-node-parameters') @RestController('/dynamic-node-parameters')
export class DynamicNodeParametersController { export class DynamicNodeParametersController {
@@ -22,6 +21,8 @@ export class DynamicNodeParametersController {
_res: Response, _res: Response,
@Body payload: OptionsRequestDto, @Body payload: OptionsRequestDto,
): Promise<INodePropertyOptions[]> { ): Promise<INodePropertyOptions[]> {
await this.service.scrubInaccessibleProjectId(req.user, payload);
const { const {
credentials, credentials,
currentNodeParameters, currentNodeParameters,
@@ -33,17 +34,7 @@ export class DynamicNodeParametersController {
} = payload; } = payload;
const additionalData = await getBase(req.user.id, currentNodeParameters); const additionalData = await getBase(req.user.id, currentNodeParameters);
additionalData.dataTableProjectId = projectId;
if (projectId) {
if (await userHasScopes(req.user, ['dataStore:listProject'], false, { projectId })) {
// Project ID is currently only added on the additionalData if the user
// has data store listing permission for that project. We should consider
// turning this into a more general check, but as of now data stores are
// the only nodes with project specific resource locators where we want to ensure
// that only data stores belonging to their respective projects are shown.
additionalData.dataStoreProjectId = projectId;
}
}
if (methodName) { if (methodName) {
return await this.service.getOptionsViaMethodName( return await this.service.getOptionsViaMethodName(
@@ -75,6 +66,8 @@ export class DynamicNodeParametersController {
_res: Response, _res: Response,
@Body payload: ResourceLocatorRequestDto, @Body payload: ResourceLocatorRequestDto,
) { ) {
await this.service.scrubInaccessibleProjectId(req.user, payload);
const { const {
path, path,
methodName, methodName,
@@ -87,17 +80,7 @@ export class DynamicNodeParametersController {
} = payload; } = payload;
const additionalData = await getBase(req.user.id, currentNodeParameters); const additionalData = await getBase(req.user.id, currentNodeParameters);
additionalData.dataTableProjectId = projectId;
if (projectId) {
if (await userHasScopes(req.user, ['dataStore:listProject'], false, { projectId })) {
// Project ID is currently only added on the additionalData if the user
// has data store listing permission for that project. We should consider
// turning this into a more general check, but as of now data stores are
// the only nodes with project specific resource locators where we want to ensure
// that only data stores belonging to their respective projects are shown.
additionalData.dataStoreProjectId = projectId;
}
}
return await this.service.getResourceLocatorResults( return await this.service.getResourceLocatorResults(
methodName, methodName,
@@ -117,21 +100,13 @@ export class DynamicNodeParametersController {
_res: Response, _res: Response,
@Body payload: ResourceMapperFieldsRequestDto, @Body payload: ResourceMapperFieldsRequestDto,
) { ) {
await this.service.scrubInaccessibleProjectId(req.user, payload);
const { path, methodName, credentials, currentNodeParameters, nodeTypeAndVersion, projectId } = const { path, methodName, credentials, currentNodeParameters, nodeTypeAndVersion, projectId } =
payload; payload;
const additionalData = await getBase(req.user.id, currentNodeParameters); const additionalData = await getBase(req.user.id, currentNodeParameters);
additionalData.dataTableProjectId = projectId;
if (projectId) {
if (await userHasScopes(req.user, ['dataStore:listProject'], false, { projectId })) {
// Project ID is currently only added on the additionalData if the user
// has data store listing permission for that project. We should consider
// turning this into a more general check, but as of now data stores are
// the only nodes with project specific resource locators where we want to ensure
// that only data stores belonging to their respective projects are shown.
additionalData.dataStoreProjectId = projectId;
}
}
return await this.service.getResourceMappingFields( return await this.service.getResourceMappingFields(
methodName, methodName,

View File

@@ -49,10 +49,10 @@ export class DataStoreProxyService implements DataStoreProxyProvider {
async getDataStoreAggregateProxy( async getDataStoreAggregateProxy(
workflow: Workflow, workflow: Workflow,
node: INode, node: INode,
dataStoreProjectId?: string, projectId?: string,
): Promise<IDataStoreProjectAggregateService> { ): Promise<IDataStoreProjectAggregateService> {
this.validateRequest(node); this.validateRequest(node);
const projectId = dataStoreProjectId ?? (await this.getProjectId(workflow)); projectId = projectId ?? (await this.getProjectId(workflow));
return this.makeAggregateOperations(projectId); return this.makeAggregateOperations(projectId);
} }
@@ -61,10 +61,10 @@ export class DataStoreProxyService implements DataStoreProxyProvider {
workflow: Workflow, workflow: Workflow,
node: INode, node: INode,
dataStoreId: string, dataStoreId: string,
dataStoreProjectId?: string, projectId?: string,
): Promise<IDataStoreProjectService> { ): Promise<IDataStoreProjectService> {
this.validateRequest(node); this.validateRequest(node);
const projectId = dataStoreProjectId ?? (await this.getProjectId(workflow)); projectId = projectId ?? (await this.getProjectId(workflow));
return this.makeDataStoreOperations(projectId, dataStoreId); return this.makeDataStoreOperations(projectId, dataStoreId);
} }

View File

@@ -26,6 +26,9 @@ import { Workflow, UnexpectedError } from 'n8n-workflow';
import { NodeTypes } from '@/node-types'; import { NodeTypes } from '@/node-types';
import { WorkflowLoaderService } from './workflow-loader.service'; import { WorkflowLoaderService } from './workflow-loader.service';
import { User } from '@n8n/db';
import { userHasScopes } from '@/permissions.ee/check-access';
import { Logger } from '@n8n/backend-common';
type LocalResourceMappingMethod = ( type LocalResourceMappingMethod = (
this: ILocalLoadOptionsFunctions, this: ILocalLoadOptionsFunctions,
@@ -52,10 +55,29 @@ type NodeMethod =
@Service() @Service()
export class DynamicNodeParametersService { export class DynamicNodeParametersService {
constructor( constructor(
private logger: Logger,
private nodeTypes: NodeTypes, private nodeTypes: NodeTypes,
private workflowLoaderService: WorkflowLoaderService, private workflowLoaderService: WorkflowLoaderService,
) {} ) {}
async scrubInaccessibleProjectId(user: User, payload: { projectId?: string }) {
// We want to avoid relying on generic project:read permissions to enable
// a future with fine-grained permission control dependent on the respective resource
// For now we use the dataStore:listProject scope as this is the existing consumer of
// the project id
if (
payload.projectId &&
!(await userHasScopes(user, ['dataStore:listProject'], false, {
projectId: payload.projectId,
}))
) {
this.logger.warn(
`Scrubbed inaccessible projectId ${payload.projectId} from DynamicNodeParameters request`,
);
payload.projectId = undefined;
}
}
/** Returns the available options via a predefined method */ /** Returns the available options via a predefined method */
async getOptionsViaMethodName( async getOptionsViaMethodName(
methodName: string, methodName: string,

View File

@@ -8,7 +8,10 @@ declare module 'n8n-workflow' {
hooks?: ExecutionLifecycleHooks; hooks?: ExecutionLifecycleHooks;
externalSecretsProxy: ExternalSecretsProxy; externalSecretsProxy: ExternalSecretsProxy;
'data-table'?: { dataStoreProxyProvider: DataStoreProxyProvider }; 'data-table'?: { dataStoreProxyProvider: DataStoreProxyProvider };
dataStoreProjectId?: string; // Project ID is currently only added on the additionalData if the user
// has data table listing permission for that project. We should consider
// that only data tables belonging to their respective projects are shown.
dataTableProjectId?: string;
} }
} }

View File

@@ -17,14 +17,14 @@ export function getDataStoreHelperFunctions(
await dataStoreProxyProvider.getDataStoreAggregateProxy( await dataStoreProxyProvider.getDataStoreAggregateProxy(
workflow, workflow,
node, node,
additionalData.dataStoreProjectId, additionalData.dataTableProjectId,
), ),
getDataStoreProxy: async (dataStoreId: string) => getDataStoreProxy: async (dataStoreId: string) =>
await dataStoreProxyProvider.getDataStoreProxy( await dataStoreProxyProvider.getDataStoreProxy(
workflow, workflow,
node, node,
dataStoreId, dataStoreId,
additionalData.dataStoreProjectId, additionalData.dataTableProjectId,
), ),
}; };
} }

View File

@@ -924,13 +924,13 @@ export type DataStoreProxyProvider = {
getDataStoreAggregateProxy( getDataStoreAggregateProxy(
workflow: Workflow, workflow: Workflow,
node: INode, node: INode,
dataStoreProjectId?: string, projectId?: string,
): Promise<IDataStoreProjectAggregateService>; ): Promise<IDataStoreProjectAggregateService>;
getDataStoreProxy( getDataStoreProxy(
workflow: Workflow, workflow: Workflow,
node: INode, node: INode,
dataStoreId: string, dataStoreId: string,
dataStoreProjectId?: string, projectId?: string,
): Promise<IDataStoreProjectService>; ): Promise<IDataStoreProjectService>;
}; };