fix(core): Block deletion of the SSO role mapping default condition role (#35086)

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Guillaume Jacquart
2026-07-28 17:47:24 +02:00
committed by GitHub
parent 1f6cddd1ab
commit 587b23d886
2 changed files with 57 additions and 9 deletions
@@ -1,19 +1,27 @@
import type { ProvisioningConfigDto } from '@n8n/api-types';
import type { RoleMappingRuleRepository } from '@n8n/db';
import { mock } from 'vitest-mock-extended';
import { ProvisioningRoleDeletionChecker } from '../role-deletion-checker.ee';
import type { ProvisioningService } from '../provisioning.service.ee';
describe('ProvisioningRoleDeletionChecker', () => {
const roleMappingRuleRepository = mock<RoleMappingRuleRepository>();
const checker = new ProvisioningRoleDeletionChecker(roleMappingRuleRepository);
const provisioningService = mock<ProvisioningService>();
const checker = new ProvisioningRoleDeletionChecker(
roleMappingRuleRepository,
provisioningService,
);
beforeEach(() => {
vi.clearAllMocks();
roleMappingRuleRepository.count.mockResolvedValue(0);
provisioningService.getConfig.mockResolvedValue(
mock<ProvisioningConfigDto>({ defaultInstanceRole: undefined }),
);
});
it('reports no blockers when no mapping rule references the role', async () => {
roleMappingRuleRepository.count.mockResolvedValue(0);
it('reports no blockers when the role is unreferenced', async () => {
const blockers = await checker.findRoleDeletionBlockers('global:auditor');
expect(roleMappingRuleRepository.count).toHaveBeenCalledWith({
@@ -37,4 +45,28 @@ describe('ProvisioningRoleDeletionChecker', () => {
expect(blockers).toEqual(['referenced by 3 role mapping rules']);
});
it('reports a blocker when the role is the default condition role', async () => {
provisioningService.getConfig.mockResolvedValue(
mock<ProvisioningConfigDto>({ defaultInstanceRole: 'global:auditor' }),
);
const blockers = await checker.findRoleDeletionBlockers('global:auditor');
expect(blockers).toEqual(['configured as the role mapping default condition role']);
});
it('reports both blockers when the role is referenced by a mapping rule and is the default condition role', async () => {
roleMappingRuleRepository.count.mockResolvedValue(1);
provisioningService.getConfig.mockResolvedValue(
mock<ProvisioningConfigDto>({ defaultInstanceRole: 'global:auditor' }),
);
const blockers = await checker.findRoleDeletionBlockers('global:auditor');
expect(blockers).toEqual([
'referenced by 1 role mapping rule',
'configured as the role mapping default condition role',
]);
});
});
@@ -3,20 +3,36 @@ import { Service } from '@n8n/di';
import type { RoleDeletionChecker } from '@/services/role-deletion-check-proxy.service';
import { ProvisioningService } from './provisioning.service.ee';
/**
* Blocks deletion of a custom role while it is still targeted by an SSO role
* mapping rule. Registered on the core `RoleDeletionCheckProxy` by the
* provisioning module so `RoleService` stays decoupled from this module.
* mapping rule or configured as the role mapping default condition.
* Registered on the core `RoleDeletionCheckProxy` by the provisioning module
* so `RoleService` stays decoupled from this module.
*/
@Service()
export class ProvisioningRoleDeletionChecker implements RoleDeletionChecker {
constructor(private readonly roleMappingRuleRepository: RoleMappingRuleRepository) {}
constructor(
private readonly roleMappingRuleRepository: RoleMappingRuleRepository,
private readonly provisioningService: ProvisioningService,
) {}
async findRoleDeletionBlockers(roleSlug: string): Promise<string[]> {
const blockers: string[] = [];
const count = await this.roleMappingRuleRepository.count({
where: { role: { slug: roleSlug } },
});
if (count === 0) return [];
return [`referenced by ${count} role mapping rule${count === 1 ? '' : 's'}`];
if (count > 0) {
blockers.push(`referenced by ${count} role mapping rule${count === 1 ? '' : 's'}`);
}
const { defaultInstanceRole } = await this.provisioningService.getConfig();
if (defaultInstanceRole === roleSlug) {
blockers.push('configured as the role mapping default condition role');
}
return blockers;
}
}