diff --git a/.changeset/brave-fans-tie.md b/.changeset/brave-fans-tie.md new file mode 100644 index 0000000000000..3d2fa6b1b02af --- /dev/null +++ b/.changeset/brave-fans-tie.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Adds 4 new permissions (assigned to admins by default) to control the visibility of each tab inside the ABAC Administration panel diff --git a/apps/meteor/client/views/admin/ABAC/AdminABACPage.tsx b/apps/meteor/client/views/admin/ABAC/AdminABACPage.tsx index ee8c63dd84031..a791f07a01d37 100644 --- a/apps/meteor/client/views/admin/ABAC/AdminABACPage.tsx +++ b/apps/meteor/client/views/admin/ABAC/AdminABACPage.tsx @@ -13,6 +13,7 @@ import RoomsContextualBarWithData from './ABACRoomsTab/RoomsContextualBarWithDat import RoomsPage from './ABACRoomsTab/RoomsPage'; import SettingsPage from './ABACSettingTab/SettingsPage'; import AdminABACTabs from './AdminABACTabs'; +import { useABACTabPermissions } from './hooks/useABACTabPermissions'; import { useIsABACAvailable } from './hooks/useIsABACAvailable'; import { useExternalLink } from '../../../hooks/useExternalLink'; import { useLdapSync } from '../../../hooks/useLdapSync'; @@ -34,6 +35,7 @@ const AdminABACPage = ({ shouldShowWarning }: AdminABACPageProps) => { const abacEnabled = useSetting('ABAC_Enabled'); const handleSyncNow = useLdapSync(); const isSyncDisabled = !ldapEnabled || !abacEnabled; + const tabPermissions = useABACTabPermissions(); const handleCloseContextualbar = useEffectEvent((): void => { if (!context) { @@ -84,21 +86,21 @@ const AdminABACPage = ({ shouldShowWarning }: AdminABACPageProps) => { )} - {tab === 'settings' && } - {tab === 'room-attributes' && } - {tab === 'rooms' && } - {tab === 'logs' && } + {tab === 'settings' && tabPermissions.settings && } + {tab === 'room-attributes' && tabPermissions['room-attributes'] && } + {tab === 'rooms' && tabPermissions.rooms && } + {tab === 'logs' && tabPermissions.logs && } {isABACAvailable === true && tab !== undefined && context !== undefined && ( handleCloseContextualbar()}> - {tab === 'room-attributes' && ( + {tab === 'room-attributes' && tabPermissions['room-attributes'] && ( <> {context === 'new' && handleCloseContextualbar()} />} {context === 'edit' && _id && handleCloseContextualbar()} />} )} - {tab === 'rooms' && ( + {tab === 'rooms' && tabPermissions.rooms && ( <> {context === 'new' && handleCloseContextualbar()} />} {context === 'edit' && _id && handleCloseContextualbar()} />} diff --git a/apps/meteor/client/views/admin/ABAC/AdminABACRoute.tsx b/apps/meteor/client/views/admin/ABAC/AdminABACRoute.tsx index 05538b79448e5..6aedc09a92a7a 100644 --- a/apps/meteor/client/views/admin/ABAC/AdminABACRoute.tsx +++ b/apps/meteor/client/views/admin/ABAC/AdminABACRoute.tsx @@ -4,6 +4,8 @@ import { memo, useEffect, useLayoutEffect } from 'react'; import { useTranslation } from 'react-i18next'; import AdminABACPage from './AdminABACPage'; +import type { ABACTab } from './hooks/useABACTabPermissions'; +import { ABAC_TAB_ORDER, useABACTabPermissions } from './hooks/useABACTabPermissions'; import ABACUpsellModal from '../../../components/ABAC/ABACUpsellModal/ABACUpsellModal'; import { useUpsellActions } from '../../../components/GenericUpsellModal/hooks'; import PageSkeleton from '../../../components/PageSkeleton'; @@ -14,24 +16,28 @@ 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 { data: hasABAC = false } = useHasLicenseModule('abac'); const isModalOpen = !!useCurrentModal(); const tab = useRouteParameter('tab'); const router = useRouter(); + const tabPermissions = useABACTabPermissions(); + const firstAllowedTab = ABAC_TAB_ORDER.find((t) => tabPermissions[t]); + const isAllowedTab = (ABAC_TAB_ORDER as readonly string[]).includes(tab ?? '') && tabPermissions[tab as ABACTab]; - // 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' }, - }); + if (firstAllowedTab && !isAllowedTab) { + router.navigate( + { + name: 'admin-ABAC', + params: { tab: firstAllowedTab }, + }, + { replace: true }, + ); } - }, [tab, router]); + }, [router, firstAllowedTab, isAllowedTab]); const { shouldShowUpsell, handleManageSubscription } = useUpsellActions(hasABAC); @@ -48,7 +54,7 @@ const AdminABACRoute = (): ReactElement => { return ; } - if (!canViewABACPage || (ABACEnabledSetting === undefined && !hasABAC)) { + if (!canViewABACPage || !firstAllowedTab || (ABACEnabledSetting === undefined && !hasABAC)) { return ; } diff --git a/apps/meteor/client/views/admin/ABAC/AdminABACTabs.tsx b/apps/meteor/client/views/admin/ABAC/AdminABACTabs.tsx index 11b315242ac76..c4d77e92d0492 100644 --- a/apps/meteor/client/views/admin/ABAC/AdminABACTabs.tsx +++ b/apps/meteor/client/views/admin/ABAC/AdminABACTabs.tsx @@ -2,10 +2,13 @@ import { Tabs, TabsItem } from '@rocket.chat/fuselage'; import { useRouteParameter, useRouter } from '@rocket.chat/ui-contexts'; import { useTranslation } from 'react-i18next'; +import { useABACTabPermissions } from './hooks/useABACTabPermissions'; + const AdminABACTabs = () => { const { t } = useTranslation(); const router = useRouter(); const tab = useRouteParameter('tab'); + const tabPermissions = useABACTabPermissions(); const handleTabClick = (tab: string) => { router.navigate({ name: 'admin-ABAC', @@ -14,18 +17,26 @@ const AdminABACTabs = () => { }; return ( - handleTabClick('settings')}> - {t('Settings')} - - handleTabClick('room-attributes')}> - {t('ABAC_Room_Attributes')} - - handleTabClick('rooms')}> - {t('Rooms')} - - handleTabClick('logs')}> - {t('ABAC_Logs')} - + {tabPermissions.settings && ( + handleTabClick('settings')}> + {t('Settings')} + + )} + {tabPermissions['room-attributes'] && ( + handleTabClick('room-attributes')}> + {t('ABAC_Room_Attributes')} + + )} + {tabPermissions.rooms && ( + handleTabClick('rooms')}> + {t('Rooms')} + + )} + {tabPermissions.logs && ( + handleTabClick('logs')}> + {t('ABAC_Logs')} + + )} ); }; diff --git a/apps/meteor/client/views/admin/ABAC/hooks/useABACTabPermissions.ts b/apps/meteor/client/views/admin/ABAC/hooks/useABACTabPermissions.ts new file mode 100644 index 0000000000000..2f50ab1388924 --- /dev/null +++ b/apps/meteor/client/views/admin/ABAC/hooks/useABACTabPermissions.ts @@ -0,0 +1,14 @@ +import { usePermission } from '@rocket.chat/ui-contexts'; + +export type ABACTab = 'settings' | 'room-attributes' | 'rooms' | 'logs'; + +export const ABAC_TAB_ORDER: ABACTab[] = ['settings', 'room-attributes', 'rooms', 'logs']; + +export const useABACTabPermissions = (): Record => { + return { + 'settings': usePermission('manage-abac-admin-settings'), + 'room-attributes': usePermission('manage-abac-admin-room-attributes'), + 'rooms': usePermission('manage-abac-admin-rooms'), + 'logs': usePermission('view-abac-admin-audit'), + }; +}; diff --git a/apps/meteor/client/views/admin/sidebarItems.ts b/apps/meteor/client/views/admin/sidebarItems.ts index aa986c5ca1e84..3c8867ee26236 100644 --- a/apps/meteor/client/views/admin/sidebarItems.ts +++ b/apps/meteor/client/views/admin/sidebarItems.ts @@ -68,7 +68,14 @@ export const { href: '/admin/ABAC', i18nLabel: 'ABAC', icon: 'team-lock', - permissionGranted: (): boolean => hasPermission('abac-management'), + permissionGranted: (): boolean => + hasPermission('abac-management') && + hasAtLeastOnePermission([ + 'manage-abac-admin-settings', + 'manage-abac-admin-room-attributes', + 'manage-abac-admin-rooms', + 'view-abac-admin-audit', + ]), }, { href: '/admin/device-management', diff --git a/apps/meteor/ee/server/api/abac/index.ts b/apps/meteor/ee/server/api/abac/index.ts index 6901fa0c6f17c..bee8f7061c795 100644 --- a/apps/meteor/ee/server/api/abac/index.ts +++ b/apps/meteor/ee/server/api/abac/index.ts @@ -46,7 +46,7 @@ const abacEndpoints = API.v1 'abac/rooms/:rid/attributes', { authRequired: true, - permissionsRequired: ['abac-management'], + permissionsRequired: ['abac-management', 'manage-abac-admin-rooms'], body: POSTRoomAbacAttributesBodySchema, response: { 200: GenericSuccessSchema, @@ -74,7 +74,7 @@ const abacEndpoints = API.v1 'abac/rooms/:rid/attributes', { authRequired: true, - permissionsRequired: ['abac-management'], + permissionsRequired: ['abac-management', 'manage-abac-admin-rooms'], response: { 200: GenericSuccessSchema, 401: validateUnauthorizedErrorResponse, @@ -97,7 +97,7 @@ const abacEndpoints = API.v1 'abac/rooms/:rid/attributes/:key', { authRequired: true, - permissionsRequired: ['abac-management'], + permissionsRequired: ['abac-management', 'manage-abac-admin-rooms'], license: ['abac'], body: POSTSingleRoomAbacAttributeBodySchema, response: { @@ -124,7 +124,7 @@ const abacEndpoints = API.v1 'abac/rooms/:rid/attributes/:key', { authRequired: true, - permissionsRequired: ['abac-management'], + permissionsRequired: ['abac-management', 'manage-abac-admin-rooms'], body: PUTRoomAbacAttributeValuesBodySchema, response: { 200: GenericSuccessSchema, @@ -151,7 +151,7 @@ const abacEndpoints = API.v1 'abac/rooms/:rid/attributes/:key', { authRequired: true, - permissionsRequired: ['abac-management'], + permissionsRequired: ['abac-management', 'manage-abac-admin-rooms'], response: { 200: GenericSuccessSchema, 401: validateUnauthorizedErrorResponse, @@ -172,7 +172,7 @@ const abacEndpoints = API.v1 'abac/attributes', { authRequired: true, - permissionsRequired: ['abac-management'], + permissionsRequired: ['abac-management', 'manage-abac-admin-room-attributes'], query: GETAbacAttributesQuerySchema, response: { 200: GETAbacAttributesResponseSchema, @@ -203,7 +203,7 @@ const abacEndpoints = API.v1 'abac/users/sync', { authRequired: true, - permissionsRequired: ['abac-management'], + permissionsRequired: ['abac-management', 'manage-abac-admin-room-attributes'], license: ['abac', 'ldap-enterprise'], body: POSTAbacUsersSyncBodySchema, response: { @@ -229,7 +229,7 @@ const abacEndpoints = API.v1 'abac/attributes', { authRequired: true, - permissionsRequired: ['abac-management'], + permissionsRequired: ['abac-management', 'manage-abac-admin-room-attributes'], license: ['abac'], body: POSTAbacAttributeDefinitionSchema, response: { @@ -253,7 +253,7 @@ const abacEndpoints = API.v1 'abac/attributes/:_id', { authRequired: true, - permissionsRequired: ['abac-management'], + permissionsRequired: ['abac-management', 'manage-abac-admin-room-attributes'], license: ['abac'], body: PUTAbacAttributeUpdateBodySchema, response: { @@ -278,7 +278,7 @@ const abacEndpoints = API.v1 'abac/attributes/:_id', { authRequired: true, - permissionsRequired: ['abac-management'], + permissionsRequired: ['abac-management', 'manage-abac-admin-room-attributes'], response: { 200: GETAbacAttributeByIdResponseSchema, 401: validateUnauthorizedErrorResponse, @@ -297,7 +297,7 @@ const abacEndpoints = API.v1 'abac/attributes/:_id', { authRequired: true, - permissionsRequired: ['abac-management'], + permissionsRequired: ['abac-management', 'manage-abac-admin-room-attributes'], response: { 200: GenericSuccessSchema, 401: validateUnauthorizedErrorResponse, @@ -316,7 +316,7 @@ const abacEndpoints = API.v1 'abac/attributes/:key/is-in-use', { authRequired: true, - permissionsRequired: ['abac-management'], + permissionsRequired: ['abac-management', 'manage-abac-admin-room-attributes'], response: { 200: GETAbacAttributeIsInUseResponseSchema, 401: validateUnauthorizedErrorResponse, @@ -334,7 +334,7 @@ const abacEndpoints = API.v1 'abac/rooms', { authRequired: true, - permissionsRequired: ['abac-management'], + permissionsRequired: ['abac-management', 'manage-abac-admin-rooms'], response: { 200: GETAbacRoomsResponseValidator, 401: validateUnauthorizedErrorResponse, @@ -364,7 +364,7 @@ const abacEndpoints = API.v1 'abac/pdp/health', { authRequired: true, - permissionsRequired: ['abac-management'], + permissionsRequired: ['abac-management', 'manage-abac-admin-settings'], rateLimiterOptions: { numRequestsAllowed: 5, intervalTimeInMS: 60000, @@ -396,7 +396,7 @@ const abacEndpoints = API.v1 }, query: GETAbacAuditEventsQuerySchema, authRequired: true, - permissionsRequired: ['abac-management'], + permissionsRequired: ['abac-management', 'view-abac-admin-audit'], license: ['abac', 'auditing'], }, async function action() { diff --git a/apps/meteor/ee/server/lib/abac/index.ts b/apps/meteor/ee/server/lib/abac/index.ts index 1b03e8d457441..e6c5599582c6e 100644 --- a/apps/meteor/ee/server/lib/abac/index.ts +++ b/apps/meteor/ee/server/lib/abac/index.ts @@ -1,7 +1,13 @@ import { Permissions } from '@rocket.chat/models'; export const createPermissions = async () => { - const permissions = [{ _id: 'abac-management', roles: ['admin'] }]; + const permissions = [ + { _id: 'abac-management', roles: ['admin'] }, + { _id: 'manage-abac-admin-settings', roles: ['admin'] }, + { _id: 'manage-abac-admin-room-attributes', roles: ['admin'] }, + { _id: 'manage-abac-admin-rooms', roles: ['admin'] }, + { _id: 'view-abac-admin-audit', roles: ['admin'] }, + ]; for (const permission of permissions) { void Permissions.create(permission._id, permission.roles); diff --git a/apps/meteor/tests/end-to-end/api/abac.ts b/apps/meteor/tests/end-to-end/api/abac.ts index 6bb74773508bf..1d264f4ec3359 100644 --- a/apps/meteor/tests/end-to-end/api/abac.ts +++ b/apps/meteor/tests/end-to-end/api/abac.ts @@ -55,7 +55,13 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I before(async () => { connection = await MongoClient.connect(URL_MONGODB); - await updatePermission('abac-management', ['admin']); + await Promise.all([ + updatePermission('abac-management', ['admin']), + updatePermission('manage-abac-admin-settings', ['admin']), + updatePermission('manage-abac-admin-room-attributes', ['admin']), + updatePermission('manage-abac-admin-rooms', ['admin']), + updatePermission('view-abac-admin-audit', ['admin']), + ]); await updateSetting('ABAC_Enabled', true); testRoom = (await createRoom({ type: 'p', name: `abac-test-${Date.now()}` })).body.group; @@ -90,6 +96,136 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]'); }); }); + + describe('Granular admin permissions (with abac-management, missing the granular)', () => { + const dummyKey = 'granular_perm_dummy_key'; + const dummyId = 'granular_perm_dummy_id'; + + after(async () => { + await Promise.all([ + updatePermission('manage-abac-admin-settings', ['admin']), + updatePermission('manage-abac-admin-room-attributes', ['admin']), + updatePermission('manage-abac-admin-rooms', ['admin']), + updatePermission('view-abac-admin-audit', ['admin']), + ]); + }); + + describe('without manage-abac-admin-rooms', () => { + before(async () => { + await updatePermission('manage-abac-admin-rooms', []); + }); + + after(async () => { + await updatePermission('manage-abac-admin-rooms', ['admin']); + }); + + it('POST /abac/rooms/:rid/attributes should return 403', async () => { + await request.post(`${v1}/abac/rooms/${testRoom._id}/attributes`).set(credentials).send({ attributes: {} }).expect(403); + }); + + it('DELETE /abac/rooms/:rid/attributes should return 403', async () => { + await request.delete(`${v1}/abac/rooms/${testRoom._id}/attributes`).set(credentials).expect(403); + }); + + it('POST /abac/rooms/:rid/attributes/:key should return 403', async () => { + await request + .post(`${v1}/abac/rooms/${testRoom._id}/attributes/${dummyKey}`) + .set(credentials) + .send({ values: ['v1'] }) + .expect(403); + }); + + it('PUT /abac/rooms/:rid/attributes/:key should return 403', async () => { + await request + .put(`${v1}/abac/rooms/${testRoom._id}/attributes/${dummyKey}`) + .set(credentials) + .send({ values: ['v1'] }) + .expect(403); + }); + + it('DELETE /abac/rooms/:rid/attributes/:key should return 403', async () => { + await request.delete(`${v1}/abac/rooms/${testRoom._id}/attributes/${dummyKey}`).set(credentials).expect(403); + }); + + it('GET /abac/rooms should return 403', async () => { + await request.get(`${v1}/abac/rooms`).set(credentials).expect(403); + }); + }); + + describe('without manage-abac-admin-room-attributes', () => { + before(async () => { + await updatePermission('manage-abac-admin-room-attributes', []); + }); + + after(async () => { + await updatePermission('manage-abac-admin-room-attributes', ['admin']); + }); + + it('GET /abac/attributes should return 403', async () => { + await request.get(`${v1}/abac/attributes`).set(credentials).expect(403); + }); + + it('POST /abac/attributes should return 403', async () => { + await request + .post(`${v1}/abac/attributes`) + .set(credentials) + .send({ key: dummyKey, values: ['v1'] }) + .expect(403); + }); + + it('PUT /abac/attributes/:_id should return 403', async () => { + await request.put(`${v1}/abac/attributes/${dummyId}`).set(credentials).send({ key: dummyKey }).expect(403); + }); + + it('GET /abac/attributes/:_id should return 403', async () => { + await request.get(`${v1}/abac/attributes/${dummyId}`).set(credentials).expect(403); + }); + + it('DELETE /abac/attributes/:_id should return 403', async () => { + await request.delete(`${v1}/abac/attributes/${dummyId}`).set(credentials).expect(403); + }); + + it('GET /abac/attributes/:key/is-in-use should return 403', async () => { + await request.get(`${v1}/abac/attributes/${dummyKey}/is-in-use`).set(credentials).expect(403); + }); + + it('POST /abac/users/sync should return 403', async () => { + await request + .post(`${v1}/abac/users/sync`) + .set(credentials) + .send({ usernames: ['x'] }) + .expect(403); + }); + }); + + describe('without manage-abac-admin-settings', () => { + before(async () => { + await updatePermission('manage-abac-admin-settings', []); + }); + + after(async () => { + await updatePermission('manage-abac-admin-settings', ['admin']); + }); + + it('GET /abac/pdp/health should return 403', async () => { + await request.get(`${v1}/abac/pdp/health`).set(credentials).expect(403); + }); + }); + + describe('without view-abac-admin-audit', () => { + before(async () => { + await updatePermission('view-abac-admin-audit', []); + }); + + after(async () => { + await updatePermission('view-abac-admin-audit', ['admin']); + }); + + it('GET /abac/audit should return 403', async () => { + await request.get(`${v1}/abac/audit`).set(credentials).expect(403); + }); + }); + }); }); describe('Attribute Definition - Validations & CRUD', () => { @@ -2586,7 +2722,13 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I const healthy = await mockServerHealthy(); expect(healthy, 'mock-server is not reachable — ensure it is running').to.be.true; - await updatePermission('abac-management', ['admin']); + await Promise.all([ + updatePermission('abac-management', ['admin']), + updatePermission('manage-abac-admin-settings', ['admin']), + updatePermission('manage-abac-admin-room-attributes', ['admin']), + updatePermission('manage-abac-admin-rooms', ['admin']), + updatePermission('view-abac-admin-audit', ['admin']), + ]); await updateSetting('ABAC_Enabled', true); await updateSetting('ABAC_PDP_Type', 'virtru'); await Promise.all([ diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index b07442bc90a9f..32f43dcc56245 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -91,6 +91,14 @@ "ABAC_Update_room_confirmation_modal_title": "Update ABAC room", "ABAC_Update_room_content": "{{roomName}} is currently ABAC-managed. Changes may alter who can access this room.", "abac-management": "Manage ABAC configuration", + "manage-abac-admin-settings": "Manage ABAC settings", + "manage-abac-admin-settings_description": "Permission to view and modify ABAC settings", + "manage-abac-admin-room-attributes": "Manage ABAC room attributes", + "manage-abac-admin-room-attributes_description": "Permission to view, create, edit and delete ABAC attribute definitions", + "manage-abac-admin-rooms": "Manage ABAC rooms", + "manage-abac-admin-rooms_description": "Permission to view rooms and assign or remove ABAC attributes on them", + "view-abac-admin-audit": "View ABAC audit log", + "view-abac-admin-audit_description": "Permission to view the ABAC audit log", "abac_removed_user_from_the_room": "was removed by ABAC", "ABAC_No_attributes": "No Attributes", "ABAC_No_attributes_description": "Create custom characteristics to determine who can access a room.",