mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
feat: RBAC (#8922)
Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> Co-authored-by: Val <68596159+valya@users.noreply.github.com> Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in> Co-authored-by: Valya Bullions <valya@n8n.io> Co-authored-by: Danny Martini <danny@n8n.io> Co-authored-by: Danny Martini <despair.blue@gmail.com> Co-authored-by: Iván Ovejero <ivov.src@gmail.com> Co-authored-by: Omar Ajoue <krynble@gmail.com> Co-authored-by: oleg <me@olegivaniv.com> Co-authored-by: Michael Kret <michael.k@radency.com> Co-authored-by: Michael Kret <88898367+michael-radency@users.noreply.github.com> Co-authored-by: Elias Meire <elias@meire.dev> Co-authored-by: Giulio Andreini <andreini@netseven.it> Co-authored-by: Giulio Andreini <g.andreini@gmail.com> Co-authored-by: Ayato Hayashi <go12limchangyong@gmail.com>
This commit is contained in:
@@ -13,6 +13,11 @@ import { SharedCredentialsRepository } from '@/databases/repositories/sharedCred
|
||||
import { mockInstance } from '../../shared/mocking';
|
||||
import type { SourceControlledFile } from '@/environments/sourceControl/types/sourceControlledFile';
|
||||
import type { ExportableCredential } from '@/environments/sourceControl/types/exportableCredential';
|
||||
import { createTeamProject, getPersonalProject } from '../shared/db/projects';
|
||||
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||
import { saveCredential } from '../shared/db/credentials';
|
||||
import { randomCredentialPayload } from '../shared/random';
|
||||
import { CredentialsRepository } from '@/databases/repositories/credentials.repository';
|
||||
|
||||
describe('SourceControlImportService', () => {
|
||||
let service: SourceControlImportService;
|
||||
@@ -66,9 +71,11 @@ describe('SourceControlImportService', () => {
|
||||
importingUser.id,
|
||||
);
|
||||
|
||||
const personalProject = await getPersonalProject(member);
|
||||
|
||||
const sharing = await Container.get(SharedCredentialsRepository).findOneBy({
|
||||
credentialsId: CREDENTIAL_ID,
|
||||
userId: member.id,
|
||||
projectId: personalProject.id,
|
||||
role: 'credential:owner',
|
||||
});
|
||||
|
||||
@@ -101,9 +108,11 @@ describe('SourceControlImportService', () => {
|
||||
importingUser.id,
|
||||
);
|
||||
|
||||
const personalProject = await getPersonalProject(importingUser);
|
||||
|
||||
const sharing = await Container.get(SharedCredentialsRepository).findOneBy({
|
||||
credentialsId: CREDENTIAL_ID,
|
||||
userId: importingUser.id,
|
||||
projectId: personalProject.id,
|
||||
role: 'credential:owner',
|
||||
});
|
||||
|
||||
@@ -136,9 +145,11 @@ describe('SourceControlImportService', () => {
|
||||
importingUser.id,
|
||||
);
|
||||
|
||||
const personalProject = await getPersonalProject(importingUser);
|
||||
|
||||
const sharing = await Container.get(SharedCredentialsRepository).findOneBy({
|
||||
credentialsId: CREDENTIAL_ID,
|
||||
userId: importingUser.id,
|
||||
projectId: personalProject.id,
|
||||
role: 'credential:owner',
|
||||
});
|
||||
|
||||
@@ -146,4 +157,199 @@ describe('SourceControlImportService', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('if owner specified by `ownedBy` does not exist at target instance', () => {
|
||||
it('should assign the credential ownership to the importing user if it was owned by a personal project in the source instance', async () => {
|
||||
const importingUser = await getGlobalOwner();
|
||||
|
||||
fsp.readFile = jest.fn().mockResolvedValue(Buffer.from('some-content'));
|
||||
|
||||
const CREDENTIAL_ID = nanoid();
|
||||
|
||||
const stub: ExportableCredential = {
|
||||
id: CREDENTIAL_ID,
|
||||
name: 'My Credential',
|
||||
type: 'someCredentialType',
|
||||
data: {},
|
||||
ownedBy: {
|
||||
type: 'personal',
|
||||
personalEmail: 'test@example.com',
|
||||
}, // user at source instance owns credential
|
||||
};
|
||||
|
||||
jest.spyOn(utils, 'jsonParse').mockReturnValue(stub);
|
||||
|
||||
cipher.encrypt.mockReturnValue('some-encrypted-data');
|
||||
|
||||
await service.importCredentialsFromWorkFolder(
|
||||
[mock<SourceControlledFile>({ id: CREDENTIAL_ID })],
|
||||
importingUser.id,
|
||||
);
|
||||
|
||||
const personalProject = await getPersonalProject(importingUser);
|
||||
|
||||
const sharing = await Container.get(SharedCredentialsRepository).findOneBy({
|
||||
credentialsId: CREDENTIAL_ID,
|
||||
projectId: personalProject.id,
|
||||
role: 'credential:owner',
|
||||
});
|
||||
|
||||
expect(sharing).toBeTruthy(); // original user missing, so importing user owns credential
|
||||
});
|
||||
|
||||
it('should create a new team project if the credential was owned by a team project in the source instance', async () => {
|
||||
const importingUser = await getGlobalOwner();
|
||||
|
||||
fsp.readFile = jest.fn().mockResolvedValue(Buffer.from('some-content'));
|
||||
|
||||
const CREDENTIAL_ID = nanoid();
|
||||
|
||||
const stub: ExportableCredential = {
|
||||
id: CREDENTIAL_ID,
|
||||
name: 'My Credential',
|
||||
type: 'someCredentialType',
|
||||
data: {},
|
||||
ownedBy: {
|
||||
type: 'team',
|
||||
teamId: '1234-asdf',
|
||||
teamName: 'Marketing',
|
||||
}, // user at source instance owns credential
|
||||
};
|
||||
|
||||
jest.spyOn(utils, 'jsonParse').mockReturnValue(stub);
|
||||
|
||||
cipher.encrypt.mockReturnValue('some-encrypted-data');
|
||||
|
||||
{
|
||||
const project = await Container.get(ProjectRepository).findOne({
|
||||
where: [
|
||||
{
|
||||
id: '1234-asdf',
|
||||
},
|
||||
{ name: 'Marketing' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(project?.id).not.toBe('1234-asdf');
|
||||
expect(project?.name).not.toBe('Marketing');
|
||||
}
|
||||
|
||||
await service.importCredentialsFromWorkFolder(
|
||||
[mock<SourceControlledFile>({ id: CREDENTIAL_ID })],
|
||||
importingUser.id,
|
||||
);
|
||||
|
||||
const sharing = await Container.get(SharedCredentialsRepository).findOne({
|
||||
where: {
|
||||
credentialsId: CREDENTIAL_ID,
|
||||
role: 'credential:owner',
|
||||
},
|
||||
relations: { project: true },
|
||||
});
|
||||
|
||||
expect(sharing?.project.id).toBe('1234-asdf');
|
||||
expect(sharing?.project.name).toBe('Marketing');
|
||||
expect(sharing?.project.type).toBe('team');
|
||||
|
||||
expect(sharing).toBeTruthy(); // original user missing, so importing user owns credential
|
||||
});
|
||||
});
|
||||
|
||||
describe('if owner specified by `ownedBy` does exist at target instance', () => {
|
||||
it('should use the existing team project if credential owning project is found', async () => {
|
||||
const importingUser = await getGlobalOwner();
|
||||
|
||||
fsp.readFile = jest.fn().mockResolvedValue(Buffer.from('some-content'));
|
||||
|
||||
const CREDENTIAL_ID = nanoid();
|
||||
|
||||
const project = await createTeamProject('Sales');
|
||||
|
||||
const stub: ExportableCredential = {
|
||||
id: CREDENTIAL_ID,
|
||||
name: 'My Credential',
|
||||
type: 'someCredentialType',
|
||||
data: {},
|
||||
ownedBy: {
|
||||
type: 'team',
|
||||
teamId: project.id,
|
||||
teamName: 'Sales',
|
||||
},
|
||||
};
|
||||
|
||||
jest.spyOn(utils, 'jsonParse').mockReturnValue(stub);
|
||||
|
||||
cipher.encrypt.mockReturnValue('some-encrypted-data');
|
||||
|
||||
await service.importCredentialsFromWorkFolder(
|
||||
[mock<SourceControlledFile>({ id: CREDENTIAL_ID })],
|
||||
importingUser.id,
|
||||
);
|
||||
|
||||
const sharing = await Container.get(SharedCredentialsRepository).findOneBy({
|
||||
credentialsId: CREDENTIAL_ID,
|
||||
projectId: project.id,
|
||||
role: 'credential:owner',
|
||||
});
|
||||
|
||||
expect(sharing).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not change the owner if the credential is owned by somebody else on the target instance', async () => {
|
||||
cipher.encrypt.mockReturnValue('some-encrypted-data');
|
||||
|
||||
const importingUser = await getGlobalOwner();
|
||||
|
||||
fsp.readFile = jest.fn().mockResolvedValue(Buffer.from('some-content'));
|
||||
|
||||
const targetProject = await createTeamProject('Marketing');
|
||||
const credential = await saveCredential(randomCredentialPayload(), {
|
||||
project: targetProject,
|
||||
role: 'credential:owner',
|
||||
});
|
||||
|
||||
const sourceProjectId = nanoid();
|
||||
|
||||
const stub: ExportableCredential = {
|
||||
id: credential.id,
|
||||
name: 'My Credential',
|
||||
type: 'someCredentialType',
|
||||
data: {},
|
||||
ownedBy: {
|
||||
type: 'team',
|
||||
teamId: sourceProjectId,
|
||||
teamName: 'Sales',
|
||||
},
|
||||
};
|
||||
|
||||
jest.spyOn(utils, 'jsonParse').mockReturnValue(stub);
|
||||
|
||||
await service.importCredentialsFromWorkFolder(
|
||||
[mock<SourceControlledFile>({ id: credential.id })],
|
||||
importingUser.id,
|
||||
);
|
||||
|
||||
await expect(
|
||||
Container.get(SharedCredentialsRepository).findBy({
|
||||
credentialsId: credential.id,
|
||||
}),
|
||||
).resolves.toMatchObject([
|
||||
{
|
||||
projectId: targetProject.id,
|
||||
role: 'credential:owner',
|
||||
},
|
||||
]);
|
||||
await expect(
|
||||
Container.get(CredentialsRepository).findBy({
|
||||
id: credential.id,
|
||||
}),
|
||||
).resolves.toMatchObject([
|
||||
{
|
||||
name: stub.name,
|
||||
type: stub.type,
|
||||
data: 'some-encrypted-data',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user