Merge branch 'main' into fix/hide-modules-tab

This commit is contained in:
Johnson Cherian 2025-06-21 16:15:39 +05:30 committed by GitHub
commit 0d7ff2d1f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 1743 additions and 658 deletions

View file

@ -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"]',

View file

@ -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',

View file

@ -4,6 +4,8 @@
"cancel": "Cancel",
"save": "Save",
"savechanges": "Save changes",
"execute": "Execute",
"Build": "Build",
"back": "Back",
"edit": "Edit",
"search": "Search",

View file

@ -282,9 +282,9 @@ class AppComponent extends React.Component {
exact
path="/:workspaceId/workflows/*"
element={
<AdminRoute {...this.props}>
<PrivateRoute>
<Workflows switchDarkMode={this.switchDarkMode} darkMode={this.darkMode} />
</AdminRoute>
</PrivateRoute>
}
/>
)}

View file

@ -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) {

View file

@ -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,

View file

@ -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;
}

View file

@ -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}
/>
// </div>
);
@ -201,4 +237,5 @@ AppsSelect.defaultProps = {
value: '*',
isAllField: true,
},
resourceType: RESOURCE_TYPE.APPS,
};

View file

@ -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 (
<AppResourcePermissions
updateOnlyGranularPermissions={this.updateOnlyGranularPermissions}
permissions={permissions}
currentGroupPermission={currentGroupPermission}
openEditPermissionModal={this.openEditPermissionModal}
isBasicPlan={isBasicPlan}
key={index}
/>
);
case RESOURCE_TYPE.DATA_SOURCES:
return (
<DataSourceResourcePermissions
updateOnlyGranularPermissions={this.updateOnlyGranularPermissions}
permissions={permissions}
currentGroupPermission={currentGroupPermission}
openEditPermissionModal={this.openEditPermissionModal}
isBasicPlan={isBasicPlan}
key={index}
/>
);
case RESOURCE_TYPE.WORKFLOWS:
return (
<WorkflowResourcePermissions
updateOnlyGranularPermissions={this.updateOnlyGranularPermissions}
permissions={permissions}
currentGroupPermission={currentGroupPermission}
openEditPermissionModal={this.openEditPermissionModal}
isBasicPlan={isBasicPlan}
key={index}
/>
);
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}
/>
<AddEditResourcePermissionsModal
handleClose={this.closeAddPermissionModal}
handleConfirm={
modalType === 'add'
? this.createGranularPermissions
: () => {
this.updateGranularPermissions();
}
}
updateParentState={this.updateState}
resourceType={resourceType}
currentState={this.state}
show={showAddPermissionModal}
title={
<div className="my-3 permission-manager-title" data-cy="modal-title">
<span className="font-weight-500">
<SolidIcon name={resourceType == 'Apps' ? 'apps' : 'datasource'} fill="var(--slate8)" />
</span>
<div className="tj-text-md font-weight-500 modal-name" data-cy="modal-title">
{modalTitle}
</div>
{modalType === 'edit' && !isRoleGroup && (
<div className="delete-icon-cont">
<ButtonSolid
leftIcon="delete"
iconWidth="15px"
className="icon-class"
variant="tertiary"
onClick={() => {
this.setState({
deleteConfirmationModal: true,
showAddPermissionModal: false,
});
}}
data-cy="delete-button"
{showAddPermissionModal && (
<AddEditResourcePermissionsModal
handleClose={this.closeAddPermissionModal}
handleConfirm={
modalType === 'add'
? this.createGranularPermissions
: () => {
this.updateGranularPermissions();
}
}
updateParentState={this.updateState}
resourceType={resourceType}
currentState={this.state}
show={showAddPermissionModal}
title={
<div className="my-3 permission-manager-title" data-cy="modal-title">
<span className="font-weight-500">
<SolidIcon
name={
resourceType === RESOURCE_TYPE.APPS
? 'apps'
: resourceType === RESOURCE_TYPE.WORKFLOWS
? 'workflows'
: 'datasource'
}
fill="var(--slate8)"
/>
</span>
<div className="tj-text-md font-weight-500 modal-name" data-cy="modal-title">
{modalTitle}
</div>
)}
</div>
}
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 && (
<div className="delete-icon-cont">
<ButtonSolid
leftIcon="delete"
iconWidth="15px"
className="icon-class"
variant="tertiary"
onClick={() => {
this.setState({
deleteConfirmationModal: true,
showAddPermissionModal: false,
});
}}
data-cy="delete-button"
/>
</div>
)}
</div>
}
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 ? (
<div className="empty-container">
<div className="icon-container" data-cy="empty-page-svg">
@ -735,28 +784,12 @@ class BaseManageGranularAccess extends React.Component {
) : (
<>
{granularPermissions.map((permissions, index) => {
if (permissions.type === 'app')
return (
<AppResourcePermissions
updateOnlyGranularPermissions={this.updateOnlyGranularPermissions}
permissions={permissions}
currentGroupPermission={currentGroupPermission}
openEditPermissionModal={this.openEditPermissionModal}
isBasicPlan={isBasicPlan}
key={index}
/>
);
else
return (
<DataSourceResourcePermissions
updateOnlyGranularPermissions={this.updateOnlyGranularPermissions}
permissions={permissions}
currentGroupPermission={currentGroupPermission}
openEditPermissionModal={this.openEditPermissionModal}
isBasicPlan={isBasicPlan}
key={index}
/>
);
return this.renderResourcePermissions({
permissions,
currentGroupPermission,
isBasicPlan,
index,
});
})}
</>
)}

View file

@ -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 (
<AppPermissionsActions
handleClickEdit={() => {
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 (
<WorkflowPermissionsActions
handleClickEdit={() => {
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 (
<DsPermissionsActions
handleClickConfigure={() => {
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 (
<ModalBase
size="md"
@ -79,46 +208,7 @@ function AddEditResourcePermissionsModal({
<label className="form-label bold-text" data-cy="permission-label">
Permission
</label>
{resourceType === 'Apps' ? (
<AppPermissionsActions
handleClickEdit={() => {
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}
/>
) : (
<DsPermissionsActions
updateParentState={updateParentState}
disableBuilderLevelUpdate={disableBuilderLevelUpdate}
initialPermissionStateDs={initialPermissionStateDs}
/>
)}
{renderPermissionActions(resourceType)}
</div>
<div className="form-group mb-3">
@ -139,15 +229,15 @@ function AddEditResourcePermissionsModal({
<div>
<span
className="form-check-label"
data-cy={`${allResourceTitle.toLowerCase().replace(/\s+/g, '-')}-label`}
data-cy={`${getAllResourceLabel(resourceType).toLowerCase().replace(/\s+/g, '-')}-label`}
>
{allResourceTitle}
{getAllResourceLabel(resourceType)}
</span>
<span
className="tj-text-xsm"
data-cy={`${allResourceTitle.toLowerCase().replace(/\s+/g, '-')}-info-text`}
data-cy={`${getAllResourceText(resourceType).toLowerCase().replace(/\s+/g, '-')}-info-text`}
>
{allResourceText}
{getAllResourceText(resourceType)}
</span>
</div>
</label>
@ -167,7 +257,9 @@ function AddEditResourcePermissionsModal({
<input
className="form-check-input"
type="radio"
disabled={addableApps.length === 0 || disableBuilderLevelUpdate || groupName === 'builder'}
disabled={
!addableApps || addableApps?.length === 0 || disableBuilderLevelUpdate || groupName === 'builder'
}
checked={isCustom}
onClick={() => {
!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
</span>
</div>
</label>

View file

@ -0,0 +1,8 @@
import React from 'react';
import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
function WorkflowPermissionActionContainer() {
return <></>;
}
export default withEditionSpecificComponent(WorkflowPermissionActionContainer, 'WorkspaceSettings');

View file

@ -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 ? (
<OverlayTrigger
rootClose={true}
@ -25,17 +45,17 @@ function AddResourcePermissionsMenu({
iconWidth="17"
fill="var(--slate9)"
className="apps-remove-btn permission-type remove-decoration tj-text-xsm font-weight-600 remove-disabled-bg"
leftIcon={resource === 'Apps' ? 'apps' : 'datasource'}
leftIcon={selectResourceIcon(resource)}
onClick={() => {
openAddPermissionModal(resource);
}}
disabled={currentGroupPermission.name === 'end-user' && resource === 'Data Sources'}
disabled={currentGroupPermission.name === 'end-user' && resource === RESOURCE_TYPE.DATA_SOURCES}
>
<OverlayTrigger
key={index}
placement="right"
overlay={
currentGroupPermission.name === 'end-user' && resource === 'Data Sources' ? (
currentGroupPermission.name === 'end-user' && resource === RESOURCE_TYPE.DATA_SOURCES ? (
<Tooltip id={`tooltip-${index}`} style={{ maxWidth: '120px' }}>
End-user cannot access data sources
</Tooltip>
@ -44,7 +64,7 @@ function AddResourcePermissionsMenu({
)
}
>
<span>{resource === 'Data Sources' ? 'Data source' : resource}</span>
<span>{resourceNameMapping[resource]}</span>
</OverlayTrigger>
</ButtonSolid>
))}
@ -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"
>

View file

@ -0,0 +1,8 @@
import React from 'react';
import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
function WorkflowResourcePermissions() {
return <></>;
}
export default withEditionSpecificComponent(WorkflowResourcePermissions, 'WorkspaceSettings');

View file

@ -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 {
)}
</p>
</div>
<div className="permission-body">
<div className={`${showPermissionInfo ? 'permissions-body-one' : 'permissions-body-two'}`}>
{isLoadingGroup ? (
<tr>
<td className="col-auto">
@ -958,6 +959,13 @@ class BaseManageGroupPermissionResources extends React.Component {
</div>
{/* //App till here */}
</div>
{/* Worklfow Permission */}
<WorkflowPermissionsUI
groupPermission={groupPermission}
disablePermissionUpdate={disablePermissionUpdate}
updateGroupPermission={this.updateGroupPermission}
updateState={this.updateParamState}
/>
{/* Data source */}
<DataSourcePermissionsUI

View file

@ -141,7 +141,21 @@
}
.permission-body {
.permissions-body-one {
overflow-y: auto;
max-height: calc(100vh - 320px - 100px);
min-height: calc(100vh - 320px - 100px);
.tj-text-xxsm {
color: var(--slate11)
}
}
.permissions-body-two {
overflow-y: auto;
max-height: calc(100vh - 260px - 100px);
min-height: calc(100vh - 260px - 100px);
.tj-text-xxsm {
color: var(--slate11)
}

View file

@ -827,6 +827,7 @@ class BaseManageGroupPermissions extends React.Component {
value: group.name,
};
})}
workflowEnabled={false}
featureAccess={featureAccess}
/>
)}

View file

@ -0,0 +1,8 @@
import React from 'react';
import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
const WorkflowPermissionsUI = () => {
return <></>;
};
export default withEditionSpecificComponent(WorkflowPermissionsUI, 'WorkspaceSettings');

View file

@ -0,0 +1 @@
export { default } from './WorkflowPermissionsUI.jsx';

View file

@ -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',
};

View file

@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddWorkflowPermissionsInGroupPermissions1746705301652 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(`
ALTER TABLE permission_groups
DROP COLUMN workflow_delete,
DROP COLUMN workflow_create;
`);
}
}

View file

@ -0,0 +1,11 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddWorkflowTypeInResourceType1746705371665 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TYPE "resource_type" ADD VALUE IF NOT EXISTS 'workflow';
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {}
}

View file

@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddAppTypeInAppsGroupPermissions1746705448788 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(`
ALTER TABLE "apps_group_permissions"
DROP COLUMN "app_type";
DROP TYPE IF EXISTS "app_type";
`);
}
}

View file

@ -20645,4 +20645,4 @@
}
}
}
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;
}

View file

@ -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;

View file

@ -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,

View file

@ -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;
}
}

View file

@ -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 {

View file

@ -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<string, any> } {
const conditions: string[] = [];
const params: Record<string, any> = {};
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<GroupPermissions>,
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<GroupPermissions>, 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<GroupPermissions>,
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);

View file

@ -46,7 +46,7 @@ export abstract class AbilityFactory<TActions extends string, TSubject> {
await this.defineAbilityFor(
can,
{ userPermission, superAdmin, isAdmin, isBuilder, isEndUser, user },
{ userPermission, superAdmin, isAdmin, isBuilder, isEndUser, user, resource },
extractedMetadata,
request
);

View file

@ -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;

View file

@ -111,13 +111,13 @@ export class AppModuleLoader {
*
*
*/
const dynamicModules: DynamicModule[] = [];
const dynamicModules: Promise<DynamicModule>[] = [];
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);
}

View file

@ -11,6 +11,7 @@ export interface UserAllPermissions {
isBuilder: boolean;
isEndUser: boolean;
user: User;
resource: ResourceDetails[];
}
export interface FeatureConfig {

View file

@ -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<FeatureAbility>['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);
}
}

View file

@ -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;

View file

@ -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<typeof App> | 'all';
export type FeatureAbility = Ability<[FEATURE_KEY, Subjects]>;
@ -21,72 +21,7 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
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);
}
}

View file

@ -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<FeatureAbility>['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}`);
}
}

View file

@ -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<FeatureAbility>['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);
}
}

View file

@ -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;

View file

@ -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 }));

View file

@ -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<App> {
async create(name: string, user: User, type: APP_TYPES, manager: EntityManager): Promise<App> {
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<AppBase[]> {
//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<string>,
type?: string
): SelectQueryBuilder<AppBase> {
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<AppBase>,
userAppPermissions: UserAppsPermissions,
viewableApps: string[]
): SelectQueryBuilder<AppBase> {
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<number> {
async count(user: User, searchKey, type: APP_TYPES): Promise<number> {
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,

View file

@ -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<FeatureAbility>['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;
}
}

View file

@ -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<FeatureAbility>['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;
}
}

View file

@ -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,

View file

@ -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<typeof App> | 'all';
export type FeatureAbility = Ability<[FEATURE_KEY, Subjects]>;
@ -21,80 +23,18 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
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}`);
}
}
}

View file

@ -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 }>;
}

View file

@ -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: [] };

View file

@ -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<Folder[]> {
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<Folder> {
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<Folder> {
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<AppBase> {
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<AppBase>,
folderAppIds: string[],
userAppPermissions: UserAppsPermissions
): SelectQueryBuilder<AppBase> {
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;
}
}

View file

@ -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',
};

View file

@ -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<USER_ROLE, Record<ResourceType, CreateResourcePermissionObject<any>>>;

View file

@ -31,6 +31,14 @@ export class UpdateGroupPermissionDto {
@IsOptional()
orgConstantCRUD: boolean;
@IsBoolean()
@IsOptional()
workflowCreate: boolean;
@IsBoolean()
@IsOptional()
workflowDelete: boolean;
@IsBoolean()
@IsOptional()
dataSourceCreate: boolean;

View file

@ -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,
};
});
});

View file

@ -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 | ResourceType.DATA_SOURCE> =
T extends ResourceType.APP ? CreateAppsPermissionsObject : CreateDataSourcePermissionsObject;
type CreateResourcePermissionMap = {
[ResourceType.APP]: CreateAppsPermissionsObject;
[ResourceType.DATA_SOURCE]: CreateDataSourcePermissionsObject;
[ResourceType.WORKFLOWS]: CreateWorkflowPermissionsObject;
};
export interface CreateAppsPermissionsObject {
export type CreateResourcePermissionObject<T extends ResourceType> = CreateResourcePermissionMap[T];
export interface CreateBaseAppsPermissionsObject {
canEdit?: boolean;
canView?: boolean;
appType?: APP_TYPES;
resourcesToAdd?: GranularPermissionAddResourceItems<ResourceType.APP | ResourceType.WORKFLOWS>;
}
export interface CreateAppsPermissionsObject extends CreateBaseAppsPermissionsObject {
hideFromDashboard?: boolean;
}
export interface CreateWorkflowPermissionsObject extends CreateBaseAppsPermissionsObject {}
export interface CreateAppsPermissionsObject extends CreateBaseAppsPermissionsObject {
resourcesToAdd?: GranularPermissionAddResourceItems<ResourceType.APP>;
}
export interface CreateWorkflowPermissionsObject extends CreateBaseAppsPermissionsObject {
resourcesToAdd?: GranularPermissionAddResourceItems<ResourceType.WORKFLOWS>;
}
export interface CreateDataSourcePermissionsObject {
action?: DataSourcesGroupPermissionsActions;
resourcesToAdd?: GranularPermissionAddResourceItems<ResourceType.DATA_SOURCE>;
@ -32,10 +50,28 @@ export interface CreateGranularPermissionObject {
organizationId: string;
}
export type GranularPermissionAddResourceItems<T extends ResourceType.APP | ResourceType.DATA_SOURCE> =
T extends ResourceType.APP ? AppsPermissionAddResourceItem[] : DataSourcesPermissionResourceItem[];
type ResourceToPermissionItemMap = {
[ResourceType.APP]: AppsPermissionAddResourceItem[];
[ResourceType.DATA_SOURCE]: DataSourcesPermissionResourceItem[];
[ResourceType.WORKFLOWS]: WorkflowsPermissionAddResourceItem[];
};
export interface AppsPermissionAddResourceItem {
export type GranularPermissionAddResourceItems<T extends ResourceType> = 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<any>;
}
export interface UpdateResourceGroupPermissionsObject<T extends ResourceType.APP | ResourceType.DATA_SOURCE> {
export interface UpdateResourceGroupPermissionsObject<
T extends ResourceType.APP | ResourceType.DATA_SOURCE | ResourceType.WORKFLOWS
> {
group: GroupPermissions;
granularPermissions: GranularPermissions;
actions: ResourceGroupActions<T>;
@ -81,9 +119,13 @@ export interface GranularPermissionDeleteResourceItem {
id: string;
}
export type ResourceGroupActions<T extends ResourceType.APP | ResourceType.DATA_SOURCE> = T extends ResourceType.APP
? AppsGroupPermissionsActions
: DataSourcesGroupPermissionsActions;
type ResourceActionMap = {
[ResourceType.APP]: AppsGroupPermissionsActions;
[ResourceType.DATA_SOURCE]: DataSourcesGroupPermissionsActions;
[ResourceType.WORKFLOWS]: WorkflowsGroupPermissionsActions;
};
export type ResourceGroupActions<T extends ResourceType> = ResourceActionMap[T];
export interface ValidateResourceAction {
isBuilderPermissions: boolean;

View file

@ -8,6 +8,8 @@ export interface CreateDefaultGroupObject {
name: string;
appCreate?: boolean;
appDelete?: boolean;
workflowCreate?: boolean;
workflowDelete?: boolean;
folderCRUD?: boolean;
orgConstantCRUD?: boolean;
dataSourceCreate?: boolean;

View file

@ -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<ResourceType.APP>
actions: ResourceGroupActions<ResourceType.APP | ResourceType.WORKFLOWS>
) {
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<ResourceType.APP>,
createAppPermissionsObj?: CreateResourcePermissionObject<ResourceType.APP | ResourceType.WORKFLOWS>,
manager?: EntityManager
): Promise<void> {
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<ResourceType.APP>,
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<ResourceType.APP | ResourceType.WORKFLOWS>
);
const { canEdit } = actions;
await this.validateResourceAction(
{

View file

@ -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<any>
> = DEFAULT_RESOURCE_PERMISSIONS[group.name];
for (const resource of Object.keys(groupGranularPermissions)) {
if (getTooljetEdition() === TOOLJET_EDITIONS.CE && resource == ResourceType.WORKFLOWS) continue;
const createResourcePermissionObj: CreateResourcePermissionObject<any> = groupGranularPermissions[resource];
const dtoObject = {

View file

@ -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<number> {
const apps = await manager.find(App, {
where: {
type: 'front-end',
type: APP_TYPES.FRONT_END,
organization: {
status: 'active',
},

View file

@ -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,
},
});

View file

@ -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
) {

View file

@ -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<number> {
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<number> {
const apps = await manager.find(App, {
where: {
type: 'front-end',
type: APP_TYPES.FRONT_END,
organization: {
status: 'active',
},

View file

@ -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<FeatureAbility>['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);
}
}

View file

@ -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;
}
}
}

View file

@ -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<typeof App> | 'all';
export type FeatureAbility = Ability<[FEATURE_KEY, Subjects]>;
@ -21,80 +21,7 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
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);
}
}

View file

@ -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<FeatureAbility>['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}`);
}
}

View file

@ -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<FeatureAbility>['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);
}
}