From 1606852abe7bf2b1c00099aa668f55181dc28e78 Mon Sep 17 00:00:00 2001 From: kriks7iitk <34170719+kriks7iitk@users.noreply.github.com> Date: Sat, 21 Jun 2025 13:10:05 +0530 Subject: [PATCH] Feature/workflow permission 2 (#12869) * Added worklfow permsiions * Fixed bugs in workflow permission * fix bugs in server for worklfow permsiion * fix bugs for workflow permissions * reverted not required files * reverted package lock json * revert back .gitmodules url * revered package lock * plugins import fix * fix * Update groups test cases --------- Co-authored-by: Adish M Co-authored-by: Muhsin Shah C P Co-authored-by: gsmithun4 Co-authored-by: ajith-k-v --- .../constants/selectors/manageGroups.js | 2 +- .../cypress/constants/texts/manageGroups.js | 2 +- frontend/assets/translations/en.json | 2 + frontend/src/App/App.jsx | 4 +- frontend/src/HomePage/HomePage.jsx | 101 +++-- .../src/_services/authentication.service.js | 1 + frontend/src/_styles/theme.scss | 2 +- frontend/src/_ui/Modal/AppsSelect.jsx | 41 +- .../BaseManageGranularAccess.jsx | 371 ++++++++++-------- .../AddEditResourcePermissionsModal.jsx | 195 ++++++--- .../WorkflowPermissionActionContainer.jsx | 8 + .../components/AddResourcePermissionsMenu.jsx | 30 +- .../components/WorkflowResourcePermission.jsx | 8 + .../BaseManageGroupPermissionResources.jsx | 10 +- .../group-permission-resources.styles.scss | 16 +- .../BaseManageGroupPermissions.jsx | 1 + .../WorkflowPermissionsUI.jsx | 8 + .../components/WorkflowPermissionsUI/index.js | 1 + .../WorkspaceSettings/pages/Groups/index.js | 17 + ...ddWorkflowPermissionsInGroupPermissions.ts | 19 + ...705371665-AddWorkflowTypeInResourceType.ts | 11 + ...448788-AddAppTypeInAppsGroupPermissions.ts | 25 ++ server/package-lock.json | 2 +- server/src/entities/app.entity.ts | 10 +- server/src/entities/app_base.entity.ts | 10 +- .../entities/apps_group_permissions.entity.ts | 8 + server/src/entities/group_apps.entity.ts | 2 +- .../src/entities/group_permissions.entity.ts | 6 + server/src/modules/ability/constants.ts | 23 +- server/src/modules/ability/service.ts | 8 +- server/src/modules/ability/types.ts | 9 + server/src/modules/ability/util.service.ts | 110 ++++-- server/src/modules/app/ability-factory.ts | 2 +- .../src/modules/app/guards/ability.guard.ts | 10 + server/src/modules/app/loader.ts | 6 +- server/src/modules/app/types.ts | 1 + .../src/modules/apps/ability/app.ability.ts | 78 ++++ server/src/modules/apps/ability/guard.ts | 17 +- server/src/modules/apps/ability/index.ts | 71 +--- server/src/modules/apps/ability/utility.ts | 27 ++ .../modules/apps/ability/workflow.ability.ts | 78 ++++ server/src/modules/apps/service.ts | 9 +- .../modules/apps/services/workflow.service.ts | 4 +- server/src/modules/apps/util.service.ts | 124 ++++-- .../ability/app/data-query-app.ability.ts | 84 ++++ .../app/data-query-workflow.ability.ts | 92 +++++ .../modules/data-queries/ability/app/guard.ts | 16 +- .../modules/data-queries/ability/app/index.ts | 88 +---- .../folder-apps/interfaces/IUtilService.ts | 8 +- server/src/modules/folder-apps/service.ts | 19 +- .../src/modules/folder-apps/util.service.ts | 139 ++++--- .../constants/granular_permissions.ts | 1 + .../group-permissions/constants/index.ts | 19 + .../modules/group-permissions/dto/index.ts | 8 + .../services/granular-permissions.service.ts | 2 +- .../types/granular_permissions.ts | 68 +++- .../modules/group-permissions/types/index.ts | 2 + .../granular-permissions.util.service.ts | 35 +- .../modules/group-permissions/util.service.ts | 4 + .../src/modules/licensing/guards/app.guard.ts | 3 +- .../modules/licensing/guards/webhook.guard.ts | 3 +- .../licensing/guards/workflowcount.guard.ts | 5 +- .../licensing/services/count.service.ts | 5 +- .../versions/ability/app-version.ability.ts | 114 ++++++ server/src/modules/versions/ability/guard.ts | 17 +- server/src/modules/versions/ability/index.ts | 79 +--- .../src/modules/versions/ability/utility.ts | 25 ++ .../ability/workflow-version.ability.ts | 75 ++++ 68 files changed, 1743 insertions(+), 658 deletions(-) create mode 100644 frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGranularAccess/components/AddEditResourceModal/WorkflowPermissionActionContainer.jsx create mode 100644 frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGranularAccess/components/WorkflowResourcePermission.jsx create mode 100644 frontend/src/modules/WorkspaceSettings/pages/Groups/components/WorkflowPermissionsUI/WorkflowPermissionsUI.jsx create mode 100644 frontend/src/modules/WorkspaceSettings/pages/Groups/components/WorkflowPermissionsUI/index.js create mode 100644 server/migrations/1746705301652-AddWorkflowPermissionsInGroupPermissions.ts create mode 100644 server/migrations/1746705371665-AddWorkflowTypeInResourceType.ts create mode 100644 server/migrations/1746705448788-AddAppTypeInAppsGroupPermissions.ts create mode 100644 server/src/modules/apps/ability/app.ability.ts create mode 100644 server/src/modules/apps/ability/utility.ts create mode 100644 server/src/modules/apps/ability/workflow.ability.ts create mode 100644 server/src/modules/data-queries/ability/app/data-query-app.ability.ts create mode 100644 server/src/modules/data-queries/ability/app/data-query-workflow.ability.ts create mode 100644 server/src/modules/versions/ability/app-version.ability.ts create mode 100644 server/src/modules/versions/ability/utility.ts create mode 100644 server/src/modules/versions/ability/workflow-version.ability.ts diff --git a/cypress-tests/cypress/constants/selectors/manageGroups.js b/cypress-tests/cypress/constants/selectors/manageGroups.js index 26ae4f3219..5066342ff6 100644 --- a/cypress-tests/cypress/constants/selectors/manageGroups.js +++ b/cypress-tests/cypress/constants/selectors/manageGroups.js @@ -40,7 +40,7 @@ export const groupsSelector = { resourceLabel: '[data-cy="resource-label"]', allAppsRadio: '[data-cy="all-apps-radio"]', allAppsLabel: '[data-cy="all-apps-label"]', - allAppsHelperText: '[data-cy="all-apps-info-text"]', + allAppsHelperText: '[data-cy="this-will-select-all-apps-in-the-workspace-including-any-new-apps-created-info-text"]', customradio: '[data-cy="custom-radio"]', customLabel: '[data-cy="custom-label"]', customHelperText: '[data-cy="custom-info-text"]', diff --git a/cypress-tests/cypress/constants/texts/manageGroups.js b/cypress-tests/cypress/constants/texts/manageGroups.js index e20dbd87e1..7d1a556cd0 100644 --- a/cypress-tests/cypress/constants/texts/manageGroups.js +++ b/cypress-tests/cypress/constants/texts/manageGroups.js @@ -78,7 +78,7 @@ export const groupsText = { allAppsLabel: 'All apps', allAppsHelperText: 'This will select all apps in the workspace including any new apps created', customLabel: 'Custom', - customHelperText: 'Select specific applications you want to add to the group', + customHelperText: 'Select specific apps you want to add to the group', updateButtonText: 'Update', addButtonText: 'Add', userRole: 'User role', diff --git a/frontend/assets/translations/en.json b/frontend/assets/translations/en.json index b76668a062..1feaf4277d 100644 --- a/frontend/assets/translations/en.json +++ b/frontend/assets/translations/en.json @@ -4,6 +4,8 @@ "cancel": "Cancel", "save": "Save", "savechanges": "Save changes", + "execute": "Execute", + "Build": "Build", "back": "Back", "edit": "Edit", "search": "Search", diff --git a/frontend/src/App/App.jsx b/frontend/src/App/App.jsx index e2967e892d..7e47c82ce2 100644 --- a/frontend/src/App/App.jsx +++ b/frontend/src/App/App.jsx @@ -282,9 +282,9 @@ class AppComponent extends React.Component { exact path="/:workspaceId/workflows/*" element={ - + - + } /> )} diff --git a/frontend/src/HomePage/HomePage.jsx b/frontend/src/HomePage/HomePage.jsx index dba8b21962..3d883cd4b2 100644 --- a/frontend/src/HomePage/HomePage.jsx +++ b/frontend/src/HomePage/HomePage.jsx @@ -64,6 +64,7 @@ class HomePageComponent extends React.Component { id: currentSession?.current_user.id, organization_id: currentSession?.current_organization_id, }, + shouldRedirect: false, users: null, isLoading: true, creatingApp: false, @@ -131,6 +132,13 @@ class HomePageComponent extends React.Component { }; componentDidMount() { + if (this.props.appType === 'workflow') { + if (!this.canViewWorkflow()) { + toast.error('You do not have permission to view workflows'); + this.setState({ shouldRedirect: true }); + return; + } + } fetchAndSetWindowTitle({ page: pageTitles.DASHBOARD }); this.fetchApps(1, this.state.currentFolder.id); this.fetchFolders(); @@ -148,7 +156,7 @@ class HomePageComponent extends React.Component { } } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps, prevState) { if (prevProps.appType != this.props.appType) { this.fetchFolders(); this.fetchApps(1); @@ -156,6 +164,10 @@ class HomePageComponent extends React.Component { if (Object.keys(this.props.featureAccess).length && !this.state.featureAccess) { this.setState({ featureAccess: this.props.featureAccess, featuresLoaded: this.props.featuresLoaded }); } + if (this.state.shouldRedirect && !prevState.shouldRedirect) { + const workspaceId = getWorkspaceId(); + this.props.navigate(`/${workspaceId}`); + } } fetchFeatureAccesss = () => { @@ -450,37 +462,70 @@ class HomePageComponent extends React.Component { } }; + canViewWorkflow = () => { + return this.canUserPerform(this.state.currentUser, 'view'); + }; + canUserPerform(user, action, app) { - if (authenticationService.currentSessionValue?.super_admin) return true; const currentSession = authenticationService.currentSessionValue; - const appPermission = currentSession.app_group_permissions; - const canUpdateApp = - appPermission && (appPermission.is_all_editable || appPermission.editable_apps_id.includes(app?.id)); - const canReadApp = - (appPermission && canUpdateApp) || - appPermission.is_all_viewable || - appPermission.viewable_apps_id.includes(app?.id); - let permissionGrant; + const { user_permissions, app_group_permissions, workflow_group_permissions, super_admin, admin } = currentSession; - switch (action) { - case 'create': - permissionGrant = currentSession.user_permissions.app_create; - break; - case 'read': - permissionGrant = this.isUserOwnerOfApp(user, app) || canReadApp; - break; - case 'update': - permissionGrant = canUpdateApp || this.isUserOwnerOfApp(user, app); - break; - case 'delete': - permissionGrant = currentSession.user_permissions.app_delete || this.isUserOwnerOfApp(user, app); - break; - default: - permissionGrant = false; - break; + if (super_admin) return true; + + if (this.props.appType === 'workflow') { + const canCreateWorkflow = admin || user_permissions?.workflow_create; + const canUpdateWorkflow = + workflow_group_permissions?.is_all_editable || + workflow_group_permissions?.editable_workflows_id?.includes(app?.id); + const canExecuteWorkflow = + canUpdateWorkflow || + workflow_group_permissions?.is_all_executable || + workflow_group_permissions?.executable_workflows_id?.includes(app?.id); + const canDeleteWorkflow = user_permissions?.workflow_delete || admin; + + switch (action) { + case 'create': + return canCreateWorkflow; + case 'read': + return canCreateWorkflow || canUpdateWorkflow || canDeleteWorkflow || canExecuteWorkflow; + case 'update': + return canUpdateWorkflow; + case 'delete': + return canDeleteWorkflow; + case 'view': + return ( + canCreateWorkflow || + canUpdateWorkflow || + canDeleteWorkflow || + canExecuteWorkflow || + workflow_group_permissions?.editable_workflows_id?.length > 0 || + workflow_group_permissions?.executable_workflows_id?.length > 0 + ); + default: + return false; + } + } else { + const canUpdateApp = + app_group_permissions && + (app_group_permissions.is_all_editable || app_group_permissions.editable_apps_id.includes(app?.id)); + const canReadApp = + (app_group_permissions && canUpdateApp) || + app_group_permissions.is_all_viewable || + app_group_permissions.viewable_apps_id.includes(app?.id); + + switch (action) { + case 'create': + return user_permissions.app_create; + case 'read': + return this.isUserOwnerOfApp(user, app) || canReadApp; + case 'update': + return canUpdateApp || this.isUserOwnerOfApp(user, app); + case 'delete': + return user_permissions.app_delete || this.isUserOwnerOfApp(user, app); + default: + return false; + } } - - return permissionGrant; } isUserOwnerOfApp(user, app) { diff --git a/frontend/src/_services/authentication.service.js b/frontend/src/_services/authentication.service.js index 3c05faf924..1eb6a00c3e 100644 --- a/frontend/src/_services/authentication.service.js +++ b/frontend/src/_services/authentication.service.js @@ -21,6 +21,7 @@ const currentSessionSubject = new BehaviorSubject({ group_permissions: null, app_group_permissions: null, data_source_group_permissions: null, + workflow_group_permissions: null, role: null, organizations: [], isUserLoggingIn: false, diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss index b64a0cdc37..fefe527bf7 100644 --- a/frontend/src/_styles/theme.scss +++ b/frontend/src/_styles/theme.scss @@ -10661,7 +10661,6 @@ tbody { .manage-groups-body { padding: 12px 12px 10px 12px; font-size: 12px; - overflow-y: auto; height: calc(100vh - 300px); .group-users-list-container { @@ -10882,6 +10881,7 @@ tbody { } .permission-body { + .form-check { margin-bottom: 0px !important; } diff --git a/frontend/src/_ui/Modal/AppsSelect.jsx b/frontend/src/_ui/Modal/AppsSelect.jsx index e2550efcf9..78ffb34146 100644 --- a/frontend/src/_ui/Modal/AppsSelect.jsx +++ b/frontend/src/_ui/Modal/AppsSelect.jsx @@ -6,10 +6,46 @@ import Select, { components } from 'react-select'; import { FilterPreview } from '@/_components'; import './appSelect.theme.scss'; +export const RESOURCE_TYPE = { + APPS: 'app', + DATA_SOURCES: 'data_source', + WORKFLOWS: 'workflow', +}; + +export const getResourceTypeConfig = (resourceType) => { + switch (resourceType) { + case RESOURCE_TYPE.APPS: + return { + placeholder: 'Select apps..', + noOptionsMessage: 'No apps found', + icon: 'apps', + }; + case RESOURCE_TYPE.DATA_SOURCES: + return { + placeholder: 'Select data sources..', + noOptionsMessage: 'No data sources found', + icon: 'datasource', + }; + case RESOURCE_TYPE.WORKFLOWS: + return { + placeholder: 'Select workflows..', + noOptionsMessage: 'No workflows found', + icon: 'workflows', + }; + default: + return { + placeholder: 'Select resources..', + noOptionsMessage: 'No resources found', + icon: 'apps', + }; + } +}; + export function AppsSelect(props) { const navigate = useNavigate(); const workspaceId = getWorkspaceId(); const darkMode = localStorage.getItem('darkMode') === 'true'; + const resourceConfig = getResourceTypeConfig(props.resourceType); //Will be used when workspace routing settings have been merged const Menu = (props) => { @@ -188,8 +224,8 @@ export function AppsSelect(props) { }} options={[props.allowSelectAll ? props.allOption : null, ...props.options]} styles={selectStyles} - placeholder={props.resourceType === 'Apps' ? 'Select apps..' : 'Select data sources..'} - noOptionsMessage={() => 'No apps found'} + placeholder={resourceConfig.placeholder} + noOptionsMessage={() => resourceConfig.noOptionsMessage} /> // ); @@ -201,4 +237,5 @@ AppsSelect.defaultProps = { value: '*', isAllField: true, }, + resourceType: RESOURCE_TYPE.APPS, }; diff --git a/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGranularAccess/BaseManageGranularAccess.jsx b/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGranularAccess/BaseManageGranularAccess.jsx index 79dd244d79..118340107c 100644 --- a/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGranularAccess/BaseManageGranularAccess.jsx +++ b/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGranularAccess/BaseManageGranularAccess.jsx @@ -11,7 +11,9 @@ import AddResourcePermissionsMenu from './components/AddResourcePermissionsMenu' import { ConfirmDialog } from '@/_components'; import AddEditResourcePermissionsModal from './components/AddEditResourceModal/AddEditResourcePermissionsModal'; import DataSourceResourcePermissions from './components/DataSourceResourcePermission'; +import WorkflowResourcePermissions from './components/WorkflowResourcePermission'; import Spinner from 'react-bootstrap/Spinner'; +import { RESOURCE_TYPE, APP_TYPES, RESOURCE_NAME_MAPPING } from '../..'; class BaseManageGranularAccess extends React.Component { constructor(props) { @@ -24,7 +26,7 @@ class BaseManageGranularAccess extends React.Component { errors: {}, values: {}, customSelected: true, - selectedApps: [], + selectedResources: [], type: null, newPermissionName: null, initialPermissionState: { @@ -47,13 +49,11 @@ class BaseManageGranularAccess extends React.Component { updateType: '', deleteConfirmationModal: false, deletingPermissions: false, - initialPermissionStateDs: { canUse: false, canView: false, }, - selectedDs: [], - resourceType: '', + resourceType: null, hasChanges: false, initialState: { type: 'app', @@ -66,8 +66,7 @@ class BaseManageGranularAccess extends React.Component { canUse: false, canConfigure: false, }, - selectedDs: [], - selectedApps: [], + selectedResources: [], isAll: true, newPermissionName: null, }, @@ -86,15 +85,27 @@ class BaseManageGranularAccess extends React.Component { groupPermissionV2Service .fetchAddableApps() .then((data) => { - const addableApps = data.map((app) => { - return { - name: app.name, - value: app.id, - label: app.name, - }; - }); + const addableApps = data + .filter((app) => app.type === APP_TYPES.FRONT_END) + .map((app) => { + return { + name: app.name, + value: app.id, + label: app.name, + }; + }); + const addableWorkflows = data + .filter((app) => app.type === APP_TYPES.WORKFLOW) + .map((app) => { + return { + name: app.name, + value: app.id, + label: app.name, + }; + }); this.setState({ addableApps, + addableWorkflows, }); }) .catch((err) => { @@ -137,19 +148,15 @@ class BaseManageGranularAccess extends React.Component { }); }; + getSelectedResources = () => { + return this.state.selectedResources; + }; + createGranularPermissions = () => { - const { - initialPermissionState, - initialPermissionStateDs, - isAll, - newPermissionName, - isCustom, - selectedApps, - selectedDs, - resourceType, - } = this.state; - const type = resourceType === 'Apps' ? 'app' : 'data_source'; - const selectedResource = type == 'app' ? selectedApps : selectedDs; + const { initialPermissionState, initialPermissionStateDs, isAll, newPermissionName, isCustom, resourceType } = + this.state; + const type = resourceType; + const selectedResource = this.getSelectedResources(); if (isCustom && selectedResource.length == 0) { toast.error('Please select the resources to continue'); return; @@ -157,7 +164,7 @@ class BaseManageGranularAccess extends React.Component { const resourcesToAdd = selectedResource .filter((res) => !res?.isAllField) .map((option) => { - if (type === 'app') { + if (type === RESOURCE_TYPE.APPS || type === RESOURCE_TYPE.WORKFLOWS) { return { appId: option.value, }; @@ -173,8 +180,8 @@ class BaseManageGranularAccess extends React.Component { groupId: this.props.groupPermissionId, isAll: isAll, createResourcePermissionObject: { - ...(type == 'app' && initialPermissionState), - ...(type == 'data_source' && { action: initialPermissionStateDs }), + ...((type === RESOURCE_TYPE.APPS || type === RESOURCE_TYPE.WORKFLOWS) && initialPermissionState), + ...(type == RESOURCE_TYPE.DATA_SOURCES && { action: initialPermissionStateDs }), resourcesToAdd: resourcesToAdd, }, }; @@ -226,8 +233,8 @@ class BaseManageGranularAccess extends React.Component { canUse: dataSourcesGroupPermission?.canUse, canConfigure: dataSourcesGroupPermission?.canConfigure, }, - resourceType: 'Data sources', - selectedDs: + resourceType: RESOURCE_TYPE.DATA_SOURCES, + selectedResources: currentDs?.length > 0 ? currentDs?.map(({ dataSource }) => { return { @@ -238,14 +245,14 @@ class BaseManageGranularAccess extends React.Component { }) : [], initialState: { - type: 'data_source', + type: RESOURCE_TYPE.DATA_SOURCES, initialPermissionStateDs: { canUse: dataSourcesGroupPermission?.canUse, canConfigure: dataSourcesGroupPermission?.canConfigure, }, isAll: !!granularPermission.isAll, newPermissionName: granularPermission?.name, - selectedDs: + selectedResources: currentDs?.length > 0 ? currentDs?.map(({ dataSource }) => { return { @@ -257,30 +264,32 @@ class BaseManageGranularAccess extends React.Component { : [], }, }); - } else if (granularPermission.type === 'app') { + } else if (granularPermission.type === RESOURCE_TYPE.APPS || granularPermission.type === RESOURCE_TYPE.WORKFLOWS) { const currentApps = granularPermission?.appsGroupPermissions?.groupApps; const appsGroupPermission = granularPermission?.appsGroupPermissions; + const selectedResources = + currentApps?.length > 0 + ? currentApps?.map(({ app }) => { + return { + name: app.name, + value: app.id, + label: app.name, + }; + }) + : []; + this.setState({ ...fixedState, - modalTitle: `Edit app permissions`, - resourceType: 'Apps', + modalTitle: `Edit ${granularPermission.type} permissions`, + resourceType: granularPermission.type, initialPermissionState: { canEdit: appsGroupPermission.canEdit, canView: appsGroupPermission.canView, hideFromDashboard: appsGroupPermission.hideFromDashboard, }, - selectedApps: - currentApps?.length > 0 - ? currentApps?.map(({ app }) => { - return { - name: app.name, - value: app.id, - label: app.name, - }; - }) - : [], + selectedResources: selectedResources, initialState: { - type: 'app', + type: granularPermission.type, initialPermissionState: { canEdit: appsGroupPermission?.canEdit, canView: appsGroupPermission?.canView, @@ -288,21 +297,68 @@ class BaseManageGranularAccess extends React.Component { }, isAll: !!granularPermission.isAll, newPermissionName: granularPermission?.name, - selectedApps: - currentApps?.length > 0 - ? currentApps?.map(({ app }) => { - return { - name: app.name, - value: app.id, - label: app.name, - }; - }) - : [], + selectedResources: selectedResources, }, }); } }; + renderResourcePermissions = (props) => { + const { permissions, currentGroupPermission, isBasicPlan, index } = props; + const { type } = permissions; + + switch (type) { + case RESOURCE_TYPE.APPS: + return ( + + ); + case RESOURCE_TYPE.DATA_SOURCES: + return ( + + ); + case RESOURCE_TYPE.WORKFLOWS: + return ( + + ); + default: + return null; + } + }; + + getAddableResources = (resourceType) => { + switch (resourceType) { + case RESOURCE_TYPE.APPS: + return this.state.addableApps; + case RESOURCE_TYPE.DATA_SOURCES: + return this.state.addableDs; + case RESOURCE_TYPE.WORKFLOWS: + return this.state.addableWorkflows; + default: + return []; + } + }; + updateOnlyGranularPermissions = (permission, actions = {}, allowRoleChange) => { const body = { actions: actions, @@ -340,32 +396,25 @@ class BaseManageGranularAccess extends React.Component { }; updateGranularPermissions = (allowRoleChange) => { - const { - currentEditingPermissions, - selectedApps, - selectedDs, - newPermissionName, - isAll, - initialPermissionState, - initialPermissionStateDs, - } = this.state; + const { currentEditingPermissions, newPermissionName, isAll, initialPermissionState, initialPermissionStateDs } = + this.state; const type = currentEditingPermissions.type; const currentResource = - type === 'app' + type === RESOURCE_TYPE.APPS || type === RESOURCE_TYPE.WORKFLOWS ? currentEditingPermissions?.appsGroupPermissions?.groupApps?.map((app) => { return app.app.id; }) : currentEditingPermissions?.dataSourcesGroupPermission?.groupDataSources?.map((ds) => { return ds.dataSource.id; }); - const selectedResourceEnitities = type === 'app' ? selectedApps : selectedDs; + const selectedResourceEnitities = this.getSelectedResources(); const selectedResource = selectedResourceEnitities .filter((res) => !res?.isAllField) ?.map((resource) => resource.value); const resourcesToAdd = selectedResource ?.filter((item) => !currentResource.includes(item)) .map((id) => { - if (type === 'app') + if (type === RESOURCE_TYPE.APPS || type === RESOURCE_TYPE.WORKFLOWS) return { appId: id, }; @@ -377,7 +426,7 @@ class BaseManageGranularAccess extends React.Component { }); const resourceItemsToDelete = currentResource?.filter((item) => !selectedResource?.includes(item)); const groupResToDelete = - type === 'app' + type === RESOURCE_TYPE.APPS || type === RESOURCE_TYPE.WORKFLOWS ? currentEditingPermissions?.appsGroupPermissions?.groupApps?.filter((groupApp) => resourceItemsToDelete?.includes(groupApp.appId) ) @@ -392,7 +441,10 @@ class BaseManageGranularAccess extends React.Component { const body = { name: newPermissionName, isAll: isAll, - actions: type === 'app' ? initialPermissionState : initialPermissionStateDs, + actions: + type === RESOURCE_TYPE.APPS || type === RESOURCE_TYPE.WORKFLOWS + ? initialPermissionState + : initialPermissionStateDs, resourcesToAdd, resourcesToDelete, allowRoleChange, @@ -457,7 +509,7 @@ class BaseManageGranularAccess extends React.Component { openAddPermissionModal = (resourceType) => { this.setState((prevState) => ({ - modalTitle: `Add ${resourceType?.toLowerCase()} permissions`, + modalTitle: `Add ${RESOURCE_NAME_MAPPING[resourceType].toLowerCase()} permissions`, resourceType, showAddPermissionModal: true, initialPermissionState: { ...prevState.initialPermissionState, canView: true }, @@ -484,22 +536,14 @@ class BaseManageGranularAccess extends React.Component { canUse: false, canConfigure: false, }, - selectedDs: [], - selectedApps: [], + selectedResources: [], resourceType: '', hasChanges: false, }); }; - setSelectedApps = (values) => { - this.setState({ selectedApps: values }, () => { - const hasChanges = this.hasStateChanged(this.state); - this.setState({ hasChanges }); - }); - }; - - setSelectedDs = (values) => { - this.setState({ selectedDs: values }, () => { + setSelectedResources = (values) => { + this.setState({ selectedResources: values }, () => { const hasChanges = this.hasStateChanged(this.state); this.setState({ hasChanges }); }); @@ -525,15 +569,13 @@ class BaseManageGranularAccess extends React.Component { hasStateChanged = (newState) => { const { type } = this.state.initialState; - const selectedItems = - type === 'data_source' ? this.state.initialState?.selectedDs : this.state.initialState?.selectedApps; - - const newSelectedItems = type === 'data_source' ? newState.selectedDs : newState.selectedApps; + const selectedItems = this.state.initialState?.selectedResources; + const newSelectedItems = newState.selectedResources; const newPermissionState = - type === 'data_source' ? newState.initialPermissionStateDs : newState.initialPermissionState; + type === RESOURCE_TYPE.DATA_SOURCES ? newState.initialPermissionStateDs : newState.initialPermissionState; const permissionStateChanged = - type === 'data_source' + type === RESOURCE_TYPE.DATA_SOURCES ? this.state.initialState.initialPermissionStateDs?.canUse !== newPermissionState?.canUse || this.state.initialPermissionStateDs?.canConfigure !== newPermissionState?.canConfigure : this.state.initialState.initialPermissionState?.canEdit !== newPermissionState?.canEdit || @@ -574,11 +616,9 @@ class BaseManageGranularAccess extends React.Component { render() { const { showAddPermissionModal, - selectedApps, isCustom, granularPermissions, isLoading, - addableApps, modalTitle, modalType, newPermissionName, @@ -589,7 +629,6 @@ class BaseManageGranularAccess extends React.Component { deleteConfirmationModal, deletingPermissions, resourceType, - selectedDs, hasChanges, } = this.state; @@ -601,7 +640,7 @@ class BaseManageGranularAccess extends React.Component { const showPermissionInfo = currentGroupPermission.name == 'admin' || currentGroupPermission.name == 'end-user'; const addPermissionTooltipMessage = !newPermissionName ? 'Please input permissions name' - : isCustom && selectedApps.length === 0 + : isCustom && this.getSelectedResources().length === 0 ? 'Please select apps or select all apps option' : ''; const isBasicPlan = this.props.isBasicPlan; @@ -632,63 +671,73 @@ class BaseManageGranularAccess extends React.Component { darkMode={this.props.darkMode} isLoading={isLoading} /> - { - this.updateGranularPermissions(); - } - } - updateParentState={this.updateState} - resourceType={resourceType} - currentState={this.state} - show={showAddPermissionModal} - title={ -
- - - -
- {modalTitle} -
- {modalType === 'edit' && !isRoleGroup && ( -
- { - this.setState({ - deleteConfirmationModal: true, - showAddPermissionModal: false, - }); - }} - data-cy="delete-button" + {showAddPermissionModal && ( + { + this.updateGranularPermissions(); + } + } + updateParentState={this.updateState} + resourceType={resourceType} + currentState={this.state} + show={showAddPermissionModal} + title={ +
+ + + +
+ {modalTitle}
- )} -
- } - confirmBtnProps={{ - title: `${modalType === 'edit' ? 'Update' : 'Add'}`, - iconLeft: 'plus', - disabled: - (modalType === 'add' && !newPermissionName) || - (modalType === 'edit' && !hasChanges) || - (isCustom && selectedApps.length === 0 && resourceType === 'Apps') || - (isCustom && selectedDs.length === 0 && resourceType === 'Data Sources'), - tooltipMessage: addPermissionTooltipMessage, - }} - disableBuilderLevelUpdate={disableEditUpdate} - selectedApps={resourceType === 'Apps' ? selectedApps : selectedDs} - setSelectedApps={resourceType === 'Apps' ? this.setSelectedApps : this.setSelectedDs} - addableApps={resourceType === 'Apps' ? addableApps : addableDs} - darkMode={this.props.darkMode} - groupName={currentGroupPermission.name} - /> + {modalType === 'edit' && !isRoleGroup && ( +
+ { + this.setState({ + deleteConfirmationModal: true, + showAddPermissionModal: false, + }); + }} + data-cy="delete-button" + /> +
+ )} +
+ } + confirmBtnProps={{ + title: `${modalType === 'edit' ? 'Update' : 'Add'}`, + iconLeft: 'plus', + disabled: + (modalType === 'add' && !newPermissionName) || + (modalType === 'edit' && !hasChanges) || + (isCustom && this.getSelectedResources().length === 0), + tooltipMessage: addPermissionTooltipMessage, + }} + disableBuilderLevelUpdate={disableEditUpdate} + selectedApps={this.getSelectedResources()} + setSelectedApps={(values) => this.setSelectedResources(values)} + addableApps={this.getAddableResources(resourceType)} + darkMode={this.props.darkMode} + groupName={currentGroupPermission.name} + /> + )} {!granularPermissions.length && !isLoading ? (
@@ -735,28 +784,12 @@ class BaseManageGranularAccess extends React.Component { ) : ( <> {granularPermissions.map((permissions, index) => { - if (permissions.type === 'app') - return ( - - ); - else - return ( - - ); + return this.renderResourcePermissions({ + permissions, + currentGroupPermission, + isBasicPlan, + index, + }); })} )} diff --git a/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGranularAccess/components/AddEditResourceModal/AddEditResourcePermissionsModal.jsx b/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGranularAccess/components/AddEditResourceModal/AddEditResourcePermissionsModal.jsx index a10b806270..97a4be5cb0 100644 --- a/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGranularAccess/components/AddEditResourceModal/AddEditResourcePermissionsModal.jsx +++ b/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGranularAccess/components/AddEditResourceModal/AddEditResourcePermissionsModal.jsx @@ -5,6 +5,8 @@ import { AppsSelect } from '@/_ui/Modal/AppsSelect'; import AppPermissionsActions from './AppPermissionActionContainer'; import { OverlayTrigger, Tooltip } from 'react-bootstrap'; import DsPermissionsActions from './DataSourcPermissionActionContainer'; +import WorkflowPermissionsActions from './WorkflowPermissionActionContainer'; +import { RESOURCE_TYPE } from '../../../../index'; function AddEditResourcePermissionsModal({ handleClose, @@ -28,11 +30,138 @@ function AddEditResourcePermissionsModal({ const initialPermissionStateDs = currentState?.initialPermissionStateDs; const errors = currentState?.errors; const isAll = currentState?.isAll; - const allResourceText = - resourceType === 'Apps' - ? 'This will select all apps in the workspace including any new apps created' - : 'This will select all data sources in the workspace including any new connections created'; - const allResourceTitle = resourceType === 'Apps' ? 'All apps' : 'All data sources'; + const getAllResourceText = (resourceType) => { + switch (resourceType) { + case RESOURCE_TYPE.APPS: + return 'This will select all apps in the workspace including any new apps created'; + case RESOURCE_TYPE.WORKFLOWS: + return 'This will select all workflows in the workspace including any new workflows created'; + case RESOURCE_TYPE.DATA_SOURCES: + return 'This will select all data sources in the workspace including any new connections created'; + } + }; + + const RESOURCE_NAME_MAPPING = { + [RESOURCE_TYPE.APPS]: 'apps', + [RESOURCE_TYPE.WORKFLOWS]: 'workflows', + [RESOURCE_TYPE.DATA_SOURCES]: 'data sources', + }; + + const getAllResourceLabel = (resourceType) => { + switch (resourceType) { + case RESOURCE_TYPE.APPS: + return 'All apps'; + case RESOURCE_TYPE.WORKFLOWS: + return 'All workflows'; + case RESOURCE_TYPE.DATA_SOURCES: + return 'All data sources'; + default: + return 'All resources'; + } + }; + + const renderPermissionActions = (resourceType) => { + switch (resourceType) { + case RESOURCE_TYPE.APPS: + return ( + { + updateParentState((prevState) => ({ + initialPermissionState: { + ...prevState.initialPermissionState, + canEdit: !prevState.initialPermissionState.canEdit, + canView: prevState.initialPermissionState.canEdit, + ...(!prevState.initialPermissionState.canEdit && { hideFromDashboard: false }), + }, + })); + }} + handleClickView={() => { + updateParentState((prevState) => ({ + initialPermissionState: { + ...prevState.initialPermissionState, + canView: !prevState.initialPermissionState.canView, + canEdit: prevState.initialPermissionState.canView, + ...(prevState.initialPermissionState.canEdit && { hideFromDashboard: false }), + }, + })); + }} + handleHideFromDashboard={() => { + updateParentState((prevState) => ({ + initialPermissionState: { + ...prevState.initialPermissionState, + hideFromDashboard: !prevState.initialPermissionState.hideFromDashboard, + }, + })); + }} + disableBuilderLevelUpdate={disableBuilderLevelUpdate} + initialPermissionState={initialPermissionState} + /> + ); + + case RESOURCE_TYPE.WORKFLOWS: + return ( + { + updateParentState((prevState) => ({ + initialPermissionState: { + ...prevState.initialPermissionState, + canEdit: !prevState.initialPermissionState.canEdit, + canView: prevState.initialPermissionState.canEdit, + ...(!prevState.initialPermissionState.canEdit && { hideFromDashboard: false }), + }, + })); + }} + handleClickView={() => { + updateParentState((prevState) => ({ + initialPermissionState: { + ...prevState.initialPermissionState, + canView: !prevState.initialPermissionState.canView, + canEdit: prevState.initialPermissionState.canView, + ...(prevState.initialPermissionState.canEdit && { hideFromDashboard: false }), + }, + })); + }} + handleHideFromDashboard={() => { + updateParentState((prevState) => ({ + initialPermissionState: { + ...prevState.initialPermissionState, + hideFromDashboard: !prevState.initialPermissionState.hideFromDashboard, + }, + })); + }} + disableBuilderLevelUpdate={disableBuilderLevelUpdate} + initialPermissionState={initialPermissionState} + /> + ); + + case RESOURCE_TYPE.DATA_SOURCES: + default: + return ( + { + updateParentState((prevState) => ({ + initialPermissionStateDs: { + ...prevState.initialPermissionStateDs, + canConfigure: !prevState.initialPermissionStateDs.canConfigure, + canUse: !!prevState.initialPermissionStateDs.canConfigure, + }, + })); + }} + handleClickUse={() => { + updateParentState((prevState) => ({ + initialPermissionStateDs: { + ...prevState.initialPermissionStateDs, + canUse: !prevState.initialPermissionStateDs.canUse, + canConfigure: !!prevState.initialPermissionStateDs.canUse, + }, + })); + }} + disableBuilderLevelUpdate={disableBuilderLevelUpdate} + initialPermissionStateDs={initialPermissionStateDs} + /> + ); + } + }; return ( Permission - {resourceType === 'Apps' ? ( - { - updateParentState((prevState) => ({ - initialPermissionState: { - ...prevState.initialPermissionState, - canEdit: !prevState.initialPermissionState.canEdit, - canView: prevState.initialPermissionState.canEdit, - ...(!prevState.initialPermissionState.canEdit && { hideFromDashboard: false }), - }, - })); - }} - handleClickView={() => { - updateParentState((prevState) => ({ - initialPermissionState: { - ...prevState.initialPermissionState, - canView: !prevState.initialPermissionState.canView, - canEdit: prevState.initialPermissionState.canView, - ...(prevState.initialPermissionState.canEdit && { hideFromDashboard: false }), - }, - })); - }} - handleHideFromDashboard={() => { - updateParentState((prevState) => ({ - initialPermissionState: { - ...initialPermissionState, - hideFromDashboard: !prevState.initialPermissionState.hideFromDashboard, - }, - })); - }} - disableBuilderLevelUpdate={disableBuilderLevelUpdate} - initialPermissionState={initialPermissionState} - /> - ) : ( - - )} + {renderPermissionActions(resourceType)}
@@ -139,15 +229,15 @@ function AddEditResourcePermissionsModal({
- {allResourceTitle} + {getAllResourceLabel(resourceType)} - {allResourceText} + {getAllResourceText(resourceType)}
@@ -167,7 +257,9 @@ function AddEditResourcePermissionsModal({ { !isCustom && @@ -188,8 +280,7 @@ function AddEditResourcePermissionsModal({ style={{ color: disableBuilderLevelUpdate ? 'var(--text-disabled)' : '' }} data-cy="custom-info-text" > - Select specific {resourceType === 'Apps' ? 'applications' : 'data sources'} you want to add to the - group + Select specific {RESOURCE_NAME_MAPPING[resourceType]} you want to add to the group
diff --git a/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGranularAccess/components/AddEditResourceModal/WorkflowPermissionActionContainer.jsx b/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGranularAccess/components/AddEditResourceModal/WorkflowPermissionActionContainer.jsx new file mode 100644 index 0000000000..e8206dfd5d --- /dev/null +++ b/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGranularAccess/components/AddEditResourceModal/WorkflowPermissionActionContainer.jsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent'; + +function WorkflowPermissionActionContainer() { + return <>; +} + +export default withEditionSpecificComponent(WorkflowPermissionActionContainer, 'WorkspaceSettings'); diff --git a/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGranularAccess/components/AddResourcePermissionsMenu.jsx b/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGranularAccess/components/AddResourcePermissionsMenu.jsx index 76d3156576..973418976e 100644 --- a/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGranularAccess/components/AddResourcePermissionsMenu.jsx +++ b/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGranularAccess/components/AddResourcePermissionsMenu.jsx @@ -2,6 +2,7 @@ import React from 'react'; import '../../../resources/styles/group-permissions.styles.scss'; import { ButtonSolid } from '@/_ui/AppButton/AppButton'; import { OverlayTrigger, Tooltip } from 'react-bootstrap'; +import { RESOURCE_TYPE } from '../../../index'; function AddResourcePermissionsMenu({ openAddPermissionModal, @@ -10,6 +11,25 @@ function AddResourcePermissionsMenu({ darkMode, isBasicPlan, }) { + const selectResourceIcon = (resourceType) => { + switch (resourceType) { + case RESOURCE_TYPE.APPS: + return 'apps'; + case RESOURCE_TYPE.WORKFLOWS: + return 'workflows'; + case RESOURCE_TYPE.DATA_SOURCES: + return 'datasource'; + default: + return ''; + } + }; + + const resourceNameMapping = { + [RESOURCE_TYPE.APPS]: 'Apps', + [RESOURCE_TYPE.WORKFLOWS]: 'Workflows', + [RESOURCE_TYPE.DATA_SOURCES]: 'Data source', + }; + return resourcesOptions.length > 1 ? ( { openAddPermissionModal(resource); }} - disabled={currentGroupPermission.name === 'end-user' && resource === 'Data Sources'} + disabled={currentGroupPermission.name === 'end-user' && resource === RESOURCE_TYPE.DATA_SOURCES} > End-user cannot access data sources @@ -44,7 +64,7 @@ function AddResourcePermissionsMenu({ ) } > - {resource === 'Data Sources' ? 'Data source' : resource} + {resourceNameMapping[resource]} ))} @@ -75,7 +95,7 @@ function AddResourcePermissionsMenu({ leftIcon="plus" disabled={currentGroupPermission.name === 'admin' || isBasicPlan} onClick={() => { - openAddPermissionModal('Apps'); + openAddPermissionModal(RESOURCE_TYPE.APPS); }} data-cy="add-apps-buton" > diff --git a/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGranularAccess/components/WorkflowResourcePermission.jsx b/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGranularAccess/components/WorkflowResourcePermission.jsx new file mode 100644 index 0000000000..9d9d0d935d --- /dev/null +++ b/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGranularAccess/components/WorkflowResourcePermission.jsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent'; + +function WorkflowResourcePermissions() { + return <>; +} + +export default withEditionSpecificComponent(WorkflowResourcePermissions, 'WorkspaceSettings'); diff --git a/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGroupPermissionResources/BaseManageGroupPermissionResources.jsx b/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGroupPermissionResources/BaseManageGroupPermissionResources.jsx index 016768be67..efc01bb72c 100644 --- a/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGroupPermissionResources/BaseManageGroupPermissionResources.jsx +++ b/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGroupPermissionResources/BaseManageGroupPermissionResources.jsx @@ -21,6 +21,7 @@ import ChangeRoleModal from '../ChangeRoleModal'; import { ToolTip } from '@/_components/ToolTip'; import Avatar from '@/_ui/Avatar'; import DataSourcePermissionsUI from '../DataSourcePermissionsUI'; +import WorkflowPermissionsUI from '../WorkflowPermissionsUI'; class BaseManageGroupPermissionResources extends React.Component { constructor(props) { @@ -876,7 +877,7 @@ class BaseManageGroupPermissionResources extends React.Component { )}

-
+
{isLoadingGroup ? ( @@ -958,6 +959,13 @@ class BaseManageGroupPermissionResources extends React.Component {
{/* //App till here */}
+ {/* Worklfow Permission */} + {/* Data source */} )} diff --git a/frontend/src/modules/WorkspaceSettings/pages/Groups/components/WorkflowPermissionsUI/WorkflowPermissionsUI.jsx b/frontend/src/modules/WorkspaceSettings/pages/Groups/components/WorkflowPermissionsUI/WorkflowPermissionsUI.jsx new file mode 100644 index 0000000000..e018aae80b --- /dev/null +++ b/frontend/src/modules/WorkspaceSettings/pages/Groups/components/WorkflowPermissionsUI/WorkflowPermissionsUI.jsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent'; + +const WorkflowPermissionsUI = () => { + return <>; +}; + +export default withEditionSpecificComponent(WorkflowPermissionsUI, 'WorkspaceSettings'); diff --git a/frontend/src/modules/WorkspaceSettings/pages/Groups/components/WorkflowPermissionsUI/index.js b/frontend/src/modules/WorkspaceSettings/pages/Groups/components/WorkflowPermissionsUI/index.js new file mode 100644 index 0000000000..d7281c66c6 --- /dev/null +++ b/frontend/src/modules/WorkspaceSettings/pages/Groups/components/WorkflowPermissionsUI/index.js @@ -0,0 +1 @@ +export { default } from './WorkflowPermissionsUI.jsx'; diff --git a/frontend/src/modules/WorkspaceSettings/pages/Groups/index.js b/frontend/src/modules/WorkspaceSettings/pages/Groups/index.js index 89a07985c0..f813d7b946 100644 --- a/frontend/src/modules/WorkspaceSettings/pages/Groups/index.js +++ b/frontend/src/modules/WorkspaceSettings/pages/Groups/index.js @@ -1 +1,18 @@ export { default } from './ManageGroupPermissionsPage'; + +export const RESOURCE_TYPE = { + APPS: 'app', + DATA_SOURCES: 'data_source', + WORKFLOWS: 'workflow', +}; + +export const APP_TYPES = { + FRONT_END: 'front-end', + WORKFLOW: 'workflow', +}; + +export const RESOURCE_NAME_MAPPING = { + [RESOURCE_TYPE.APPS]: 'Apps', + [RESOURCE_TYPE.DATA_SOURCES]: 'Data Sources', + [RESOURCE_TYPE.WORKFLOWS]: 'Workflows', +}; diff --git a/server/migrations/1746705301652-AddWorkflowPermissionsInGroupPermissions.ts b/server/migrations/1746705301652-AddWorkflowPermissionsInGroupPermissions.ts new file mode 100644 index 0000000000..93b0a09767 --- /dev/null +++ b/server/migrations/1746705301652-AddWorkflowPermissionsInGroupPermissions.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddWorkflowPermissionsInGroupPermissions1746705301652 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE permission_groups + ADD COLUMN workflow_create BOOLEAN NOT NULL DEFAULT FALSE, + ADD COLUMN workflow_delete BOOLEAN NOT NULL DEFAULT FALSE; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE permission_groups + DROP COLUMN workflow_delete, + DROP COLUMN workflow_create; + `); + } +} diff --git a/server/migrations/1746705371665-AddWorkflowTypeInResourceType.ts b/server/migrations/1746705371665-AddWorkflowTypeInResourceType.ts new file mode 100644 index 0000000000..8e8620f742 --- /dev/null +++ b/server/migrations/1746705371665-AddWorkflowTypeInResourceType.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddWorkflowTypeInResourceType1746705371665 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TYPE "resource_type" ADD VALUE IF NOT EXISTS 'workflow'; + `); + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/server/migrations/1746705448788-AddAppTypeInAppsGroupPermissions.ts b/server/migrations/1746705448788-AddAppTypeInAppsGroupPermissions.ts new file mode 100644 index 0000000000..7a933e454c --- /dev/null +++ b/server/migrations/1746705448788-AddAppTypeInAppsGroupPermissions.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddAppTypeInAppsGroupPermissions1746705448788 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'app_type') THEN + CREATE TYPE "app_type" AS ENUM ('front-end', 'workflow'); + END IF; + END + $$; + ALTER TABLE "apps_group_permissions" + ADD COLUMN "app_type" "app_type" NOT NULL DEFAULT 'front-end'; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "apps_group_permissions" + DROP COLUMN "app_type"; + DROP TYPE IF EXISTS "app_type"; + `); + } +} diff --git a/server/package-lock.json b/server/package-lock.json index 3cef36d54f..fa1e6d68ba 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -20645,4 +20645,4 @@ } } } -} +} \ No newline at end of file diff --git a/server/src/entities/app.entity.ts b/server/src/entities/app.entity.ts index 257da6233e..7449af495c 100644 --- a/server/src/entities/app.entity.ts +++ b/server/src/entities/app.entity.ts @@ -20,14 +20,20 @@ import { GroupApps } from './group_apps.entity'; import { AppGroupPermission } from './app_group_permission.entity'; import { AiConversation } from './ai_conversation.entity'; import { Organization } from './organization.entity'; +import { APP_TYPES } from '@modules/apps/constants'; @Entity({ name: 'apps' }) export class App extends BaseEntity { @PrimaryGeneratedColumn('uuid') id: string; - @Column({ name: 'type' }) - type: string = 'front-end'; + @Column({ + name: 'type', + type: 'enum', + enum: APP_TYPES, + default: APP_TYPES.FRONT_END, + }) + type: APP_TYPES; @Column({ name: 'name' }) name: string; diff --git a/server/src/entities/app_base.entity.ts b/server/src/entities/app_base.entity.ts index f0d3724df4..24ccbbe201 100644 --- a/server/src/entities/app_base.entity.ts +++ b/server/src/entities/app_base.entity.ts @@ -15,6 +15,7 @@ import { User } from './user.entity'; import { AppVersion } from './app_version.entity'; import { GroupPermission } from './group_permission.entity'; import { AppGroupPermission } from './app_group_permission.entity'; +import { APP_TYPES } from '@modules/apps/constants'; @Entity({ name: 'apps' }) export class AppBase extends BaseEntity { @@ -24,8 +25,13 @@ export class AppBase extends BaseEntity { @Column({ name: 'name' }) name: string; - @Column({ name: 'type' }) - type: string = 'front-end'; + @Column({ + name: 'type', + type: 'enum', + enum: APP_TYPES, + default: APP_TYPES.FRONT_END, + }) + type: APP_TYPES; @Column({ name: 'slug', unique: true }) slug: string; diff --git a/server/src/entities/apps_group_permissions.entity.ts b/server/src/entities/apps_group_permissions.entity.ts index 12abd55183..a88ca67048 100644 --- a/server/src/entities/apps_group_permissions.entity.ts +++ b/server/src/entities/apps_group_permissions.entity.ts @@ -12,6 +12,7 @@ import { } from 'typeorm'; import { GranularPermissions } from './granular_permissions.entity'; import { GroupApps } from './group_apps.entity'; +import { APP_TYPES } from '@modules/apps/constants'; @Entity({ name: 'apps_group_permissions' }) export class AppsGroupPermissions extends BaseEntity { @@ -22,6 +23,13 @@ export class AppsGroupPermissions extends BaseEntity { @Column({ name: 'granular_permission_id', unique: true }) granularPermissionId: string; + @Column({ + name: 'app_type', + type: 'enum', + enum: APP_TYPES, + }) + appType: APP_TYPES; + @Column({ name: 'can_edit', nullable: false, default: false }) canEdit: boolean; diff --git a/server/src/entities/group_apps.entity.ts b/server/src/entities/group_apps.entity.ts index 6f6503b248..8bcd84d3a9 100644 --- a/server/src/entities/group_apps.entity.ts +++ b/server/src/entities/group_apps.entity.ts @@ -34,7 +34,7 @@ export class GroupApps extends BaseEntity { @JoinColumn({ name: 'app_id' }) app: App; - @ManyToOne(() => AppsGroupPermissions, (appsPermissions) => appsPermissions.id) + @ManyToOne(() => AppsGroupPermissions, (appsPermissions) => appsPermissions.groupApps) @JoinColumn({ name: 'apps_group_permissions_id' }) appsPermissions: AppsGroupPermissions; } diff --git a/server/src/entities/group_permissions.entity.ts b/server/src/entities/group_permissions.entity.ts index 693f4f930c..d321d84b5f 100644 --- a/server/src/entities/group_permissions.entity.ts +++ b/server/src/entities/group_permissions.entity.ts @@ -35,6 +35,12 @@ export class GroupPermissions extends BaseEntity { @Column({ name: 'app_delete', default: false }) appDelete: boolean; + @Column({ name: 'workflow_create', default: false }) + workflowCreate: boolean; + + @Column({ name: 'workflow_delete', default: false }) + workflowDelete: boolean; + @Column({ name: 'folder_crud', default: false }) folderCRUD: boolean; diff --git a/server/src/modules/ability/constants.ts b/server/src/modules/ability/constants.ts index b0b7319b82..f160095d88 100644 --- a/server/src/modules/ability/constants.ts +++ b/server/src/modules/ability/constants.ts @@ -1,5 +1,6 @@ import { MODULES } from '@modules/app/constants/modules'; -import { UserAppsPermissions, UserDataSourcePermissions, UserPermissions } from './types'; +import { UserAppsPermissions, UserDataSourcePermissions, UserPermissions, UserWorkflowPermissions } from './types'; +import { APP_TYPES } from '@modules/apps/constants'; export const DEFAULT_USER_PERMISSIONS: UserPermissions = { isSuperAdmin: false, @@ -8,6 +9,8 @@ export const DEFAULT_USER_PERMISSIONS: UserPermissions = { isEndUser: false, appCreate: false, appDelete: false, + workflowCreate: false, + workflowDelete: false, dataSourceCreate: false, dataSourceDelete: false, folderCRUD: false, @@ -21,8 +24,19 @@ export const DEFAULT_USER_PERMISSIONS: UserPermissions = { hiddenAppsId: [], hideAll: false, }, + [MODULES.WORKFLOWS]: { + editableWorkflowsId: [], + isAllEditable: false, + executableWorkflowsId: [], + isAllExecutable: false, + }, }; +export const RESOURCE_TO_APP_TYPE_MAP = { + [MODULES.APP]: APP_TYPES.FRONT_END, + [MODULES.WORKFLOWS]: APP_TYPES.WORKFLOW, +} as const; + export const DEFAULT_USER_APPS_PERMISSIONS: UserAppsPermissions = { editableAppsId: [], isAllEditable: false, @@ -32,6 +46,13 @@ export const DEFAULT_USER_APPS_PERMISSIONS: UserAppsPermissions = { hideAll: false, }; +export const DEFAULT_USER_WORKFLOW_PERMISSIONS: UserWorkflowPermissions = { + editableWorkflowsId: [], + isAllEditable: false, + executableWorkflowsId: [], + isAllExecutable: false, +}; + export const DEFAULT_USER_DATA_SOURCE_PERMISSIONS: UserDataSourcePermissions = { usableDataSourcesId: [], isAllUsable: false, diff --git a/server/src/modules/ability/service.ts b/server/src/modules/ability/service.ts index 4e061ba27d..f9149449eb 100644 --- a/server/src/modules/ability/service.ts +++ b/server/src/modules/ability/service.ts @@ -53,12 +53,14 @@ export class AbilityService extends IAbilityService { folderCRUD: acc.folderCRUD || group.folderCRUD, orgConstantCRUD: acc.orgConstantCRUD || group.orgConstantCRUD, orgVariableCRUD: acc.orgVariableCRUD, + workflowCreate: acc.workflowCreate || group.workflowCreate, + workflowDelete: acc.workflowDelete || group.workflowDelete, }; }, DEFAULT_USER_PERMISSIONS); userPermissions.isAdmin = adminGroup; userPermissions.isSuperAdmin = false; - + if (!adminGroup) { const isBuilder = await this.abilityUtilService.isBuilder(user); if (isBuilder) { @@ -84,8 +86,8 @@ export class AbilityService extends IAbilityService { dsGranularPermissions ); - if(userPermissions.isBuilder) { - /* in community edition. builder can use the datasources */ + if (userPermissions.isBuilder) { + /* in community edition. builder can use the datasources */ userPermissions[MODULES.GLOBAL_DATA_SOURCE].isAllUsable = true; } } diff --git a/server/src/modules/ability/types.ts b/server/src/modules/ability/types.ts index 3e7e175b2f..30838bb739 100644 --- a/server/src/modules/ability/types.ts +++ b/server/src/modules/ability/types.ts @@ -17,6 +17,8 @@ export interface UserPermissions { isEndUser: boolean; appCreate: boolean; appDelete: boolean; + workflowCreate: boolean; + workflowDelete: boolean; dataSourceCreate: boolean; dataSourceDelete: boolean; folderCRUD: boolean; @@ -24,6 +26,13 @@ export interface UserPermissions { orgVariableCRUD: boolean; [MODULES.APP]?: UserAppsPermissions; [MODULES.GLOBAL_DATA_SOURCE]?: UserDataSourcePermissions; + [MODULES.WORKFLOWS]?: UserWorkflowPermissions; +} +export interface UserWorkflowPermissions { + editableWorkflowsId: string[]; + isAllEditable: boolean; + executableWorkflowsId: string[]; + isAllExecutable: boolean; } export interface UserAppsPermissions { diff --git a/server/src/modules/ability/util.service.ts b/server/src/modules/ability/util.service.ts index 6c4da719a9..ebc06ae81a 100644 --- a/server/src/modules/ability/util.service.ts +++ b/server/src/modules/ability/util.service.ts @@ -8,12 +8,79 @@ import { AppBase } from '@entities/app_base.entity'; import { User } from '@entities/user.entity'; import { dbTransactionWrap } from '@helpers/database.helper'; import { USER_ROLE } from '@modules/group-permissions/constants'; -import { DEFAULT_USER_APPS_PERMISSIONS } from './constants'; +import { DEFAULT_USER_APPS_PERMISSIONS, RESOURCE_TO_APP_TYPE_MAP } from './constants'; import { RolesRepository } from '@modules/roles/repository'; +import { APP_TYPES } from '@modules/apps/constants'; @Injectable() export class AbilityUtilService { constructor(private readonly roleRepository: RolesRepository) {} + + private getAppTypeConditions(resourcesList: ResourcesItem[]): { conditions: string[]; params: Record } { + const conditions: string[] = []; + const params: Record = {}; + let paramIndex = 0; + + // Get unique resource types from the list + const resourceTypes = Array.from(new Set(resourcesList.map((item) => item.resource))); + + resourceTypes.forEach((resourceType) => { + const appType = RESOURCE_TO_APP_TYPE_MAP[resourceType]; + if (appType) { + const paramName = `appType${paramIndex}`; + conditions.push(`appsGroupPermissions.appType = :${paramName}`); + params[paramName] = appType; + paramIndex++; + } + }); + + return { conditions, params }; + } + + private addAppsAndWorkflowPermissionsTOQuery( + query: SelectQueryBuilder, + resourcesList?: ResourcesItem[] + ) { + query + .leftJoin('granularPermissions.appsGroupPermissions', 'appsGroupPermissions') + .leftJoin('appsGroupPermissions.groupApps', 'groupApps') + .addSelect([ + 'groupApps.appId', + 'appsGroupPermissions.canEdit', + 'appsGroupPermissions.canView', + 'appsGroupPermissions.hideFromDashboard', + 'appsGroupPermissions.appType', + ]); + + const resourceIdList = Array.from( + new Set(resourcesList?.filter((item) => item?.resourceId).map((item) => item.resourceId)) + ); + + if (resourceIdList?.length) { + query.andWhere( + new Brackets((qb) => { + resourceIdList.forEach((resourceId, index) => { + if (index === 0) { + const { conditions, params } = this.getAppTypeConditions(resourcesList); + + // Combine conditions with OR if multiple types are present + const typeCondition = conditions.length > 1 ? `(${conditions.join(' OR ')})` : conditions[0]; + + qb.where(`(${typeCondition}) AND groupApps.appId = :resourceId`, { + resourceId, + ...params, + }) + .orWhere('granularPermissions.isAll = true') + .orWhere('groupApps.id IS NULL'); + } else { + qb.orWhere('groupApps.appId = :resourceId', { resourceId }); + } + }); + }) + ); + } + } + getUserPermissionsQuery( userId: string, resourcePermissionObject: ResourcePermissionQueryObject, @@ -36,10 +103,13 @@ export class AbilityUtilService { } if (resources?.length) { - const appsResourcesList = resources.filter((item) => item.resource === MODULES.APP); + const appsAndWorkflowResourcesList = resources.filter( + (item) => item.resource === MODULES.APP || item.resource === MODULES.WORKFLOWS + ); const dataSourcesResourcesList = resources.filter((item) => item.resource === MODULES.GLOBAL_DATA_SOURCE); - if (appsResourcesList?.length) { - this.addAppsPermissionsTOQuery(query, appsResourcesList); + + if (appsAndWorkflowResourcesList?.length) { + this.addAppsAndWorkflowPermissionsTOQuery(query, appsAndWorkflowResourcesList); } if (dataSourcesResourcesList?.length) { this.addDataSourcesPermissionsTOQuery(query, dataSourcesResourcesList); @@ -49,36 +119,6 @@ export class AbilityUtilService { return query; } - private addAppsPermissionsTOQuery(query: SelectQueryBuilder, appsList?: ResourcesItem[]) { - query - .leftJoin('granularPermissions.appsGroupPermissions', 'appsGroupPermissions') - .leftJoin('appsGroupPermissions.groupApps', 'groupApps') - .addSelect([ - 'groupApps.appId', - 'appsGroupPermissions.canEdit', - 'appsGroupPermissions.canView', - 'appsGroupPermissions.hideFromDashboard', - ]); - - const appsIdList = Array.from(new Set(appsList?.filter((item) => item?.resourceId).map((item) => item.resourceId))); - - if (appsIdList?.length) { - query.andWhere( - new Brackets((qb) => { - appsIdList.forEach((appId, index) => { - if (index === 0) { - qb.where('groupApps.appId = :appId', { appId }) - .orWhere('granularPermissions.isAll = true') - .orWhere('groupApps.id IS NULL'); - } else { - qb.orWhere('groupApps.appId = :appId', { appId }); - } - }); - }) - ); - } - } - private addDataSourcesPermissionsTOQuery( query: SelectQueryBuilder, dataSourcesList?: ResourcesItem[] @@ -145,7 +185,7 @@ export class AbilityUtilService { // Use the provided manager to perform database operations await dbTransactionWrap(async (manager: EntityManager) => { const appsOwnedByUser = await manager.find(AppBase, { - where: { userId: user.id, organizationId: user.organizationId }, + where: { userId: user.id, organizationId: user.organizationId, type: APP_TYPES.FRONT_END }, }); const appsIdOwnedByUser = appsOwnedByUser.map((app) => app.id); diff --git a/server/src/modules/app/ability-factory.ts b/server/src/modules/app/ability-factory.ts index d461d53788..62b14c6c10 100644 --- a/server/src/modules/app/ability-factory.ts +++ b/server/src/modules/app/ability-factory.ts @@ -46,7 +46,7 @@ export abstract class AbilityFactory { await this.defineAbilityFor( can, - { userPermission, superAdmin, isAdmin, isBuilder, isEndUser, user }, + { userPermission, superAdmin, isAdmin, isBuilder, isEndUser, user, resource }, extractedMetadata, request ); diff --git a/server/src/modules/app/guards/ability.guard.ts b/server/src/modules/app/guards/ability.guard.ts index 7532b61376..4fd98e60e8 100644 --- a/server/src/modules/app/guards/ability.guard.ts +++ b/server/src/modules/app/guards/ability.guard.ts @@ -22,6 +22,13 @@ export abstract class AbilityGuard implements CanActivate { protected forwardAbility(): boolean { return false; } + protected resource: any; + protected getResourceObject(): any { + return this.resource; + } + protected setResourceObject(resource: any): void { + this.resource = resource; + } protected getResource(): ResourceDetails | ResourceDetails[] { return; } @@ -37,6 +44,9 @@ export abstract class AbilityGuard implements CanActivate { const request = context.switchToHttp().getRequest(); const user = request.user; const app: App = request.tj_app; + if (app) { + this.setResourceObject(app); + } if (!features?.length) { return false; diff --git a/server/src/modules/app/loader.ts b/server/src/modules/app/loader.ts index 7c505ab8a2..ab6cb2ce72 100644 --- a/server/src/modules/app/loader.ts +++ b/server/src/modules/app/loader.ts @@ -111,13 +111,13 @@ export class AppModuleLoader { * █ █ * ███████████████████████████████████████████████████████████████████████████████ */ - const dynamicModules: DynamicModule[] = []; + const dynamicModules: Promise[] = []; try { const { LogToFileModule } = await import(`${await getImportPath(configs.IS_GET_CONTEXT)}/log-to-file/module`); const { AuditLogsModule } = await import(`${await getImportPath(configs.IS_GET_CONTEXT)}/audit-logs/module`); - dynamicModules.push(await LogToFileModule.register(configs)); - dynamicModules.push(await AuditLogsModule.register(configs)); + dynamicModules.push(LogToFileModule.register(configs)); + dynamicModules.push(AuditLogsModule.register(configs)); } catch (error) { console.error('Error loading dynamic modules:', error); } diff --git a/server/src/modules/app/types.ts b/server/src/modules/app/types.ts index 4ecc5960f5..16ee1ce0eb 100644 --- a/server/src/modules/app/types.ts +++ b/server/src/modules/app/types.ts @@ -11,6 +11,7 @@ export interface UserAllPermissions { isBuilder: boolean; isEndUser: boolean; user: User; + resource: ResourceDetails[]; } export interface FeatureConfig { diff --git a/server/src/modules/apps/ability/app.ability.ts b/server/src/modules/apps/ability/app.ability.ts new file mode 100644 index 0000000000..eb51ebc716 --- /dev/null +++ b/server/src/modules/apps/ability/app.ability.ts @@ -0,0 +1,78 @@ +import { AbilityBuilder } from '@casl/ability'; +import { UserAllPermissions } from '@modules/app/types'; +import { FEATURE_KEY } from '../constants'; +import { App } from '@entities/app.entity'; +import { MODULES } from '@modules/app/constants/modules'; +import { FeatureAbility } from './index'; + +export function defineAppAbility( + can: AbilityBuilder['can'], + UserAllPermissions: UserAllPermissions, + appId?: string +): void { + const { superAdmin, isAdmin, userPermission } = UserAllPermissions; + const userAppPermissions = userPermission?.[MODULES.APP]; + const isAllAppsEditable = !!userAppPermissions?.isAllEditable; + const isAllAppsCreatable = !!userPermission?.appCreate; + const isAllAppsDeletable = !!userPermission?.appDelete; + const isAllAppsViewable = !!userAppPermissions?.isAllViewable; + + // App listing is available to all + can(FEATURE_KEY.GET, App); + + if (isAdmin || superAdmin) { + // Admin or super admin and do all operations + can( + [ + FEATURE_KEY.CREATE, + FEATURE_KEY.UPDATE, + FEATURE_KEY.DELETE, + FEATURE_KEY.GET_ASSOCIATED_TABLES, + FEATURE_KEY.GET_ONE, + FEATURE_KEY.GET_BY_SLUG, + FEATURE_KEY.RELEASE, + FEATURE_KEY.VALIDATE_PRIVATE_APP_ACCESS, + FEATURE_KEY.UPDATE_ICON, + FEATURE_KEY.VALIDATE_RELEASED_APP_ACCESS, + ], + App + ); + return; + } + + if (isAllAppsCreatable) { + can(FEATURE_KEY.CREATE, App); + } + + if ( + isAllAppsEditable || + (userAppPermissions?.editableAppsId?.length && appId && userAppPermissions.editableAppsId.includes(appId)) + ) { + can( + [ + FEATURE_KEY.UPDATE, + FEATURE_KEY.GET_ASSOCIATED_TABLES, + FEATURE_KEY.GET_ONE, + FEATURE_KEY.GET_BY_SLUG, + FEATURE_KEY.RELEASE, + FEATURE_KEY.VALIDATE_PRIVATE_APP_ACCESS, + FEATURE_KEY.UPDATE_ICON, + FEATURE_KEY.VALIDATE_RELEASED_APP_ACCESS, + ], + App + ); + if (isAllAppsDeletable) { + // Gives delete permission only for editable apps + can(FEATURE_KEY.DELETE, App); + } + return; + } + + if ( + isAllAppsViewable || + (userAppPermissions?.viewableAppsId?.length && appId && userAppPermissions.viewableAppsId.includes(appId)) + ) { + // add view permissions for all apps or specific app + can([FEATURE_KEY.GET_ONE, FEATURE_KEY.GET_BY_SLUG, FEATURE_KEY.VALIDATE_RELEASED_APP_ACCESS], App); + } +} diff --git a/server/src/modules/apps/ability/guard.ts b/server/src/modules/apps/ability/guard.ts index 2601231e14..cb79a718cd 100644 --- a/server/src/modules/apps/ability/guard.ts +++ b/server/src/modules/apps/ability/guard.ts @@ -4,13 +4,24 @@ import { AbilityGuard } from '@modules/app/guards/ability.guard'; import { App } from '@entities/app.entity'; import { ResourceDetails } from '@modules/app/types'; import { MODULES } from '@modules/app/constants/modules'; +import { APP_TYPES } from '../constants'; @Injectable() export class FeatureAbilityGuard extends AbilityGuard { protected getResource(): ResourceDetails { - return { - resourceType: MODULES.APP, - }; + const resource = this.getResourceObject(); + switch (resource?.type) { + case APP_TYPES.FRONT_END: + return { + resourceType: MODULES.APP, + }; + case APP_TYPES.WORKFLOW: + return { + resourceType: MODULES.WORKFLOWS, + }; + default: + return null; + } } protected getAbilityFactory() { return FeatureAbilityFactory; diff --git a/server/src/modules/apps/ability/index.ts b/server/src/modules/apps/ability/index.ts index d53309202f..970986ffa4 100644 --- a/server/src/modules/apps/ability/index.ts +++ b/server/src/modules/apps/ability/index.ts @@ -4,7 +4,7 @@ import { AbilityFactory } from '@modules/app/ability-factory'; import { UserAllPermissions } from '@modules/app/types'; import { FEATURE_KEY } from '../constants'; import { App } from '@entities/app.entity'; -import { MODULES } from '@modules/app/constants/modules'; +import { createAbility } from './utility'; type Subjects = InferSubjects | 'all'; export type FeatureAbility = Ability<[FEATURE_KEY, Subjects]>; @@ -21,72 +21,7 @@ export class FeatureAbilityFactory extends AbilityFactory extractedMetadata: { moduleName: string; features: string[] }, request?: any ): void { - const appId = request?.tj_resource_id; - const { superAdmin, isAdmin, userPermission } = UserAllPermissions; - - const userAppPermissions = userPermission?.[MODULES.APP]; - const isAllAppsEditable = !!userAppPermissions?.isAllEditable; - const isAllAppsCreatable = !!userPermission?.appCreate; - const isAllAppsDeletable = !!userPermission?.appDelete; - const isAllAppsViewable = !!userAppPermissions?.isAllViewable; - - // App listing is available to all - can(FEATURE_KEY.GET, App); - - if (isAdmin || superAdmin) { - // Admin or super admin and do all operations - can( - [ - FEATURE_KEY.CREATE, - FEATURE_KEY.UPDATE, - FEATURE_KEY.DELETE, - FEATURE_KEY.GET_ASSOCIATED_TABLES, - FEATURE_KEY.GET_ONE, - FEATURE_KEY.GET_BY_SLUG, - FEATURE_KEY.RELEASE, - FEATURE_KEY.VALIDATE_PRIVATE_APP_ACCESS, - FEATURE_KEY.UPDATE_ICON, - FEATURE_KEY.VALIDATE_RELEASED_APP_ACCESS, - ], - App - ); - return; - } - - if (isAllAppsCreatable) { - can(FEATURE_KEY.CREATE, App); - } - - if ( - isAllAppsEditable || - (userAppPermissions?.editableAppsId?.length && appId && userAppPermissions.editableAppsId.includes(appId)) - ) { - can( - [ - FEATURE_KEY.UPDATE, - FEATURE_KEY.GET_ASSOCIATED_TABLES, - FEATURE_KEY.GET_ONE, - FEATURE_KEY.GET_BY_SLUG, - FEATURE_KEY.RELEASE, - FEATURE_KEY.VALIDATE_PRIVATE_APP_ACCESS, - FEATURE_KEY.UPDATE_ICON, - FEATURE_KEY.VALIDATE_RELEASED_APP_ACCESS, - ], - App - ); - if (isAllAppsDeletable) { - // Gives delete permission only for editable apps - can(FEATURE_KEY.DELETE, App); - } - return; - } - - if ( - isAllAppsViewable || - (userAppPermissions?.viewableAppsId?.length && appId && userAppPermissions.viewableAppsId.includes(appId)) - ) { - // add view permissions for all apps or specific app - can([FEATURE_KEY.GET_ONE, FEATURE_KEY.GET_BY_SLUG, FEATURE_KEY.VALIDATE_RELEASED_APP_ACCESS], App); - } + const resourceId = request?.tj_resource_id; + createAbility(can, UserAllPermissions, resourceId); } } diff --git a/server/src/modules/apps/ability/utility.ts b/server/src/modules/apps/ability/utility.ts new file mode 100644 index 0000000000..3247722661 --- /dev/null +++ b/server/src/modules/apps/ability/utility.ts @@ -0,0 +1,27 @@ +import { AbilityBuilder } from '@casl/ability'; +import { UserAllPermissions } from '@modules/app/types'; +import { MODULES } from '@modules/app/constants/modules'; +import { FeatureAbility } from './index'; +import { defineAppAbility } from './app.ability'; +import { defineWorkflowAbility } from './workflow.ability'; + +export function createAbility( + can: AbilityBuilder['can'], + UserAllPermissions: UserAllPermissions, + resourceId?: string +): void { + const resourceType = UserAllPermissions?.resource[0]?.resourceType + ? UserAllPermissions.resource[0].resourceType + : MODULES.APP; + + switch (resourceType) { + case MODULES.APP: + defineAppAbility(can, UserAllPermissions, resourceId); + break; + case MODULES.WORKFLOWS: + defineWorkflowAbility(can, UserAllPermissions, resourceId); + break; + default: + throw new Error(`Unsupported resource type: ${resourceType}`); + } +} diff --git a/server/src/modules/apps/ability/workflow.ability.ts b/server/src/modules/apps/ability/workflow.ability.ts new file mode 100644 index 0000000000..d02443c5a4 --- /dev/null +++ b/server/src/modules/apps/ability/workflow.ability.ts @@ -0,0 +1,78 @@ +import { AbilityBuilder } from '@casl/ability'; +import { UserAllPermissions } from '@modules/app/types'; +import { FEATURE_KEY } from '../constants'; +import { App } from '@entities/app.entity'; +import { MODULES } from '@modules/app/constants/modules'; +import { FeatureAbility } from './index'; + +export function defineWorkflowAbility( + can: AbilityBuilder['can'], + UserAllPermissions: UserAllPermissions, + workflowId?: string +): void { + const { superAdmin, isAdmin, userPermission } = UserAllPermissions; + const userWorkflowPermissions = userPermission?.[MODULES.WORKFLOWS]; + const isAllWorkflowsEditable = !!userWorkflowPermissions?.isAllEditable; + const isAllWorkflowsCreatable = !!userPermission?.workflowCreate; + const isAllWorkflowsDeletable = !!userPermission?.workflowDelete; + const isAllWorkflowsExecutable = !!userWorkflowPermissions?.isAllExecutable; + + // Workflow listing is available to all + can(FEATURE_KEY.GET, App); + + if (isAdmin || superAdmin) { + // Admin or super admin can do all operations + can( + [ + FEATURE_KEY.CREATE, + FEATURE_KEY.UPDATE, + FEATURE_KEY.DELETE, + FEATURE_KEY.GET_ONE, + FEATURE_KEY.GET_BY_SLUG, + FEATURE_KEY.RELEASE, + FEATURE_KEY.VALIDATE_PRIVATE_APP_ACCESS, + FEATURE_KEY.VALIDATE_RELEASED_APP_ACCESS, + ], + App + ); + return; + } + + if (isAllWorkflowsCreatable) { + can(FEATURE_KEY.CREATE, App); + } + + if ( + isAllWorkflowsEditable || + (userWorkflowPermissions?.editableWorkflowsId?.length && + workflowId && + userWorkflowPermissions.editableWorkflowsId.includes(workflowId)) + ) { + can( + [ + FEATURE_KEY.UPDATE, + FEATURE_KEY.GET_ONE, + FEATURE_KEY.GET_BY_SLUG, + FEATURE_KEY.RELEASE, + FEATURE_KEY.VALIDATE_PRIVATE_APP_ACCESS, + FEATURE_KEY.VALIDATE_RELEASED_APP_ACCESS, + ], + App + ); + if (isAllWorkflowsDeletable) { + // Gives delete permission only for editable workflows + can(FEATURE_KEY.DELETE, App); + } + return; + } + + if ( + isAllWorkflowsExecutable || + (userWorkflowPermissions?.executableWorkflowsId?.length && + workflowId && + userWorkflowPermissions.executableWorkflowsId.includes(workflowId)) + ) { + // add view permissions for all workflows or specific workflow + can([FEATURE_KEY.GET_ONE, FEATURE_KEY.GET_BY_SLUG, FEATURE_KEY.VALIDATE_RELEASED_APP_ACCESS], App); + } +} diff --git a/server/src/modules/apps/service.ts b/server/src/modules/apps/service.ts index 6d09dba04f..66c0c681bb 100644 --- a/server/src/modules/apps/service.ts +++ b/server/src/modules/apps/service.ts @@ -17,7 +17,7 @@ import { ValidateAppAccessResponseDto, VersionReleaseDto, } from './dto'; -import { FEATURE_KEY } from './constants'; +import { APP_TYPES, FEATURE_KEY } from './constants'; import { camelizeKeys, decamelizeKeys } from 'humps'; import { App } from '@entities/app.entity'; import { AppsUtilService } from './util.service'; @@ -62,7 +62,7 @@ export class AppsService implements IAppsService { async create(user: User, appCreateDto: AppCreateDto) { const { name, icon, type } = appCreateDto; return await dbTransactionWrap(async (manager: EntityManager) => { - const app = await this.appsUtilService.create(name, user, type, manager); + const app = await this.appsUtilService.create(name, user, type as APP_TYPES, manager); const appUpdateDto = new AppUpdateDto(); appUpdateDto.name = name; @@ -200,7 +200,8 @@ export class AppsService implements IAppsService { user, folder, parseInt(page || '1'), - searchKey + searchKey, + type as APP_TYPES ); apps = viewableApps; totalFolderCount = totalCount; @@ -215,7 +216,7 @@ export class AppsService implements IAppsService { } } - const totalCount = await this.appsUtilService.count(user, searchKey, type); + const totalCount = await this.appsUtilService.count(user, searchKey, type as APP_TYPES); const totalPageCount = folderId ? totalFolderCount : totalCount; diff --git a/server/src/modules/apps/services/workflow.service.ts b/server/src/modules/apps/services/workflow.service.ts index b92b3bb573..47c6c4d397 100644 --- a/server/src/modules/apps/services/workflow.service.ts +++ b/server/src/modules/apps/services/workflow.service.ts @@ -1,14 +1,14 @@ import { Injectable } from '@nestjs/common'; import { AppsRepository } from '../repository'; import { IWorkflowService } from '../interfaces/services/IWorkflowService'; - +import { APP_TYPES } from '../constants'; @Injectable() export class WorkflowService implements IWorkflowService { constructor(protected readonly appsRepository: AppsRepository) {} async getWorkflows(organizationId: string) { const workflowApps = await this.appsRepository.find({ - where: { type: 'workflow', organizationId }, + where: { type: APP_TYPES.WORKFLOW, organizationId }, }); const result = workflowApps.map((workflowApp) => ({ id: workflowApp.id, name: workflowApp.name })); diff --git a/server/src/modules/apps/util.service.ts b/server/src/modules/apps/util.service.ts index e90e98a467..5e94c25beb 100644 --- a/server/src/modules/apps/util.service.ts +++ b/server/src/modules/apps/util.service.ts @@ -32,18 +32,16 @@ import { cloneDeep } from 'lodash'; import { merge } from 'lodash'; import { mergeWith } from 'lodash'; import { isArray } from 'lodash'; -import { UserAppsPermissions } from '@modules/ability/types'; +import { UserAppsPermissions, UserWorkflowPermissions } from '@modules/ability/types'; import { AbilityService } from '@modules/ability/interfaces/IService'; import { DataSourcesRepository } from '@modules/data-sources/repository'; import { IAppsUtilService } from './interfaces/IUtilService'; import { DataSourcesUtilService } from '@modules/data-sources/util.service'; import { AppVersionUpdateDto } from '@dto/app-version-update.dto'; +import { APP_TYPES } from './constants'; import { Component } from 'src/entities/component.entity'; import { Layout } from 'src/entities/layout.entity'; import { WorkspaceAppsResponseDto } from '@modules/external-apis/dto'; -import { RequestContext } from '@modules/request-context/service'; -import { RenameAppOrVersionDto } from '@modules/app-git/dto'; -import got from 'got'; import { DataQuery } from '@entities/data_query.entity'; @Injectable() @@ -58,7 +56,7 @@ export class AppsUtilService implements IAppsUtilService { protected readonly dataSourceRepository: DataSourcesRepository, protected readonly dataSourceUtilService: DataSourcesUtilService ) { } - async create(name: string, user: User, type: string, manager: EntityManager): Promise { + async create(name: string, user: User, type: APP_TYPES, manager: EntityManager): Promise { return await dbTransactionWrap(async (manager: EntityManager) => { const app = await catchDbException(() => { return manager.save( @@ -69,8 +67,8 @@ export class AppsUtilService implements IAppsUtilService { updatedAt: new Date(), organizationId: user.organizationId, userId: user.id, - isMaintenanceOn: type === 'workflow' ? true : false, - ...(type === 'workflow' && { workflowApiToken: uuidv4() }), + isMaintenanceOn: type === APP_TYPES.WORKFLOW ? true : false, + ...(type === APP_TYPES.WORKFLOW && { workflowApiToken: uuidv4() }), }) ); }, [{ dbConstraint: DataBaseConstraints.APP_NAME_UNIQUE, message: 'This app name is already taken.' }]); @@ -410,14 +408,26 @@ export class AppsUtilService implements IAppsUtilService { async all(user: User, page: number, searchKey: string, type: string): Promise { //Migrate it to app utility files + let resourceType: MODULES; + + switch (type) { + case APP_TYPES.WORKFLOW: + resourceType = MODULES.WORKFLOWS; + break; + case APP_TYPES.FRONT_END: + resourceType = MODULES.APP; + break; + default: + resourceType = MODULES.APP; + } const userPermission = await this.abilityService.resourceActionsPermission(user, { - resources: [{ resource: MODULES.APP }], + resources: [{ resource: resourceType }], organizationId: user.organizationId, }); return await dbTransactionWrap(async (manager: EntityManager) => { const viewableAppsQb = this.viewableAppsQueryUsingPermissions( user, - userPermission[MODULES.APP], + userPermission[resourceType], manager, searchKey, undefined, @@ -436,79 +446,119 @@ export class AppsUtilService implements IAppsUtilService { protected viewableAppsQueryUsingPermissions( user: User, - userAppPermissions: UserAppsPermissions, + userAppPermissions: UserAppsPermissions | UserWorkflowPermissions, manager: EntityManager, searchKey?: string, select?: Array, type?: string ): SelectQueryBuilder { - const viewableApps = userAppPermissions.hideAll - ? [null, ...userAppPermissions.editableAppsId] - : [ - null, - ...Array.from( - new Set([ - ...userAppPermissions.editableAppsId, - ...userAppPermissions.viewableAppsId.filter((id) => !userAppPermissions.hiddenAppsId.includes(id)), - ]) - ), - ]; const viewableAppsQb = manager - .createQueryBuilder(AppBase, 'viewable_apps') - .innerJoin('viewable_apps.user', 'user') + .createQueryBuilder(AppBase, 'apps') + .innerJoin('apps.user', 'user') .addSelect(['user.firstName', 'user.lastName']) - .where('viewable_apps.organizationId = :organizationId', { organizationId: user.organizationId }); + .where('apps.organizationId = :organizationId', { organizationId: user.organizationId }); - if (type === 'module') { + if (type === APP_TYPES.MODULE) { viewableAppsQb.leftJoinAndSelect('viewable_apps.appVersions', 'versions'); } - if (type) viewableAppsQb.andWhere('viewable_apps.type = :type', { type: type }); + if (type) { + viewableAppsQb.andWhere('apps.type = :type', { type }); + } if (searchKey) { - viewableAppsQb.andWhere('LOWER(viewable_apps.name) like :searchKey', { + viewableAppsQb.andWhere('LOWER(apps.name) like :searchKey', { searchKey: `%${searchKey && searchKey.toLowerCase()}%`, }); } if (select) { - viewableAppsQb.select(select.map((col) => `viewable_apps.${col}`)); + viewableAppsQb.select(select.map((col) => `apps.${col}`)); } - viewableAppsQb.orderBy('viewable_apps.createdAt', 'DESC'); + + viewableAppsQb.orderBy('apps.createdAt', 'DESC'); + if (this.isSuperAdmin(user)) { return viewableAppsQb; } + const viewableApps = this.calculateViewableFrontEndApps(userAppPermissions as unknown as UserAppsPermissions); + + switch (type) { + case APP_TYPES.FRONT_END: + default: + return this.addViewableFrontEndAppsFilter( + viewableAppsQb, + userAppPermissions as unknown as UserAppsPermissions, + viewableApps + ); + } + } + + private calculateViewableFrontEndApps(userAppPermissions: UserAppsPermissions): string[] { + return userAppPermissions.hideAll + ? [null, ...userAppPermissions.editableAppsId] + : [ + null, + ...Array.from( + new Set([ + ...userAppPermissions.editableAppsId, + ...userAppPermissions.viewableAppsId.filter((id) => !userAppPermissions.hiddenAppsId.includes(id)), + ]) + ), + ]; + } + + private addViewableFrontEndAppsFilter( + query: SelectQueryBuilder, + userAppPermissions: UserAppsPermissions, + viewableApps: string[] + ): SelectQueryBuilder { const { isAllEditable, isAllViewable, hideAll } = userAppPermissions; - if (isAllEditable) return viewableAppsQb; + if (isAllEditable) return query; + if ((isAllViewable && hideAll) || (!isAllViewable && !hideAll) || (!isAllViewable && hideAll)) { - viewableAppsQb.andWhere('viewable_apps.id IN (:...viewableApps)', { + query.andWhere('apps.id IN (:...viewableApps)', { viewableApps, }); - return viewableAppsQb; + return query; } + const hiddenApps = userAppPermissions.hiddenAppsId.filter((id) => !userAppPermissions.editableAppsId.includes(id)); if (!userAppPermissions.hideAll && isAllViewable && hiddenApps.length > 0) { - viewableAppsQb.andWhere('viewable_apps.id NOT IN (:...hiddenApps)', { + query.andWhere('apps.id NOT IN (:...hiddenApps)', { hiddenApps, }); } - return viewableAppsQb; + + return query; } protected isSuperAdmin(user: User) { return !!(user?.userType === USER_TYPE.INSTANCE); } - async count(user: User, searchKey, type: string): Promise { + async count(user: User, searchKey, type: APP_TYPES): Promise { + let resourceType: MODULES; + + switch (type) { + case APP_TYPES.WORKFLOW: + resourceType = MODULES.WORKFLOWS; + break; + case APP_TYPES.FRONT_END: + resourceType = MODULES.APP; + break; + default: + resourceType = MODULES.APP; + } const userPermission = await this.abilityService.resourceActionsPermission(user, { - resources: [{ resource: MODULES.APP }], + resources: [{ resource: resourceType }], organizationId: user.organizationId, }); return await dbTransactionWrap(async (manager: EntityManager) => { const apps = await this.viewableAppsQueryUsingPermissions( user, - userPermission[MODULES.APP], + userPermission[resourceType], manager, searchKey, undefined, diff --git a/server/src/modules/data-queries/ability/app/data-query-app.ability.ts b/server/src/modules/data-queries/ability/app/data-query-app.ability.ts new file mode 100644 index 0000000000..0d9a77201a --- /dev/null +++ b/server/src/modules/data-queries/ability/app/data-query-app.ability.ts @@ -0,0 +1,84 @@ +import { AbilityBuilder } from '@casl/ability'; +import { UserAllPermissions } from '@modules/app/types'; +import { FEATURE_KEY } from '../../constants'; +import { App } from '@entities/app.entity'; +import { MODULES } from '@modules/app/constants/modules'; +import { FeatureAbility } from './index'; + +export function defineDataQueryAppAbility( + can: AbilityBuilder['can'], + UserAllPermissions: UserAllPermissions, + appId?: string +): void { + const { superAdmin, isAdmin, userPermission } = UserAllPermissions; + const resourcePermissions = userPermission?.[MODULES.APP]; + const isAllEditable = !!resourcePermissions?.isAllEditable; + const isCanCreate = userPermission.appCreate; + const isCanDelete = userPermission.appDelete; + const isAllViewable = !!resourcePermissions?.isAllViewable; + + // Always grant RUN_EDITOR and RUN_VIEWER permissions + can([FEATURE_KEY.RUN_EDITOR, FEATURE_KEY.RUN_VIEWER], App); + + if (isAdmin || superAdmin) { + can( + [ + FEATURE_KEY.CREATE, + FEATURE_KEY.GET, + FEATURE_KEY.UPDATE, + FEATURE_KEY.DELETE, + FEATURE_KEY.UPDATE_DATA_SOURCE, + FEATURE_KEY.UPDATE_ONE, + FEATURE_KEY.RUN_EDITOR, + FEATURE_KEY.RUN_VIEWER, + FEATURE_KEY.PREVIEW, + ], + App + ); + return; + } + + if (isAllEditable || isCanCreate || isCanDelete) { + can( + [ + FEATURE_KEY.GET, + FEATURE_KEY.UPDATE, + FEATURE_KEY.UPDATE_ONE, + FEATURE_KEY.RUN_EDITOR, + FEATURE_KEY.RUN_VIEWER, + FEATURE_KEY.PREVIEW, + FEATURE_KEY.DELETE, + FEATURE_KEY.CREATE, + ], + App + ); + return; + } + + if (resourcePermissions?.editableAppsId?.length && appId && resourcePermissions?.editableAppsId?.includes(appId)) { + can( + [ + FEATURE_KEY.GET, + FEATURE_KEY.UPDATE, + FEATURE_KEY.UPDATE_ONE, + FEATURE_KEY.RUN_EDITOR, + FEATURE_KEY.RUN_VIEWER, + FEATURE_KEY.PREVIEW, + FEATURE_KEY.DELETE, + FEATURE_KEY.CREATE, + ], + App + ); + return; + } + + if (isAllViewable) { + can([FEATURE_KEY.GET, FEATURE_KEY.PREVIEW, FEATURE_KEY.RUN_VIEWER, FEATURE_KEY.RUN_EDITOR], App); + return; + } + + if (resourcePermissions?.viewableAppsId?.length && appId && resourcePermissions?.viewableAppsId?.includes(appId)) { + can([FEATURE_KEY.GET, FEATURE_KEY.PREVIEW, FEATURE_KEY.RUN_VIEWER, FEATURE_KEY.RUN_EDITOR], App); + return; + } +} diff --git a/server/src/modules/data-queries/ability/app/data-query-workflow.ability.ts b/server/src/modules/data-queries/ability/app/data-query-workflow.ability.ts new file mode 100644 index 0000000000..fc852d4a89 --- /dev/null +++ b/server/src/modules/data-queries/ability/app/data-query-workflow.ability.ts @@ -0,0 +1,92 @@ +import { AbilityBuilder } from '@casl/ability'; +import { UserAllPermissions } from '@modules/app/types'; +import { FEATURE_KEY } from '../../constants'; +import { App } from '@entities/app.entity'; +import { MODULES } from '@modules/app/constants/modules'; +import { FeatureAbility } from './index'; + +export function defineDataQueryWorkflowAbility( + can: AbilityBuilder['can'], + UserAllPermissions: UserAllPermissions, + workflowId?: string +): void { + const { superAdmin, isAdmin, userPermission } = UserAllPermissions; + const resourcePermissions = userPermission?.[MODULES.WORKFLOWS]; + const isAllEditable = !!resourcePermissions?.isAllEditable; + const isCanCreate = userPermission.workflowCreate; + const isCanDelete = userPermission.workflowDelete; + const isAllExecutable = !!resourcePermissions?.isAllExecutable; + + // Always grant RUN_EDITOR and RUN_VIEWER permissions + can([FEATURE_KEY.RUN_EDITOR, FEATURE_KEY.RUN_VIEWER], App); + + if (isAdmin || superAdmin) { + can( + [ + FEATURE_KEY.CREATE, + FEATURE_KEY.GET, + FEATURE_KEY.UPDATE, + FEATURE_KEY.DELETE, + FEATURE_KEY.UPDATE_DATA_SOURCE, + FEATURE_KEY.UPDATE_ONE, + FEATURE_KEY.RUN_EDITOR, + FEATURE_KEY.RUN_VIEWER, + FEATURE_KEY.PREVIEW, + ], + App + ); + return; + } + + if (isAllEditable || isCanCreate || isCanDelete) { + can( + [ + FEATURE_KEY.GET, + FEATURE_KEY.UPDATE, + FEATURE_KEY.UPDATE_ONE, + FEATURE_KEY.RUN_EDITOR, + FEATURE_KEY.RUN_VIEWER, + FEATURE_KEY.PREVIEW, + FEATURE_KEY.DELETE, + FEATURE_KEY.CREATE, + ], + App + ); + return; + } + + if ( + resourcePermissions?.editableWorkflowsId?.length && + workflowId && + resourcePermissions?.editableWorkflowsId?.includes(workflowId) + ) { + can( + [ + FEATURE_KEY.GET, + FEATURE_KEY.UPDATE, + FEATURE_KEY.UPDATE_ONE, + FEATURE_KEY.RUN_EDITOR, + FEATURE_KEY.RUN_VIEWER, + FEATURE_KEY.PREVIEW, + FEATURE_KEY.DELETE, + FEATURE_KEY.CREATE, + ], + App + ); + return; + } + + if (isAllExecutable) { + can([FEATURE_KEY.GET, FEATURE_KEY.PREVIEW, FEATURE_KEY.RUN_VIEWER, FEATURE_KEY.RUN_EDITOR], App); + return; + } + + if ( + resourcePermissions?.executableWorkflowsId?.length && + workflowId && + resourcePermissions?.executableWorkflowsId?.includes(workflowId) + ) { + can([FEATURE_KEY.GET, FEATURE_KEY.PREVIEW, FEATURE_KEY.RUN_VIEWER, FEATURE_KEY.RUN_EDITOR], App); + return; + } +} diff --git a/server/src/modules/data-queries/ability/app/guard.ts b/server/src/modules/data-queries/ability/app/guard.ts index 87d8c2d92b..250682f32b 100644 --- a/server/src/modules/data-queries/ability/app/guard.ts +++ b/server/src/modules/data-queries/ability/app/guard.ts @@ -4,6 +4,7 @@ import { AbilityGuard } from '@modules/app/guards/ability.guard'; import { MODULES } from '@modules/app/constants/modules'; import { ResourceDetails } from '@modules/app/types'; import { App } from '@entities/app.entity'; +import { APP_TYPES } from '@modules/apps/constants'; @Injectable() export class FeatureAbilityGuard extends AbilityGuard { @@ -14,10 +15,23 @@ export class FeatureAbilityGuard extends AbilityGuard { protected getSubjectType() { return App; } + + private getAppResourceType(): MODULES { + const appResource: App = this.getResourceObject(); + switch (appResource.type) { + case APP_TYPES.FRONT_END: + return MODULES.APP; + case APP_TYPES.WORKFLOW: + return MODULES.WORKFLOWS; + default: + return MODULES.APP; + } + } protected getResource(): ResourceDetails | ResourceDetails[] { + const appResource: MODULES = this.getAppResourceType(); return [ { - resourceType: MODULES.APP, + resourceType: appResource, }, { resourceType: MODULES.GLOBAL_DATA_SOURCE, diff --git a/server/src/modules/data-queries/ability/app/index.ts b/server/src/modules/data-queries/ability/app/index.ts index a6bd868805..fa614e5c51 100644 --- a/server/src/modules/data-queries/ability/app/index.ts +++ b/server/src/modules/data-queries/ability/app/index.ts @@ -3,8 +3,10 @@ import { Ability, AbilityBuilder, InferSubjects } from '@casl/ability'; import { AbilityFactory } from '@modules/app/ability-factory'; import { UserAllPermissions } from '@modules/app/types'; import { FEATURE_KEY } from '../../constants'; -import { MODULES } from '@modules/app/constants/modules'; import { App } from '@entities/app.entity'; +import { MODULES } from '@modules/app/constants/modules'; +import { defineDataQueryAppAbility } from './data-query-app.ability'; +import { defineDataQueryWorkflowAbility } from './data-query-workflow.ability'; type Subjects = InferSubjects | 'all'; export type FeatureAbility = Ability<[FEATURE_KEY, Subjects]>; @@ -21,80 +23,18 @@ export class FeatureAbilityFactory extends AbilityFactory extractedMetadata: { moduleName: string; features: string[] }, request?: any ): void { - const { superAdmin, isAdmin, userPermission } = UserAllPermissions; + const resourceId = request?.tj_resource_id; + const resourceType = UserAllPermissions.resource[0].resourceType; - const resourcePermissions = userPermission?.[MODULES.APP]; - const isAllEditable = !!resourcePermissions?.isAllEditable; - const isCanCreate = userPermission.appCreate; - const isCanDelete = userPermission.appDelete; - const isAllViewable = !!resourcePermissions?.isAllViewable; - - const appId = request?.tj_resource_id; - - // Always grant RUN_EDITOR and RUN_VIEWER permissions - can([FEATURE_KEY.RUN_EDITOR, FEATURE_KEY.RUN_VIEWER], App); - - // Admin or super admin and do all operations - if (isAdmin || superAdmin) { - can( - [ - FEATURE_KEY.CREATE, - FEATURE_KEY.GET, - FEATURE_KEY.UPDATE, - FEATURE_KEY.DELETE, - FEATURE_KEY.UPDATE_DATA_SOURCE, - FEATURE_KEY.UPDATE_ONE, - FEATURE_KEY.RUN_EDITOR, - FEATURE_KEY.RUN_VIEWER, - FEATURE_KEY.PREVIEW, - ], - App - ); - return; - } - - if (isAllEditable || isCanCreate || isCanDelete) { - // Can create and can delete has master permissions - can( - [ - FEATURE_KEY.GET, - FEATURE_KEY.UPDATE, - FEATURE_KEY.UPDATE_ONE, - FEATURE_KEY.RUN_EDITOR, - FEATURE_KEY.RUN_VIEWER, - FEATURE_KEY.PREVIEW, - FEATURE_KEY.DELETE, - FEATURE_KEY.CREATE, - ], - App - ); - return; - } - - if (resourcePermissions?.editableAppsId?.length && appId && resourcePermissions?.editableAppsId?.includes(appId)) { - can( - [ - FEATURE_KEY.GET, - FEATURE_KEY.UPDATE, - FEATURE_KEY.UPDATE_ONE, - FEATURE_KEY.RUN_EDITOR, - FEATURE_KEY.RUN_VIEWER, - FEATURE_KEY.PREVIEW, - FEATURE_KEY.DELETE, - FEATURE_KEY.CREATE, - ], - App - ); - return; - } - - if (isAllViewable) { - can([FEATURE_KEY.GET, FEATURE_KEY.PREVIEW, FEATURE_KEY.RUN_VIEWER, FEATURE_KEY.RUN_EDITOR], App); - return; - } - if (resourcePermissions?.viewableAppsId?.length && appId && resourcePermissions?.viewableAppsId?.includes(appId)) { - can([FEATURE_KEY.GET, FEATURE_KEY.PREVIEW, FEATURE_KEY.RUN_VIEWER, FEATURE_KEY.RUN_EDITOR], App); - return; + switch (resourceType) { + case MODULES.APP: + defineDataQueryAppAbility(can, UserAllPermissions, resourceId); + break; + case MODULES.WORKFLOWS: + defineDataQueryWorkflowAbility(can, UserAllPermissions, resourceId); + break; + default: + throw new Error(`Unsupported resource type: ${resourceType}`); } } } diff --git a/server/src/modules/folder-apps/interfaces/IUtilService.ts b/server/src/modules/folder-apps/interfaces/IUtilService.ts index 15f6e753b8..e073a62420 100644 --- a/server/src/modules/folder-apps/interfaces/IUtilService.ts +++ b/server/src/modules/folder-apps/interfaces/IUtilService.ts @@ -2,12 +2,13 @@ import { Folder } from '@entities/folder.entity'; import { User } from '@entities/user.entity'; import { EntityManager } from 'typeorm'; import { AppBase } from '@entities/app_base.entity'; -import { UserAppsPermissions } from '@modules/ability/types'; +import { UserAppsPermissions, UserWorkflowPermissions } from '@modules/ability/types'; +import { APP_TYPES } from '@modules/apps/constants'; export interface IFolderAppsUtilService { allFoldersWithAppCount( user: User, - userAppPermissions: UserAppsPermissions, + userAppPermissions: UserAppsPermissions | UserWorkflowPermissions, manager: EntityManager, type?: string, searchKey?: string @@ -16,6 +17,7 @@ export interface IFolderAppsUtilService { user: User, folder: Folder, page: number, - searchKey: string + searchKey: string, + type?: APP_TYPES ): Promise<{ viewableApps: AppBase[]; totalCount: number }>; } diff --git a/server/src/modules/folder-apps/service.ts b/server/src/modules/folder-apps/service.ts index 1299af06dd..7ed80df160 100644 --- a/server/src/modules/folder-apps/service.ts +++ b/server/src/modules/folder-apps/service.ts @@ -10,6 +10,7 @@ import { MODULES } from '@modules/app/constants/modules'; import { AbilityService } from '@modules/ability/interfaces/IService'; import { User } from '@entities/user.entity'; import { USER_ROLE } from '@modules/group-permissions/constants'; +import { APP_TYPES } from '@modules/apps/constants'; @Injectable() export class FolderAppsService implements IFolderAppsService { constructor( @@ -49,16 +50,30 @@ export class FolderAppsService implements IFolderAppsService { return await manager.delete(FolderApp, { folderId, appId }); }); } + + private getResourceTypefromAppType(type: APP_TYPES) { + switch (type) { + case APP_TYPES.FRONT_END: + return MODULES.APP; + case APP_TYPES.WORKFLOW: + return MODULES.WORKFLOWS; + default: + throw new BadRequestException('Invalid resource type'); + } + } + async getFolders(user: User, query) { return dbTransactionWrap(async (manager: EntityManager) => { const type = query.type; const searchKey = query.searchKey; + const resourceType = this.getResourceTypefromAppType(type as APP_TYPES); const userAppPermissions = ( await this.abilityService.resourceActionsPermission(user, { - resources: [{ resource: MODULES.APP }], + resources: [{ resource: resourceType }], organizationId: user.organizationId, }) - )?.[MODULES.APP]; + )?.[resourceType]; + const allFolderList = await this.foldersUtilService.allFolders(user, manager, type); if (allFolderList.length === 0) { return { folders: [] }; diff --git a/server/src/modules/folder-apps/util.service.ts b/server/src/modules/folder-apps/util.service.ts index 5613cd2be2..f7a50fb18d 100644 --- a/server/src/modules/folder-apps/util.service.ts +++ b/server/src/modules/folder-apps/util.service.ts @@ -7,26 +7,59 @@ import { AppBase } from '@entities/app_base.entity'; import { dbTransactionWrap } from '@helpers/database.helper'; import { FolderApp } from '@entities/folder_app.entity'; import { MODULES } from '@modules/app/constants/modules'; -import { UserAppsPermissions } from '@modules/ability/types'; +import { UserAppsPermissions, UserWorkflowPermissions } from '@modules/ability/types'; import { AbilityService } from '@modules/ability/interfaces/IService'; +import { APP_TYPES } from '@modules/apps/constants'; + @Injectable() export class FolderAppsUtilService implements IFolderAppsUtilService { constructor(protected readonly abilityService: AbilityService) {} + async allFoldersWithAppCount( user: User, - userAppPermissions: UserAppsPermissions, + userAppPermissions: UserAppsPermissions | UserWorkflowPermissions, manager: EntityManager, - type = 'front-end', + type = APP_TYPES.FRONT_END, searchKey?: string ): Promise { - return this.getFolderQuery(user.organizationId, manager, userAppPermissions, type, searchKey).distinct().getMany(); + return this.getFolderQuery(user.organizationId, manager, userAppPermissions as UserAppsPermissions, type, searchKey) + .distinct() + .getMany(); } - private getFolderQuery( + protected getBaseFolderQuery( + organizationId: string, + manager: EntityManager, + type: APP_TYPES, + searchKey?: string + ): SelectQueryBuilder { + const query = manager.createQueryBuilder(Folder, 'folders'); + query.leftJoinAndSelect('folders.folderApps', 'folder_apps'); + query.leftJoin('folder_apps.app', 'app'); + + if (searchKey) { + query.andWhere('LOWER(app.name) like :searchKey', { + searchKey: `%${searchKey && searchKey.toLowerCase()}%`, + }); + } + + query + .andWhere('folders.organization_id = :organizationId', { + organizationId, + }) + .andWhere('folders.type = :type', { + type, + }) + .orderBy('folders.name', 'ASC'); + + return query; + } + + protected getFolderQuery( organizationId: string, manager: EntityManager, userAppPermissions: UserAppsPermissions, - type = 'front-end', + type = APP_TYPES.FRONT_END, searchKey?: string ): SelectQueryBuilder { const { isAllEditable, isAllViewable, hideAll } = userAppPermissions; @@ -44,9 +77,8 @@ export class FolderAppsUtilService implements IFolderAppsUtilService { const hiddenApps = [ ...userAppPermissions.hiddenAppsId.filter((id) => !userAppPermissions.editableAppsId.includes(id)), ]; - const query = manager.createQueryBuilder(Folder, 'folders'); - query.leftJoinAndSelect('folders.folderApps', 'folder_apps'); - query.leftJoin('folder_apps.app', 'app'); + + const query = this.getBaseFolderQuery(organizationId, manager, type, searchKey); if (!isAllEditable) { // Not all apps are editable - filter with view privilege @@ -66,27 +98,34 @@ export class FolderAppsUtilService implements IFolderAppsUtilService { } } + return query; + } + + protected getBaseAppsQuery( + manager: EntityManager, + folderAppIds: string[], + searchKey?: string + ): SelectQueryBuilder { + const query = manager + .createQueryBuilder(AppBase, 'apps') + .innerJoin('apps.user', 'user') + .addSelect(['user.firstName', 'user.lastName']); + if (searchKey) { - query.andWhere('LOWER(app.name) like :searchKey', { - searchKey: `%${searchKey && searchKey.toLowerCase()}%`, + query.andWhere('LOWER(apps.name) LIKE :searchKey', { + searchKey: `%${searchKey.toLowerCase()}%`, }); } - query - .andWhere('folders.organization_id = :organizationId', { - organizationId, - }) - .andWhere('folders.type = :type', { - type, - }) - .orderBy('folders.name', 'ASC'); return query; } + async getAppsFor( user: User, folder: Folder, page: number, - searchKey: string + searchKey: string, + type: APP_TYPES ): Promise<{ viewableApps: AppBase[]; totalCount: number; @@ -113,33 +152,9 @@ export class FolderAppsUtilService implements IFolderAppsUtilService { totalCount: 0, }; } - const { isAllEditable, isAllViewable, hideAll } = userAppPermissions; - const viewableAppsTotal = isAllEditable - ? [null, ...folderAppIds] - : hideAll - ? [null, ...userAppPermissions.editableAppsId] - : isAllViewable - ? [null, ...folderAppIds].filter((id) => !userAppPermissions.hiddenAppsId.includes(id)) - : [ - null, - ...Array.from( - new Set([ - ...userAppPermissions.editableAppsId, - ...userAppPermissions.viewableAppsId.filter((id) => !userAppPermissions.hiddenAppsId.includes(id)), - ]) - ), - ]; - const viewableAppIds = [null, ...viewableAppsTotal.filter((id) => folderAppIds.includes(id))]; - - const viewableAppsInFolder = manager - .createQueryBuilder(AppBase, 'apps') - .innerJoin('apps.user', 'user') - .addSelect(['user.firstName', 'user.lastName']); - - viewableAppsInFolder.where('apps.id IN (:...viewableAppIds)', { - viewableAppIds: viewableAppIds, - }); + const viewableAppsInFolder = this.getBaseAppsQuery(manager, folderAppIds, searchKey); + this.addViewableFrontendFilter(viewableAppsInFolder, folderAppIds, userAppPermissions); const [viewableApps, totalCount] = await Promise.all([ viewableAppsInFolder @@ -156,4 +171,36 @@ export class FolderAppsUtilService implements IFolderAppsUtilService { }; }); } + + protected addViewableFrontendFilter( + query: SelectQueryBuilder, + folderAppIds: string[], + userAppPermissions: UserAppsPermissions + ): SelectQueryBuilder { + const { isAllEditable, isAllViewable, hideAll } = userAppPermissions; + + const viewableAppsTotal = isAllEditable + ? [null, ...folderAppIds] + : hideAll + ? [null, ...userAppPermissions.editableAppsId] + : isAllViewable + ? [null, ...folderAppIds].filter((id) => !userAppPermissions.hiddenAppsId.includes(id)) + : [ + null, + ...Array.from( + new Set([ + ...userAppPermissions.editableAppsId, + ...userAppPermissions.viewableAppsId.filter((id) => !userAppPermissions.hiddenAppsId.includes(id)), + ]) + ), + ]; + + const viewableAppIds = [null, ...viewableAppsTotal.filter((id) => folderAppIds.includes(id))]; + + query.where('apps.id IN (:...viewableAppIds)', { + viewableAppIds, + }); + + return query; + } } diff --git a/server/src/modules/group-permissions/constants/granular_permissions.ts b/server/src/modules/group-permissions/constants/granular_permissions.ts index 726f07bbd8..c45915c0e7 100644 --- a/server/src/modules/group-permissions/constants/granular_permissions.ts +++ b/server/src/modules/group-permissions/constants/granular_permissions.ts @@ -3,4 +3,5 @@ import { ResourceType } from './index'; export const DEFAULT_GRANULAR_PERMISSIONS_NAME = { [ResourceType.APP]: 'Apps', [ResourceType.DATA_SOURCE]: 'Data sources', + [ResourceType.WORKFLOWS]: 'Workflows', }; diff --git a/server/src/modules/group-permissions/constants/index.ts b/server/src/modules/group-permissions/constants/index.ts index d63934d84f..7610c9e881 100644 --- a/server/src/modules/group-permissions/constants/index.ts +++ b/server/src/modules/group-permissions/constants/index.ts @@ -17,6 +17,7 @@ export const HUMANIZED_USER_LIST = ['End-user', 'Builder', 'Admin']; export enum ResourceType { APP = 'app', DATA_SOURCE = 'data_source', + WORKFLOWS = 'workflow', } export const DEFAULT_GROUP_PERMISSIONS = { @@ -26,6 +27,8 @@ export const DEFAULT_GROUP_PERMISSIONS = { appCreate: true, appDelete: true, folderCRUD: true, + workflowCreate: true, + workflowDelete: true, orgConstantCRUD: true, dataSourceCreate: true, dataSourceDelete: true, @@ -37,6 +40,8 @@ export const DEFAULT_GROUP_PERMISSIONS = { appCreate: true, appDelete: true, folderCRUD: true, + workflowCreate: true, + workflowDelete: true, orgConstantCRUD: true, dataSourceCreate: true, dataSourceDelete: true, @@ -47,6 +52,8 @@ export const DEFAULT_GROUP_PERMISSIONS = { type: GROUP_PERMISSIONS_TYPE.DEFAULT, appCreate: false, appDelete: false, + workflowCreate: false, + workflowDelete: false, folderCRUD: false, orgConstantCRUD: false, dataSourceCreate: false, @@ -68,6 +75,10 @@ export const DEFAULT_RESOURCE_PERMISSIONS = { canUse: false, }, }, + [ResourceType.WORKFLOWS]: { + canEdit: true, + canView: false, + }, }, [USER_ROLE.END_USER]: { [ResourceType.APP]: { @@ -75,6 +86,10 @@ export const DEFAULT_RESOURCE_PERMISSIONS = { canView: true, hideFromDashboard: false, }, + [ResourceType.WORKFLOWS]: { + canEdit: false, + canView: true, + }, }, [USER_ROLE.BUILDER]: { [ResourceType.APP]: { @@ -88,6 +103,10 @@ export const DEFAULT_RESOURCE_PERMISSIONS = { canUse: false, }, }, + [ResourceType.WORKFLOWS]: { + canEdit: true, + canView: false, + }, }, } as Record>>; diff --git a/server/src/modules/group-permissions/dto/index.ts b/server/src/modules/group-permissions/dto/index.ts index 703fac73f5..651659df43 100644 --- a/server/src/modules/group-permissions/dto/index.ts +++ b/server/src/modules/group-permissions/dto/index.ts @@ -31,6 +31,14 @@ export class UpdateGroupPermissionDto { @IsOptional() orgConstantCRUD: boolean; + @IsBoolean() + @IsOptional() + workflowCreate: boolean; + + @IsBoolean() + @IsOptional() + workflowDelete: boolean; + @IsBoolean() @IsOptional() dataSourceCreate: boolean; diff --git a/server/src/modules/group-permissions/services/granular-permissions.service.ts b/server/src/modules/group-permissions/services/granular-permissions.service.ts index 7f3d03ed28..a5f543b6b7 100644 --- a/server/src/modules/group-permissions/services/granular-permissions.service.ts +++ b/server/src/modules/group-permissions/services/granular-permissions.service.ts @@ -45,7 +45,6 @@ export class GranularPermissionsService implements IGranularPermissionsService { return await dbTransactionWrap(async (manager: EntityManager) => { const apps = await manager.find(AppBase, { where: { - type: 'front-end', organizationId, }, }); @@ -53,6 +52,7 @@ export class GranularPermissionsService implements IGranularPermissionsService { return { name: app.name, id: app.id, + type: app.type, }; }); }); diff --git a/server/src/modules/group-permissions/types/granular_permissions.ts b/server/src/modules/group-permissions/types/granular_permissions.ts index ffaf1ccd35..5ed7d8ebfc 100644 --- a/server/src/modules/group-permissions/types/granular_permissions.ts +++ b/server/src/modules/group-permissions/types/granular_permissions.ts @@ -2,21 +2,39 @@ import { GranularPermissions } from '@entities/granular_permissions.entity'; import { ResourceType } from '../constants'; import { CreateGranularPermissionDto, UpdateGranularPermissionDto } from '../dto/granular-permissions'; import { GroupPermissions } from '@entities/group_permissions.entity'; +import { APP_TYPES } from '@modules/apps/constants'; export interface AddableResourceItem { name: string; id: string; } -export type CreateResourcePermissionObject = - T extends ResourceType.APP ? CreateAppsPermissionsObject : CreateDataSourcePermissionsObject; +type CreateResourcePermissionMap = { + [ResourceType.APP]: CreateAppsPermissionsObject; + [ResourceType.DATA_SOURCE]: CreateDataSourcePermissionsObject; + [ResourceType.WORKFLOWS]: CreateWorkflowPermissionsObject; +}; -export interface CreateAppsPermissionsObject { +export type CreateResourcePermissionObject = CreateResourcePermissionMap[T]; + +export interface CreateBaseAppsPermissionsObject { canEdit?: boolean; canView?: boolean; + appType?: APP_TYPES; + resourcesToAdd?: GranularPermissionAddResourceItems; +} +export interface CreateAppsPermissionsObject extends CreateBaseAppsPermissionsObject { hideFromDashboard?: boolean; +} + +export interface CreateWorkflowPermissionsObject extends CreateBaseAppsPermissionsObject {} +export interface CreateAppsPermissionsObject extends CreateBaseAppsPermissionsObject { resourcesToAdd?: GranularPermissionAddResourceItems; } +export interface CreateWorkflowPermissionsObject extends CreateBaseAppsPermissionsObject { + resourcesToAdd?: GranularPermissionAddResourceItems; +} + export interface CreateDataSourcePermissionsObject { action?: DataSourcesGroupPermissionsActions; resourcesToAdd?: GranularPermissionAddResourceItems; @@ -32,10 +50,28 @@ export interface CreateGranularPermissionObject { organizationId: string; } -export type GranularPermissionAddResourceItems = - T extends ResourceType.APP ? AppsPermissionAddResourceItem[] : DataSourcesPermissionResourceItem[]; +type ResourceToPermissionItemMap = { + [ResourceType.APP]: AppsPermissionAddResourceItem[]; + [ResourceType.DATA_SOURCE]: DataSourcesPermissionResourceItem[]; + [ResourceType.WORKFLOWS]: WorkflowsPermissionAddResourceItem[]; +}; -export interface AppsPermissionAddResourceItem { +export type GranularPermissionAddResourceItems = ResourceToPermissionItemMap[T]; + +interface BaseAppsPermissionAddResourceItem { + appId: string; +} + +export interface AppsPermissionAddResourceItem extends BaseAppsPermissionAddResourceItem {} + +export interface WorkflowsPermissionAddResourceItem extends BaseAppsPermissionAddResourceItem {} + +interface BaseAppsGroupPermissionsActions { + canEdit: boolean; + canView: boolean; +} + +interface BaseAppsPermissionAddResourceItem { appId: string; } @@ -43,12 +79,12 @@ export interface DataSourcesPermissionResourceItem { dataSourceId: string; } -export interface AppsGroupPermissionsActions { - canEdit: boolean; - canView: boolean; +export interface AppsGroupPermissionsActions extends BaseAppsGroupPermissionsActions { hideFromDashboard: boolean; } +export interface WorkflowsGroupPermissionsActions extends BaseAppsGroupPermissionsActions {} + export interface ResourcePermissionMetaData { granularPermissions: GranularPermissions; organizationId: string; @@ -66,7 +102,9 @@ export interface UpdateGranularPermissionObject { updateGranularPermissionDto: UpdateGranularPermissionDto; } -export interface UpdateResourceGroupPermissionsObject { +export interface UpdateResourceGroupPermissionsObject< + T extends ResourceType.APP | ResourceType.DATA_SOURCE | ResourceType.WORKFLOWS +> { group: GroupPermissions; granularPermissions: GranularPermissions; actions: ResourceGroupActions; @@ -81,9 +119,13 @@ export interface GranularPermissionDeleteResourceItem { id: string; } -export type ResourceGroupActions = T extends ResourceType.APP - ? AppsGroupPermissionsActions - : DataSourcesGroupPermissionsActions; +type ResourceActionMap = { + [ResourceType.APP]: AppsGroupPermissionsActions; + [ResourceType.DATA_SOURCE]: DataSourcesGroupPermissionsActions; + [ResourceType.WORKFLOWS]: WorkflowsGroupPermissionsActions; +}; + +export type ResourceGroupActions = ResourceActionMap[T]; export interface ValidateResourceAction { isBuilderPermissions: boolean; diff --git a/server/src/modules/group-permissions/types/index.ts b/server/src/modules/group-permissions/types/index.ts index ac338fa52e..e241de91fc 100644 --- a/server/src/modules/group-permissions/types/index.ts +++ b/server/src/modules/group-permissions/types/index.ts @@ -8,6 +8,8 @@ export interface CreateDefaultGroupObject { name: string; appCreate?: boolean; appDelete?: boolean; + workflowCreate?: boolean; + workflowDelete?: boolean; folderCRUD?: boolean; orgConstantCRUD?: boolean; dataSourceCreate?: boolean; diff --git a/server/src/modules/group-permissions/util-services/granular-permissions.util.service.ts b/server/src/modules/group-permissions/util-services/granular-permissions.util.service.ts index 2344490c6d..e17f6c3688 100644 --- a/server/src/modules/group-permissions/util-services/granular-permissions.util.service.ts +++ b/server/src/modules/group-permissions/util-services/granular-permissions.util.service.ts @@ -23,6 +23,7 @@ import * as _ from 'lodash'; import { DEFAULT_GRANULAR_PERMISSIONS_NAME } from '../constants/granular_permissions'; import { RolesRepository } from '@modules/roles/repository'; import { IGranularPermissionsUtilService } from '../interfaces/IUtilService'; +import { APP_TYPES } from '@modules/apps/constants'; @Injectable() export class GranularPermissionsUtilService implements IGranularPermissionsUtilService { @@ -52,7 +53,7 @@ export class GranularPermissionsUtilService implements IGranularPermissionsUtilS protected validateAppResourcePermissionUpdateOperation( group: GroupPermissions, - actions: ResourceGroupActions + actions: ResourceGroupActions ) { if (group.name === USER_ROLE.END_USER && actions.canEdit) { throw new BadRequestException(ERROR_HANDLER.EDITOR_LEVEL_PERMISSION_NOT_ALLOWED_END_USER); @@ -120,7 +121,7 @@ export class GranularPermissionsUtilService implements IGranularPermissionsUtilS protected async createAppGroupPermission( organizationId: string, granularPermissions: GranularPermissions, - createAppPermissionsObj?: CreateResourcePermissionObject, + createAppPermissionsObj?: CreateResourcePermissionObject, manager?: EntityManager ): Promise { const { resourcesToAdd, canEdit } = createAppPermissionsObj; @@ -133,8 +134,8 @@ export class GranularPermissionsUtilService implements IGranularPermissionsUtilS }, manager ); - - const appGRoupPermissions = await manager.save( + createAppPermissionsObj.appType = this.getAppTypeFromResourceType(granularPermissions.type); + const appGroupPermissions = await manager.save( manager.create(AppsGroupPermissions, { ...createAppPermissionsObj, granularPermissionId: granularPermissions.id, @@ -143,12 +144,23 @@ export class GranularPermissionsUtilService implements IGranularPermissionsUtilS if (resourcesToAdd?.length) { await manager.insert( GroupApps, - resourcesToAdd.map((app) => ({ appId: app.appId, appsGroupPermissionsId: appGRoupPermissions.id })) + resourcesToAdd.map((app) => ({ appId: app.appId, appsGroupPermissionsId: appGroupPermissions.id })) ); } }, manager); } + private getAppTypeFromResourceType(type: ResourceType) { + switch (type) { + case ResourceType.APP: + return APP_TYPES.FRONT_END; + case ResourceType.WORKFLOWS: + return APP_TYPES.WORKFLOW; + default: + throw new BadRequestException('Invalid resource type'); + } + } + async validateResourceCreation(params: ResourceCreateValidation, manager: EntityManager) { const { groupId, organizationId, isBuilderPermissions } = params; if (!isBuilderPermissions) { @@ -188,6 +200,8 @@ export class GranularPermissionsUtilService implements IGranularPermissionsUtilS appGranularPermission.isAll = true; appGranularPermission.type = ResourceType.APP; appGroupPermissions.canEdit = true; + appGroupPermissions.appType = APP_TYPES.FRONT_END; + return [appGranularPermission]; case USER_ROLE.END_USER: @@ -195,6 +209,7 @@ export class GranularPermissionsUtilService implements IGranularPermissionsUtilS appGranularPermission.isAll = true; appGranularPermission.type = ResourceType.APP; appGroupPermissions.canView = true; + return [appGranularPermission]; default: @@ -224,6 +239,7 @@ export class GranularPermissionsUtilService implements IGranularPermissionsUtilS resourcesToAdd, allowRoleChange, }; + await catchDbException(async () => { if (Object.keys(updateGranularPermission).length > 0) await manager.update(GranularPermissions, id, updateGranularPermission); @@ -251,7 +267,9 @@ export class GranularPermissionsUtilService implements IGranularPermissionsUtilS } protected async updateAppsGroupPermission( - UpdateResourceGroupPermissionsObject: UpdateResourceGroupPermissionsObject, + UpdateResourceGroupPermissionsObject: UpdateResourceGroupPermissionsObject< + ResourceType.APP | ResourceType.WORKFLOWS + >, organizationId: string, manager?: EntityManager ) { @@ -259,7 +277,10 @@ export class GranularPermissionsUtilService implements IGranularPermissionsUtilS const { granularPermissions, actions, resourcesToDelete, resourcesToAdd, group, allowRoleChange } = UpdateResourceGroupPermissionsObject; - this.validateAppResourcePermissionUpdateOperation(group, actions); + this.validateAppResourcePermissionUpdateOperation( + group, + actions as ResourceGroupActions + ); const { canEdit } = actions; await this.validateResourceAction( { diff --git a/server/src/modules/group-permissions/util.service.ts b/server/src/modules/group-permissions/util.service.ts index d8a27697ad..7af22ba987 100644 --- a/server/src/modules/group-permissions/util.service.ts +++ b/server/src/modules/group-permissions/util.service.ts @@ -30,6 +30,9 @@ import { UserRepository } from '@modules/users/repository'; import { USER_STATUS, WORKSPACE_USER_STATUS } from '@modules/users/constants/lifecycle'; import { IGroupPermissionsUtilService } from './interfaces/IUtilService'; import { GroupPermissionLicenseUtilService } from './util-services/license.util.service'; +import { getTooljetEdition } from '@helpers/utils.helper'; +import { TOOLJET_EDITIONS } from '@modules/app/constants'; + @Injectable() export class GroupPermissionsUtilService implements IGroupPermissionsUtilService { constructor( @@ -159,6 +162,7 @@ export class GroupPermissionsUtilService implements IGroupPermissionsUtilService CreateResourcePermissionObject > = DEFAULT_RESOURCE_PERMISSIONS[group.name]; for (const resource of Object.keys(groupGranularPermissions)) { + if (getTooljetEdition() === TOOLJET_EDITIONS.CE && resource == ResourceType.WORKFLOWS) continue; const createResourcePermissionObj: CreateResourcePermissionObject = groupGranularPermissions[resource]; const dtoObject = { diff --git a/server/src/modules/licensing/guards/app.guard.ts b/server/src/modules/licensing/guards/app.guard.ts index e7c001b030..0bc1610525 100644 --- a/server/src/modules/licensing/guards/app.guard.ts +++ b/server/src/modules/licensing/guards/app.guard.ts @@ -4,6 +4,7 @@ import { LicenseTermsService } from '../interfaces/IService'; import { EntityManager } from 'typeorm'; import { dbTransactionWrap } from '@helpers/database.helper'; import { App } from '@entities/app.entity'; +import { APP_TYPES } from '@modules/apps/constants'; @Injectable() export class AppCountGuard implements CanActivate { @@ -13,7 +14,7 @@ export class AppCountGuard implements CanActivate { async fetchTotalAppCount(manager: EntityManager): Promise { const apps = await manager.find(App, { where: { - type: 'front-end', + type: APP_TYPES.FRONT_END, organization: { status: 'active', }, diff --git a/server/src/modules/licensing/guards/webhook.guard.ts b/server/src/modules/licensing/guards/webhook.guard.ts index f8ca2af0fa..1a5d084643 100644 --- a/server/src/modules/licensing/guards/webhook.guard.ts +++ b/server/src/modules/licensing/guards/webhook.guard.ts @@ -4,6 +4,7 @@ import { ConfigService } from '@nestjs/config'; import { LicenseTermsService } from '../interfaces/IService'; import { LICENSE_FIELD, LICENSE_LIMIT } from '../constants'; import { AppsRepository } from '@modules/apps/repository'; +import { APP_TYPES } from '@modules/apps/constants'; @Injectable() export class WebhookGuard implements CanActivate { @@ -21,7 +22,7 @@ export class WebhookGuard implements CanActivate { const workflowApp = await this.appsRepository.findOne({ where: { id: request?.params?.id, - type: 'workflow', + type: APP_TYPES.WORKFLOW, }, }); diff --git a/server/src/modules/licensing/guards/workflowcount.guard.ts b/server/src/modules/licensing/guards/workflowcount.guard.ts index 44edf5965e..5dc723feae 100644 --- a/server/src/modules/licensing/guards/workflowcount.guard.ts +++ b/server/src/modules/licensing/guards/workflowcount.guard.ts @@ -2,6 +2,7 @@ import { CanActivate, ExecutionContext, HttpException, Injectable } from '@nestj import { LicenseTermsService } from '../interfaces/IService'; import { LICENSE_FIELD, LICENSE_LIMIT } from '../constants'; import { AppsRepository } from '@modules/apps/repository'; +import { APP_TYPES } from '@modules/apps/constants'; @Injectable() export class WorkflowCountGuard implements CanActivate { @@ -24,7 +25,7 @@ export class WorkflowCountGuard implements CanActivate { (await this.appsRepository.count({ where: { organizationId: request?.headers['tj-workspace-id'] ?? '', - type: 'workflow', + type: APP_TYPES.WORKFLOW, }, })) >= workflowsLimit.workspace.total ) { @@ -37,7 +38,7 @@ export class WorkflowCountGuard implements CanActivate { workflowsLimit.instance.total !== LICENSE_LIMIT.UNLIMITED && (await this.appsRepository.count({ where: { - type: 'workflow', + type: APP_TYPES.WORKFLOW, }, })) >= workflowsLimit.instance.total ) { diff --git a/server/src/modules/licensing/services/count.service.ts b/server/src/modules/licensing/services/count.service.ts index d41c5f4a2e..14c2421a6e 100644 --- a/server/src/modules/licensing/services/count.service.ts +++ b/server/src/modules/licensing/services/count.service.ts @@ -8,6 +8,7 @@ import { Organization } from '@entities/organization.entity'; import { UserRepository } from '@modules/users/repository'; import { USER_ROLE } from '@modules/group-permissions/constants'; import { ILicenseCountsService } from '../interfaces/IService'; +import { APP_TYPES } from '@modules/apps/constants'; @Injectable() export class LicenseCountsService implements ILicenseCountsService { @@ -143,7 +144,7 @@ export class LicenseCountsService implements ILicenseCountsService { fetchTotalWorkflowsCount(workspaceId: string, manager: EntityManager): Promise { return manager.count(App, { where: { - type: 'workflow', + type: APP_TYPES.WORKFLOW, ...(workspaceId && { organizationId: workspaceId }), }, }); @@ -188,7 +189,7 @@ export class LicenseCountsService implements ILicenseCountsService { async fetchTotalAppCount(manager: EntityManager): Promise { const apps = await manager.find(App, { where: { - type: 'front-end', + type: APP_TYPES.FRONT_END, organization: { status: 'active', }, diff --git a/server/src/modules/versions/ability/app-version.ability.ts b/server/src/modules/versions/ability/app-version.ability.ts new file mode 100644 index 0000000000..16ccb14ccd --- /dev/null +++ b/server/src/modules/versions/ability/app-version.ability.ts @@ -0,0 +1,114 @@ +import { AbilityBuilder } from '@casl/ability'; +import { UserAllPermissions } from '@modules/app/types'; +import { FEATURE_KEY } from '../constants'; +import { App } from '@entities/app.entity'; +import { FeatureAbility } from './index'; + +export function defineAppVersionAbility( + can: AbilityBuilder['can'], + UserAllPermissions: UserAllPermissions, + resourceId?: string +): void { + const { superAdmin, isAdmin, userPermission, resource } = UserAllPermissions; + const userAppPermissions = userPermission?.[resource[0].resourceType]; + + if (isAdmin || superAdmin) { + can( + [ + FEATURE_KEY.GET, + FEATURE_KEY.DELETE, + FEATURE_KEY.CREATE, + FEATURE_KEY.GET_ONE, + FEATURE_KEY.UPDATE, + FEATURE_KEY.UPDATE_SETTINGS, + FEATURE_KEY.PROMOTE, + 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.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, + ], + App + ); + return; + } + + const isAllEditable = !!userAppPermissions?.isAllEditable; + const isAllViewable = !!userAppPermissions?.isAllViewable; + + if (isAllEditable) { + can( + [ + FEATURE_KEY.GET, + FEATURE_KEY.DELETE, + FEATURE_KEY.CREATE, + FEATURE_KEY.GET_ONE, + FEATURE_KEY.UPDATE, + FEATURE_KEY.UPDATE_SETTINGS, + FEATURE_KEY.PROMOTE, + 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.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, + ], + App + ); + } else if ( + userAppPermissions?.editableAppsId?.length && + resourceId && + userAppPermissions.editableAppsId.includes(resourceId) + ) { + can( + [ + FEATURE_KEY.GET, + FEATURE_KEY.DELETE, + FEATURE_KEY.CREATE, + FEATURE_KEY.GET_ONE, + FEATURE_KEY.UPDATE, + FEATURE_KEY.UPDATE_SETTINGS, + FEATURE_KEY.PROMOTE, + 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.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, + ], + App + ); + } + + if (isAllViewable) { + can([FEATURE_KEY.GET_EVENTS], App); + } else if ( + userAppPermissions?.viewableAppsId?.length && + resourceId && + userAppPermissions.viewableAppsId.includes(resourceId) + ) { + can([FEATURE_KEY.GET_EVENTS], App); + } +} diff --git a/server/src/modules/versions/ability/guard.ts b/server/src/modules/versions/ability/guard.ts index 64740690c6..5a163c4a40 100644 --- a/server/src/modules/versions/ability/guard.ts +++ b/server/src/modules/versions/ability/guard.ts @@ -4,6 +4,7 @@ import { AbilityGuard } from '@modules/app/guards/ability.guard'; import { ResourceDetails } from '@modules/app/types'; import { MODULES } from '@modules/app/constants/modules'; import { App } from '@entities/app.entity'; +import { APP_TYPES } from '@modules/apps/constants'; @Injectable() export class FeatureAbilityGuard extends AbilityGuard { @@ -16,8 +17,18 @@ export class FeatureAbilityGuard extends AbilityGuard { } protected getResource(): ResourceDetails { - return { - resourceType: MODULES.APP, - }; + const resource: App = this.getResourceObject(); + switch (resource?.type) { + case APP_TYPES.FRONT_END: + return { + resourceType: MODULES.APP, + }; + case APP_TYPES.WORKFLOW: + return { + resourceType: MODULES.WORKFLOWS, + }; + default: + return null; + } } } diff --git a/server/src/modules/versions/ability/index.ts b/server/src/modules/versions/ability/index.ts index 5237a97ec9..2026a67347 100644 --- a/server/src/modules/versions/ability/index.ts +++ b/server/src/modules/versions/ability/index.ts @@ -3,8 +3,8 @@ import { Ability, AbilityBuilder, InferSubjects } from '@casl/ability'; import { AbilityFactory } from '@modules/app/ability-factory'; import { UserAllPermissions } from '@modules/app/types'; import { FEATURE_KEY } from '../constants'; -import { MODULES } from '@modules/app/constants/modules'; import { App } from '@entities/app.entity'; +import { createVersionAbility } from './utility'; type Subjects = InferSubjects | 'all'; export type FeatureAbility = Ability<[FEATURE_KEY, Subjects]>; @@ -21,80 +21,7 @@ export class FeatureAbilityFactory extends AbilityFactory extractedMetadata: { moduleName: string; features: string[] }, request?: any ): void { - const appId = request?.tj_resource_id; - const { superAdmin, isAdmin, userPermission } = UserAllPermissions; - - const userAppPermissions = userPermission?.[MODULES.APP]; - const isAllAppsEditable = !!userAppPermissions?.isAllEditable; - const isAllAppsViewable = !!userAppPermissions?.isAllViewable; - - if (isAdmin || superAdmin || isAllAppsEditable) { - // Admin or super admin and do all operations - can( - [ - FEATURE_KEY.GET, - FEATURE_KEY.DELETE, - FEATURE_KEY.CREATE, - FEATURE_KEY.GET_ONE, - FEATURE_KEY.UPDATE, - FEATURE_KEY.UPDATE_SETTINGS, - FEATURE_KEY.PROMOTE, - 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.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, - ], - App - ); - return; - } - - if (userAppPermissions?.editableAppsId?.length && appId && userAppPermissions.editableAppsId.includes(appId)) { - can( - [ - FEATURE_KEY.GET, - FEATURE_KEY.DELETE, - FEATURE_KEY.CREATE, - FEATURE_KEY.GET_ONE, - FEATURE_KEY.UPDATE, - FEATURE_KEY.UPDATE_SETTINGS, - FEATURE_KEY.PROMOTE, - 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.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, - ], - App - ); - } - - if (isAllAppsViewable) { - // add view permissions for all apps - can([FEATURE_KEY.GET_EVENTS], App); - } else if ( - userAppPermissions?.viewableAppsId?.length && - appId && - userAppPermissions.viewableAppsId.includes(appId) - ) { - can([FEATURE_KEY.GET_EVENTS], App); - } + const resourceId = request?.tj_resource_id; + createVersionAbility(can, UserAllPermissions, resourceId); } } diff --git a/server/src/modules/versions/ability/utility.ts b/server/src/modules/versions/ability/utility.ts new file mode 100644 index 0000000000..545a3420df --- /dev/null +++ b/server/src/modules/versions/ability/utility.ts @@ -0,0 +1,25 @@ +import { AbilityBuilder } from '@casl/ability'; +import { UserAllPermissions } from '@modules/app/types'; +import { MODULES } from '@modules/app/constants/modules'; +import { FeatureAbility } from './index'; +import { defineAppVersionAbility } from './app-version.ability'; +import { defineWorkflowVersionAbility } from './workflow-version.ability'; + +export function createVersionAbility( + can: AbilityBuilder['can'], + UserAllPermissions: UserAllPermissions, + resourceId?: string +): void { + const resourceType = UserAllPermissions.resource[0].resourceType ? UserAllPermissions.resource[0].resourceType : null; + + switch (resourceType) { + case MODULES.APP: + defineAppVersionAbility(can, UserAllPermissions, resourceId); + break; + case MODULES.WORKFLOWS: + defineWorkflowVersionAbility(can, UserAllPermissions, resourceId); + break; + default: + throw new Error(`Unsupported resource type: ${resourceType}`); + } +} diff --git a/server/src/modules/versions/ability/workflow-version.ability.ts b/server/src/modules/versions/ability/workflow-version.ability.ts new file mode 100644 index 0000000000..16c54f8e92 --- /dev/null +++ b/server/src/modules/versions/ability/workflow-version.ability.ts @@ -0,0 +1,75 @@ +import { AbilityBuilder } from '@casl/ability'; +import { UserAllPermissions } from '@modules/app/types'; +import { FEATURE_KEY } from '../constants'; +import { App } from '@entities/app.entity'; +import { FeatureAbility } from './index'; + +export function defineWorkflowVersionAbility( + can: AbilityBuilder['can'], + UserAllPermissions: UserAllPermissions, + resourceId?: string +): void { + const { superAdmin, isAdmin, userPermission, resource } = UserAllPermissions; + const userWorkflowPermissions = userPermission?.[resource[0].resourceType]; + + if (isAdmin || superAdmin) { + can( + [ + FEATURE_KEY.GET, + FEATURE_KEY.DELETE, + FEATURE_KEY.CREATE, + FEATURE_KEY.GET_ONE, + FEATURE_KEY.UPDATE, + FEATURE_KEY.UPDATE_SETTINGS, + FEATURE_KEY.PROMOTE, + ], + App + ); + return; + } + + const isAllEditable = !!userWorkflowPermissions?.isAllEditable; + const isAllExecutable = !!userWorkflowPermissions?.isAllExecutable; + + if (isAllEditable) { + can( + [ + FEATURE_KEY.GET, + FEATURE_KEY.DELETE, + FEATURE_KEY.CREATE, + FEATURE_KEY.GET_ONE, + FEATURE_KEY.UPDATE, + FEATURE_KEY.UPDATE_SETTINGS, + FEATURE_KEY.PROMOTE, + ], + App + ); + } else if ( + userWorkflowPermissions?.editableWorkflowsId?.length && + resourceId && + userWorkflowPermissions.editableWorkflowsId.includes(resourceId) + ) { + can( + [ + FEATURE_KEY.GET, + FEATURE_KEY.DELETE, + FEATURE_KEY.CREATE, + FEATURE_KEY.GET_ONE, + FEATURE_KEY.UPDATE, + FEATURE_KEY.UPDATE_SETTINGS, + FEATURE_KEY.PROMOTE, + ], + App + ); + } + + if (isAllExecutable) { + can([FEATURE_KEY.GET_EVENTS], App); + } else if ( + userWorkflowPermissions?.executableWorkflowsId?.length && + resourceId && + userWorkflowPermissions.executableWorkflowsId.includes(resourceId) + ) { + can([FEATURE_KEY.GET_EVENTS], App); + } +}