Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions shell/assets/translations/en-us.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5802,17 +5802,29 @@ rbac:
noContext:
label: No Context
globalRoles:
notBound: 'No users bound <i class="icon icon-checkmark" style="margin-left: 5px"></i>'
unableToCheck: Unable to check if any user is bound to the role(s). Please try again.
notBound: 'No users or groups bound <i class="icon icon-checkmark" style="margin-left: 5px"></i>'
unableToCheck: Unable to check if any users or groups are bound to the role(s). Please try again.
waiting: |-
{count, plural,
=1 { Checking if there are any users bound to this role <i class="icon-spin icon icon-spinner" style="margin-left: 5px"></i> }
other { Checking if there are any users bound to these roles <i class="icon-spin icon icon-spinner" style="margin-left: 5px"></i> }
=1 { Checking if there are any users or groups bound to this role <i class="icon-spin icon icon-spinner" style="margin-left: 5px"></i> }
other { Checking if there are any users or groups bound to these roles <i class="icon-spin icon icon-spinner" style="margin-left: 5px"></i> }
}
usersBound: |-
{count, plural,
=1 { Caution: There is 1 user bound to the role(s) about to be deleted. Do you want to proceed? }
other { Caution: There are {count} users bound to the role(s) about to be deleted. Do you want to proceed? }
bound: |-
{users, plural,
=0 {{groups, plural,
=1 { Caution: There is 1 group bound to the role(s) about to be deleted. Do you want to proceed? }
other { Caution: There are {groups} groups bound to the role(s) about to be deleted. Do you want to proceed? }
}}
=1 {{groups, plural,
=0 { Caution: There is 1 user bound to the role(s) about to be deleted. Do you want to proceed? }
=1 { Caution: There is 1 user and 1 group bound to the role(s) about to be deleted. Do you want to proceed? }
other { Caution: There is 1 user and {groups} groups bound to the role(s) about to be deleted. Do you want to proceed? }
}}
other {{groups, plural,
=0 { Caution: There are {users} users bound to the role(s) about to be deleted. Do you want to proceed? }
=1 { Caution: There are {users} users and 1 group bound to the role(s) about to be deleted. Do you want to proceed? }
other { Caution: There are {users} users and {groups} groups bound to the role(s) about to be deleted. Do you want to proceed? }
}}
}
types:
global:
Expand Down
264 changes: 264 additions & 0 deletions shell/promptRemove/mixin/__tests__/roleDeletionCheck.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
import RoleDeletionCheck from '@shell/promptRemove/mixin/roleDeletionCheck';
import { MANAGEMENT } from '@shell/config/types';

describe('roleDeletionCheck mixin', () => {
describe('handleRoleDeletionCheck', () => {
it('should show "notBound" when no bindings exist', async() => {
const mockDispatch = jest.fn()
.mockResolvedValueOnce({ data: [] }) // role bindings
.mockResolvedValueOnce({ data: [] }); // users

const mockT = jest.fn().mockImplementation((key: string) => {
if (key === 'rbac.globalRoles.notBound') {
return 'No users or groups bound';
}

return key;
});

// Create a context object that mimics the component's `this`
const context = {
t: mockT,
warning: '',
info: '',
$store: { dispatch: mockDispatch },
};

const rolesToRemove = [{ id: 'test-role', type: MANAGEMENT.GLOBAL_ROLE }];

await RoleDeletionCheck.methods.handleRoleDeletionCheck.call(context, rolesToRemove, MANAGEMENT.GLOBAL_ROLE, '');

expect(mockT).toHaveBeenCalledWith('rbac.globalRoles.notBound', null, true);
expect(context.info).toContain('No users or groups bound');
expect(context.warning).toBe('');
});

it('should warn when only users are bound to roles', async() => {
const roleBindings = [
{ globalRoleName: 'test-role', userName: 'user-1' },
{ globalRoleName: 'test-role', userName: 'user-2' }
];
const users = [
{ id: 'user-1', username: 'admin' },
{ id: 'user-2', username: 'viewer' }
];

const mockDispatch = jest.fn()
.mockResolvedValueOnce({ data: roleBindings })
.mockResolvedValueOnce({ data: users });

const mockT = jest.fn().mockImplementation((key: string, params?: any) => {
if (key === 'rbac.globalRoles.bound') {
return `Caution: ${ params?.users } users and ${ params?.groups } groups bound`;
}

return key;
});

const context = {
t: mockT,
warning: '',
info: '',
$store: { dispatch: mockDispatch },
};

const rolesToRemove = [{ id: 'test-role', type: MANAGEMENT.GLOBAL_ROLE }];

await RoleDeletionCheck.methods.handleRoleDeletionCheck.call(context, rolesToRemove, MANAGEMENT.GLOBAL_ROLE, '');

expect(mockT).toHaveBeenCalledWith('rbac.globalRoles.bound', { users: 2, groups: 0 });
expect(context.warning).toContain('2 users');
expect(context.warning).toContain('0 groups');
expect(context.info).toBe('');
});

it('should warn when only groups are bound to roles', async() => {
const roleBindings = [
{ globalRoleName: 'test-role', groupPrincipalName: 'github_team://123' },
{ globalRoleName: 'test-role', groupPrincipalName: 'github_team://456' }
];
const users: any[] = [];

const mockDispatch = jest.fn()
.mockResolvedValueOnce({ data: roleBindings })
.mockResolvedValueOnce({ data: users });

const mockT = jest.fn().mockImplementation((key: string, params?: any) => {
if (key === 'rbac.globalRoles.bound') {
return `Caution: ${ params?.users } users and ${ params?.groups } groups bound`;
}

return key;
});

const context = {
t: mockT,
warning: '',
info: '',
$store: { dispatch: mockDispatch },
};

const rolesToRemove = [{ id: 'test-role', type: MANAGEMENT.GLOBAL_ROLE }];

await RoleDeletionCheck.methods.handleRoleDeletionCheck.call(context, rolesToRemove, MANAGEMENT.GLOBAL_ROLE, '');

expect(mockT).toHaveBeenCalledWith('rbac.globalRoles.bound', { users: 0, groups: 2 });
expect(context.warning).toContain('0 users');
expect(context.warning).toContain('2 groups');
expect(context.info).toBe('');
});

it('should warn when both users and groups are bound to roles', async() => {
const roleBindings = [
{ globalRoleName: 'test-role', userName: 'user-1' },
{ globalRoleName: 'test-role', groupPrincipalName: 'github_team://123' }
];
const users = [
{ id: 'user-1', username: 'admin' }
];

const mockDispatch = jest.fn()
.mockResolvedValueOnce({ data: roleBindings })
.mockResolvedValueOnce({ data: users });

const mockT = jest.fn().mockImplementation((key: string, params?: any) => {
if (key === 'rbac.globalRoles.bound') {
return `Caution: ${ params?.users } users and ${ params?.groups } groups bound`;
}

return key;
});

const context = {
t: mockT,
warning: '',
info: '',
$store: { dispatch: mockDispatch },
};

const rolesToRemove = [{ id: 'test-role', type: MANAGEMENT.GLOBAL_ROLE }];

await RoleDeletionCheck.methods.handleRoleDeletionCheck.call(context, rolesToRemove, MANAGEMENT.GLOBAL_ROLE, '');

expect(mockT).toHaveBeenCalledWith('rbac.globalRoles.bound', { users: 1, groups: 1 });
expect(context.warning).toContain('1 users');
expect(context.warning).toContain('1 groups');
expect(context.info).toBe('');
});

it('should count unique groups across multiple roles', async() => {
const roleBindings = [
{ globalRoleName: 'role-1', groupPrincipalName: 'github_team://123' },
{ globalRoleName: 'role-2', groupPrincipalName: 'github_team://123' }, // same group, different role
{ globalRoleName: 'role-2', groupPrincipalName: 'github_team://456' }
];
const users: any[] = [];

const mockDispatch = jest.fn()
.mockResolvedValueOnce({ data: roleBindings })
.mockResolvedValueOnce({ data: users });

const mockT = jest.fn().mockImplementation((key: string, params?: any) => {
if (key === 'rbac.globalRoles.bound') {
return `Caution: ${ params?.users } users and ${ params?.groups } groups bound`;
}

return key;
});

const context = {
t: mockT,
warning: '',
info: '',
$store: { dispatch: mockDispatch },
};

const rolesToRemove = [
{ id: 'role-1', type: MANAGEMENT.GLOBAL_ROLE },
{ id: 'role-2', type: MANAGEMENT.GLOBAL_ROLE }
];

await RoleDeletionCheck.methods.handleRoleDeletionCheck.call(context, rolesToRemove, MANAGEMENT.GLOBAL_ROLE, '');

expect(mockT).toHaveBeenCalledWith('rbac.globalRoles.bound', { users: 0, groups: 2 });
expect(context.warning).toContain('2 groups');
expect(context.info).toBe('');
});

it('should handle single group bound to a role', async() => {
const roleBindings = [
{ globalRoleName: 'test-role', groupPrincipalName: 'github_team://123' }
];
const users: any[] = [];

const mockDispatch = jest.fn()
.mockResolvedValueOnce({ data: roleBindings })
.mockResolvedValueOnce({ data: users });

const mockT = jest.fn().mockImplementation((key: string, params?: any) => {
if (key === 'rbac.globalRoles.bound') {
return `Caution: ${ params?.users } users and ${ params?.groups } groups bound`;
}

return key;
});

const context = {
t: mockT,
warning: '',
info: '',
$store: { dispatch: mockDispatch },
};

const rolesToRemove = [{ id: 'test-role', type: MANAGEMENT.GLOBAL_ROLE }];

await RoleDeletionCheck.methods.handleRoleDeletionCheck.call(context, rolesToRemove, MANAGEMENT.GLOBAL_ROLE, '');

expect(mockT).toHaveBeenCalledWith('rbac.globalRoles.bound', { users: 0, groups: 1 });
expect(context.warning).toContain('1 groups');
expect(context.info).toBe('');
});

it('should not count groups when binding has no groupPrincipalName', async() => {
const roleBindings = [
{
globalRoleName: 'test-role', userName: 'user-1', groupPrincipalName: undefined
},
{
globalRoleName: 'test-role', userName: 'user-2', groupPrincipalName: null
}
];
const users = [
{ id: 'user-1', username: 'admin' },
{ id: 'user-2', username: 'viewer' }
];

const mockDispatch = jest.fn()
.mockResolvedValueOnce({ data: roleBindings })
.mockResolvedValueOnce({ data: users });

const mockT = jest.fn().mockImplementation((key: string, params?: any) => {
if (key === 'rbac.globalRoles.bound') {
return `Caution: ${ params?.users } users and ${ params?.groups } groups bound`;
}

return key;
});

const context = {
t: mockT,
warning: '',
info: '',
$store: { dispatch: mockDispatch },
};

const rolesToRemove = [{ id: 'test-role', type: MANAGEMENT.GLOBAL_ROLE }];

await RoleDeletionCheck.methods.handleRoleDeletionCheck.call(context, rolesToRemove, MANAGEMENT.GLOBAL_ROLE, '');

expect(mockT).toHaveBeenCalledWith('rbac.globalRoles.bound', { users: 2, groups: 0 });
expect(context.warning).toContain('2 users');
expect(context.warning).toContain('0 groups');
});
});
});
12 changes: 9 additions & 3 deletions shell/promptRemove/mixin/roleDeletionCheck.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export default {
let propToMatch;
let numberOfRolesWithBinds = 0;
const uniqueUsersWithBinds = new Set();
const uniqueGroupsWithBinds = new Set();

this.info = this.t('rbac.globalRoles.waiting', { count: rolesToRemove.length });

Expand Down Expand Up @@ -80,17 +81,22 @@ export default {

if (usedRoles.length) {
const uniqueUsers = [...new Set(usedRoles.map((item) => item.userName).filter((user) => userMap[user]))];
const uniqueGroups = [...new Set(usedRoles.map((item) => item.groupPrincipalName).filter(Boolean))];

if (uniqueUsers.length) {
if (uniqueUsers.length || uniqueGroups.length) {
numberOfRolesWithBinds++;
uniqueUsers.forEach((user) => uniqueUsersWithBinds.add(user));
uniqueGroups.forEach((group) => uniqueGroupsWithBinds.add(group));
}
}
});

if (numberOfRolesWithBinds && uniqueUsersWithBinds.size) {
if (numberOfRolesWithBinds && (uniqueUsersWithBinds.size || uniqueGroupsWithBinds.size)) {
this.info = '';
this.warning = this.t('rbac.globalRoles.usersBound', { count: uniqueUsersWithBinds.size });
this.warning = this.t('rbac.globalRoles.bound', {
users: uniqueUsersWithBinds.size,
groups: uniqueGroupsWithBinds.size
});
} else {
this.info = this.t('rbac.globalRoles.notBound', null, true);
}
Expand Down