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.",