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:
Csaba Tuncsik
2024-05-17 10:53:15 +02:00
committed by GitHub
parent b1f977ebd0
commit 596c472ecc
292 changed files with 14129 additions and 3989 deletions

View File

@@ -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',
},
]);
});
});
});