mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat(core): Add endpoint for querying credentials used in workflows (#15691) (no-changelog)
This commit is contained in:
@@ -72,6 +72,29 @@ export class ProjectController {
|
||||
}
|
||||
}
|
||||
|
||||
@Get('/:folderId/credentials')
|
||||
@ProjectScope('folder:read')
|
||||
async getFolderUsedCredentials(
|
||||
req: AuthenticatedRequest<{ projectId: string; folderId: string }>,
|
||||
_res: Response,
|
||||
) {
|
||||
const { projectId, folderId } = req.params;
|
||||
|
||||
try {
|
||||
const credentials = await this.enterpriseWorkflowService.getFolderUsedCredentials(
|
||||
req.user,
|
||||
folderId,
|
||||
projectId,
|
||||
);
|
||||
return credentials;
|
||||
} catch (e) {
|
||||
if (e instanceof FolderNotFoundError) {
|
||||
throw new NotFoundError(e.message);
|
||||
}
|
||||
throw new InternalServerError(undefined, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Patch('/:folderId')
|
||||
@ProjectScope('folder:update')
|
||||
async updateFolder(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SharedWorkflow, User } from '@n8n/db';
|
||||
import { SharedWorkflowRepository } from '@n8n/db';
|
||||
import { SharedWorkflowRepository, FolderRepository } from '@n8n/db';
|
||||
import { Service } from '@n8n/di';
|
||||
import { hasGlobalScope, rolesWithScope, type Scope } from '@n8n/permissions';
|
||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
@@ -9,7 +9,10 @@ import { In } from '@n8n/typeorm';
|
||||
|
||||
@Service()
|
||||
export class WorkflowFinderService {
|
||||
constructor(private readonly sharedWorkflowRepository: SharedWorkflowRepository) {}
|
||||
constructor(
|
||||
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
|
||||
private readonly folderRepository: FolderRepository,
|
||||
) {}
|
||||
|
||||
async findWorkflowForUser(
|
||||
workflowId: string,
|
||||
@@ -52,9 +55,28 @@ export class WorkflowFinderService {
|
||||
return sharedWorkflow.workflow;
|
||||
}
|
||||
|
||||
async findAllWorkflowsForUser(user: User, scopes: Scope[]) {
|
||||
async findAllWorkflowsForUser(
|
||||
user: User,
|
||||
scopes: Scope[],
|
||||
folderId?: string,
|
||||
projectId?: string,
|
||||
) {
|
||||
let where: FindOptionsWhere<SharedWorkflow> = {};
|
||||
|
||||
if (folderId) {
|
||||
const subFolderIds = await this.folderRepository.getAllFolderIdsInHierarchy(
|
||||
folderId,
|
||||
projectId,
|
||||
);
|
||||
|
||||
where = {
|
||||
...where,
|
||||
workflow: {
|
||||
parentFolder: In([folderId, ...subFolderIds]),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!hasGlobalScope(user, scopes, { mode: 'allOf' })) {
|
||||
const projectRoles = rolesWithScope('project', scopes);
|
||||
const workflowRoles = rolesWithScope('workflow', scopes);
|
||||
@@ -63,6 +85,7 @@ export class WorkflowFinderService {
|
||||
...where,
|
||||
role: In(workflowRoles),
|
||||
project: {
|
||||
...(projectId && { id: projectId }),
|
||||
projectRelations: {
|
||||
role: In(projectRoles),
|
||||
userId: user.id,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
CredentialsEntity,
|
||||
CredentialUsedByWorkflow,
|
||||
User,
|
||||
WorkflowEntity,
|
||||
WorkflowWithSharingsAndCredentials,
|
||||
@@ -350,6 +351,29 @@ export class EnterpriseWorkflowService {
|
||||
return;
|
||||
}
|
||||
|
||||
async getFolderUsedCredentials(user: User, folderId: string, projectId: string) {
|
||||
await this.folderService.findFolderInProjectOrFail(folderId, projectId);
|
||||
|
||||
const workflows = await this.workflowFinderService.findAllWorkflowsForUser(
|
||||
user,
|
||||
['workflow:read'],
|
||||
folderId,
|
||||
projectId,
|
||||
);
|
||||
|
||||
const usedCredentials = new Map<string, CredentialUsedByWorkflow>();
|
||||
|
||||
for (const workflow of workflows) {
|
||||
const workflowWithMetaData = this.addOwnerAndSharings(workflow as unknown as WorkflowEntity);
|
||||
await this.addCredentialsToWorkflow(workflowWithMetaData, user);
|
||||
for (const credential of workflowWithMetaData?.usedCredentials ?? []) {
|
||||
usedCredentials.set(credential.id, credential);
|
||||
}
|
||||
}
|
||||
|
||||
return [...usedCredentials.values()];
|
||||
}
|
||||
|
||||
async transferFolder(
|
||||
user: User,
|
||||
sourceProjectId: string,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { Project, ProjectRole } from '@n8n/db';
|
||||
import type { User } from '@n8n/db';
|
||||
import { FolderRepository } from '@n8n/db';
|
||||
@@ -10,6 +11,7 @@ import { ApplicationError, PROJECT_ROOT } from 'n8n-workflow';
|
||||
import { ActiveWorkflowManager } from '@/active-workflow-manager';
|
||||
import { mockInstance } from '@test/mocking';
|
||||
import {
|
||||
createCredentials,
|
||||
getCredentialSharings,
|
||||
saveCredential,
|
||||
shareCredentialWithProjects,
|
||||
@@ -306,6 +308,111 @@ describe('GET /projects/:projectId/folders/:folderId/tree', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /projects/:projectId/folders/:folderId/credentials', () => {
|
||||
test('should not get folder credentials when project does not exist', async () => {
|
||||
await authOwnerAgent
|
||||
.get('/projects/non-existing-id/folders/some-folder-id/credentials')
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
test('should not get folder credentials when folder does not exist', async () => {
|
||||
const project = await createTeamProject('test project', owner);
|
||||
|
||||
await authOwnerAgent
|
||||
.get(`/projects/${project.id}/folders/non-existing-folder/credentials`)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
test('should not get folder credentials if user has no access to project', async () => {
|
||||
const project = await createTeamProject('test project', owner);
|
||||
const folder = await createFolder(project);
|
||||
|
||||
await authMemberAgent
|
||||
.get(`/projects/${project.id}/folders/${folder.id}/credentials`)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
test("should not allow getting folder credentials from another user's personal project", async () => {
|
||||
const ownerPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id);
|
||||
const folder = await createFolder(ownerPersonalProject);
|
||||
|
||||
await authMemberAgent
|
||||
.get(`/projects/${ownerPersonalProject.id}/folders/${folder.id}/credentials`)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
test('should get all used credentials from workflows within the folder and subfolders', async () => {
|
||||
const project = await createTeamProject('test', owner);
|
||||
const rootFolder = await createFolder(project, { name: 'Root' });
|
||||
|
||||
const childFolder1 = await createFolder(project, {
|
||||
name: 'Child 1',
|
||||
parentFolder: rootFolder,
|
||||
});
|
||||
|
||||
await createFolder(project, {
|
||||
name: 'Child 2',
|
||||
parentFolder: rootFolder,
|
||||
});
|
||||
|
||||
const grandchildFolder = await createFolder(project, {
|
||||
name: 'Grandchild',
|
||||
parentFolder: childFolder1,
|
||||
});
|
||||
|
||||
for (const folder of [rootFolder, childFolder1, grandchildFolder]) {
|
||||
const credential = await createCredentials(
|
||||
{
|
||||
name: `Test credential ${folder.name}`,
|
||||
data: '',
|
||||
type: 'test',
|
||||
},
|
||||
project,
|
||||
);
|
||||
|
||||
await createWorkflow(
|
||||
{
|
||||
name: 'Test Workflow',
|
||||
parentFolder: folder,
|
||||
active: false,
|
||||
nodes: [
|
||||
{
|
||||
parameters: {},
|
||||
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||
typeVersion: 1.2,
|
||||
position: [0, 0],
|
||||
id: faker.string.uuid(),
|
||||
name: 'OpenAI Chat Model',
|
||||
credentials: {
|
||||
openAiApi: {
|
||||
id: credential.id,
|
||||
name: credential.name,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
owner,
|
||||
);
|
||||
}
|
||||
|
||||
const response = await authOwnerAgent
|
||||
.get(`/projects/${project.id}/folders/${childFolder1.id}/credentials`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.data).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: expect.stringContaining('Test credential Child 1'),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: expect.stringContaining('Test credential Grandchild'),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /projects/:projectId/folders/:folderId', () => {
|
||||
test('should not update folder when project does not exist', async () => {
|
||||
const payload = {
|
||||
|
||||
Reference in New Issue
Block a user