mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-24 09:28:31 +00:00
* fix: add FK constraints, unique constraint, and down() to group_admins migration * fix: add explicit FK constraint names to group_admins migration * feat: add GroupAdmin TypeORM entity * fix: add Organization relation to GroupAdmin entity * feat: add group-admin FEATURE_KEY entries and feature configs * feat: add CE no-op stub for GroupAdminService * feat: register GroupAdminService/Controller in module; add group-admin controller (CE stub + EE impl) * feat: pass user to getAllGroup(); update interface and CE service signature * test: add e2e tests for group-admin assign, revoke, scoped access, and auto-revocation * fix: correct imports and tsconfig for group-admin e2e test - fix entity imports: use @entities alias instead of src/entities - fix module imports: use @modules alias instead of src/modules - remove unused findEntityOrFail import from test-helper - add jest to tsconfig types array for test type definitions - add test-helper path mapping to tsconfig for proper resolution * fix: register GroupAdmin entity in TypeOrmModule.forFeature for DI * fix: grant workspace admins all group-admin feature keys in ability factory * feat: add group-admin service methods Add getGroupAdmins, getAddableAdmins, assignGroupAdmin, revokeGroupAdmin methods to groupPermissionV2Service following existing fetch pattern. * feat: allow group-admin builders to access Groups link in workspace settings Add is_group_admin and canManageGroups to conditionObj (both useState initializer and subscribe handler). Change Groups link condition from ['admin'] to ['canManageGroups'] to allow admin OR group-admin builders. * feat: add Group Admins tab to group permission resources * fix: handle missing default groups for group-admin builders; hide admin-only controls * feat: make permissions and granular access tabs read-only for group-admin builders; hide role-change in users tab * feat: implement GroupAdminOrAdminRoute for session validation; enhance group admin management features * feat: add GET_USER_ADMIN_GROUPS feature and update related permissions; enhance user group management * feat: enhance group admin functionality; add builder role and improve admin assignment tests * Fix: Add compiler options to tsconfig.build.json * refactor: revert configs for test suite * feat: enhance group admin functionality with feature access checks and user removal permissions * feat: add option to duplicate group admins in duplicate group * feat: enhance group permissions management - Updated FeatureAbilityFactory to include additional checks for builder permissions and group-specific access. - Modified GroupPermissionsController to apply FeatureAbilityGuard for various endpoints, ensuring proper permission checks. - Enhanced GranularPermissionsController with appropriate guards for better access control. - Refactored GroupExistenceGuard to improve group validation logic and error handling. - Updated GroupPermissionsModule to streamline service and utility registrations. - Added unit tests for FeatureAbilityFactory and GroupExistenceGuard to ensure robust permission handling. * feat: implement afterUpdateUserRole method in RolesService * feat: refactor GroupPermissionsUtilService usage in GroupPermissionsModule * feat: enhance group permissions copyright and icons, minor bug fixes: * Bug fixes Co-authored-by: Copilot <copilot@github.com> * bugz Co-authored-by: Copilot <copilot@github.com> --------- Co-authored-by: Rudhra Deep Biswas <rudra21ultra@gmail.com> Co-authored-by: Copilot <copilot@github.com>
287 lines
11 KiB
TypeScript
287 lines
11 KiB
TypeScript
import { Ability, AbilityBuilder, AbilityClass } from '@casl/ability';
|
|
import { FeatureAbilityFactory, FeatureAbility } from '@modules/group-permissions/ability';
|
|
import { AbilityService } from '@modules/ability/interfaces/IService';
|
|
import { FEATURE_KEY, GROUP_PERMISSIONS_TYPE } from '@modules/group-permissions/constants';
|
|
import { GroupPermissions } from '@entities/group_permissions.entity';
|
|
|
|
const makeBuilder = () => new AbilityBuilder<FeatureAbility>(Ability as AbilityClass<FeatureAbility>);
|
|
|
|
// All features granted to admins/superAdmins
|
|
const ALL_ADMIN_FEATURES = [
|
|
FEATURE_KEY.ADD_GROUP_USER,
|
|
FEATURE_KEY.CREATE,
|
|
FEATURE_KEY.DELETE,
|
|
FEATURE_KEY.DELETE_GROUP_USER,
|
|
FEATURE_KEY.DUPLICATE,
|
|
FEATURE_KEY.GET_ADDABLE_USERS,
|
|
FEATURE_KEY.GET_ONE,
|
|
FEATURE_KEY.GET_ALL,
|
|
FEATURE_KEY.UPDATE,
|
|
FEATURE_KEY.GET_ALL_GROUP_USER,
|
|
FEATURE_KEY.DELETE_GRANULAR_APP_PERMISSIONS,
|
|
FEATURE_KEY.DELETE_GRANULAR_DATA_PERMISSIONS,
|
|
FEATURE_KEY.CREATE_GRANULAR_APP_PERMISSIONS,
|
|
FEATURE_KEY.CREATE_GRANULAR_DATA_PERMISSIONS,
|
|
FEATURE_KEY.GET_ALL_GRANULAR_PERMISSIONS,
|
|
FEATURE_KEY.GET_ADDABLE_APPS,
|
|
FEATURE_KEY.UPDATE_GRANULAR_APP_PERMISSIONS,
|
|
FEATURE_KEY.UPDATE_GRANULAR_DATA_PERMISSIONS,
|
|
FEATURE_KEY.GET_ADDABLE_DS,
|
|
FEATURE_KEY.USER_ROLE_CHANGE,
|
|
FEATURE_KEY.CREATE_GRANULAR_FOLDER_PERMISSIONS,
|
|
FEATURE_KEY.UPDATE_GRANULAR_FOLDER_PERMISSIONS,
|
|
FEATURE_KEY.DELETE_GRANULAR_FOLDER_PERMISSIONS,
|
|
FEATURE_KEY.GET_ADDABLE_FOLDERS,
|
|
FEATURE_KEY.ASSIGN_GROUP_ADMIN,
|
|
FEATURE_KEY.REVOKE_GROUP_ADMIN,
|
|
FEATURE_KEY.GET_GROUP_ADMINS,
|
|
FEATURE_KEY.GET_ADDABLE_ADMINS,
|
|
FEATURE_KEY.GET_USER_ADMIN_GROUPS,
|
|
];
|
|
|
|
// Features a group-admin builder gets regardless of which group is requested
|
|
const BUILDER_LIST_FEATURES = [
|
|
FEATURE_KEY.GET_ALL,
|
|
FEATURE_KEY.GET_ADDABLE_APPS,
|
|
FEATURE_KEY.GET_ADDABLE_DS,
|
|
FEATURE_KEY.GET_ADDABLE_FOLDERS,
|
|
FEATURE_KEY.GET_USER_ADMIN_GROUPS,
|
|
];
|
|
|
|
// Features a group-admin builder gets on their own administered custom group
|
|
const BUILDER_ADMIN_GROUP_FEATURES = [
|
|
FEATURE_KEY.GET_ONE,
|
|
FEATURE_KEY.ADD_GROUP_USER,
|
|
FEATURE_KEY.DELETE_GROUP_USER,
|
|
FEATURE_KEY.GET_ADDABLE_USERS,
|
|
FEATURE_KEY.GET_ALL_GROUP_USER,
|
|
FEATURE_KEY.GET_GROUP_ADMINS,
|
|
FEATURE_KEY.GET_ALL_GRANULAR_PERMISSIONS,
|
|
];
|
|
|
|
// Features that builders must NEVER get (admin-escalation guard)
|
|
const BUILDER_BLOCKED_FEATURES = [
|
|
FEATURE_KEY.CREATE,
|
|
FEATURE_KEY.UPDATE,
|
|
FEATURE_KEY.DELETE,
|
|
FEATURE_KEY.DUPLICATE,
|
|
FEATURE_KEY.ASSIGN_GROUP_ADMIN,
|
|
FEATURE_KEY.REVOKE_GROUP_ADMIN,
|
|
FEATURE_KEY.GET_ADDABLE_ADMINS,
|
|
FEATURE_KEY.USER_ROLE_CHANGE,
|
|
FEATURE_KEY.CREATE_GRANULAR_APP_PERMISSIONS,
|
|
FEATURE_KEY.CREATE_GRANULAR_DATA_PERMISSIONS,
|
|
FEATURE_KEY.CREATE_GRANULAR_FOLDER_PERMISSIONS,
|
|
FEATURE_KEY.UPDATE_GRANULAR_APP_PERMISSIONS,
|
|
FEATURE_KEY.UPDATE_GRANULAR_DATA_PERMISSIONS,
|
|
FEATURE_KEY.UPDATE_GRANULAR_FOLDER_PERMISSIONS,
|
|
FEATURE_KEY.DELETE_GRANULAR_APP_PERMISSIONS,
|
|
FEATURE_KEY.DELETE_GRANULAR_DATA_PERMISSIONS,
|
|
FEATURE_KEY.DELETE_GRANULAR_FOLDER_PERMISSIONS,
|
|
];
|
|
|
|
describe('FeatureAbilityFactory :: group permissions', () => {
|
|
const factory = new FeatureAbilityFactory({} as AbilityService);
|
|
|
|
async function build(perms: Partial<typeof basePerms>, request?: any) {
|
|
const { can, build: buildAbility } = makeBuilder();
|
|
await (factory as any).defineAbilityFor(can, { ...basePerms, ...perms }, undefined, request);
|
|
return buildAbility();
|
|
}
|
|
|
|
const basePerms = {
|
|
superAdmin: false,
|
|
isAdmin: false,
|
|
isBuilder: false,
|
|
isEndUser: false,
|
|
user: { id: 'user-1', organizationId: 'org-1' },
|
|
userPermission: {} as any,
|
|
resource: [],
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// End-users
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('end-user', () => {
|
|
it('gets no group-permissions features whatsoever', async () => {
|
|
const ability = await build({ isEndUser: true });
|
|
for (const feature of ALL_ADMIN_FEATURES) {
|
|
expect(ability.can(feature, GroupPermissions)).toBe(false);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Admin
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('admin', () => {
|
|
it('gets all features', async () => {
|
|
const ability = await build({ isAdmin: true });
|
|
for (const feature of ALL_ADMIN_FEATURES) {
|
|
expect(ability.can(feature, GroupPermissions)).toBe(true);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// SuperAdmin
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('superAdmin', () => {
|
|
it('gets all features', async () => {
|
|
const ability = await build({ superAdmin: true });
|
|
for (const feature of ALL_ADMIN_FEATURES) {
|
|
expect(ability.can(feature, GroupPermissions)).toBe(true);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Builder with NO group-admin assignments
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('builder — no group-admin assignments', () => {
|
|
it('gets no features when tj_admin_groups is missing', async () => {
|
|
const ability = await build({ isBuilder: true }, { params: {} });
|
|
for (const feature of ALL_ADMIN_FEATURES) {
|
|
expect(ability.can(feature, GroupPermissions)).toBe(false);
|
|
}
|
|
});
|
|
|
|
it('gets no features when tj_admin_groups is empty array', async () => {
|
|
const ability = await build({ isBuilder: true }, { tj_admin_groups: [], params: {} });
|
|
for (const feature of ALL_ADMIN_FEATURES) {
|
|
expect(ability.can(feature, GroupPermissions)).toBe(false);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Builder-admin — list-level access (no specific group requested)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('builder-admin — no group context (list-level)', () => {
|
|
const request = {
|
|
tj_admin_groups: [{ id: 'group-1', name: 'ops' }],
|
|
params: {},
|
|
};
|
|
|
|
it('grants list-level features', async () => {
|
|
const ability = await build({ isBuilder: true }, request);
|
|
for (const feature of BUILDER_LIST_FEATURES) {
|
|
expect(ability.can(feature, GroupPermissions)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('does not grant any group-specific or write features', async () => {
|
|
const ability = await build({ isBuilder: true }, request);
|
|
for (const feature of BUILDER_BLOCKED_FEATURES) {
|
|
expect(ability.can(feature, GroupPermissions)).toBe(false);
|
|
}
|
|
expect(ability.can(FEATURE_KEY.GET_ONE, GroupPermissions)).toBe(false);
|
|
expect(ability.can(FEATURE_KEY.ADD_GROUP_USER, GroupPermissions)).toBe(false);
|
|
expect(ability.can(FEATURE_KEY.DELETE_GROUP_USER, GroupPermissions)).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Builder-admin — administered custom group
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('builder-admin — their own administered custom group', () => {
|
|
const request = {
|
|
tj_admin_groups: [{ id: 'group-1', name: 'ops' }],
|
|
tj_group: { id: 'group-1', type: GROUP_PERMISSIONS_TYPE.CUSTOM_GROUP },
|
|
tj_resource_id: 'group-1',
|
|
params: {},
|
|
};
|
|
|
|
it('grants user-management features on the administered group', async () => {
|
|
const ability = await build({ isBuilder: true }, request);
|
|
for (const feature of BUILDER_ADMIN_GROUP_FEATURES) {
|
|
expect(ability.can(feature, GroupPermissions)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('also retains list-level features', async () => {
|
|
const ability = await build({ isBuilder: true }, request);
|
|
for (const feature of BUILDER_LIST_FEATURES) {
|
|
expect(ability.can(feature, GroupPermissions)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('never grants write/destructive or admin-escalation features', async () => {
|
|
const ability = await build({ isBuilder: true }, request);
|
|
for (const feature of BUILDER_BLOCKED_FEATURES) {
|
|
expect(ability.can(feature, GroupPermissions)).toBe(false);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Builder-admin — custom group they do NOT administer
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('builder-admin — custom group they do not administer', () => {
|
|
const request = {
|
|
tj_admin_groups: [{ id: 'group-1', name: 'ops' }],
|
|
tj_group: { id: 'group-99', type: GROUP_PERMISSIONS_TYPE.CUSTOM_GROUP },
|
|
tj_resource_id: 'group-99',
|
|
params: {},
|
|
};
|
|
|
|
it('gets only list-level features, no group-specific access', async () => {
|
|
const ability = await build({ isBuilder: true }, request);
|
|
for (const feature of BUILDER_LIST_FEATURES) {
|
|
expect(ability.can(feature, GroupPermissions)).toBe(true);
|
|
}
|
|
expect(ability.can(FEATURE_KEY.GET_ONE, GroupPermissions)).toBe(false);
|
|
expect(ability.can(FEATURE_KEY.ADD_GROUP_USER, GroupPermissions)).toBe(false);
|
|
expect(ability.can(FEATURE_KEY.DELETE_GROUP_USER, GroupPermissions)).toBe(false);
|
|
expect(ability.can(FEATURE_KEY.GET_ALL_GROUP_USER, GroupPermissions)).toBe(false);
|
|
});
|
|
|
|
it('never grants write or admin-escalation features', async () => {
|
|
const ability = await build({ isBuilder: true }, request);
|
|
for (const feature of BUILDER_BLOCKED_FEATURES) {
|
|
expect(ability.can(feature, GroupPermissions)).toBe(false);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Builder-admin — default group (read-only)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('builder-admin — default group (not administered)', () => {
|
|
const request = {
|
|
tj_admin_groups: [{ id: 'group-1', name: 'ops' }],
|
|
tj_group: { id: 'default-group', type: GROUP_PERMISSIONS_TYPE.DEFAULT },
|
|
tj_resource_id: 'default-group',
|
|
params: {},
|
|
};
|
|
|
|
it('can read group details and list users/granular-permissions', async () => {
|
|
const ability = await build({ isBuilder: true }, request);
|
|
expect(ability.can(FEATURE_KEY.GET_ONE, GroupPermissions)).toBe(true);
|
|
expect(ability.can(FEATURE_KEY.GET_ALL_GROUP_USER, GroupPermissions)).toBe(true);
|
|
expect(ability.can(FEATURE_KEY.GET_ALL_GRANULAR_PERMISSIONS, GroupPermissions)).toBe(true);
|
|
});
|
|
|
|
it('cannot mutate users on a default group', async () => {
|
|
const ability = await build({ isBuilder: true }, request);
|
|
expect(ability.can(FEATURE_KEY.ADD_GROUP_USER, GroupPermissions)).toBe(false);
|
|
expect(ability.can(FEATURE_KEY.DELETE_GROUP_USER, GroupPermissions)).toBe(false);
|
|
expect(ability.can(FEATURE_KEY.GET_ADDABLE_USERS, GroupPermissions)).toBe(false);
|
|
});
|
|
|
|
it('never grants write or admin-escalation features', async () => {
|
|
const ability = await build({ isBuilder: true }, request);
|
|
for (const feature of BUILDER_BLOCKED_FEATURES) {
|
|
expect(ability.can(feature, GroupPermissions)).toBe(false);
|
|
}
|
|
});
|
|
});
|
|
});
|