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:
+2
-1
@@ -2,6 +2,7 @@ import { type ConfigVariableFilterCategory } from '@/settings/admin-panel/config
|
||||
import { type ConfigVariableGroupFilter } from '@/settings/admin-panel/config-variables/types/ConfigVariableGroupFilter';
|
||||
import { type ConfigVariableSourceFilter } from '@/settings/admin-panel/config-variables/types/ConfigVariableSourceFilter';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useState } from 'react';
|
||||
import { IconSettings } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
@@ -41,7 +42,7 @@ export const ConfigVariableFilterDropdown = ({
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
title="Options"
|
||||
title={t`Options`}
|
||||
Icon={IconSettings}
|
||||
/>
|
||||
}
|
||||
|
||||
+5
-4
@@ -3,12 +3,13 @@ import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableBody } from '@/ui/layout/table/components/TableBody';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import {
|
||||
SettingsAvailableStandardObjectItemTableRow,
|
||||
StyledAvailableStandardObjectTableRow,
|
||||
} from './SettingsAvailableStandardObjectItemTableRow';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
|
||||
type SettingsAvailableStandardObjectsSectionProps = {
|
||||
objectItems: ObjectMetadataItem[];
|
||||
@@ -23,8 +24,8 @@ export const SettingsAvailableStandardObjectsSection = ({
|
||||
}: SettingsAvailableStandardObjectsSectionProps) => (
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Available"
|
||||
description="Select one or several standard objects to activate below"
|
||||
title={t`Available`}
|
||||
description={t`Select one or several standard objects to activate below`}
|
||||
/>
|
||||
<Table>
|
||||
<StyledAvailableStandardObjectTableRow>
|
||||
|
||||
+4
-3
@@ -1,7 +1,8 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { SettingsObjectIndexTable } from '~/pages/settings/data-model/SettingsObjectIndexTable';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { SettingsObjectIndexTable } from '~/pages/settings/data-model/SettingsObjectIndexTable';
|
||||
|
||||
type ObjectIndexesProps = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
@@ -11,8 +12,8 @@ export const ObjectIndexes = ({ objectMetadataItem }: ObjectIndexesProps) => {
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Indexes"
|
||||
description={`Advanced feature to improve the performance of queries and to enforce unicity constraints.`}
|
||||
title={t`Indexes`}
|
||||
description={t`Advanced feature to improve the performance of queries and to enforce unicity constraints.`}
|
||||
/>
|
||||
<SettingsObjectIndexTable objectMetadataItem={objectMetadataItem} />
|
||||
</Section>
|
||||
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { SettingsDnsRecordsTable } from '@/settings/components/SettingsDnsRecordsTable';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
@@ -57,8 +58,8 @@ export const SettingsDomainRecords = ({
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Domain Setup"
|
||||
description="Configure these DNS records with your domain provider"
|
||||
title={t`Domain Setup`}
|
||||
description={t`Configure these DNS records with your domain provider`}
|
||||
/>
|
||||
<SettingsDnsRecordsTable records={transformedRecords} />
|
||||
</Section>
|
||||
|
||||
+5
-4
@@ -2,9 +2,10 @@ import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { type SettingsIntegration } from '@/settings/integrations/types/SettingsIntegration';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Pill } from 'twenty-ui/components';
|
||||
import {
|
||||
IconArrowUpRight,
|
||||
IconBolt,
|
||||
@@ -12,7 +13,7 @@ import {
|
||||
IconPlus,
|
||||
Status,
|
||||
} from 'twenty-ui/display';
|
||||
import { Pill } from 'twenty-ui/components';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
|
||||
interface SettingsIntegrationComponentProps {
|
||||
@@ -92,7 +93,7 @@ export const SettingsIntegrationComponent = ({
|
||||
<Button
|
||||
to={integration.link}
|
||||
Icon={IconPlus}
|
||||
title="Add"
|
||||
title={t`Add`}
|
||||
size="small"
|
||||
/>
|
||||
) : integration.type === 'Use' ? (
|
||||
@@ -100,7 +101,7 @@ export const SettingsIntegrationComponent = ({
|
||||
to={integration.link}
|
||||
target="_blank"
|
||||
Icon={IconBolt}
|
||||
title="Use"
|
||||
title={t`Use`}
|
||||
size="small"
|
||||
/>
|
||||
) : integration.type === 'Copy' ? (
|
||||
|
||||
+4
-3
@@ -1,9 +1,10 @@
|
||||
import { type FetchResult } from '@apollo/client';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { type SyncRemoteTableSchemaChangesMutation } from '~/generated-metadata/graphql';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { IconReload } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { type SyncRemoteTableSchemaChangesMutation } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledText = styled.h3`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
@@ -27,7 +28,7 @@ export const SettingsIntegrationRemoteTableSchemaUpdate = ({
|
||||
{updatesText && (
|
||||
<Button
|
||||
Icon={IconReload}
|
||||
title="Update"
|
||||
title={t`Update`}
|
||||
size="small"
|
||||
onClick={onUpdate}
|
||||
/>
|
||||
|
||||
+56
-4
@@ -3,23 +3,32 @@ import { SettingsRolePermissionsSettingsTableHeader } from '@/settings/roles/rol
|
||||
import { SettingsRolePermissionsSettingsTableRow } from '@/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsSettingsTableRow';
|
||||
import { type SettingsRolePermissionsSettingPermission } from '@/settings/roles/role-permissions/permission-flags/types/SettingsRolePermissionsSettingPermission';
|
||||
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import {
|
||||
H2Title,
|
||||
IconApps,
|
||||
IconCode,
|
||||
IconCreditCard,
|
||||
IconHierarchy,
|
||||
IconKey,
|
||||
IconLayoutSidebarRightCollapse,
|
||||
IconLockOpen,
|
||||
IconSettings,
|
||||
IconSettingsAutomation,
|
||||
IconShield,
|
||||
IconSparkles,
|
||||
IconSpy,
|
||||
IconUsers,
|
||||
} from 'twenty-ui/display';
|
||||
import { AnimatedExpandableContainer, Card, Section } from 'twenty-ui/layout';
|
||||
import { PermissionFlagType } from '~/generated-metadata/graphql';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
PermissionFlagType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledTable = styled.div`
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
@@ -47,8 +56,13 @@ export const SettingsRolePermissionsSettingsSection = ({
|
||||
settingsDraftRoleFamilyState(roleId),
|
||||
);
|
||||
|
||||
const settingsPermissionsConfig: SettingsRolePermissionsSettingPermission[] =
|
||||
[
|
||||
const isAIEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
|
||||
const isApplicationEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_APPLICATION_ENABLED,
|
||||
);
|
||||
|
||||
const settingsPermissionsConfig = useMemo(() => {
|
||||
const allPermissions: SettingsRolePermissionsSettingPermission[] = [
|
||||
{
|
||||
key: PermissionFlagType.API_KEYS_AND_WEBHOOKS,
|
||||
name: t`API Keys & Webhooks`,
|
||||
@@ -76,7 +90,7 @@ export const SettingsRolePermissionsSettingsSection = ({
|
||||
{
|
||||
key: PermissionFlagType.DATA_MODEL,
|
||||
name: t`Data Model`,
|
||||
description: t`Edit CRM data structure and fields`,
|
||||
description: t`Edit data structure and fields`,
|
||||
Icon: IconHierarchy,
|
||||
},
|
||||
{
|
||||
@@ -103,8 +117,46 @@ export const SettingsRolePermissionsSettingsSection = ({
|
||||
description: t`Impersonate workspace users`,
|
||||
Icon: IconSpy,
|
||||
},
|
||||
{
|
||||
key: PermissionFlagType.APPLICATIONS,
|
||||
name: t`Applications`,
|
||||
description: t`Install and manage applications`,
|
||||
Icon: IconApps,
|
||||
},
|
||||
{
|
||||
key: PermissionFlagType.LAYOUTS,
|
||||
name: t`Layouts`,
|
||||
description: t`Customize page layouts and UI structure`,
|
||||
Icon: IconLayoutSidebarRightCollapse,
|
||||
},
|
||||
{
|
||||
key: PermissionFlagType.BILLING,
|
||||
name: t`Billing`,
|
||||
description: t`Manage billing and subscriptions`,
|
||||
Icon: IconCreditCard,
|
||||
},
|
||||
{
|
||||
key: PermissionFlagType.AI_SETTINGS,
|
||||
name: t`AI`,
|
||||
description: t`Create and configure AI agents`,
|
||||
Icon: IconSparkles,
|
||||
},
|
||||
];
|
||||
|
||||
return allPermissions.filter((permission) => {
|
||||
if (permission.key === PermissionFlagType.AI_SETTINGS && !isAIEnabled) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
permission.key === PermissionFlagType.APPLICATIONS &&
|
||||
!isApplicationEnabled
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [isAIEnabled, isApplicationEnabled]);
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title title={t`Settings`} description={t`Settings permissions`} />
|
||||
|
||||
+56
-3
@@ -3,19 +3,28 @@ import { SettingsRolePermissionsSettingsTableHeader } from '@/settings/roles/rol
|
||||
import { SettingsRolePermissionsSettingsTableRow } from '@/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsSettingsTableRow';
|
||||
import { type SettingsRolePermissionsSettingPermission } from '@/settings/roles/role-permissions/permission-flags/types/SettingsRolePermissionsSettingPermission';
|
||||
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import {
|
||||
H2Title,
|
||||
IconAt,
|
||||
IconDownload,
|
||||
IconFileExport,
|
||||
IconFileImport,
|
||||
IconFileUpload,
|
||||
IconMail,
|
||||
IconSparkles,
|
||||
IconTable,
|
||||
IconTool,
|
||||
} from 'twenty-ui/display';
|
||||
import { AnimatedExpandableContainer, Card, Section } from 'twenty-ui/layout';
|
||||
import { PermissionFlagType } from '~/generated-metadata/graphql';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
PermissionFlagType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledTable = styled.div`
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
@@ -43,11 +52,34 @@ export const SettingsRolePermissionsToolSection = ({
|
||||
settingsDraftRoleFamilyState(roleId),
|
||||
);
|
||||
|
||||
const toolPermissionsConfig: SettingsRolePermissionsSettingPermission[] = [
|
||||
const isAIEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
|
||||
|
||||
const allPermissions: SettingsRolePermissionsSettingPermission[] = [
|
||||
{
|
||||
key: PermissionFlagType.AI,
|
||||
name: t`Ask AI`,
|
||||
description: t`Chat with AI agents and use AI features`,
|
||||
Icon: IconSparkles,
|
||||
isToolPermission: true,
|
||||
},
|
||||
{
|
||||
key: PermissionFlagType.UPLOAD_FILE,
|
||||
name: t`Upload Files`,
|
||||
description: t`Allow uploading files and attachments`,
|
||||
Icon: IconFileUpload,
|
||||
isToolPermission: true,
|
||||
},
|
||||
{
|
||||
key: PermissionFlagType.DOWNLOAD_FILE,
|
||||
name: t`Download Files`,
|
||||
description: t`Allow downloading files and attachments`,
|
||||
Icon: IconDownload,
|
||||
isToolPermission: true,
|
||||
},
|
||||
{
|
||||
key: PermissionFlagType.SEND_EMAIL_TOOL,
|
||||
name: t`Send Email`,
|
||||
description: t`Allow sending emails using connected accounts`,
|
||||
description: t`Send emails via connected accounts`,
|
||||
Icon: IconMail,
|
||||
isToolPermission: true,
|
||||
},
|
||||
@@ -65,8 +97,29 @@ export const SettingsRolePermissionsToolSection = ({
|
||||
Icon: IconFileExport,
|
||||
isToolPermission: true,
|
||||
},
|
||||
{
|
||||
key: PermissionFlagType.CONNECTED_ACCOUNTS,
|
||||
name: t`Sync Account`,
|
||||
description: t`Sync email and calendar accounts`,
|
||||
Icon: IconAt,
|
||||
isToolPermission: true,
|
||||
},
|
||||
{
|
||||
key: PermissionFlagType.VIEWS,
|
||||
name: t`Manage Views`,
|
||||
description: t`Create, edit, and delete workspace views`,
|
||||
Icon: IconTable,
|
||||
isToolPermission: true,
|
||||
},
|
||||
];
|
||||
|
||||
const toolPermissionsConfig = allPermissions.filter((permission) => {
|
||||
if (permission.key === PermissionFlagType.AI && !isAIEnabled) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title title={t`Actions`} description={t`Actions permissions`} />
|
||||
|
||||
+2
-2
@@ -133,7 +133,7 @@ export const SettingsSecurityAuthProvidersOptionsList = () => {
|
||||
{authProviders.google === true && (
|
||||
<SettingsOptionCardContentToggle
|
||||
Icon={IconGoogle}
|
||||
title="Google"
|
||||
title={t`Google`}
|
||||
description={t`Allow logins through Google's single sign-on functionality.`}
|
||||
checked={currentWorkspace.isGoogleAuthEnabled}
|
||||
advancedMode
|
||||
@@ -146,7 +146,7 @@ export const SettingsSecurityAuthProvidersOptionsList = () => {
|
||||
{authProviders.microsoft === true && (
|
||||
<SettingsOptionCardContentToggle
|
||||
Icon={IconMicrosoft}
|
||||
title="Microsoft"
|
||||
title={t`Microsoft`}
|
||||
description={t`Allow logins through Microsoft's single sign-on functionality.`}
|
||||
checked={currentWorkspace.isMicrosoftAuthEnabled}
|
||||
advancedMode
|
||||
|
||||
+5
-1
@@ -2,6 +2,7 @@ import { type ServerlessFunctionNewFormValues } from '@/settings/serverless-func
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { TextArea } from '@/ui/input/components/TextArea';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
|
||||
@@ -25,7 +26,10 @@ export const SettingsServerlessFunctionNewForm = ({
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title title="About" description="Name and describe your function" />
|
||||
<H2Title
|
||||
title={t`About`}
|
||||
description={t`Name and describe your function`}
|
||||
/>
|
||||
<StyledInputsContainer>
|
||||
<SettingsTextInput
|
||||
instanceId={nameTextInputId}
|
||||
|
||||
+4
-3
@@ -7,6 +7,7 @@ import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { H2Title, IconPlayerPlay } from 'twenty-ui/display';
|
||||
import { Button, CoreEditorHeader } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
@@ -32,7 +33,7 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
|
||||
);
|
||||
const TestButton = (
|
||||
<Button
|
||||
title="Test"
|
||||
title={t`Test`}
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
size="small"
|
||||
@@ -54,8 +55,8 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Code your function"
|
||||
description="Write your function (in typescript) below"
|
||||
title={t`Code your function`}
|
||||
description={t`Write your function (in typescript) below`}
|
||||
/>
|
||||
<CoreEditorHeader leftNodes={[HeaderTabList]} rightNodes={[TestButton]} />
|
||||
{activeTabId && (
|
||||
|
||||
Reference in New Issue
Block a user