Skip to content

Commit

Permalink
feat(core): Add list query middleware to credentials (#7041)
Browse files Browse the repository at this point in the history
  • Loading branch information
ivov authored Sep 4, 2023
1 parent 413e0bc commit fd78021
Show file tree
Hide file tree
Showing 9 changed files with 400 additions and 25 deletions.
4 changes: 2 additions & 2 deletions packages/cli/src/WorkflowHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import type { RoleNames } from '@db/entities/Role';
import { RoleService } from './services/role.service';
import { ExecutionRepository, RoleRepository } from './databases/repositories';
import { VariablesService } from './environments/variables/variables.service';
import type { Credentials } from './requests';
import type { CredentialsEntity } from './databases/entities/CredentialsEntity';

const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType');

Expand Down Expand Up @@ -539,7 +539,7 @@ export function getNodesWithInaccessibleCreds(workflow: WorkflowEntity, userCred
export function validateWorkflowCredentialUsage(
newWorkflowVersion: WorkflowEntity,
previousWorkflowVersion: WorkflowEntity,
credentialsUserHasAccessTo: Credentials.WithOwnedByAndSharedWith[],
credentialsUserHasAccessTo: CredentialsEntity[],
) {
/**
* We only need to check nodes that use credentials the current user cannot access,
Expand Down
8 changes: 5 additions & 3 deletions packages/cli/src/credentials/credentials.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import { EECredentialsController } from './credentials.controller.ee';
import { CredentialsService } from './credentials.service';

import type { ICredentialsDb } from '@/Interfaces';
import type { CredentialRequest } from '@/requests';
import type { CredentialRequest, ListQuery } from '@/requests';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
import { listQueryMiddleware } from '@/middlewares';

export const credentialsController = express.Router();

Expand All @@ -35,8 +36,9 @@ credentialsController.use('/', EECredentialsController);
*/
credentialsController.get(
'/',
ResponseHelper.send(async (req: CredentialRequest.GetAll) => {
return CredentialsService.getMany(req.user);
listQueryMiddleware,
ResponseHelper.send(async (req: ListQuery.Request) => {
return CredentialsService.getMany(req.user, { listQueryOptions: req.listQueryOptions });
}),
);

Expand Down
69 changes: 53 additions & 16 deletions packages/cli/src/credentials/credentials.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
import { CREDENTIAL_EMPTY_VALUE, deepCopy, LoggerProxy, NodeHelpers } from 'n8n-workflow';
import { Container } from 'typedi';
import type { FindManyOptions, FindOptionsWhere } from 'typeorm';
import { In } from 'typeorm';
import { In, Like } from 'typeorm';

import * as Db from '@/Db';
import * as ResponseHelper from '@/ResponseHelper';
Expand All @@ -21,7 +21,7 @@ import { SharedCredentials } from '@db/entities/SharedCredentials';
import { validateEntity } from '@/GenericHelpers';
import { ExternalHooks } from '@/ExternalHooks';
import type { User } from '@db/entities/User';
import type { CredentialRequest } from '@/requests';
import type { CredentialRequest, ListQuery } from '@/requests';
import { CredentialTypes } from '@/CredentialTypes';
import { RoleService } from '@/services/role.service';
import { OwnershipService } from '@/services/ownership.service';
Expand All @@ -37,33 +37,70 @@ export class CredentialsService {
});
}

static async getMany(user: User, options?: { disableGlobalRole: boolean }) {
type Select = Array<keyof ICredentialsDb>;
private static toFindManyOptions(listQueryOptions?: ListQuery.Options) {
const findManyOptions: FindManyOptions<CredentialsEntity> = {};

const select: Select = ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt'];
type Select = Array<keyof CredentialsEntity>;

const relations = ['shared', 'shared.role', 'shared.user'];
const defaultRelations = ['shared', 'shared.role', 'shared.user'];
const defaultSelect: Select = ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt'];

const returnAll = user.globalRole.name === 'owner' && options?.disableGlobalRole !== true;
if (!listQueryOptions) return { select: defaultSelect, relations: defaultRelations };

const addOwnedByAndSharedWith = (c: CredentialsEntity) =>
Container.get(OwnershipService).addOwnedByAndSharedWith(c);
const { filter, select, take, skip } = listQueryOptions;

if (typeof filter?.name === 'string' && filter?.name !== '') {
filter.name = Like(`%${filter.name}%`);
}

if (typeof filter?.type === 'string' && filter?.type !== '') {
filter.type = Like(`%${filter.type}%`);
}

if (filter) findManyOptions.where = filter;
if (select) findManyOptions.select = select;
if (take) findManyOptions.take = take;
if (skip) findManyOptions.skip = skip;

if (take && select && !select?.id) {
findManyOptions.select = { ...findManyOptions.select, id: true }; // pagination requires id
}

if (!findManyOptions.select) {
findManyOptions.select = defaultSelect;
findManyOptions.relations = defaultRelations;
}

return findManyOptions;
}

private static addOwnedByAndSharedWith(credentials: CredentialsEntity[]) {
return credentials.map((c) => Container.get(OwnershipService).addOwnedByAndSharedWith(c));
}

static async getMany(
user: User,
options: { listQueryOptions?: ListQuery.Options; onlyOwn?: boolean } = {},
) {
const findManyOptions = this.toFindManyOptions(options.listQueryOptions);

const returnAll = user.globalRole.name === 'owner' && !options.onlyOwn;
const isDefaultSelect = !options.listQueryOptions?.select;

if (returnAll) {
const credentials = await Db.collections.Credentials.find({ select, relations });
const credentials = await Db.collections.Credentials.find(findManyOptions);

return credentials.map(addOwnedByAndSharedWith);
return isDefaultSelect ? this.addOwnedByAndSharedWith(credentials) : credentials;
}

const ids = await CredentialsService.getAccessibleCredentials(user.id);
const ids = await this.getAccessibleCredentials(user.id);

const credentials = await Db.collections.Credentials.find({
select,
relations,
where: { id: In(ids) },
...findManyOptions,
where: { ...findManyOptions.where, id: In(ids) }, // only accessible credentials
});

return credentials.map(addOwnedByAndSharedWith);
return isDefaultSelect ? this.addOwnedByAndSharedWith(credentials) : credentials;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { IsOptional, IsString } from 'class-validator';
import { Expose } from 'class-transformer';
import { BaseFilter } from './base.filter.dto';

export class CredentialsFilter extends BaseFilter {
@IsString()
@IsOptional()
@Expose()
name?: string;

@IsString()
@IsOptional()
@Expose()
type?: string;

static async fromString(rawFilter: string) {
return this.toFilter(rawFilter, CredentialsFilter);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { BaseSelect } from './base.select.dto';

export class CredentialsSelect extends BaseSelect {
static get selectableFields() {
return new Set([
'id', // always included downstream
'name',
'type',
]);
}

static fromString(rawFilter: string) {
return this.toSelect(rawFilter, CredentialsSelect);
}
}
3 changes: 3 additions & 0 deletions packages/cli/src/middlewares/listQuery/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import * as ResponseHelper from '@/ResponseHelper';
import { WorkflowFilter } from './dtos/workflow.filter.dto';
import { CredentialsFilter } from './dtos/credentials.filter.dto';
import { UserFilter } from './dtos/user.filter.dto';
import { toError } from '@/utils';

Expand All @@ -21,6 +22,8 @@ export const filterListQueryMiddleware = async (

if (req.baseUrl.endsWith('workflows')) {
Filter = WorkflowFilter;
} else if (req.baseUrl.endsWith('credentials')) {
Filter = CredentialsFilter;
} else if (req.baseUrl.endsWith('users')) {
Filter = UserFilter;
} else {
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/middlewares/listQuery/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { WorkflowSelect } from './dtos/workflow.select.dto';
import { UserSelect } from './dtos/user.select.dto';
import { CredentialsSelect } from './dtos/credentials.select.dto';
import * as ResponseHelper from '@/ResponseHelper';
import { toError } from '@/utils';

Expand All @@ -17,6 +18,8 @@ export const selectListQueryMiddleware: RequestHandler = (req: ListQuery.Request

if (req.baseUrl.endsWith('workflows')) {
Select = WorkflowSelect;
} else if (req.baseUrl.endsWith('credentials')) {
Select = CredentialsSelect;
} else if (req.baseUrl.endsWith('users')) {
Select = UserSelect;
} else {
Expand Down
7 changes: 3 additions & 4 deletions packages/cli/src/workflows/workflows.services.ee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { CredentialsService } from '@/credentials/credentials.service';
import { NodeOperationError } from 'n8n-workflow';
import { RoleService } from '@/services/role.service';
import Container from 'typedi';
import type { CredentialsEntity } from '@/databases/entities/CredentialsEntity';
import type { Credentials } from '@/requests';

Check failure on line 20 in packages/cli/src/workflows/workflows.services.ee.ts

View workflow job for this annotation

GitHub Actions / Lint changes (18.x)

'Credentials' is defined but never used

Check failure on line 20 in packages/cli/src/workflows/workflows.services.ee.ts

View workflow job for this annotation

GitHub Actions / Lint changes (20.x)

'Credentials' is defined but never used

export class EEWorkflowsService extends WorkflowsService {
Expand Down Expand Up @@ -106,9 +107,7 @@ export class EEWorkflowsService extends WorkflowsService {
currentUser: User,
): Promise<void> {
workflow.usedCredentials = [];
const userCredentials = await CredentialsService.getMany(currentUser, {
disableGlobalRole: true,
});
const userCredentials = await CredentialsService.getMany(currentUser, { onlyOwn: true });
const credentialIdsUsedByWorkflow = new Set<string>();
workflow.nodes.forEach((node) => {
if (!node.credentials) {
Expand Down Expand Up @@ -151,7 +150,7 @@ export class EEWorkflowsService extends WorkflowsService {

static validateCredentialPermissionsToUser(
workflow: WorkflowEntity,
allowedCredentials: Credentials.WithOwnedByAndSharedWith[],
allowedCredentials: CredentialsEntity[],
) {
workflow.nodes.forEach((node) => {
if (!node.credentials) {
Expand Down
Loading

0 comments on commit fd78021

Please sign in to comment.