-
Notifications
You must be signed in to change notification settings - Fork 13.2k
feat: add ABAC admin settings #37139
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4591df3
55aa068
4af4120
13f704f
d4eef8c
79b00e8
a7eb879
38d4537
2b8713d
c556139
f5e2dac
797c95c
93341e4
1ef3c25
ebfd204
cd6667c
7ca1ee3
a9c79e7
cc13efe
9fc8ad1
a844fe1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import { Box, Button, Callout } from '@rocket.chat/fuselage'; | ||
| import { useRouteParameter } from '@rocket.chat/ui-contexts'; | ||
| import { Trans, useTranslation } from 'react-i18next'; | ||
|
|
||
| import AdminABACSettings from './AdminABACSettings'; | ||
| import AdminABACTabs from './AdminABACTabs'; | ||
| import { Page, PageContent, PageHeader } from '../../../components/Page'; | ||
| import { useExternalLink } from '../../../hooks/useExternalLink'; | ||
| import { links } from '../../../lib/links'; | ||
|
|
||
| type AdminABACPageProps = { | ||
| shouldShowWarning: boolean; | ||
| }; | ||
|
|
||
| const AdminABACPage = ({ shouldShowWarning }: AdminABACPageProps) => { | ||
| const { t } = useTranslation(); | ||
| const tab = useRouteParameter('tab'); | ||
| const learnMore = useExternalLink(); | ||
|
|
||
| return ( | ||
| <Page flexDirection='row'> | ||
| <Page> | ||
| <PageHeader title={t('ABAC')}> | ||
| <Button icon='new-window' secondary onClick={() => learnMore(links.go.abacDocs)}> | ||
| {t('ABAC_Learn_More')} | ||
| </Button> | ||
| </PageHeader> | ||
| {shouldShowWarning && ( | ||
| <Box mi={24} mb={16}> | ||
| <Callout type='warning' title={t('ABAC_automatically_disabled_callout')}> | ||
| <Trans i18nKey='ABAC_automatically_disabled_callout_description'> | ||
| Renew your license to continue using all{' '} | ||
| <a href={links.go.abacLicenseRenewalUrl} rel='noopener noreferrer' target='_blank'> | ||
| ABAC capabilities without restriction. | ||
| </a> | ||
| </Trans> | ||
| </Callout> | ||
| </Box> | ||
| )} | ||
| <AdminABACTabs /> | ||
| <PageContent>{tab === 'settings' && <AdminABACSettings />}</PageContent> | ||
| </Page> | ||
| </Page> | ||
| ); | ||
| }; | ||
|
|
||
| export default AdminABACPage; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import { usePermission, useSetModal, useCurrentModal, useRouter, useRouteParameter, useSettingStructure } from '@rocket.chat/ui-contexts'; | ||
| import type { ReactElement } from 'react'; | ||
| import { memo, useEffect, useLayoutEffect } from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
|
|
||
| import AdminABACPage from './AdminABACPage'; | ||
| import ABACUpsellModal from '../../../components/ABAC/ABACUpsellModal/ABACUpsellModal'; | ||
| import { useUpsellActions } from '../../../components/GenericUpsellModal/hooks'; | ||
| import PageSkeleton from '../../../components/PageSkeleton'; | ||
| import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; | ||
| import SettingsProvider from '../../../providers/SettingsProvider'; | ||
| import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; | ||
| import EditableSettingsProvider from '../settings/EditableSettingsProvider'; | ||
|
|
||
| const AdminABACRoute = (): ReactElement => { | ||
| const { t } = useTranslation(); | ||
| // TODO: Check what permission is needed to view the ABAC page | ||
| const canViewABACPage = usePermission('abac-management'); | ||
| const hasABAC = useHasLicenseModule('abac') === true; | ||
| const isModalOpen = !!useCurrentModal(); | ||
| const tab = useRouteParameter('tab'); | ||
| const router = useRouter(); | ||
|
|
||
| // Check if setting exists in the DB to decide if we show warning or upsell | ||
| const ABACEnabledSetting = useSettingStructure('ABAC_Enabled'); | ||
|
|
||
| useLayoutEffect(() => { | ||
| if (!tab) { | ||
| router.navigate({ | ||
| name: 'admin-ABAC', | ||
| params: { tab: 'settings' }, | ||
| }); | ||
| } | ||
| }, [tab, router]); | ||
|
|
||
| const { shouldShowUpsell, handleManageSubscription } = useUpsellActions(hasABAC); | ||
|
|
||
| const setModal = useSetModal(); | ||
|
|
||
| useEffect(() => { | ||
| // WS has never activated ABAC | ||
| if (shouldShowUpsell && ABACEnabledSetting === undefined) { | ||
| setModal(<ABACUpsellModal onClose={() => setModal(null)} onConfirm={handleManageSubscription} />); | ||
| } | ||
| }, [shouldShowUpsell, setModal, t, handleManageSubscription, ABACEnabledSetting]); | ||
|
|
||
| if (isModalOpen) { | ||
| return <PageSkeleton />; | ||
| } | ||
|
|
||
| if (!canViewABACPage || (ABACEnabledSetting === undefined && !hasABAC)) { | ||
| return <NotAuthorizedPage />; | ||
| } | ||
|
|
||
| return ( | ||
| <SettingsProvider> | ||
| <EditableSettingsProvider> | ||
| <AdminABACPage shouldShowWarning={ABACEnabledSetting !== undefined && !hasABAC} /> | ||
| </EditableSettingsProvider> | ||
| </SettingsProvider> | ||
| ); | ||
| }; | ||
|
|
||
| export default memo(AdminABACRoute); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Import ISetting from the correct package. The import path for Apply this diff to fix the import: -import type { ISetting } from '@rocket.chat/apps-engine/definition/settings';
+import type { ISetting } from '@rocket.chat/core-typings';🤖 Prompt for AI Agents |
||
| import { mockAppRoot } from '@rocket.chat/mock-providers'; | ||
| import { render, screen, waitFor } from '@testing-library/react'; | ||
| import userEvent from '@testing-library/user-event'; | ||
| import { axe } from 'jest-axe'; | ||
|
|
||
| import AdminABACSettingToggle from './AdminABACSettingToggle'; | ||
| import EditableSettingsProvider from '../settings/EditableSettingsProvider'; | ||
|
|
||
| const settingStructure = { | ||
| packageValue: false, | ||
| blocked: false, | ||
| public: true, | ||
| type: 'boolean', | ||
| i18nLabel: 'ABAC_Enabled', | ||
| i18nDescription: 'ABAC_Enabled_Description', | ||
| } as Partial<ISetting>; | ||
|
|
||
| const baseAppRoot = mockAppRoot() | ||
| .wrap((children) => <EditableSettingsProvider>{children}</EditableSettingsProvider>) | ||
| .withTranslations('en', 'core', { | ||
| ABAC_Enabled: 'Enable ABAC', | ||
| ABAC_Enabled_Description: 'Enable Attribute-Based Access Control', | ||
| ABAC_Warning_Modal_Title: 'Disable ABAC', | ||
| ABAC_Warning_Modal_Confirm_Text: 'Disable', | ||
| Cancel: 'Cancel', | ||
| }); | ||
|
|
||
| describe('AdminABACSettingToggle', () => { | ||
| it('should render the setting toggle when setting exists', () => { | ||
| const { baseElement } = render(<AdminABACSettingToggle hasABAC={true} />, { | ||
| wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(), | ||
| }); | ||
| expect(baseElement).toMatchSnapshot(); | ||
| }); | ||
|
|
||
| it('should show warning modal when disabling ABAC', async () => { | ||
| const user = userEvent.setup(); | ||
| render(<AdminABACSettingToggle hasABAC={true} />, { | ||
| wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(), | ||
| }); | ||
|
|
||
| const toggle = screen.getByRole('checkbox'); | ||
| await waitFor(() => { | ||
| expect(toggle).not.toBeDisabled(); | ||
| }); | ||
| await user.click(toggle); | ||
|
|
||
| await waitFor(() => { | ||
| expect(screen.getByText('Disable ABAC')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| // TODO: discover how to automatically unmount all modals after each test | ||
| const cancelButton = screen.getByRole('button', { name: /cancel/i }); | ||
| await user.click(cancelButton); | ||
| }); | ||
|
Comment on lines
+37
to
+56
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Implement proper modal cleanup to ensure test isolation. The TODO comments at lines 54 and 85 highlight a test isolation concern. Manually dismissing modals in each test is fragile and could cause test pollution if the modal persists between tests. Consider implementing an afterEach(() => {
// Close any open modals
const cancelButtons = screen.queryAllByRole('button', { name: /cancel/i });
cancelButtons.forEach(button => button.click());
});Alternatively, investigate if the Also applies to: 71-87 🤖 Prompt for AI Agents |
||
|
|
||
| it('should not show warning modal when enabling ABAC', async () => { | ||
| const user = userEvent.setup(); | ||
| render(<AdminABACSettingToggle hasABAC={true} />, { | ||
| wrapper: baseAppRoot.withSetting('ABAC_Enabled', false, settingStructure).build(), | ||
| }); | ||
|
|
||
| const toggle = screen.getByRole('checkbox'); | ||
| await user.click(toggle); | ||
|
|
||
| // The modal should not appear when enabling ABAC | ||
| expect(screen.queryByText('Disable ABAC')).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('should show warning modal when resetting setting', async () => { | ||
| const user = userEvent.setup(); | ||
| render(<AdminABACSettingToggle hasABAC={true} />, { | ||
| wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(), | ||
| }); | ||
|
|
||
| const resetButton = screen.getByRole('button', { name: /reset/i }); | ||
| await user.click(resetButton); | ||
|
|
||
| await waitFor(() => { | ||
| expect(screen.getByText('Disable ABAC')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| // TODO: discover how to automatically unmount all modals after each test | ||
| const cancelButton = screen.getByRole('button', { name: /cancel/i }); | ||
| await user.click(cancelButton); | ||
| }); | ||
|
Comment on lines
+37
to
+87
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Add test coverage for the confirmation flow. The existing tests verify that the warning modal appears when disabling or resetting, but they don't test the actual confirmation path. Consider adding a test that clicks the "Disable" button in the modal and verifies the setting is actually changed. Add a test case like: it('should disable ABAC when confirmation button is clicked', async () => {
const user = userEvent.setup();
render(<AdminABACSettingToggle hasABAC={true} />, {
wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(),
});
const toggle = screen.getByRole('checkbox');
await user.click(toggle);
await waitFor(() => {
expect(screen.getByText('Disable ABAC')).toBeInTheDocument();
});
const disableButton = screen.getByRole('button', { name: /disable/i });
await user.click(disableButton);
// Verify the setting was actually changed
await waitFor(() => {
expect(toggle).not.toBeChecked();
});
});🤖 Prompt for AI Agents |
||
|
|
||
| it('should have no accessibility violations', async () => { | ||
| const { container } = render(<AdminABACSettingToggle hasABAC={true} />, { | ||
| wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(), | ||
| }); | ||
| const results = await axe(container); | ||
| expect(results).toHaveNoViolations(); | ||
| }); | ||
|
|
||
| it('should handle setting change correctly', async () => { | ||
| const user = userEvent.setup(); | ||
| render(<AdminABACSettingToggle hasABAC={true} />, { | ||
| wrapper: baseAppRoot.withSetting('ABAC_Enabled', false, settingStructure).build(), | ||
| }); | ||
|
|
||
| const toggle = await screen.findByRole('checkbox', { busy: false }); | ||
| expect(toggle).not.toBeChecked(); | ||
|
|
||
| await user.click(toggle); | ||
| expect(toggle).toBeChecked(); | ||
| }); | ||
|
|
||
| it('should be disabled when abac license is not installed', () => { | ||
| const { baseElement } = render(<AdminABACSettingToggle hasABAC={false} />, { | ||
| wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(), | ||
| }); | ||
|
|
||
| const toggle = screen.getByRole('checkbox'); | ||
| expect(toggle).toBeDisabled(); | ||
| expect(baseElement).toMatchSnapshot(); | ||
| }); | ||
|
|
||
| it('should show skeleton when loading', () => { | ||
| const { baseElement } = render(<AdminABACSettingToggle hasABAC='loading' />, { | ||
| wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(), | ||
| }); | ||
| expect(baseElement).toMatchSnapshot(); | ||
| }); | ||
|
|
||
| it('should show reset button when value differs from package value', () => { | ||
| render(<AdminABACSettingToggle hasABAC={true} />, { | ||
| wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(), | ||
| }); | ||
|
|
||
| expect(screen.getByRole('button', { name: /reset/i })).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('should not show reset button when value matches package value', () => { | ||
| render(<AdminABACSettingToggle hasABAC={true} />, { | ||
| wrapper: baseAppRoot.withSetting('ABAC_Enabled', false, settingStructure).build(), | ||
| }); | ||
|
|
||
| expect(screen.queryByRole('button', { name: /reset/i })).not.toBeInTheDocument(); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import { mockAppRoot } from '@rocket.chat/mock-providers'; | ||
| import type { Meta, StoryObj } from '@storybook/react'; | ||
|
|
||
| import AdminABACSettingToggle from './AdminABACSettingToggle'; | ||
| import EditableSettingsProvider from '../settings/EditableSettingsProvider'; | ||
|
|
||
| const meta: Meta<typeof AdminABACSettingToggle> = { | ||
| title: 'Admin/ABAC/AdminABACSettingToggle', | ||
| component: AdminABACSettingToggle, | ||
| parameters: { | ||
| layout: 'padded', | ||
| }, | ||
| decorators: [ | ||
| (Story) => { | ||
| const AppRoot = mockAppRoot() | ||
| .wrap((children) => <EditableSettingsProvider>{children}</EditableSettingsProvider>) | ||
| .withTranslations('en', 'core', { | ||
| ABAC_Enabled: 'Enable ABAC', | ||
| ABAC_Enabled_Description: 'Enable Attribute-Based Access Control', | ||
| ABAC_Warning_Modal_Title: 'Disable ABAC', | ||
| ABAC_Warning_Modal_Confirm_Text: 'Disable', | ||
| Cancel: 'Cancel', | ||
| }) | ||
| .withSetting('ABAC_Enabled', true, { | ||
| packageValue: false, | ||
| blocked: false, | ||
| public: true, | ||
| type: 'boolean', | ||
| i18nLabel: 'ABAC_Enabled', | ||
| i18nDescription: 'ABAC_Enabled_Description', | ||
| }) | ||
| .build(); | ||
|
|
||
| return ( | ||
| <AppRoot> | ||
| <Story /> | ||
| </AppRoot> | ||
| ); | ||
| }, | ||
| ], | ||
| args: { | ||
| hasABAC: true, | ||
| }, | ||
| }; | ||
|
|
||
| export default meta; | ||
| type Story = StoryObj<typeof AdminABACSettingToggle>; | ||
|
|
||
| export const Default: Story = { | ||
| args: { | ||
| hasABAC: true, | ||
| }, | ||
| }; | ||
|
|
||
| export const Loading: Story = { | ||
| args: { | ||
| hasABAC: 'loading', | ||
| }, | ||
| }; | ||
|
|
||
| export const False: Story = { | ||
| args: { | ||
| hasABAC: false, | ||
| }, | ||
| }; |
Uh oh!
There was an error while loading. Please reload this page.