Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(core): Consolidate CredentialsService.getMany() (no-changelog) #7028

Merged
merged 39 commits into from
Sep 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
ea1a3c8
feat(core): Add filtering, selection and pagination to users
ivov Aug 23, 2023
936a14c
Complete implementation
ivov Aug 24, 2023
c509bc3
De-duplicate validation
ivov Aug 24, 2023
be06c1f
Cleanup
ivov Aug 24, 2023
d4506af
Consolidate feature flags
ivov Aug 24, 2023
28c3cf1
More deduplication
ivov Aug 24, 2023
294c890
Readability
ivov Aug 24, 2023
61eb6ba
Remove unrelated change
ivov Aug 24, 2023
ac4c694
Merge master, resolve conflicts
ivov Aug 24, 2023
d242209
Fix MFA issue
ivov Aug 24, 2023
954b97d
Fix test
ivov Aug 24, 2023
b7e8eb3
Fix pending tests
ivov Aug 24, 2023
160daa9
Support `firstName` and `lastName` filters
ivov Aug 25, 2023
8ae8522
Fix combination of options requiring auxiliary fields
ivov Aug 25, 2023
87e8d00
Merge master, resolve conflicts
ivov Aug 25, 2023
07b6fcd
Fix `take` without selection
ivov Aug 25, 2023
9fb9cb5
Remove early return
ivov Aug 25, 2023
22c8684
Fix lint
ivov Aug 25, 2023
ed185c5
Cleanup
ivov Aug 25, 2023
460b591
Add clarifying comment
ivov Aug 25, 2023
c023c83
Initial setup
ivov Aug 28, 2023
b8e7253
Fix build
ivov Aug 28, 2023
d763fb4
Cleanup
ivov Aug 28, 2023
a078178
Fix `credentials.test.ts`
ivov Aug 28, 2023
e58f46e
Fix `workflows.controller.ee.test.ts`
ivov Aug 28, 2023
bdf9ff0
Remove logging
ivov Aug 28, 2023
89f72b3
Cleanup
ivov Aug 28, 2023
69f57ba
Add missing param
ivov Aug 28, 2023
5a875f4
Merge branch 'master' into pay-647
ivov Aug 28, 2023
f1cd686
Move method
ivov Aug 28, 2023
e154504
Reference base service
ivov Aug 28, 2023
b8af009
Skip test causing timeouts
ivov Aug 28, 2023
802c2af
Remove duplication
ivov Aug 28, 2023
e805ee1
Improve typing
ivov Aug 28, 2023
006f2ff
Simplify typings
ivov Aug 28, 2023
b7fad9c
Merge branch 'master' into pay-647
ivov Sep 1, 2023
23af1ad
Fix lint
ivov Sep 1, 2023
a3d282c
Unit tests for `addOwnedByAndSharedWith()`
ivov Sep 1, 2023
90ed65e
Merge branch 'master' into pay-647
ivov Sep 4, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions packages/cli/src/WorkflowHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,7 @@ import {
} from 'n8n-workflow';
import { v4 as uuid } from 'uuid';
import * as Db from '@/Db';
import type {
ICredentialsDb,
IExecutionDb,
IWorkflowErrorData,
IWorkflowExecutionDataProcess,
} from '@/Interfaces';
import type { IExecutionDb, IWorkflowErrorData, IWorkflowExecutionDataProcess } from '@/Interfaces';
import { NodeTypes } from '@/NodeTypes';
// eslint-disable-next-line import/no-cycle
import { WorkflowRunner } from '@/WorkflowRunner';
Expand All @@ -45,6 +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';

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

Expand Down Expand Up @@ -543,7 +539,7 @@ export function getNodesWithInaccessibleCreds(workflow: WorkflowEntity, userCred
export function validateWorkflowCredentialUsage(
newWorkflowVersion: WorkflowEntity,
previousWorkflowVersion: WorkflowEntity,
credentialsUserHasAccessTo: ICredentialsDb[],
credentialsUserHasAccessTo: Credentials.WithOwnedByAndSharedWith[],
) {
/**
* We only need to check nodes that use credentials the current user cannot access,
Expand Down
31 changes: 5 additions & 26 deletions packages/cli/src/credentials/credentials.controller.ee.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import express from 'express';
import type { INodeCredentialTestResult } from 'n8n-workflow';
import { deepCopy, LoggerProxy } from 'n8n-workflow';
import { deepCopy } from 'n8n-workflow';
import * as Db from '@/Db';
import * as ResponseHelper from '@/ResponseHelper';
import type { CredentialsEntity } from '@db/entities/CredentialsEntity';

import type { CredentialRequest } from '@/requests';
import { isSharingEnabled, rightDiff } from '@/UserManagement/UserManagementHelper';
import { EECredentialsService as EECredentials } from './credentials.service.ee';
import type { CredentialWithSharings } from './credentials.types';
import { OwnershipService } from '@/services/ownership.service';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
import type { CredentialsEntity } from '@/databases/entities/CredentialsEntity';

// eslint-disable-next-line @typescript-eslint/naming-convention
export const EECredentialsController = express.Router();
Expand All @@ -25,27 +25,6 @@ EECredentialsController.use((req, res, next) => {
next();
});

/**
* GET /credentials
*/
EECredentialsController.get(
'/',
ResponseHelper.send(async (req: CredentialRequest.GetAll): Promise<CredentialWithSharings[]> => {
try {
const allCredentials = await EECredentials.getAll(req.user, {
relations: ['shared', 'shared.role', 'shared.user'],
});

return allCredentials.map((credential: CredentialsEntity & CredentialWithSharings) =>
EECredentials.addOwnerAndSharings(credential),
);
} catch (error) {
LoggerProxy.error('Request to list credentials failed', error as Error);
throw error;
}
}),
);

/**
* GET /credentials/:id
*/
Expand All @@ -59,7 +38,7 @@ EECredentialsController.get(
let credential = (await EECredentials.get(
{ id: credentialId },
{ relations: ['shared', 'shared.role', 'shared.user'] },
)) as CredentialsEntity & CredentialWithSharings;
)) as CredentialsEntity;

if (!credential) {
throw new ResponseHelper.NotFoundError(
Expand All @@ -73,7 +52,7 @@ EECredentialsController.get(
throw new ResponseHelper.UnauthorizedError('Forbidden.');
}

credential = EECredentials.addOwnerAndSharings(credential);
credential = Container.get(OwnershipService).addOwnedByAndSharedWith(credential);

if (!includeDecryptedData || !userSharing || userSharing.role.name !== 'owner') {
const { data: _, ...rest } = credential;
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/credentials/credentials.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ credentialsController.use('/', EECredentialsController);
*/
credentialsController.get(
'/',
ResponseHelper.send(async (req: CredentialRequest.GetAll): Promise<ICredentialsDb[]> => {
return CredentialsService.getAll(req.user, { roles: ['owner'] });
ResponseHelper.send(async (req: CredentialRequest.GetAll) => {
return CredentialsService.getMany(req.user);
}),
);

Expand Down
24 changes: 0 additions & 24 deletions packages/cli/src/credentials/credentials.service.ee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { SharedCredentials } from '@db/entities/SharedCredentials';
import type { User } from '@db/entities/User';
import { UserService } from '@/services/user.service';
import { CredentialsService } from './credentials.service';
import type { CredentialWithSharings } from './credentials.types';
import { RoleService } from '@/services/role.service';
import Container from 'typedi';

Expand Down Expand Up @@ -93,27 +92,4 @@ export class EECredentialsService extends CredentialsService {

return transaction.save(newSharedCredentials);
}

static addOwnerAndSharings(
credential: CredentialsEntity & CredentialWithSharings,
): CredentialsEntity & CredentialWithSharings {
credential.ownedBy = null;
credential.sharedWith = [];

credential.shared?.forEach(({ user, role }) => {
const { id, email, firstName, lastName } = user;

if (role.name === 'owner') {
credential.ownedBy = { id, email, firstName, lastName };
return;
}

credential.sharedWith?.push({ id, email, firstName, lastName });
});

// @ts-ignore
delete credential.shared;

return credential;
}
}
77 changes: 44 additions & 33 deletions packages/cli/src/credentials/credentials.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type { User } from '@db/entities/User';
import type { CredentialRequest } from '@/requests';
import { CredentialTypes } from '@/CredentialTypes';
import { RoleService } from '@/services/role.service';
import { OwnershipService } from '@/services/ownership.service';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { OwnershipService } from '@/services/ownership.service';
import { OwnershipService } from '@/services/ownership.service';
import { CredentialsRepository } from '@/databases/repositories';

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, what do you mean? I don't see CredentialsRepository used in this file?


export class CredentialsService {
static async get(
Expand All @@ -36,48 +37,58 @@ export class CredentialsService {
});
}

static async getAll(
user: User,
options?: { relations?: string[]; roles?: string[]; disableGlobalRole?: boolean },
): Promise<ICredentialsDb[]> {
const SELECT_FIELDS: Array<keyof ICredentialsDb> = [
'id',
'name',
'type',
'nodesAccess',
'createdAt',
'updatedAt',
];

// if instance owner, return all credentials

if (user.globalRole.name === 'owner' && options?.disableGlobalRole !== true) {
return Db.collections.Credentials.find({
select: SELECT_FIELDS,
relations: options?.relations,
});
static async getMany(user: User, options?: { disableGlobalRole: boolean }) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a good bit of logic in this function, can we add some tests to it? We need to assert that the repository is called with the correct relations, select dolumns and the correct query is performed depending on the user types and arguments.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type Select = Array<keyof ICredentialsDb>;

const select: Select = ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt'];

const relations = ['shared', 'shared.role', 'shared.user'];

const returnAll = user.globalRole.name === 'owner' && options?.disableGlobalRole !== true;

const addOwnedByAndSharedWith = (c: CredentialsEntity) =>
Container.get(OwnershipService).addOwnedByAndSharedWith(c);

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const credentials = await Db.collections.Credentials.find({ select, relations });
const credentials = await Container.get(CredentialsRepository).find({ select, relations });

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can try to remove references to Db.collections.Credentials and use the repository instead wdyt?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My reasoning for why this wasn't worth it is that, from what I understand, the root issue here is that CredentialsService (and many others like it) do not currently allow for dependency injection. This means, I can add calls to Container.get() but we'd have to touch this again when we refactor this properly, so it seems not worth it at this time.


return credentials.map(addOwnedByAndSharedWith);
}

// if member, return credentials owned by or shared with member
const userSharings = await Db.collections.SharedCredentials.find({
where: {
userId: user.id,
...(options?.roles?.length ? { role: { name: In(options.roles) } } : {}),
},
relations: options?.roles?.length ? ['role'] : [],
const ids = await CredentialsService.getAccessibleCredentials(user.id);

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

return Db.collections.Credentials.find({
select: SELECT_FIELDS,
relations: options?.relations,
return credentials.map(addOwnedByAndSharedWith);
}

/**
* Get the IDs of all credentials owned by or shared with a user.
*/
private static async getAccessibleCredentials(userId: string) {
const sharings = await Db.collections.SharedCredentials.find({
relations: ['role'],
where: {
id: In(userSharings.map((x) => x.credentialsId)),
userId,
role: { name: In(['owner', 'user']), scope: 'credential' },
},
});

return sharings.map((s) => s.credentialsId);
}

static async getMany(filter: FindManyOptions<ICredentialsDb>): Promise<ICredentialsDb[]> {
return Db.collections.Credentials.find(filter);
static async getManyByIds(ids: string[], { withSharings } = { withSharings: false }) {
const options: FindManyOptions<CredentialsEntity> = { where: { id: In(ids) } };

if (withSharings) {
options.relations = ['shared', 'shared.user', 'shared.role'];
}

return Db.collections.Credentials.find(options);
}

/**
Expand Down
7 changes: 0 additions & 7 deletions packages/cli/src/credentials/credentials.types.ts

This file was deleted.

11 changes: 11 additions & 0 deletions packages/cli/src/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type { User } from '@db/entities/User';
import type { UserManagementMailer } from '@/UserManagement/email';
import type { Variables } from '@db/entities/Variables';
import type { WorkflowEntity } from './databases/entities/WorkflowEntity';
import type { CredentialsEntity } from './databases/entities/CredentialsEntity';

export class UserUpdatePayload implements Pick<User, 'email' | 'firstName' | 'lastName'> {
@IsEmail()
Expand Down Expand Up @@ -162,6 +163,16 @@ export namespace ListQuery {
}
}

export namespace Credentials {
type SlimUser = Pick<IUser, 'id' | 'email' | 'firstName' | 'lastName'>;

type OwnedByField = { ownedBy: SlimUser | null };

type SharedWithField = { sharedWith: SlimUser[] };

export type WithOwnedByAndSharedWith = CredentialsEntity & OwnedByField & SharedWithField;
}

export function hasSharing(
workflows: ListQuery.Workflow.Plain[] | ListQuery.Workflow.WithSharing[],
): workflows is ListQuery.Workflow.WithSharing[] {
Expand Down
24 changes: 23 additions & 1 deletion packages/cli/src/services/ownership.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { SharedWorkflowRepository } from '@/databases/repositories';
import type { User } from '@/databases/entities/User';
import { RoleService } from './role.service';
import { UserService } from './user.service';
import type { ListQuery } from '@/requests';
import type { Credentials, ListQuery } from '@/requests';
import type { Role } from '@/databases/entities/Role';
import type { CredentialsEntity } from '@/databases/entities/CredentialsEntity';

@Service()
export class OwnershipService {
Expand Down Expand Up @@ -50,4 +51,25 @@ export class OwnershipService {
ownedBy: ownerId ? { id: ownerId } : null,
});
}

addOwnedByAndSharedWith(_credential: CredentialsEntity): Credentials.WithOwnedByAndSharedWith {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
addOwnedByAndSharedWith(_credential: CredentialsEntity): Credentials.WithOwnedByAndSharedWith {
addOwnedByAndSharedWithToCredentials(_credential: CredentialsEntity): Credentials.WithOwnedByAndSharedWith {

Simply because we might do the same for workflows, right? We have the addOwnedBy above that could be renamed to contain also clarification about what it applies to (maybe out of context of this PR)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also can we add some unit tests covering this function?

Copy link
Contributor Author

@ivov ivov Sep 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simply because we might do the same for workflows, right? We have the addOwnedBy above that could be renamed to contain also clarification about what it applies to (maybe out of context of this PR)

I think this is clear from the typing? Different entities require different fields, and the caller cannot misuse the methods - the typing will block them if they try to.

Also can we add some unit tests covering this function?

Will do! Edit: Done!

const { shared, ...rest } = _credential;

const credential = rest as Credentials.WithOwnedByAndSharedWith;

credential.ownedBy = null;
credential.sharedWith = [];

shared?.forEach(({ user, role }) => {
const { id, email, firstName, lastName } = user;

if (role.name === 'owner') {
credential.ownedBy = { id, email, firstName, lastName };
} else {
credential.sharedWith.push({ id, email, firstName, lastName });
}
});

return credential;
}
}
4 changes: 2 additions & 2 deletions packages/cli/src/workflows/workflows.controller.ee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { EEWorkflowsService as EEWorkflows } from './workflows.services.ee';
import { ExternalHooks } from '@/ExternalHooks';
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
import { LoggerProxy } from 'n8n-workflow';
import { EECredentialsService as EECredentials } from '../credentials/credentials.service.ee';
import { CredentialsService } from '../credentials/credentials.service';
import type { IExecutionPushResponse } from '@/Interfaces';
import * as GenericHelpers from '@/GenericHelpers';
import { In } from 'typeorm';
Expand Down Expand Up @@ -151,7 +151,7 @@ EEWorkflowController.post(
// This is a new workflow, so we simply check if the user has access to
// all used workflows

const allCredentials = await EECredentials.getAll(req.user);
const allCredentials = await CredentialsService.getMany(req.user);

try {
EEWorkflows.validateCredentialPermissionsToUser(newWorkflow, allCredentials);
Expand Down
22 changes: 11 additions & 11 deletions packages/cli/src/workflows/workflows.services.ee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { In, Not } from 'typeorm';
import * as Db from '@/Db';
import * as ResponseHelper from '@/ResponseHelper';
import * as WorkflowHelpers from '@/WorkflowHelpers';
import type { ICredentialsDb } from '@/Interfaces';
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
import type { User } from '@db/entities/User';
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
Expand All @@ -13,10 +12,11 @@ import type {
CredentialUsedByWorkflow,
WorkflowWithSharingsAndCredentials,
} from './workflows.types';
import { EECredentialsService as EECredentials } from '@/credentials/credentials.service.ee';
import { CredentialsService } from '@/credentials/credentials.service';
import { NodeOperationError } from 'n8n-workflow';
import { RoleService } from '@/services/role.service';
import Container from 'typedi';
import type { Credentials } from '@/requests';

export class EEWorkflowsService extends WorkflowsService {
static async isOwned(
Expand Down Expand Up @@ -106,7 +106,9 @@ export class EEWorkflowsService extends WorkflowsService {
currentUser: User,
): Promise<void> {
workflow.usedCredentials = [];
const userCredentials = await EECredentials.getAll(currentUser, { disableGlobalRole: true });
const userCredentials = await CredentialsService.getMany(currentUser, {
disableGlobalRole: true,
});
const credentialIdsUsedByWorkflow = new Set<string>();
workflow.nodes.forEach((node) => {
if (!node.credentials) {
Expand All @@ -120,12 +122,10 @@ export class EEWorkflowsService extends WorkflowsService {
credentialIdsUsedByWorkflow.add(credential.id);
});
});
const workflowCredentials = await EECredentials.getMany({
where: {
id: In(Array.from(credentialIdsUsedByWorkflow)),
},
relations: ['shared', 'shared.user', 'shared.role'],
});
const workflowCredentials = await CredentialsService.getManyByIds(
Array.from(credentialIdsUsedByWorkflow),
{ withSharings: true },
);
const userCredentialIds = userCredentials.map((credential) => credential.id);
workflowCredentials.forEach((credential) => {
const credentialId = credential.id;
Expand All @@ -151,7 +151,7 @@ export class EEWorkflowsService extends WorkflowsService {

static validateCredentialPermissionsToUser(
workflow: WorkflowEntity,
allowedCredentials: ICredentialsDb[],
allowedCredentials: Credentials.WithOwnedByAndSharedWith[],
) {
workflow.nodes.forEach((node) => {
if (!node.credentials) {
Expand All @@ -175,7 +175,7 @@ export class EEWorkflowsService extends WorkflowsService {
throw new ResponseHelper.NotFoundError('Workflow not found');
}

const allCredentials = await EECredentials.getAll(user);
const allCredentials = await CredentialsService.getMany(user);

try {
return WorkflowHelpers.validateWorkflowCredentialUsage(
Expand Down
Loading
Loading