Enhance role-check system with stricter checks (#15392)
## Overview This PR strengthens our permission system by introducing more granular role-based access control across the platform. ## Changes ### New Permissions Added - **Applications** - Control who can install and manage applications - **Layouts** - Control who can customize page layouts and UI structure - **AI** - Control access to AI features and agents - **Upload File** - Separate permission for file uploads - **Download File** - Separate permission for file downloads (frontend visibility) ### Security Enhancements - Implemented whitelist-based validation for workspace field updates - Added explicit permission guards to core entity resolvers - Enhanced ESLint rule to enforce permission checks on all mutations - Created `CustomPermissionGuard` and `NoPermissionGuard` for better code documentation ### Affected Components - Core entity resolvers: webhooks, files, domains, applications, layouts, postgres credentials - Workspace update mutations now use whitelist validation - Settings UI updated with new permission controls ### Developer Experience - ESLint now catches missing permission guards during development - Explicit guard markers make permission requirements clear in code review - Comprehensive test coverage for new permission logic ## Testing - ✅ All TypeScript type checks pass - ✅ ESLint validation passes - ✅ New permission guards properly enforced - ✅ Frontend UI displays new permissions correctly ## Migration Notes Existing workspaces will need to assign the new permissions to roles as needed. By default, all new permissions are set to `false` for non-admin roles.
This commit is contained in:
+1
@@ -161,6 +161,7 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
|
||||
isDefined(selectedRecord) && !selectedRecord.isRemote,
|
||||
availableOn: [ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION],
|
||||
component: <ExportMultipleRecordsAction />,
|
||||
requiredPermissionFlag: PermissionFlagType.EXPORT_CSV,
|
||||
},
|
||||
[SingleRecordActionKeys.EXPORT_FROM_RECORD_SHOW]: {
|
||||
type: ActionType.Standard,
|
||||
|
||||
+4
-3
@@ -12,6 +12,7 @@ import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/
|
||||
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
|
||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type RecordGqlOperationFilter } from 'twenty-shared/types';
|
||||
|
||||
export const RestoreMultipleRecordsAction = () => {
|
||||
@@ -86,10 +87,10 @@ export const RestoreMultipleRecordsAction = () => {
|
||||
|
||||
return (
|
||||
<ActionModal
|
||||
title="Restore Records"
|
||||
subtitle="Are you sure you want to restore these records?"
|
||||
title={t`Restore Records`}
|
||||
subtitle={t`Are you sure you want to restore these records?`}
|
||||
onConfirmClick={handleRestoreClick}
|
||||
confirmButtonText="Restore Records"
|
||||
confirmButtonText={t`Restore Records`}
|
||||
confirmButtonAccent="default"
|
||||
/>
|
||||
);
|
||||
|
||||
+2
-2
@@ -39,10 +39,10 @@ export const DeleteSingleRecordAction = () => {
|
||||
|
||||
return (
|
||||
<ActionModal
|
||||
title="Delete Record"
|
||||
title={t`Delete Record`}
|
||||
subtitle={t`Are you sure you want to delete this record? It can be recovered from the Command menu.`}
|
||||
onConfirmClick={handleDeleteClick}
|
||||
confirmButtonText="Delete Record"
|
||||
confirmButtonText={t`Delete Record`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+4
-3
@@ -3,6 +3,7 @@ import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions
|
||||
import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord';
|
||||
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
|
||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||
|
||||
@@ -31,10 +32,10 @@ export const DestroySingleRecordAction = () => {
|
||||
|
||||
return (
|
||||
<ActionModal
|
||||
title="Permanently Destroy Record"
|
||||
subtitle="Are you sure you want to destroy this record? It cannot be recovered anymore."
|
||||
title={t`Permanently Destroy Record`}
|
||||
subtitle={t`Are you sure you want to destroy this record? It cannot be recovered anymore.`}
|
||||
onConfirmClick={handleDeleteClick}
|
||||
confirmButtonText="Permanently Destroy Record"
|
||||
confirmButtonText={t`Permanently Destroy Record`}
|
||||
closeSidePanelOnShowPageOptionsActionExecution={true}
|
||||
/>
|
||||
);
|
||||
|
||||
+4
-3
@@ -3,6 +3,7 @@ import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions
|
||||
import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
|
||||
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
|
||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
export const RestoreSingleRecordAction = () => {
|
||||
const { recordIndexId, objectMetadataItem } =
|
||||
@@ -26,10 +27,10 @@ export const RestoreSingleRecordAction = () => {
|
||||
|
||||
return (
|
||||
<ActionModal
|
||||
title="Restore Record"
|
||||
subtitle="Are you sure you want to restore this record?"
|
||||
title={t`Restore Record`}
|
||||
subtitle={t`Are you sure you want to restore this record?`}
|
||||
onConfirmClick={handleRestoreClick}
|
||||
confirmButtonText="Restore Record"
|
||||
confirmButtonText={t`Restore Record`}
|
||||
confirmButtonAccent="default"
|
||||
/>
|
||||
);
|
||||
|
||||
+2
-1
@@ -13,6 +13,7 @@ import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectab
|
||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useContext } from 'react';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { getOsControlSymbol } from 'twenty-ui/utilities';
|
||||
@@ -65,7 +66,7 @@ export const CommandMenuActionMenuDropdown = () => {
|
||||
dropdownId={dropdownId}
|
||||
data-select-disable
|
||||
clickableComponent={
|
||||
<Button title="Options" hotkeys={[getOsControlSymbol(), 'O']} />
|
||||
<Button title={t`Options`} hotkeys={[getOsControlSymbol(), 'O']} />
|
||||
}
|
||||
dropdownPlacement="top-end"
|
||||
dropdownOffset={{ y: parseInt(theme.spacing(2), 10) }}
|
||||
|
||||
+2
-1
@@ -17,6 +17,7 @@ import { useComponentInstanceStateContext } from '@/ui/utilities/state/component
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilCallback, useRecoilValue } from 'recoil';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -131,7 +132,7 @@ export const RecordShowRightDrawerOpenRecordButton = ({
|
||||
|
||||
return (
|
||||
<Button
|
||||
title="Open"
|
||||
title={t`Open`}
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
size="medium"
|
||||
|
||||
Reference in New Issue
Block a user