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:
@@ -6,15 +6,19 @@ import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.
|
||||
import type { CredentialSharingRole } from '@db/entities/SharedCredentials';
|
||||
import type { ICredentialsDb } from '@/Interfaces';
|
||||
import type { CredentialPayload } from '../types';
|
||||
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||
import type { Project } from '@/databases/entities/Project';
|
||||
|
||||
async function encryptCredentialData(credential: CredentialsEntity) {
|
||||
export async function encryptCredentialData(
|
||||
credential: CredentialsEntity,
|
||||
): Promise<ICredentialsDb> {
|
||||
const { createCredentialsFromCredentialsEntity } = await import('@/CredentialsHelper');
|
||||
const coreCredential = createCredentialsFromCredentialsEntity(credential, true);
|
||||
|
||||
// @ts-ignore
|
||||
coreCredential.setData(credential.data);
|
||||
|
||||
return coreCredential.getDataToSave() as ICredentialsDb;
|
||||
return Object.assign(credential, coreCredential.getDataToSave());
|
||||
}
|
||||
|
||||
const emptyAttributes = {
|
||||
@@ -46,43 +50,89 @@ export async function createCredentials(attributes: Partial<CredentialsEntity> =
|
||||
*/
|
||||
export async function saveCredential(
|
||||
credentialPayload: CredentialPayload,
|
||||
{ user, role }: { user: User; role: CredentialSharingRole },
|
||||
options:
|
||||
| { user: User; role: CredentialSharingRole }
|
||||
| {
|
||||
project: Project;
|
||||
role: CredentialSharingRole;
|
||||
},
|
||||
) {
|
||||
const role = options.role;
|
||||
const newCredential = new CredentialsEntity();
|
||||
|
||||
Object.assign(newCredential, credentialPayload);
|
||||
|
||||
const encryptedData = await encryptCredentialData(newCredential);
|
||||
|
||||
Object.assign(newCredential, encryptedData);
|
||||
await encryptCredentialData(newCredential);
|
||||
|
||||
const savedCredential = await Container.get(CredentialsRepository).save(newCredential);
|
||||
|
||||
savedCredential.data = newCredential.data;
|
||||
|
||||
await Container.get(SharedCredentialsRepository).save({
|
||||
user,
|
||||
credentials: savedCredential,
|
||||
role,
|
||||
});
|
||||
if ('user' in options) {
|
||||
const user = options.user;
|
||||
const personalProject = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail(
|
||||
user.id,
|
||||
);
|
||||
|
||||
await Container.get(SharedCredentialsRepository).save({
|
||||
user,
|
||||
credentials: savedCredential,
|
||||
role,
|
||||
project: personalProject,
|
||||
});
|
||||
} else {
|
||||
const project = options.project;
|
||||
|
||||
await Container.get(SharedCredentialsRepository).save({
|
||||
credentials: savedCredential,
|
||||
role,
|
||||
project,
|
||||
});
|
||||
}
|
||||
|
||||
return savedCredential;
|
||||
}
|
||||
|
||||
export async function shareCredentialWithUsers(credential: CredentialsEntity, users: User[]) {
|
||||
const newSharedCredentials = users.map((user) =>
|
||||
Container.get(SharedCredentialsRepository).create({
|
||||
userId: user.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:user',
|
||||
const newSharedCredentials = await Promise.all(
|
||||
users.map(async (user) => {
|
||||
const personalProject = await Container.get(
|
||||
ProjectRepository,
|
||||
).getPersonalProjectForUserOrFail(user.id);
|
||||
|
||||
return Container.get(SharedCredentialsRepository).create({
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:user',
|
||||
projectId: personalProject.id,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return await Container.get(SharedCredentialsRepository).save(newSharedCredentials);
|
||||
}
|
||||
|
||||
export async function shareCredentialWithProjects(
|
||||
credential: CredentialsEntity,
|
||||
projects: Project[],
|
||||
) {
|
||||
const newSharedCredentials = await Promise.all(
|
||||
projects.map(async (project) => {
|
||||
return Container.get(SharedCredentialsRepository).create({
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:user',
|
||||
projectId: project.id,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return await Container.get(SharedCredentialsRepository).save(newSharedCredentials);
|
||||
}
|
||||
|
||||
export function affixRoleToSaveCredential(role: CredentialSharingRole) {
|
||||
return async (credentialPayload: CredentialPayload, { user }: { user: User }) =>
|
||||
await saveCredential(credentialPayload, { user, role });
|
||||
return async (
|
||||
credentialPayload: CredentialPayload,
|
||||
options: { user: User } | { project: Project },
|
||||
) => await saveCredential(credentialPayload, { ...options, role });
|
||||
}
|
||||
|
||||
export async function getAllCredentials() {
|
||||
|
||||
63
packages/cli/test/integration/shared/db/projects.ts
Normal file
63
packages/cli/test/integration/shared/db/projects.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import Container from 'typedi';
|
||||
|
||||
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||
import { randomName } from '../random';
|
||||
import { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository';
|
||||
import type { User } from '@/databases/entities/User';
|
||||
import type { Project } from '@/databases/entities/Project';
|
||||
import type { ProjectRelation, ProjectRole } from '@/databases/entities/ProjectRelation';
|
||||
|
||||
export const createTeamProject = async (name?: string, adminUser?: User) => {
|
||||
const projectRepository = Container.get(ProjectRepository);
|
||||
const project = await projectRepository.save(
|
||||
projectRepository.create({
|
||||
name: name ?? randomName(),
|
||||
type: 'team',
|
||||
}),
|
||||
);
|
||||
|
||||
if (adminUser) {
|
||||
await linkUserToProject(adminUser, project, 'project:admin');
|
||||
}
|
||||
|
||||
return project;
|
||||
};
|
||||
|
||||
export const linkUserToProject = async (user: User, project: Project, role: ProjectRole) => {
|
||||
const projectRelationRepository = Container.get(ProjectRelationRepository);
|
||||
await projectRelationRepository.save(
|
||||
projectRelationRepository.create({
|
||||
projectId: project.id,
|
||||
userId: user.id,
|
||||
role,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export const getPersonalProject = async (user: User): Promise<Project> => {
|
||||
return await Container.get(ProjectRepository).findOneOrFail({
|
||||
where: {
|
||||
projectRelations: {
|
||||
userId: user.id,
|
||||
role: 'project:personalOwner',
|
||||
},
|
||||
type: 'personal',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const findProject = async (id: string): Promise<Project> => {
|
||||
return await Container.get(ProjectRepository).findOneOrFail({
|
||||
where: { id },
|
||||
});
|
||||
};
|
||||
|
||||
export const getProjectRelations = async ({
|
||||
projectId,
|
||||
userId,
|
||||
role,
|
||||
}: Partial<ProjectRelation>): Promise<ProjectRelation[]> => {
|
||||
return await Container.get(ProjectRelationRepository).find({
|
||||
where: { projectId, userId, role },
|
||||
});
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import Container from 'typedi';
|
||||
import { hash } from 'bcryptjs';
|
||||
import { AuthIdentity } from '@db/entities/AuthIdentity';
|
||||
import type { GlobalRole, User } from '@db/entities/User';
|
||||
import { type GlobalRole, type User } from '@db/entities/User';
|
||||
import { AuthIdentityRepository } from '@db/repositories/authIdentity.repository';
|
||||
import { UserRepository } from '@db/repositories/user.repository';
|
||||
import { TOTPService } from '@/Mfa/totp.service';
|
||||
@@ -27,9 +27,10 @@ export async function newUser(attributes: Partial<User> = {}): Promise<User> {
|
||||
|
||||
/** Store a user object in the DB */
|
||||
export async function createUser(attributes: Partial<User> = {}): Promise<User> {
|
||||
const user = await newUser(attributes);
|
||||
const userInstance = await newUser(attributes);
|
||||
const { user } = await Container.get(UserRepository).createUserWithProject(userInstance);
|
||||
user.computeIsOwner();
|
||||
return await Container.get(UserRepository).save(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function createLdapUser(attributes: Partial<User>, ldapId: string): Promise<User> {
|
||||
@@ -90,7 +91,8 @@ export async function createUserShell(role: GlobalRole): Promise<User> {
|
||||
shell.email = randomEmail();
|
||||
}
|
||||
|
||||
return await Container.get(UserRepository).save(shell);
|
||||
const { user } = await Container.get(UserRepository).createUserWithProject(shell);
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,12 +102,15 @@ export async function createManyUsers(
|
||||
amount: number,
|
||||
attributes: Partial<User> = {},
|
||||
): Promise<User[]> {
|
||||
const users = await Promise.all(
|
||||
const result = await Promise.all(
|
||||
Array(amount)
|
||||
.fill(0)
|
||||
.map(async () => await newUser(attributes)),
|
||||
.map(async () => {
|
||||
const userInstance = await newUser(attributes);
|
||||
return await Container.get(UserRepository).createUserWithProject(userInstance);
|
||||
}),
|
||||
);
|
||||
return await Container.get(UserRepository).save(users);
|
||||
return result.map((result) => result.user);
|
||||
}
|
||||
|
||||
export async function addApiKey(user: User): Promise<User> {
|
||||
@@ -127,7 +132,7 @@ export const getUserById = async (id: string) =>
|
||||
export const getLdapIdentities = async () =>
|
||||
await Container.get(AuthIdentityRepository).find({
|
||||
where: { providerType: 'ldap' },
|
||||
relations: ['user'],
|
||||
relations: { user: true },
|
||||
});
|
||||
|
||||
export async function getGlobalOwner() {
|
||||
|
||||
@@ -2,11 +2,13 @@ import Container from 'typedi';
|
||||
import type { DeepPartial } from '@n8n/typeorm';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import type { User } from '@db/entities/User';
|
||||
import { User } from '@db/entities/User';
|
||||
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
||||
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
|
||||
import { WorkflowRepository } from '@db/repositories/workflow.repository';
|
||||
import type { SharedWorkflow } from '@db/entities/SharedWorkflow';
|
||||
import type { SharedWorkflow, WorkflowSharingRole } from '@db/entities/SharedWorkflow';
|
||||
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||
import { Project } from '@/databases/entities/Project';
|
||||
|
||||
export async function createManyWorkflows(
|
||||
amount: number,
|
||||
@@ -48,28 +50,71 @@ export function newWorkflow(attributes: Partial<WorkflowEntity> = {}): WorkflowE
|
||||
* @param attributes workflow attributes
|
||||
* @param user user to assign the workflow to
|
||||
*/
|
||||
export async function createWorkflow(attributes: Partial<WorkflowEntity> = {}, user?: User) {
|
||||
export async function createWorkflow(
|
||||
attributes: Partial<WorkflowEntity> = {},
|
||||
userOrProject?: User | Project,
|
||||
) {
|
||||
const workflow = await Container.get(WorkflowRepository).save(newWorkflow(attributes));
|
||||
|
||||
if (user) {
|
||||
await Container.get(SharedWorkflowRepository).save({
|
||||
user,
|
||||
workflow,
|
||||
role: 'workflow:owner',
|
||||
});
|
||||
if (userOrProject instanceof User) {
|
||||
const user = userOrProject;
|
||||
const project = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail(user.id);
|
||||
await Container.get(SharedWorkflowRepository).save(
|
||||
Container.get(SharedWorkflowRepository).create({
|
||||
project,
|
||||
workflow,
|
||||
role: 'workflow:owner',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (userOrProject instanceof Project) {
|
||||
const project = userOrProject;
|
||||
await Container.get(SharedWorkflowRepository).save(
|
||||
Container.get(SharedWorkflowRepository).create({
|
||||
project,
|
||||
workflow,
|
||||
role: 'workflow:owner',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return workflow;
|
||||
}
|
||||
|
||||
export async function shareWorkflowWithUsers(workflow: WorkflowEntity, users: User[]) {
|
||||
const sharedWorkflows: Array<DeepPartial<SharedWorkflow>> = users.map((user) => ({
|
||||
userId: user.id,
|
||||
workflowId: workflow.id,
|
||||
role: 'workflow:editor',
|
||||
}));
|
||||
const sharedWorkflows: Array<DeepPartial<SharedWorkflow>> = await Promise.all(
|
||||
users.map(async (user) => {
|
||||
const project = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail(
|
||||
user.id,
|
||||
);
|
||||
return {
|
||||
projectId: project.id,
|
||||
workflowId: workflow.id,
|
||||
role: 'workflow:editor',
|
||||
};
|
||||
}),
|
||||
);
|
||||
return await Container.get(SharedWorkflowRepository).save(sharedWorkflows);
|
||||
}
|
||||
|
||||
export async function shareWorkflowWithProjects(
|
||||
workflow: WorkflowEntity,
|
||||
projectsWithRole: Array<{ project: Project; role?: WorkflowSharingRole }>,
|
||||
) {
|
||||
const newSharedWorkflow = await Promise.all(
|
||||
projectsWithRole.map(async ({ project, role }) => {
|
||||
return Container.get(SharedWorkflowRepository).create({
|
||||
workflowId: workflow.id,
|
||||
role: role ?? 'workflow:editor',
|
||||
projectId: project.id,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return await Container.get(SharedWorkflowRepository).save(newSharedWorkflow);
|
||||
}
|
||||
|
||||
export async function getWorkflowSharing(workflow: WorkflowEntity) {
|
||||
return await Container.get(SharedWorkflowRepository).findBy({
|
||||
workflowId: workflow.id,
|
||||
|
||||
33
packages/cli/test/integration/shared/ldap.ts
Normal file
33
packages/cli/test/integration/shared/ldap.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/Ldap/constants';
|
||||
import type { LdapConfig } from '@/Ldap/types';
|
||||
import { SettingsRepository } from '@/databases/repositories/settings.repository';
|
||||
import { jsonParse } from 'n8n-workflow';
|
||||
import Container from 'typedi';
|
||||
|
||||
export const defaultLdapConfig = {
|
||||
...LDAP_DEFAULT_CONFIGURATION,
|
||||
loginEnabled: true,
|
||||
loginLabel: '',
|
||||
ldapIdAttribute: 'uid',
|
||||
firstNameAttribute: 'givenName',
|
||||
lastNameAttribute: 'sn',
|
||||
emailAttribute: 'mail',
|
||||
loginIdAttribute: 'mail',
|
||||
baseDn: 'baseDn',
|
||||
bindingAdminDn: 'adminDn',
|
||||
bindingAdminPassword: 'adminPassword',
|
||||
};
|
||||
|
||||
export const createLdapConfig = async (
|
||||
attributes: Partial<LdapConfig> = {},
|
||||
): Promise<LdapConfig> => {
|
||||
const { value: ldapConfig } = await Container.get(SettingsRepository).save({
|
||||
key: LDAP_FEATURE_NAME,
|
||||
value: JSON.stringify({
|
||||
...defaultLdapConfig,
|
||||
...attributes,
|
||||
}),
|
||||
loadOnStartup: true,
|
||||
});
|
||||
return await jsonParse(ldapConfig);
|
||||
};
|
||||
@@ -51,12 +51,16 @@ const repositories = [
|
||||
'AuthProviderSyncHistory',
|
||||
'Credentials',
|
||||
'EventDestinations',
|
||||
'Execution',
|
||||
'ExecutionData',
|
||||
'ExecutionMetadata',
|
||||
'Execution',
|
||||
'InstalledNodes',
|
||||
'InstalledPackages',
|
||||
'Project',
|
||||
'ProjectRelation',
|
||||
'Role',
|
||||
'Project',
|
||||
'ProjectRelation',
|
||||
'Settings',
|
||||
'SharedCredentials',
|
||||
'SharedWorkflow',
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
|
||||
import type { User } from '@db/entities/User';
|
||||
import type { BooleanLicenseFeature, ICredentialsDb, NumericLicenseFeature } from '@/Interfaces';
|
||||
import type { LicenseMocker } from './license';
|
||||
import type { Project } from '@/databases/entities/Project';
|
||||
|
||||
type EndpointGroup =
|
||||
| 'me'
|
||||
@@ -32,7 +33,9 @@ type EndpointGroup =
|
||||
| 'workflowHistory'
|
||||
| 'binaryData'
|
||||
| 'invitations'
|
||||
| 'debug';
|
||||
| 'debug'
|
||||
| 'project'
|
||||
| 'role';
|
||||
|
||||
export interface SetupProps {
|
||||
endpointGroups?: EndpointGroup[];
|
||||
@@ -57,5 +60,5 @@ export type CredentialPayload = {
|
||||
|
||||
export type SaveCredentialFunction = (
|
||||
credentialPayload: CredentialPayload,
|
||||
{ user }: { user: User },
|
||||
options: { user: User } | { project: Project },
|
||||
) => Promise<CredentialsEntity & ICredentialsDb>;
|
||||
|
||||
@@ -257,6 +257,16 @@ export const setupTestServer = ({
|
||||
const { DebugController } = await import('@/controllers/debug.controller');
|
||||
registerController(app, DebugController);
|
||||
break;
|
||||
|
||||
case 'project':
|
||||
const { ProjectController } = await import('@/controllers/project.controller');
|
||||
registerController(app, ProjectController);
|
||||
break;
|
||||
|
||||
case 'role':
|
||||
const { RoleController } = await import('@/controllers/role.controller');
|
||||
registerController(app, RoleController);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user