feat(core): Add ownership, sharing and credential details to GET /workflows (#4510)
* ⚡ Abstract into `getMany()` * ⚡ Use `getMany()` from free controller * ⚡ Use `getMany()` from paid controller * 🧪 Add tests * 🧪 Fix tests * ⚡ Add credential usage info * 🧪 Update tests * ⚡ Add type and adjust test
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { LoggerProxy } from 'n8n-workflow';
|
||||
import { FindManyOptions, FindOneOptions, ObjectLiteral } from 'typeorm';
|
||||
import { JsonObject, jsonParse, LoggerProxy } from 'n8n-workflow';
|
||||
import { FindManyOptions, FindOneOptions, In, ObjectLiteral } from 'typeorm';
|
||||
import {
|
||||
ActiveWorkflowRunner,
|
||||
Db,
|
||||
@@ -15,6 +15,26 @@ import { WorkflowEntity } from '../databases/entities/WorkflowEntity';
|
||||
import { validateEntity } from '../GenericHelpers';
|
||||
import { externalHooks } from '../Server';
|
||||
import * as TagHelpers from '../TagHelpers';
|
||||
import { getSharedWorkflowIds } from '../WorkflowHelpers';
|
||||
import { validate as jsonSchemaValidate } from 'jsonschema';
|
||||
|
||||
export interface IGetWorkflowsQueryFilter {
|
||||
id?: number | string;
|
||||
name?: string;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
const schemaGetWorkflowsQueryFilter = {
|
||||
$id: '/IGetWorkflowsQueryFilter',
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { anyOf: [{ type: 'integer' }, { type: 'string' }] },
|
||||
name: { type: 'string' },
|
||||
active: { type: 'boolean' },
|
||||
},
|
||||
};
|
||||
|
||||
const allowedWorkflowsQueryFilterFields = Object.keys(schemaGetWorkflowsQueryFilter.properties);
|
||||
|
||||
export class WorkflowsService {
|
||||
static async getSharing(
|
||||
@@ -47,6 +67,80 @@ export class WorkflowsService {
|
||||
return Db.collections.Workflow.findOne(workflow, options);
|
||||
}
|
||||
|
||||
static async getMany(user: User, rawFilter: string) {
|
||||
const sharedWorkflowIds = await getSharedWorkflowIds(user);
|
||||
if (sharedWorkflowIds.length === 0) {
|
||||
// return early since without shared workflows there can be no hits
|
||||
// (note: getSharedWorkflowIds() returns _all_ workflow ids for global owners)
|
||||
return [];
|
||||
}
|
||||
|
||||
let filter: IGetWorkflowsQueryFilter | undefined = undefined;
|
||||
if (rawFilter) {
|
||||
try {
|
||||
const filterJson: JsonObject = jsonParse(rawFilter);
|
||||
if (filterJson) {
|
||||
Object.keys(filterJson).map((key) => {
|
||||
if (!allowedWorkflowsQueryFilterFields.includes(key)) delete filterJson[key];
|
||||
});
|
||||
if (jsonSchemaValidate(filterJson, schemaGetWorkflowsQueryFilter).valid) {
|
||||
filter = filterJson as IGetWorkflowsQueryFilter;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
LoggerProxy.error('Failed to parse filter', {
|
||||
userId: user.id,
|
||||
filter,
|
||||
});
|
||||
throw new ResponseHelper.ResponseError(
|
||||
`Parameter "filter" contained invalid JSON string.`,
|
||||
500,
|
||||
500,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// safeguard against querying ids not shared with the user
|
||||
if (filter?.id !== undefined) {
|
||||
const workflowId = parseInt(filter.id.toString());
|
||||
if (workflowId && !sharedWorkflowIds.includes(workflowId)) {
|
||||
LoggerProxy.verbose(`User ${user.id} attempted to query non-shared workflow ${workflowId}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const fields: Array<keyof WorkflowEntity> = ['id', 'name', 'active', 'createdAt', 'updatedAt'];
|
||||
|
||||
const query: FindManyOptions<WorkflowEntity> = {
|
||||
select: config.get('enterprise.features.sharing') ? [...fields, 'nodes'] : fields,
|
||||
relations: config.get('enterprise.features.sharing')
|
||||
? ['tags', 'shared', 'shared.user', 'shared.role']
|
||||
: ['tags'],
|
||||
};
|
||||
|
||||
if (config.getEnv('workflowTagsDisabled')) {
|
||||
delete query.relations;
|
||||
}
|
||||
|
||||
const workflows = await Db.collections.Workflow.find(
|
||||
Object.assign(query, {
|
||||
where: {
|
||||
id: In(sharedWorkflowIds),
|
||||
...filter,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return workflows.map((workflow) => {
|
||||
const { id, ...rest } = workflow;
|
||||
|
||||
return {
|
||||
id: id.toString(),
|
||||
...rest,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
static async updateWorkflow(
|
||||
user: User,
|
||||
workflow: WorkflowEntity,
|
||||
|
||||
Reference in New Issue
Block a user