ToolJet/server/test/modules/versions/unit/app-version.ability.spec.ts
Shantanu Mane e74db2b4af
Fix permission key logic in defineAppVersionAbility function (#16305)
* fix: update permission key logic in defineAppVersionAbility function

* fix: remove unused 'resource' from UserAllPermissions destructuring in defineAppVersionAbility function

* test: add unit tests for defineAppVersionAbility function

* refactor: formatting changes

* test: add unit tests for different roles in defineAppVersionAbility function

* test: fix review issues in defineAppVersionAbility unit tests

* fix: update defineAppVersionAbility to use correct User type and improve permissions handling

* fix: update user app permissions query to include MODULE type

* fix: enhance app visibility logic for MODULE type in getAppsFor method

* fix: improve environment synchronization and permissions handling in version management

* fix: enhance permission handling for MODULE type apps across various services

* fix: enhance permission checks for module and workflow apps in environment access logic

* fix: enhance permission handling for MODULE apps and add support for end-user abilities

* fix: remove debug logging from ability definition for data query apps

* fix: enhance module folder permissions for builders and improve access checks

* fix: enhance permission checks for folder updates in module context for builder roles
2026-05-08 21:24:51 +05:30

430 lines
15 KiB
TypeScript

/// <reference types="jest" />
import { Ability, AbilityBuilder, AbilityClass } from '@casl/ability';
import { FeatureAbility } from '@modules/versions/ability/index';
import { defineAppVersionAbility } from '@modules/versions/ability/app-version.ability';
import { FEATURE_KEY } from '@modules/versions/constants';
import { MODULES } from '@modules/app/constants/modules';
import { App } from '@entities/app.entity';
import { UserAllPermissions } from '@modules/app/types';
import { DEFAULT_USER_PERMISSIONS, DEFAULT_USER_APPS_PERMISSIONS } from '@modules/ability/constants';
import { User } from '@entities/user.entity';
const makeBuilder = () => new AbilityBuilder<FeatureAbility>(Ability as AbilityClass<FeatureAbility>);
const buildPermissions = (
overrides: {
superAdmin?: boolean;
isAdmin?: boolean;
isBuilder?: boolean;
isEndUser?: boolean;
isAllEditable?: boolean;
isAllViewable?: boolean;
editableAppsId?: string[];
viewableAppsId?: string[];
resourceType?: MODULES;
appPromote?: boolean;
} = {}
): UserAllPermissions => {
const {
superAdmin = false,
isAdmin = false,
isBuilder = false,
isEndUser = false,
isAllEditable = false,
isAllViewable = false,
editableAppsId = [],
viewableAppsId = [],
resourceType = MODULES.APP,
appPromote = false,
} = overrides;
return {
superAdmin,
isAdmin,
isBuilder,
isEndUser,
user: {} as User,
resource: [{ resourceType }],
userPermission: {
...DEFAULT_USER_PERMISSIONS,
appPromote,
[MODULES.APP]: {
...DEFAULT_USER_APPS_PERMISSIONS,
isAllEditable,
isAllViewable,
editableAppsId,
viewableAppsId,
},
},
};
};
/** @group platform */
describe('defineAppVersionAbility', () => {
const ALL_ACTIONS = [
FEATURE_KEY.GET,
FEATURE_KEY.DELETE,
FEATURE_KEY.CREATE,
FEATURE_KEY.GET_ONE,
FEATURE_KEY.UPDATE,
FEATURE_KEY.UPDATE_SETTINGS,
FEATURE_KEY.CREATE_COMPONENTS,
FEATURE_KEY.UPDATE_COMPONENTS,
FEATURE_KEY.UPDATE_COMPONENT_LAYOUT,
FEATURE_KEY.DELETE_COMPONENTS,
FEATURE_KEY.CREATE_PAGES,
FEATURE_KEY.CLONE_PAGES,
FEATURE_KEY.CLONE_GROUP,
FEATURE_KEY.UPDATE_PAGES,
FEATURE_KEY.DELETE_PAGE,
FEATURE_KEY.REORDER_PAGES,
FEATURE_KEY.GET_EVENTS,
FEATURE_KEY.CREATE_EVENT,
FEATURE_KEY.UPDATE_EVENT,
FEATURE_KEY.DELETE_EVENT,
FEATURE_KEY.APP_VERSION_CREATE,
FEATURE_KEY.APP_VERSION_DELETE,
FEATURE_KEY.APP_VERSION_UPDATE,
FEATURE_KEY.APP_DRAFT_VERSION_CREATE,
];
const VIEW_ACTIONS = [FEATURE_KEY.GET, FEATURE_KEY.GET_ONE, FEATURE_KEY.GET_EVENTS];
describe('MODULES resource type', () => {
it('grants all edit actions when isAllEditable is true', () => {
const perms = buildPermissions({ isAllEditable: true, resourceType: MODULES.MODULES });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms);
const ability = build();
ALL_ACTIONS.forEach((action) => {
expect(ability.can(action, App)).toBe(true);
});
});
it('grants all edit actions when resourceId is in editableAppsId', () => {
const resourceId = 'module-uuid-1';
const perms = buildPermissions({ editableAppsId: [resourceId], resourceType: MODULES.MODULES });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms, resourceId);
const ability = build();
ALL_ACTIONS.forEach((action) => {
expect(ability.can(action, App)).toBe(true);
});
});
it('denies edit actions when resourceId is not in editableAppsId', () => {
const perms = buildPermissions({ editableAppsId: ['other-uuid'], resourceType: MODULES.MODULES });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms, 'module-uuid-1');
const ability = build();
ALL_ACTIONS.forEach((action) => {
expect(ability.can(action, App)).toBe(false);
});
});
it('grants view actions when isAllViewable is true', () => {
const perms = buildPermissions({ isAllViewable: true, resourceType: MODULES.MODULES });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms);
const ability = build();
VIEW_ACTIONS.forEach((action) => {
expect(ability.can(action, App)).toBe(true);
});
expect(ability.can(FEATURE_KEY.UPDATE_COMPONENTS, App)).toBe(false);
});
it('grants view actions when resourceId is in viewableAppsId', () => {
const resourceId = 'module-uuid-2';
const perms = buildPermissions({ viewableAppsId: [resourceId], resourceType: MODULES.MODULES });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms, resourceId);
const ability = build();
VIEW_ACTIONS.forEach((action) => {
expect(ability.can(action, App)).toBe(true);
});
expect(ability.can(FEATURE_KEY.UPDATE_COMPONENTS, App)).toBe(false);
});
it('grants all actions to isBuilder via early return', () => {
const perms = buildPermissions({ isBuilder: true, resourceType: MODULES.MODULES });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms);
const ability = build();
[...ALL_ACTIONS, FEATURE_KEY.PROMOTE].forEach((action) => {
expect(ability.can(action, App)).toBe(true);
});
});
it('denies all actions with no permissions', () => {
const perms = buildPermissions({ resourceType: MODULES.MODULES });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms);
const ability = build();
ALL_ACTIONS.forEach((action) => {
expect(ability.can(action, App)).toBe(false);
});
});
});
describe('APP resource type (regression)', () => {
it('grants all edit actions when isAllEditable is true', () => {
const perms = buildPermissions({ isAllEditable: true, resourceType: MODULES.APP });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms);
const ability = build();
ALL_ACTIONS.forEach((action) => {
expect(ability.can(action, App)).toBe(true);
});
});
it('grants all edit actions when resourceId is in editableAppsId', () => {
const resourceId = 'app-uuid-1';
const perms = buildPermissions({ editableAppsId: [resourceId], resourceType: MODULES.APP });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms, resourceId);
const ability = build();
ALL_ACTIONS.forEach((action) => {
expect(ability.can(action, App)).toBe(true);
});
});
});
describe('end user', () => {
it('denies all actions with no permissions', () => {
const perms = buildPermissions({ isEndUser: true });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms);
const ability = build();
[...ALL_ACTIONS, FEATURE_KEY.PROMOTE].forEach((action) => {
expect(ability.can(action, App)).toBe(false);
});
});
it('grants view actions when isAllViewable is true', () => {
const perms = buildPermissions({ isEndUser: true, isAllViewable: true });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms);
const ability = build();
VIEW_ACTIONS.forEach((action) => {
expect(ability.can(action, App)).toBe(true);
});
});
it('denies edit actions even when isAllViewable is true', () => {
const perms = buildPermissions({ isEndUser: true, isAllViewable: true });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms);
const ability = build();
const editOnlyActions = ALL_ACTIONS.filter((a) => !VIEW_ACTIONS.includes(a));
editOnlyActions.forEach((action) => {
expect(ability.can(action, App)).toBe(false);
});
});
it('grants view actions when resourceId is in viewableAppsId', () => {
const resourceId = 'app-uuid-end-user';
const perms = buildPermissions({ isEndUser: true, viewableAppsId: [resourceId] });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms, resourceId);
const ability = build();
VIEW_ACTIONS.forEach((action) => {
expect(ability.can(action, App)).toBe(true);
});
});
it('denies view actions when resourceId is not in viewableAppsId', () => {
const perms = buildPermissions({ isEndUser: true, viewableAppsId: ['other-uuid'] });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms, 'app-uuid-end-user');
const ability = build();
VIEW_ACTIONS.forEach((action) => {
expect(ability.can(action, App)).toBe(false);
});
});
it('does not get builder component grant on MODULES resource type', () => {
const perms = buildPermissions({ isEndUser: true, resourceType: MODULES.MODULES });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms);
const ability = build();
expect(ability.can(FEATURE_KEY.UPDATE_COMPONENTS, App)).toBe(false);
expect(ability.can(FEATURE_KEY.CREATE_COMPONENTS, App)).toBe(false);
});
it('denies PROMOTE regardless of resource type', () => {
for (const resourceType of [MODULES.APP, MODULES.MODULES]) {
const perms = buildPermissions({ isEndUser: true, resourceType });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms);
const ability = build();
expect(ability.can(FEATURE_KEY.PROMOTE, App)).toBe(false);
}
});
});
describe('admin / superAdmin bypass', () => {
it('grants all edit actions for isAdmin regardless of resource type', () => {
for (const resourceType of [MODULES.APP, MODULES.MODULES]) {
const perms = buildPermissions({ isAdmin: true, resourceType });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms);
const ability = build();
ALL_ACTIONS.forEach((action) => {
expect(ability.can(action, App)).toBe(true);
});
}
});
it('grants all edit actions for superAdmin regardless of resource type', () => {
for (const resourceType of [MODULES.APP, MODULES.MODULES]) {
const perms = buildPermissions({ superAdmin: true, resourceType });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms);
const ability = build();
ALL_ACTIONS.forEach((action) => {
expect(ability.can(action, App)).toBe(true);
});
}
});
it('grants PROMOTE to isAdmin', () => {
const perms = buildPermissions({ isAdmin: true });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms);
const ability = build();
expect(ability.can(FEATURE_KEY.PROMOTE, App)).toBe(true);
});
it('grants PROMOTE to superAdmin', () => {
const perms = buildPermissions({ superAdmin: true });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms);
const ability = build();
expect(ability.can(FEATURE_KEY.PROMOTE, App)).toBe(true);
});
it('grants all actions for admin even with no APP permissions set', () => {
// admin bypasses all permission flag checks via early return
const perms = buildPermissions({ isAdmin: true, isAllEditable: false, editableAppsId: [] });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms);
const ability = build();
[...ALL_ACTIONS, FEATURE_KEY.PROMOTE].forEach((action) => {
expect(ability.can(action, App)).toBe(true);
});
});
});
describe('PROMOTE permission', () => {
it('grants PROMOTE when appPromote is true', () => {
const perms = buildPermissions({ appPromote: true, resourceType: MODULES.APP });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms);
const ability = build();
expect(ability.can(FEATURE_KEY.PROMOTE, App)).toBe(true);
});
it('denies PROMOTE when appPromote is false', () => {
const perms = buildPermissions({ appPromote: false, resourceType: MODULES.APP });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms);
const ability = build();
expect(ability.can(FEATURE_KEY.PROMOTE, App)).toBe(false);
});
it('grants PROMOTE via appPromote on MODULES resource type', () => {
const perms = buildPermissions({ appPromote: true, resourceType: MODULES.MODULES });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms);
const ability = build();
expect(ability.can(FEATURE_KEY.PROMOTE, App)).toBe(true);
});
});
describe('builder role edge cases', () => {
it('does not apply builder component grant on APP resource type', () => {
// isBuilder + MODULES.APP → no special builder grant (only MODULES.MODULES triggers it)
const perms = buildPermissions({ isBuilder: true, resourceType: MODULES.APP });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms);
const ability = build();
// no edit permissions set, so component actions should be denied
expect(ability.can(FEATURE_KEY.UPDATE_COMPONENTS, App)).toBe(false);
expect(ability.can(FEATURE_KEY.CREATE_COMPONENTS, App)).toBe(false);
});
it('grants version management actions to isBuilder on MODULES', () => {
const perms = buildPermissions({ isBuilder: true, resourceType: MODULES.MODULES });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms);
const ability = build();
expect(ability.can(FEATURE_KEY.APP_VERSION_CREATE, App)).toBe(true);
expect(ability.can(FEATURE_KEY.APP_VERSION_DELETE, App)).toBe(true);
expect(ability.can(FEATURE_KEY.APP_VERSION_UPDATE, App)).toBe(true);
expect(ability.can(FEATURE_KEY.DELETE, App)).toBe(true);
expect(ability.can(FEATURE_KEY.CREATE_PAGES, App)).toBe(true);
});
});
describe('granular permission boundary conditions', () => {
it('denies edit actions when editableAppsId is non-empty but resourceId is undefined', () => {
const perms = buildPermissions({ editableAppsId: ['app-uuid-1'] });
const { can, build } = makeBuilder();
// no resourceId passed
defineAppVersionAbility(can, perms, undefined);
const ability = build();
expect(ability.can(FEATURE_KEY.UPDATE_COMPONENTS, App)).toBe(false);
expect(ability.can(FEATURE_KEY.CREATE_PAGES, App)).toBe(false);
});
it('denies view actions when viewableAppsId is non-empty but resourceId is undefined', () => {
const perms = buildPermissions({ viewableAppsId: ['app-uuid-1'] });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms, undefined);
const ability = build();
VIEW_ACTIONS.forEach((action) => {
expect(ability.can(action, App)).toBe(false);
});
});
it('grants edit actions from isAllEditable even without resourceId', () => {
const perms = buildPermissions({ isAllEditable: true });
const { can, build } = makeBuilder();
defineAppVersionAbility(can, perms, undefined);
const ability = build();
ALL_ACTIONS.forEach((action) => {
expect(ability.can(action, App)).toBe(true);
});
});
});
});