mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
chore(core): Move scopes and roles into database in preparation for custom roles (#17226)
This commit is contained in:
@@ -17,6 +17,8 @@ import { InvalidAuthToken } from './invalid-auth-token';
|
||||
import { ProcessedData } from './processed-data';
|
||||
import { Project } from './project';
|
||||
import { ProjectRelation } from './project-relation';
|
||||
import { Role } from './role';
|
||||
import { Scope } from './scope';
|
||||
import { Settings } from './settings';
|
||||
import { SharedCredentials } from './shared-credentials';
|
||||
import { SharedWorkflow } from './shared-workflow';
|
||||
@@ -46,6 +48,8 @@ export {
|
||||
Folder,
|
||||
Project,
|
||||
ProjectRelation,
|
||||
Role,
|
||||
Scope,
|
||||
SharedCredentials,
|
||||
SharedWorkflow,
|
||||
TagEntity,
|
||||
@@ -81,6 +85,7 @@ export const entities = {
|
||||
Folder,
|
||||
Project,
|
||||
ProjectRelation,
|
||||
Scope,
|
||||
SharedCredentials,
|
||||
SharedWorkflow,
|
||||
TagEntity,
|
||||
@@ -99,4 +104,5 @@ export const entities = {
|
||||
TestRun,
|
||||
TestCaseExecution,
|
||||
ExecutionEntity,
|
||||
Role,
|
||||
};
|
||||
|
||||
57
packages/@n8n/db/src/entities/role.ts
Normal file
57
packages/@n8n/db/src/entities/role.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Column, Entity, JoinTable, ManyToMany, PrimaryColumn } from '@n8n/typeorm';
|
||||
|
||||
import { Scope } from './scope';
|
||||
|
||||
@Entity({
|
||||
name: 'role',
|
||||
})
|
||||
export class Role {
|
||||
@PrimaryColumn({
|
||||
type: String,
|
||||
name: 'slug',
|
||||
})
|
||||
slug: string;
|
||||
|
||||
@Column({
|
||||
type: String,
|
||||
nullable: false,
|
||||
name: 'displayName',
|
||||
})
|
||||
displayName: string;
|
||||
|
||||
@Column({
|
||||
type: String,
|
||||
nullable: true,
|
||||
name: 'description',
|
||||
})
|
||||
description: string | null;
|
||||
|
||||
@Column({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
name: 'systemRole',
|
||||
})
|
||||
/**
|
||||
* Indicates if the role is managed by the system and cannot be edited.
|
||||
*/
|
||||
systemRole: boolean;
|
||||
|
||||
@Column({
|
||||
type: String,
|
||||
name: 'roleType',
|
||||
})
|
||||
/**
|
||||
* Type of the role, e.g., global, project, or workflow.
|
||||
*/
|
||||
roleType: 'global' | 'project' | 'workflow' | 'credential';
|
||||
|
||||
@ManyToMany(() => Scope, {
|
||||
eager: true,
|
||||
})
|
||||
@JoinTable({
|
||||
name: 'role_scope',
|
||||
joinColumn: { name: 'roleSlug', referencedColumnName: 'slug' },
|
||||
inverseJoinColumn: { name: 'scopeSlug', referencedColumnName: 'slug' },
|
||||
})
|
||||
scopes: Scope[];
|
||||
}
|
||||
27
packages/@n8n/db/src/entities/scope.ts
Normal file
27
packages/@n8n/db/src/entities/scope.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { Scope as ScopeType } from '@n8n/permissions';
|
||||
import { Column, Entity, PrimaryColumn } from '@n8n/typeorm';
|
||||
|
||||
@Entity({
|
||||
name: 'scope',
|
||||
})
|
||||
export class Scope {
|
||||
@PrimaryColumn({
|
||||
type: String,
|
||||
name: 'slug',
|
||||
})
|
||||
slug: ScopeType;
|
||||
|
||||
@Column({
|
||||
type: String,
|
||||
nullable: true,
|
||||
name: 'displayName',
|
||||
})
|
||||
displayName: string | null;
|
||||
|
||||
@Column({
|
||||
type: String,
|
||||
nullable: true,
|
||||
name: 'description',
|
||||
})
|
||||
description: string | null;
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { AuthPrincipal } from '@n8n/permissions';
|
||||
import { GlobalRole } from '@n8n/permissions';
|
||||
import type { AuthPrincipal, GlobalRole } from '@n8n/permissions';
|
||||
import {
|
||||
AfterLoad,
|
||||
AfterUpdate,
|
||||
|
||||
@@ -33,3 +33,5 @@ export { wrapMigration } from './migrations/migration-helpers';
|
||||
export * from './migrations/migration-types';
|
||||
export { DbConnection } from './connection/db-connection';
|
||||
export { DbConnectionOptions } from './connection/db-connection-options';
|
||||
|
||||
export { AuthRolesService } from './services/auth.roles.service';
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { MigrationContext, ReversibleMigration } from '../migration-types';
|
||||
|
||||
/*
|
||||
* We introduce a scope table, this will hold all scopes that we know about.
|
||||
*
|
||||
* The scope table should never be edited by users, on every startup
|
||||
* the system will make sure that all scopes that it knows about are stored
|
||||
* in here.
|
||||
*
|
||||
* ColumnName | Type | Description
|
||||
* =================================
|
||||
* slug | Text | Unique identifier of the scope for example: 'project:create'
|
||||
* displayName | Text | Name used to display in the UI
|
||||
* description | Text | Text describing the scope in more detail of users
|
||||
*/
|
||||
export class AddScopeTables1750252139166 implements ReversibleMigration {
|
||||
async up({ schemaBuilder: { createTable, column } }: MigrationContext) {
|
||||
await createTable('scope').withColumns(
|
||||
column('slug')
|
||||
.varchar(128)
|
||||
.primary.notNull.comment('Unique identifier of the scope for example: "project:create"'),
|
||||
column('displayName').text.default(null).comment('Name used to display in the UI'),
|
||||
column('description')
|
||||
.text.default(null)
|
||||
.comment('Text describing the scope in more detail of users'),
|
||||
);
|
||||
}
|
||||
|
||||
async down({ schemaBuilder: { dropTable } }: MigrationContext) {
|
||||
await dropTable('scope');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { MigrationContext, ReversibleMigration } from '../migration-types';
|
||||
|
||||
/*
|
||||
* We introduce roles table, this will hold all roles that we know about
|
||||
*
|
||||
* There are roles that can't be edited by users, these are marked as system-only and will
|
||||
* be managed by the system itself. On every startup, the system will ensure
|
||||
* that these roles are synchronized.
|
||||
*
|
||||
* ColumnName | Type | Description
|
||||
* =================================
|
||||
* slug | Text | Unique identifier of the role for example: 'global:owner'
|
||||
* displayName | Text | Name used to display in the UI
|
||||
* description | Text | Text describing the scope in more detail of users
|
||||
* roleType | Text | Text type of role, such as 'global', 'project', etc.
|
||||
* systemRole | Bool | Indicates if the role is managed by the system and cannot be edited by users
|
||||
*
|
||||
* For the role table there is a junction table that will hold the
|
||||
* relationships between the roles and the scopes that are associated with them.
|
||||
*/
|
||||
|
||||
export class AddRolesTables1750252139167 implements ReversibleMigration {
|
||||
async up({
|
||||
schemaBuilder: { createTable, column, createIndex },
|
||||
queryRunner,
|
||||
tablePrefix,
|
||||
dbType,
|
||||
}: MigrationContext) {
|
||||
await createTable('role').withColumns(
|
||||
column('slug')
|
||||
.varchar(128)
|
||||
.primary.notNull.comment('Unique identifier of the role for example: "global:owner"'),
|
||||
column('displayName').text.default(null).comment('Name used to display in the UI'),
|
||||
column('description')
|
||||
.text.default(null)
|
||||
.comment('Text describing the scope in more detail of users'),
|
||||
column('roleType')
|
||||
.text.default(null)
|
||||
.comment('Type of the role, e.g., global, project, or workflow'),
|
||||
column('systemRole')
|
||||
.bool.default(false)
|
||||
.notNull.comment('Indicates if the role is managed by the system and cannot be edited'),
|
||||
);
|
||||
|
||||
// MYSQL
|
||||
if (dbType === 'postgresdb' || dbType === 'sqlite') {
|
||||
// POSTGRES
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE ${tablePrefix}role_scope (
|
||||
"roleSlug" VARCHAR(128) NOT NULL,
|
||||
"scopeSlug" VARCHAR(128) NOT NULL,
|
||||
CONSTRAINT "PK_${tablePrefix}role_scope" PRIMARY KEY ("roleSlug", "scopeSlug"),
|
||||
CONSTRAINT "FK_${tablePrefix}role" FOREIGN KEY ("roleSlug") REFERENCES ${tablePrefix}role ("slug") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "FK_${tablePrefix}scope" FOREIGN KEY ("scopeSlug") REFERENCES "${tablePrefix}scope" ("slug") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);`,
|
||||
);
|
||||
} else {
|
||||
// MYSQL
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE ${tablePrefix}role_scope (
|
||||
\`roleSlug\` VARCHAR(128) NOT NULL,
|
||||
\`scopeSlug\` VARCHAR(128) NOT NULL,
|
||||
FOREIGN KEY (\`scopeSlug\`) REFERENCES ${tablePrefix}scope (\`slug\`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
FOREIGN KEY (\`roleSlug\`) REFERENCES ${tablePrefix}role (\`slug\`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
PRIMARY KEY (\`roleSlug\`, \`scopeSlug\`)
|
||||
) ENGINE=InnoDB;`,
|
||||
);
|
||||
}
|
||||
|
||||
await createIndex('role_scope', ['scopeSlug']);
|
||||
/*
|
||||
await createTable('role_scope')
|
||||
.withColumns(
|
||||
column('id').int.primary.autoGenerate2,
|
||||
column('roleSlug').varchar(128).notNull,
|
||||
column('scopeSlug').varchar(128).notNull,
|
||||
)
|
||||
.withForeignKey('roleSlug', {
|
||||
tableName: 'role',
|
||||
columnName: 'slug',
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
})
|
||||
.withForeignKey('scopeSlug', {
|
||||
tableName: 'scope',
|
||||
columnName: 'slug',
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
})
|
||||
.withIndexOn('scopeSlug') // For fast lookup of which roles have access to a scope
|
||||
.withIndexOn(['roleSlug', 'scopeSlug'], true); */
|
||||
}
|
||||
|
||||
async down({ schemaBuilder: { dropTable } }: MigrationContext) {
|
||||
await dropTable('role_scope');
|
||||
await dropTable('role');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { MigrationContext, ReversibleMigration } from '../migration-types';
|
||||
|
||||
/*
|
||||
* This migration links the role table to the user table, by adding a new column 'roleSlug'
|
||||
* to the user table. It also ensures that all users have a valid role set in the 'roleSlug' column.
|
||||
* The migration will insert the global roles that we need into the role table if they do not exist.
|
||||
*
|
||||
* The old 'role' column in the user table will be removed in a later migration.
|
||||
*/
|
||||
export class LinkRoleToUserTable1750252139168 implements ReversibleMigration {
|
||||
async up({
|
||||
schemaBuilder: { addForeignKey, addColumns, column },
|
||||
escape,
|
||||
dbType,
|
||||
runQuery,
|
||||
}: MigrationContext) {
|
||||
const roleTableName = escape.tableName('role');
|
||||
const userTableName = escape.tableName('user');
|
||||
const slugColumn = escape.columnName('slug');
|
||||
const roleColumn = escape.columnName('role');
|
||||
const roleSlugColumn = escape.columnName('roleSlug');
|
||||
const roleTypeColumn = escape.columnName('roleType');
|
||||
const systemRoleColumn = escape.columnName('systemRole');
|
||||
|
||||
const isPostgresOrSqlite = dbType === 'postgresdb' || dbType === 'sqlite';
|
||||
const upsertQuery = isPostgresOrSqlite
|
||||
? `INSERT INTO ${roleTableName} (${slugColumn}, ${roleTypeColumn}, ${systemRoleColumn}) VALUES (:slug, :roleType, :systemRole) ON CONFLICT DO NOTHING`
|
||||
: `INSERT IGNORE INTO ${roleTableName} (${slugColumn}, ${roleTypeColumn}, ${systemRoleColumn}) VALUES (:slug, :roleType, :systemRole)`;
|
||||
|
||||
// Make sure that the global roles that we need exist
|
||||
for (const role of ['global:owner', 'global:admin', 'global:member']) {
|
||||
await runQuery(upsertQuery, {
|
||||
slug: role,
|
||||
roleType: 'global',
|
||||
systemRole: true,
|
||||
});
|
||||
}
|
||||
|
||||
await addColumns('user', [column('roleSlug').varchar(128).default("'global:member'").notNull]);
|
||||
|
||||
await runQuery(
|
||||
`UPDATE ${userTableName} SET ${roleSlugColumn} = ${roleColumn} WHERE ${roleColumn} != ${roleSlugColumn}`,
|
||||
);
|
||||
|
||||
// Fallback to 'global:member' for users that do not have a correct role set
|
||||
// This should not happen in a correctly set up system, but we want to ensure
|
||||
// that all users have a role set, before we add the foreign key constraint
|
||||
await runQuery(
|
||||
`UPDATE ${userTableName} SET ${roleSlugColumn} = 'global:member' WHERE NOT EXISTS (SELECT 1 FROM ${roleTableName} WHERE ${slugColumn} = ${roleSlugColumn})`,
|
||||
);
|
||||
|
||||
await addForeignKey('user', 'roleSlug', ['role', 'slug']);
|
||||
}
|
||||
|
||||
async down({ schemaBuilder: { dropForeignKey, dropColumns } }: MigrationContext) {
|
||||
await dropForeignKey('user', 'roleSlug', ['role', 'slug']);
|
||||
await dropColumns('user', ['roleSlug']);
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,9 @@ import { AddWorkflowArchivedColumn1745934666076 } from '../common/1745934666076-
|
||||
import { DropRoleTable1745934666077 } from '../common/1745934666077-DropRoleTable';
|
||||
import { AddProjectDescriptionColumn1747824239000 } from '../common/1747824239000-AddProjectDescriptionColumn';
|
||||
import { AddLastActiveAtColumnToUser1750252139166 } from '../common/1750252139166-AddLastActiveAtColumnToUser';
|
||||
import { AddScopeTables1750252139166 } from '../common/1750252139166-AddScopeTables';
|
||||
import { AddRolesTables1750252139167 } from '../common/1750252139167-AddRolesTables';
|
||||
import { LinkRoleToUserTable1750252139168 } from '../common/1750252139168-LinkRoleToUserTable';
|
||||
import { AddInputsOutputsToTestCaseExecution1752669793000 } from '../common/1752669793000-AddInputsOutputsToTestCaseExecution';
|
||||
import { CreateDataStoreTables1754475614601 } from '../common/1754475614601-CreateDataStoreTables';
|
||||
import type { Migration } from '../migration-types';
|
||||
@@ -185,6 +188,9 @@ export const mysqlMigrations: Migration[] = [
|
||||
ClearEvaluation1745322634000,
|
||||
AddProjectDescriptionColumn1747824239000,
|
||||
AddLastActiveAtColumnToUser1750252139166,
|
||||
AddScopeTables1750252139166,
|
||||
AddRolesTables1750252139167,
|
||||
LinkRoleToUserTable1750252139168,
|
||||
AddInputsOutputsToTestCaseExecution1752669793000,
|
||||
CreateDataStoreTables1754475614601,
|
||||
];
|
||||
|
||||
@@ -89,6 +89,9 @@ import { AddWorkflowArchivedColumn1745934666076 } from '../common/1745934666076-
|
||||
import { DropRoleTable1745934666077 } from '../common/1745934666077-DropRoleTable';
|
||||
import { AddProjectDescriptionColumn1747824239000 } from '../common/1747824239000-AddProjectDescriptionColumn';
|
||||
import { AddLastActiveAtColumnToUser1750252139166 } from '../common/1750252139166-AddLastActiveAtColumnToUser';
|
||||
import { AddScopeTables1750252139166 } from '../common/1750252139166-AddScopeTables';
|
||||
import { AddRolesTables1750252139167 } from '../common/1750252139167-AddRolesTables';
|
||||
import { LinkRoleToUserTable1750252139168 } from '../common/1750252139168-LinkRoleToUserTable';
|
||||
import { CreateDataStoreTables1754475614601 } from '../common/1754475614601-CreateDataStoreTables';
|
||||
import type { Migration } from '../migration-types';
|
||||
|
||||
@@ -183,6 +186,9 @@ export const postgresMigrations: Migration[] = [
|
||||
ClearEvaluation1745322634000,
|
||||
AddProjectDescriptionColumn1747824239000,
|
||||
AddLastActiveAtColumnToUser1750252139166,
|
||||
AddScopeTables1750252139166,
|
||||
AddRolesTables1750252139167,
|
||||
LinkRoleToUserTable1750252139168,
|
||||
AddInputsOutputsToTestCaseExecution1752669793000,
|
||||
CreateDataStoreTables1754475614601,
|
||||
];
|
||||
|
||||
@@ -85,6 +85,9 @@ import { AddWorkflowArchivedColumn1745934666076 } from '../common/1745934666076-
|
||||
import { DropRoleTable1745934666077 } from '../common/1745934666077-DropRoleTable';
|
||||
import { AddProjectDescriptionColumn1747824239000 } from '../common/1747824239000-AddProjectDescriptionColumn';
|
||||
import { AddLastActiveAtColumnToUser1750252139166 } from '../common/1750252139166-AddLastActiveAtColumnToUser';
|
||||
import { AddScopeTables1750252139166 } from '../common/1750252139166-AddScopeTables';
|
||||
import { AddRolesTables1750252139167 } from '../common/1750252139167-AddRolesTables';
|
||||
import { LinkRoleToUserTable1750252139168 } from '../common/1750252139168-LinkRoleToUserTable';
|
||||
import { AddInputsOutputsToTestCaseExecution1752669793000 } from '../common/1752669793000-AddInputsOutputsToTestCaseExecution';
|
||||
import { CreateDataStoreTables1754475614601 } from '../common/1754475614601-CreateDataStoreTables';
|
||||
import type { Migration } from '../migration-types';
|
||||
@@ -177,6 +180,9 @@ const sqliteMigrations: Migration[] = [
|
||||
ClearEvaluation1745322634000,
|
||||
AddProjectDescriptionColumn1747824239000,
|
||||
AddLastActiveAtColumnToUser1750252139166,
|
||||
AddScopeTables1750252139166,
|
||||
AddRolesTables1750252139167,
|
||||
LinkRoleToUserTable1750252139168,
|
||||
AddInputsOutputsToTestCaseExecution1752669793000,
|
||||
CreateDataStoreTables1754475614601,
|
||||
];
|
||||
|
||||
@@ -11,12 +11,14 @@ export { ExecutionRepository } from './execution.repository';
|
||||
export { EventDestinationsRepository } from './event-destinations.repository';
|
||||
export { FolderRepository } from './folder.repository';
|
||||
export { FolderTagMappingRepository } from './folder-tag-mapping.repository';
|
||||
export { ScopeRepository } from './scope.repository';
|
||||
export { InstalledNodesRepository } from './installed-nodes.repository';
|
||||
export { InstalledPackagesRepository } from './installed-packages.repository';
|
||||
export { InvalidAuthTokenRepository } from './invalid-auth-token.repository';
|
||||
export { LicenseMetricsRepository } from './license-metrics.repository';
|
||||
export { ProjectRelationRepository } from './project-relation.repository';
|
||||
export { ProjectRepository } from './project.repository';
|
||||
export { RoleRepository } from './role.repository';
|
||||
export { ProcessedDataRepository } from './processed-data.repository';
|
||||
export { SettingsRepository } from './settings.repository';
|
||||
export { TagRepository } from './tag.repository';
|
||||
|
||||
11
packages/@n8n/db/src/repositories/role.repository.ts
Normal file
11
packages/@n8n/db/src/repositories/role.repository.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Service } from '@n8n/di';
|
||||
import { DataSource, Repository } from '@n8n/typeorm';
|
||||
|
||||
import { Role } from '../entities';
|
||||
|
||||
@Service()
|
||||
export class RoleRepository extends Repository<Role> {
|
||||
constructor(dataSource: DataSource) {
|
||||
super(Role, dataSource.manager);
|
||||
}
|
||||
}
|
||||
11
packages/@n8n/db/src/repositories/scope.repository.ts
Normal file
11
packages/@n8n/db/src/repositories/scope.repository.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Service } from '@n8n/di';
|
||||
import { DataSource, Repository } from '@n8n/typeorm';
|
||||
|
||||
import { Scope } from '../entities';
|
||||
|
||||
@Service()
|
||||
export class ScopeRepository extends Repository<Scope> {
|
||||
constructor(dataSource: DataSource) {
|
||||
super(Scope, dataSource.manager);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { UsersListFilterDto } from '@n8n/api-types';
|
||||
import { Service } from '@n8n/di';
|
||||
import type { GlobalRole } from '@n8n/permissions';
|
||||
import type { DeepPartial, EntityManager, SelectQueryBuilder } from '@n8n/typeorm';
|
||||
import { Brackets, DataSource, In, IsNull, Not, Repository } from '@n8n/typeorm';
|
||||
|
||||
@@ -65,13 +64,13 @@ export class UserRepository extends Repository<User> {
|
||||
const rows = (await this.createQueryBuilder()
|
||||
.select(['role', 'COUNT(role) as count'])
|
||||
.groupBy('role')
|
||||
.execute()) as Array<{ role: GlobalRole; count: string }>;
|
||||
.execute()) as Array<{ role: string; count: string }>;
|
||||
return rows.reduce(
|
||||
(acc, row) => {
|
||||
acc[row.role] = parseInt(row.count, 10);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<GlobalRole, number>,
|
||||
{} as Record<string, number>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
136
packages/@n8n/db/src/services/auth.roles.service.ts
Normal file
136
packages/@n8n/db/src/services/auth.roles.service.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { Logger } from '@n8n/backend-common';
|
||||
import { Service } from '@n8n/di';
|
||||
import { ALL_SCOPES, ALL_ROLES, scopeInformation } from '@n8n/permissions';
|
||||
|
||||
import { Scope } from '../entities';
|
||||
import { RoleRepository, ScopeRepository } from '../repositories';
|
||||
|
||||
@Service()
|
||||
export class AuthRolesService {
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly scopeRepository: ScopeRepository,
|
||||
private readonly roleRepository: RoleRepository,
|
||||
) {}
|
||||
|
||||
private async syncScopes() {
|
||||
const availableScopes = await this.scopeRepository.find({
|
||||
select: {
|
||||
slug: true,
|
||||
displayName: true,
|
||||
description: true,
|
||||
},
|
||||
});
|
||||
|
||||
const availableScopesMap = new Map(availableScopes.map((scope) => [scope.slug, scope]));
|
||||
|
||||
const scopesToUpdate = ALL_SCOPES.map((slug) => {
|
||||
const info = scopeInformation[slug] ?? {
|
||||
displayName: slug,
|
||||
description: null,
|
||||
};
|
||||
|
||||
const existingScope = availableScopesMap.get(slug);
|
||||
if (!existingScope) {
|
||||
const newScope = new Scope();
|
||||
newScope.slug = slug;
|
||||
newScope.displayName = info.displayName;
|
||||
newScope.description = info.description ?? null;
|
||||
return newScope;
|
||||
}
|
||||
|
||||
const needsUpdate =
|
||||
existingScope.displayName !== info.displayName ||
|
||||
existingScope.description !== info.description;
|
||||
|
||||
if (needsUpdate) {
|
||||
existingScope.displayName = info.displayName;
|
||||
existingScope.description = info.description ?? null;
|
||||
return existingScope;
|
||||
}
|
||||
return null;
|
||||
}).filter((scope) => scope !== null);
|
||||
|
||||
if (scopesToUpdate.length > 0) {
|
||||
this.logger.info(`Updating ${scopesToUpdate.length} scopes...`);
|
||||
await this.scopeRepository.save(scopesToUpdate);
|
||||
this.logger.info('Scopes updated successfully.');
|
||||
} else {
|
||||
this.logger.debug('No scopes to update.');
|
||||
}
|
||||
}
|
||||
|
||||
private async syncRoles() {
|
||||
const existingRoles = await this.roleRepository.find({
|
||||
select: {
|
||||
slug: true,
|
||||
displayName: true,
|
||||
description: true,
|
||||
systemRole: true,
|
||||
roleType: true,
|
||||
},
|
||||
where: {
|
||||
systemRole: true,
|
||||
},
|
||||
});
|
||||
|
||||
const allScopes = await this.scopeRepository.find({
|
||||
select: {
|
||||
slug: true,
|
||||
},
|
||||
});
|
||||
|
||||
const existingRolesMap = new Map(existingRoles.map((role) => [role.slug, role]));
|
||||
|
||||
for (const roleNamespace of Object.keys(ALL_ROLES) as Array<keyof typeof ALL_ROLES>) {
|
||||
const rolesToUpdate = ALL_ROLES[roleNamespace]
|
||||
.map((role) => {
|
||||
const existingRole = existingRolesMap.get(role.role);
|
||||
|
||||
if (!existingRole) {
|
||||
const newRole = this.roleRepository.create({
|
||||
slug: role.role,
|
||||
displayName: role.name,
|
||||
description: role.description ?? null,
|
||||
roleType: roleNamespace,
|
||||
systemRole: true,
|
||||
scopes: allScopes.filter((scope) => role.scopes.includes(scope.slug)),
|
||||
});
|
||||
return newRole;
|
||||
}
|
||||
|
||||
const needsUpdate =
|
||||
existingRole.displayName !== role.name ||
|
||||
existingRole.description !== role.description ||
|
||||
existingRole.roleType !== roleNamespace ||
|
||||
existingRole.scopes.some((scope) => !role.scopes.includes(scope.slug)) || // DB roles has scope that it should not have
|
||||
role.scopes.some((scope) => !existingRole.scopes.some((s) => s.slug === scope)); // A role has scope that is not in DB
|
||||
|
||||
if (needsUpdate) {
|
||||
existingRole.displayName = role.name;
|
||||
existingRole.description = role.description ?? null;
|
||||
existingRole.roleType = roleNamespace;
|
||||
existingRole.scopes = allScopes.filter((scope) => role.scopes.includes(scope.slug));
|
||||
return existingRole;
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((role) => role !== null);
|
||||
if (rolesToUpdate.length > 0) {
|
||||
this.logger.info(`Updating ${rolesToUpdate.length} ${roleNamespace} roles...`);
|
||||
await this.roleRepository.save(rolesToUpdate);
|
||||
this.logger.info(`${roleNamespace} roles updated successfully.`);
|
||||
} else {
|
||||
this.logger.debug(`No ${roleNamespace} roles to update.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.logger.info('Initializing AuthRolesService...');
|
||||
await this.syncScopes();
|
||||
await this.syncRoles();
|
||||
this.logger.info('AuthRolesService initialized successfully.');
|
||||
}
|
||||
}
|
||||
1
packages/@n8n/db/src/services/index.ts
Normal file
1
packages/@n8n/db/src/services/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { AuthRolesService } from './auth.roles.service';
|
||||
@@ -0,0 +1,125 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Scope Information ensure scopes are defined correctly 1`] = `
|
||||
[
|
||||
"annotationTag:create",
|
||||
"annotationTag:read",
|
||||
"annotationTag:update",
|
||||
"annotationTag:delete",
|
||||
"annotationTag:list",
|
||||
"annotationTag:*",
|
||||
"auditLogs:manage",
|
||||
"auditLogs:*",
|
||||
"banner:dismiss",
|
||||
"banner:*",
|
||||
"community:register",
|
||||
"community:*",
|
||||
"communityPackage:install",
|
||||
"communityPackage:uninstall",
|
||||
"communityPackage:update",
|
||||
"communityPackage:list",
|
||||
"communityPackage:manage",
|
||||
"communityPackage:*",
|
||||
"credential:share",
|
||||
"credential:move",
|
||||
"credential:create",
|
||||
"credential:read",
|
||||
"credential:update",
|
||||
"credential:delete",
|
||||
"credential:list",
|
||||
"credential:*",
|
||||
"externalSecretsProvider:sync",
|
||||
"externalSecretsProvider:create",
|
||||
"externalSecretsProvider:read",
|
||||
"externalSecretsProvider:update",
|
||||
"externalSecretsProvider:delete",
|
||||
"externalSecretsProvider:list",
|
||||
"externalSecretsProvider:*",
|
||||
"externalSecret:list",
|
||||
"externalSecret:use",
|
||||
"externalSecret:*",
|
||||
"eventBusDestination:test",
|
||||
"eventBusDestination:create",
|
||||
"eventBusDestination:read",
|
||||
"eventBusDestination:update",
|
||||
"eventBusDestination:delete",
|
||||
"eventBusDestination:list",
|
||||
"eventBusDestination:*",
|
||||
"ldap:sync",
|
||||
"ldap:manage",
|
||||
"ldap:*",
|
||||
"license:manage",
|
||||
"license:*",
|
||||
"logStreaming:manage",
|
||||
"logStreaming:*",
|
||||
"orchestration:read",
|
||||
"orchestration:list",
|
||||
"orchestration:*",
|
||||
"project:create",
|
||||
"project:read",
|
||||
"project:update",
|
||||
"project:delete",
|
||||
"project:list",
|
||||
"project:*",
|
||||
"saml:manage",
|
||||
"saml:*",
|
||||
"securityAudit:generate",
|
||||
"securityAudit:*",
|
||||
"sourceControl:pull",
|
||||
"sourceControl:push",
|
||||
"sourceControl:manage",
|
||||
"sourceControl:*",
|
||||
"tag:create",
|
||||
"tag:read",
|
||||
"tag:update",
|
||||
"tag:delete",
|
||||
"tag:list",
|
||||
"tag:*",
|
||||
"user:resetPassword",
|
||||
"user:changeRole",
|
||||
"user:enforceMfa",
|
||||
"user:create",
|
||||
"user:read",
|
||||
"user:update",
|
||||
"user:delete",
|
||||
"user:list",
|
||||
"user:*",
|
||||
"variable:create",
|
||||
"variable:read",
|
||||
"variable:update",
|
||||
"variable:delete",
|
||||
"variable:list",
|
||||
"variable:*",
|
||||
"workersView:manage",
|
||||
"workersView:*",
|
||||
"workflow:share",
|
||||
"workflow:execute",
|
||||
"workflow:move",
|
||||
"workflow:create",
|
||||
"workflow:read",
|
||||
"workflow:update",
|
||||
"workflow:delete",
|
||||
"workflow:list",
|
||||
"workflow:*",
|
||||
"folder:create",
|
||||
"folder:read",
|
||||
"folder:update",
|
||||
"folder:delete",
|
||||
"folder:list",
|
||||
"folder:move",
|
||||
"folder:*",
|
||||
"insights:list",
|
||||
"insights:*",
|
||||
"oidc:manage",
|
||||
"oidc:*",
|
||||
"dataStore:create",
|
||||
"dataStore:read",
|
||||
"dataStore:update",
|
||||
"dataStore:delete",
|
||||
"dataStore:list",
|
||||
"dataStore:readRow",
|
||||
"dataStore:writeRow",
|
||||
"dataStore:*",
|
||||
"*",
|
||||
]
|
||||
`;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ALL_SCOPES } from '@/scope-information';
|
||||
|
||||
describe('Scope Information', () => {
|
||||
it('ensure scopes are defined correctly', () => {
|
||||
expect(ALL_SCOPES).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ export type * from './types.ee';
|
||||
export * from './constants.ee';
|
||||
|
||||
export * from './roles/scopes/global-scopes.ee';
|
||||
export * from './scope-information';
|
||||
export * from './roles/role-maps.ee';
|
||||
export * from './roles/all-roles';
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ const mapToRoleObject = <T extends keyof typeof ROLE_NAMES>(roles: Record<T, Sco
|
||||
role,
|
||||
name: ROLE_NAMES[role],
|
||||
scopes: getRoleScopes(role),
|
||||
description: ROLE_NAMES[role],
|
||||
licensed: false,
|
||||
}));
|
||||
|
||||
|
||||
21
packages/@n8n/permissions/src/scope-information.ts
Normal file
21
packages/@n8n/permissions/src/scope-information.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { RESOURCES } from './constants.ee';
|
||||
import type { Scope, ScopeInformation } from './types.ee';
|
||||
|
||||
function buildResourceScopes() {
|
||||
const resourceScopes = Object.entries(RESOURCES).flatMap(([resource, operations]) => [
|
||||
...operations.map((op) => `${resource}:${op}` as const),
|
||||
`${resource}:*` as const,
|
||||
]) as Scope[];
|
||||
|
||||
resourceScopes.push('*' as const); // Global wildcard
|
||||
return resourceScopes;
|
||||
}
|
||||
|
||||
export const ALL_SCOPES = buildResourceScopes();
|
||||
|
||||
export const scopeInformation: Partial<Record<Scope, ScopeInformation>> = {
|
||||
'annotationTag:create': {
|
||||
displayName: 'Create Annotation Tag',
|
||||
description: 'Allows creating new annotation tags.',
|
||||
},
|
||||
};
|
||||
@@ -11,6 +11,11 @@ import type {
|
||||
workflowSharingRoleSchema,
|
||||
} from './schemas.ee';
|
||||
|
||||
export type ScopeInformation = {
|
||||
displayName: string;
|
||||
description?: string | null;
|
||||
};
|
||||
|
||||
/** Represents a resource that can have permissions applied to it */
|
||||
export type Resource = keyof typeof RESOURCES;
|
||||
|
||||
@@ -59,6 +64,7 @@ export type AllRoleTypes = GlobalRole | ProjectRole | WorkflowSharingRole | Cred
|
||||
type RoleObject<T extends AllRoleTypes> = {
|
||||
role: T;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
scopes: Scope[];
|
||||
licensed: boolean;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { LICENSE_FEATURES } from '@n8n/constants';
|
||||
import { ExecutionRepository, SettingsRepository } from '@n8n/db';
|
||||
import { AuthRolesService, ExecutionRepository, SettingsRepository } from '@n8n/db';
|
||||
import { Command } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
import glob from 'fast-glob';
|
||||
@@ -172,6 +172,9 @@ export class Start extends BaseCommand<z.infer<typeof flagsSchema>> {
|
||||
}
|
||||
|
||||
await super.init();
|
||||
|
||||
await Container.get(AuthRolesService).init();
|
||||
|
||||
this.activeWorkflowManager = Container.get(ActiveWorkflowManager);
|
||||
|
||||
const isMultiMainEnabled =
|
||||
|
||||
@@ -23,17 +23,20 @@ let expectedGlobalRoles: Array<{
|
||||
role: GlobalRole;
|
||||
scopes: Scope[];
|
||||
licensed: boolean;
|
||||
description: string;
|
||||
}>;
|
||||
let expectedProjectRoles: Array<{
|
||||
name: string;
|
||||
role: ProjectRole;
|
||||
scopes: Scope[];
|
||||
licensed: boolean;
|
||||
description: string;
|
||||
}>;
|
||||
let expectedCredentialRoles: Array<{
|
||||
name: string;
|
||||
role: CredentialSharingRole;
|
||||
scopes: Scope[];
|
||||
description: string;
|
||||
licensed: boolean;
|
||||
}>;
|
||||
let expectedWorkflowRoles: Array<{
|
||||
@@ -41,6 +44,7 @@ let expectedWorkflowRoles: Array<{
|
||||
role: WorkflowSharingRole;
|
||||
scopes: Scope[];
|
||||
licensed: boolean;
|
||||
description: string;
|
||||
}>;
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -52,18 +56,21 @@ beforeAll(async () => {
|
||||
role: 'global:owner',
|
||||
scopes: getRoleScopes('global:owner'),
|
||||
licensed: true,
|
||||
description: 'Owner',
|
||||
},
|
||||
{
|
||||
name: 'Admin',
|
||||
role: 'global:admin',
|
||||
scopes: getRoleScopes('global:admin'),
|
||||
licensed: false,
|
||||
description: 'Admin',
|
||||
},
|
||||
{
|
||||
name: 'Member',
|
||||
role: 'global:member',
|
||||
scopes: getRoleScopes('global:member'),
|
||||
licensed: true,
|
||||
description: 'Member',
|
||||
},
|
||||
];
|
||||
expectedProjectRoles = [
|
||||
@@ -72,18 +79,21 @@ beforeAll(async () => {
|
||||
role: 'project:personalOwner',
|
||||
scopes: getRoleScopes('project:personalOwner'),
|
||||
licensed: true,
|
||||
description: 'Project Owner',
|
||||
},
|
||||
{
|
||||
name: 'Project Admin',
|
||||
role: 'project:admin',
|
||||
scopes: getRoleScopes('project:admin'),
|
||||
licensed: false,
|
||||
description: 'Project Admin',
|
||||
},
|
||||
{
|
||||
name: 'Project Editor',
|
||||
role: 'project:editor',
|
||||
scopes: getRoleScopes('project:editor'),
|
||||
licensed: false,
|
||||
description: 'Project Editor',
|
||||
},
|
||||
];
|
||||
expectedCredentialRoles = [
|
||||
@@ -92,12 +102,14 @@ beforeAll(async () => {
|
||||
role: 'credential:owner',
|
||||
scopes: getRoleScopes('credential:owner'),
|
||||
licensed: true,
|
||||
description: 'Credential Owner',
|
||||
},
|
||||
{
|
||||
name: 'Credential User',
|
||||
role: 'credential:user',
|
||||
scopes: getRoleScopes('credential:user'),
|
||||
licensed: true,
|
||||
description: 'Credential User',
|
||||
},
|
||||
];
|
||||
expectedWorkflowRoles = [
|
||||
@@ -106,12 +118,14 @@ beforeAll(async () => {
|
||||
role: 'workflow:owner',
|
||||
scopes: getRoleScopes('workflow:owner'),
|
||||
licensed: true,
|
||||
description: 'Workflow Owner',
|
||||
},
|
||||
{
|
||||
name: 'Workflow Editor',
|
||||
role: 'workflow:editor',
|
||||
scopes: getRoleScopes('workflow:editor'),
|
||||
licensed: true,
|
||||
description: 'Workflow Editor',
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
@@ -82,11 +82,12 @@ const credentialRoleTranslations = computed<Record<string, string>>(() => {
|
||||
});
|
||||
|
||||
const credentialRoles = computed<AllRolesMap['credential']>(() => {
|
||||
return rolesStore.processedCredentialRoles.map(({ role, scopes, licensed }) => ({
|
||||
return rolesStore.processedCredentialRoles.map(({ role, scopes, licensed, description }) => ({
|
||||
role,
|
||||
name: credentialRoleTranslations.value[role],
|
||||
scopes,
|
||||
licensed,
|
||||
description,
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
@@ -88,8 +88,14 @@ describe('WorkflowShareModal.ee.vue', () => {
|
||||
workflowsEEStore.getWorkflowOwnerName = vi.fn(() => 'Owner Name');
|
||||
projectsStore.personalProjects = [createProjectListItem()];
|
||||
rolesStore.processedWorkflowRoles = [
|
||||
{ name: 'Editor', role: 'workflow:editor', scopes: [], licensed: false },
|
||||
{ name: 'Owner', role: 'workflow:owner', scopes: [], licensed: false },
|
||||
{
|
||||
name: 'Editor',
|
||||
role: 'workflow:editor',
|
||||
scopes: [],
|
||||
licensed: false,
|
||||
description: 'Editor',
|
||||
},
|
||||
{ name: 'Owner', role: 'workflow:owner', scopes: [], licensed: false, description: 'Owner' },
|
||||
];
|
||||
|
||||
workflowSaving = useWorkflowSaving({ router: useRouter() });
|
||||
|
||||
@@ -108,11 +108,12 @@ const workflowRoleTranslations = computed(() => ({
|
||||
}));
|
||||
|
||||
const workflowRoles = computed(() =>
|
||||
rolesStore.processedWorkflowRoles.map(({ role, scopes, licensed }) => ({
|
||||
rolesStore.processedWorkflowRoles.map(({ role, scopes, licensed, description }) => ({
|
||||
role,
|
||||
name: workflowRoleTranslations.value[role],
|
||||
scopes,
|
||||
licensed,
|
||||
description,
|
||||
})),
|
||||
);
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ describe('roles store', () => {
|
||||
{
|
||||
name: 'Project Admin',
|
||||
role: 'project:admin',
|
||||
description: 'Project Admin',
|
||||
scopes: [
|
||||
'workflow:create',
|
||||
'workflow:read',
|
||||
@@ -43,6 +44,7 @@ describe('roles store', () => {
|
||||
{
|
||||
name: 'Project Owner',
|
||||
role: 'project:personalOwner',
|
||||
description: 'Project Owner',
|
||||
scopes: [
|
||||
'workflow:create',
|
||||
'workflow:read',
|
||||
@@ -67,6 +69,7 @@ describe('roles store', () => {
|
||||
{
|
||||
name: 'Project Editor',
|
||||
role: 'project:editor',
|
||||
description: 'Project Editor',
|
||||
scopes: [
|
||||
'workflow:create',
|
||||
'workflow:read',
|
||||
@@ -87,6 +90,7 @@ describe('roles store', () => {
|
||||
{
|
||||
name: 'Project Viewer',
|
||||
role: 'project:viewer',
|
||||
description: 'Project Viewer',
|
||||
scopes: [
|
||||
'credential:list',
|
||||
'credential:read',
|
||||
|
||||
Reference in New Issue
Block a user