diff --git a/.changeset/loud-chefs-complain.md b/.changeset/loud-chefs-complain.md new file mode 100644 index 0000000000000..ffbb840f48af2 --- /dev/null +++ b/.changeset/loud-chefs-complain.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes `create-p` and `create-c` permissions not being applyed in teams creation diff --git a/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateChannelModal.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateChannelModal.tsx index f49c23719bb80..0f194a87a6b81 100644 --- a/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateChannelModal.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateChannelModal.tsx @@ -22,20 +22,14 @@ import { ModalFooterControllers, } from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { - useSetting, - useTranslation, - useEndpoint, - usePermission, - useToastMessageDispatch, - usePermissionWithScopedRoles, -} from '@rocket.chat/ui-contexts'; +import { useSetting, useTranslation, useEndpoint, useToastMessageDispatch, usePermissionWithScopedRoles } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement } from 'react'; import { useId, useEffect, useMemo } from 'react'; import { useForm, Controller } from 'react-hook-form'; import { useEncryptedRoomDescription } from './useEncryptedRoomDescription'; import UserAutoCompleteMultipleFederated from '../../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; +import { useCreateChannelTypePermission } from '../../../hooks/useCreateChannelTypePermission'; import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; import { goToRoomById } from '../../../lib/utils/goToRoomById'; @@ -75,8 +69,6 @@ const CreateChannelModal = ({ teamId = '', onClose, reload }: CreateChannelModal const federationEnabled = useSetting('Federation_Matrix_enabled', false); const e2eEnabledForPrivateByDefault = useSetting('E2E_Enabled_Default_PrivateRooms') && e2eEnabled; - const canCreateChannel = usePermission('create-c'); - const canCreatePrivateChannel = usePermission('create-p'); const getEncryptedHint = useEncryptedRoomDescription('channel'); const channelNameRegex = useMemo(() => new RegExp(`^${namesValidation}$`), [namesValidation]); @@ -89,15 +81,7 @@ const CreateChannelModal = ({ teamId = '', onClose, reload }: CreateChannelModal const dispatchToastMessage = useToastMessageDispatch(); - const canOnlyCreateOneType = useMemo(() => { - if (!canCreateChannel && canCreatePrivateChannel) { - return 'p'; - } - if (canCreateChannel && !canCreatePrivateChannel) { - return 'c'; - } - return false; - }, [canCreateChannel, canCreatePrivateChannel]); + const canOnlyCreateOneType = useCreateChannelTypePermission(); const { register, @@ -272,7 +256,7 @@ const CreateChannelModal = ({ teamId = '', onClose, reload }: CreateChannelModal id={privateId} aria-describedby={`${privateId}-hint`} ref={ref} - checked={value} + checked={canOnlyCreateOneType ? canOnlyCreateOneType === 'p' : value} disabled={!!canOnlyCreateOneType} onChange={onChange} /> diff --git a/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateTeamModal.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateTeamModal.tsx index ccb87cd8de8bd..2f2806f29d985 100644 --- a/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateTeamModal.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateTeamModal.tsx @@ -38,6 +38,7 @@ import { Controller, useForm } from 'react-hook-form'; import { useEncryptedRoomDescription } from './useEncryptedRoomDescription'; import UserAutoCompleteMultiple from '../../../components/UserAutoCompleteMultiple'; +import { useCreateChannelTypePermission } from '../../../hooks/useCreateChannelTypePermission'; import { goToRoomById } from '../../../lib/utils/goToRoomById'; type CreateTeamModalInputs = { @@ -76,6 +77,8 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => { return new RegExp(`^${namesValidation}$`); }, [allowSpecialNames, namesValidation]); + const canOnlyCreateOneType = useCreateChannelTypePermission(); + const validateTeamName = async (name: string): Promise => { if (!name) { return; @@ -100,7 +103,7 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => { formState: { errors, isSubmitting }, } = useForm({ defaultValues: { - isPrivate: true, + isPrivate: canOnlyCreateOneType ? canOnlyCreateOneType === 'p' : true, readOnly: false, encrypted: (e2eEnabledForPrivateByDefault as boolean) ?? false, broadcast: false, @@ -244,7 +247,14 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => { control={control} name='isPrivate' render={({ field: { onChange, value, ref } }): ReactElement => ( - + )} /> diff --git a/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useCreateNewItems.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useCreateNewItems.tsx index 7ac76f3f7815e..9bbc20a0fd0f9 100644 --- a/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useCreateNewItems.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useCreateNewItems.tsx @@ -63,6 +63,6 @@ export const useCreateNewItems = (): GenericMenuItemProps[] => { ...(canCreateDirectMessages ? [createDirectMessageItem] : []), ...(canCreateDiscussion && discussionEnabled ? [createDiscussionItem] : []), ...(canCreateChannel ? [createChannelItem] : []), - ...(canCreateTeam ? [createTeamItem] : []), + ...(canCreateTeam && canCreateChannel ? [createTeamItem] : []), ]; }; diff --git a/apps/meteor/client/hooks/useCreateChannelTypePermission.ts b/apps/meteor/client/hooks/useCreateChannelTypePermission.ts new file mode 100644 index 0000000000000..9a5184b39146d --- /dev/null +++ b/apps/meteor/client/hooks/useCreateChannelTypePermission.ts @@ -0,0 +1,42 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { usePermission } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +/** + * Determines if a user's permissions restrict them to creating only one type of channel. + * + * This hook checks a user's permissions for creating public and private channels, + * either globally or within a specific team. It returns a string indicating the + * single channel type they can create, or `false` if they can create both or neither. + * + * @param {string} [teamRoomId] The optional ID of the main team room to check for team-specific permissions. + * @returns {'c' | 'p' | false} A string ('c' or 'p') if the user can only create one channel type, otherwise `false`. + */ +export const useCreateChannelTypePermission = (teamRoomId?: IRoom['_id']) => { + const canCreateChannel = usePermission('create-c'); + const canCreatePrivateChannel = usePermission('create-p'); + + const canCreateTeamChannel = usePermission('create-team-channel', teamRoomId); + const canCreateTeamGroup = usePermission('create-team-group', teamRoomId); + + return useMemo(() => { + if (teamRoomId) { + if (!canCreateTeamChannel && canCreateTeamGroup) { + return 'p'; + } + + if (canCreateTeamChannel && !canCreateTeamGroup) { + return 'c'; + } + } + + if (!canCreateChannel && canCreatePrivateChannel) { + return 'p'; + } + + if (canCreateChannel && !canCreatePrivateChannel) { + return 'c'; + } + return false; + }, [canCreateChannel, canCreatePrivateChannel, canCreateTeamChannel, canCreateTeamGroup, teamRoomId]); +}; diff --git a/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx b/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx index dd2a621bd6bf6..9871ba3986e47 100644 --- a/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx +++ b/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx @@ -23,19 +23,13 @@ import { ModalFooterControllers, } from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { - useSetting, - useTranslation, - useEndpoint, - usePermission, - useToastMessageDispatch, - usePermissionWithScopedRoles, -} from '@rocket.chat/ui-contexts'; +import { useSetting, useTranslation, useEndpoint, useToastMessageDispatch, usePermissionWithScopedRoles } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement } from 'react'; import { useId, useEffect, useMemo } from 'react'; import { useForm, Controller } from 'react-hook-form'; import UserAutoCompleteMultipleFederated from '../../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; +import { useCreateChannelTypePermission } from '../../../hooks/useCreateChannelTypePermission'; import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; import { goToRoomById } from '../../../lib/utils/goToRoomById'; import { useEncryptedRoomDescription } from '../hooks/useEncryptedRoomDescription'; @@ -77,8 +71,6 @@ const CreateChannelModal = ({ teamId = '', mainRoom, onClose, reload }: CreateCh const federationEnabled = useSetting('Federation_Matrix_enabled', false); const e2eEnabledForPrivateByDefault = useSetting('E2E_Enabled_Default_PrivateRooms') && e2eEnabled; - const canCreateChannel = usePermission('create-c'); - const canCreateGroup = usePermission('create-p'); const getEncryptedHint = useEncryptedRoomDescription('channel'); const channelNameRegex = useMemo(() => new RegExp(`^${namesValidation}$`), [namesValidation]); @@ -89,20 +81,9 @@ const CreateChannelModal = ({ teamId = '', mainRoom, onClose, reload }: CreateCh const createChannel = useEndpoint('POST', '/v1/channels.create'); const createPrivateChannel = useEndpoint('POST', '/v1/groups.create'); - const canCreateTeamChannel = usePermission('create-team-channel', mainRoom?._id); - const canCreateTeamGroup = usePermission('create-team-group', mainRoom?._id); - const dispatchToastMessage = useToastMessageDispatch(); - const canOnlyCreateOneType = useMemo(() => { - if ((!teamId && !canCreateChannel && canCreateGroup) || (teamId && !canCreateTeamChannel && canCreateTeamGroup)) { - return 'p'; - } - if ((!teamId && canCreateChannel && !canCreateGroup) || (teamId && canCreateTeamChannel && !canCreateTeamGroup)) { - return 'c'; - } - return false; - }, [canCreateChannel, canCreateGroup, canCreateTeamChannel, canCreateTeamGroup, teamId]); + const canOnlyCreateOneType = useCreateChannelTypePermission(mainRoom?._id); const { register, diff --git a/apps/meteor/client/sidebar/header/CreateTeam/CreateTeamModal.tsx b/apps/meteor/client/sidebar/header/CreateTeam/CreateTeamModal.tsx index 3d553ca3230cc..d6eae08bb3526 100644 --- a/apps/meteor/client/sidebar/header/CreateTeam/CreateTeamModal.tsx +++ b/apps/meteor/client/sidebar/header/CreateTeam/CreateTeamModal.tsx @@ -34,6 +34,7 @@ import { useId, memo, useEffect, useMemo } from 'react'; import { Controller, useForm } from 'react-hook-form'; import UserAutoCompleteMultiple from '../../../components/UserAutoCompleteMultiple'; +import { useCreateChannelTypePermission } from '../../../hooks/useCreateChannelTypePermission'; import { goToRoomById } from '../../../lib/utils/goToRoomById'; import { useEncryptedRoomDescription } from '../hooks/useEncryptedRoomDescription'; @@ -68,6 +69,8 @@ const CreateTeamModal = ({ onClose }: { onClose: () => void }): ReactElement => return new RegExp(`^${namesValidation}$`); }, [allowSpecialNames, namesValidation]); + const canOnlyCreateOneType = useCreateChannelTypePermission(); + const validateTeamName = async (name: string): Promise => { if (!name) { return; @@ -92,7 +95,7 @@ const CreateTeamModal = ({ onClose }: { onClose: () => void }): ReactElement => formState: { errors, isSubmitting }, } = useForm({ defaultValues: { - isPrivate: true, + isPrivate: canOnlyCreateOneType ? canOnlyCreateOneType === 'p' : true, readOnly: false, encrypted: (e2eEnabledForPrivateByDefault as boolean) ?? false, broadcast: false, @@ -228,7 +231,14 @@ const CreateTeamModal = ({ onClose }: { onClose: () => void }): ReactElement => control={control} name='isPrivate' render={({ field: { onChange, value, ref } }): ReactElement => ( - + )} /> diff --git a/apps/meteor/client/sidebar/header/actions/hooks/useCreateRoomItems.tsx b/apps/meteor/client/sidebar/header/actions/hooks/useCreateRoomItems.tsx index 1bc64d1ba8d11..8092e0639cb28 100644 --- a/apps/meteor/client/sidebar/header/actions/hooks/useCreateRoomItems.tsx +++ b/apps/meteor/client/sidebar/header/actions/hooks/useCreateRoomItems.tsx @@ -63,6 +63,6 @@ export const useCreateRoomItems = (): GenericMenuItemProps[] => { ...(canCreateDirectMessages ? [createDirectMessageItem] : []), ...(canCreateDiscussion && discussionEnabled ? [createDiscussionItem] : []), ...(canCreateChannel ? [createChannelItem] : []), - ...(canCreateTeam ? [createTeamItem] : []), + ...(canCreateTeam && canCreateChannel ? [createTeamItem] : []), ]; }; diff --git a/apps/meteor/tests/e2e/team-management.spec.ts b/apps/meteor/tests/e2e/team-management.spec.ts index 3301b0fd52e11..d5e621097aed7 100644 --- a/apps/meteor/tests/e2e/team-management.spec.ts +++ b/apps/meteor/tests/e2e/team-management.spec.ts @@ -7,6 +7,71 @@ import { expect, test } from './utils/test'; test.use({ storageState: Users.admin.state }); +test.describe('teams-management-permissions', () => { + let poHomeTeam: HomeTeam; + + test.beforeEach(async ({ page }) => { + poHomeTeam = new HomeTeam(page); + + await page.goto('/home'); + }); + + test.afterEach(async ({ api }) => { + await api.post('/permissions.update', { + permissions: [ + { _id: 'create-p', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'create-c', roles: ['admin', 'owner', 'moderator'] }, + ], + }); + }); + + test('should not allow to create public team if user does not have the create-c permission', async ({ api }) => { + expect( + ( + await api.post('/permissions.update', { + permissions: [{ _id: 'create-c', roles: [] }], + }) + ).status(), + ).toBe(200); + + await poHomeTeam.sidenav.openNewByLabel('Team'); + + await expect(poHomeTeam.textPrivate).toBeDisabled(); + await expect(poHomeTeam.textPrivate).toBeChecked(); + }); + + test('should not allow to create private team if user does not have the create-p permission', async ({ api }) => { + expect( + ( + await api.post('/permissions.update', { + permissions: [{ _id: 'create-p', roles: [] }], + }) + ).status(), + ).toBe(200); + + await poHomeTeam.sidenav.openNewByLabel('Team'); + + await expect(poHomeTeam.textPrivate).toBeDisabled(); + await expect(poHomeTeam.textPrivate).not.toBeChecked(); + }); + + test('should not allow to create team if user does not have both create-p and create-c permissions', async ({ api, page }) => { + expect( + ( + await api.post('/permissions.update', { + permissions: [ + { _id: 'create-p', roles: [] }, + { _id: 'create-c', roles: [] }, + ], + }) + ).status(), + ).toBe(200); + + await poHomeTeam.sidenav.btnCreateNew.click(); + await expect(page.locator(`role=menuitem[name="Team"]`)).not.toBeVisible(); + }); +}); + test.describe.serial('teams-management', () => { let poHomeTeam: HomeTeam; let targetChannel: string; @@ -26,6 +91,8 @@ test.describe.serial('teams-management', () => { { _id: 'move-room-to-team', roles: ['admin', 'owner', 'moderator'] }, { _id: 'create-team-channel', roles: ['admin', 'owner', 'moderator'] }, { _id: 'create-team-group', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'create-c', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'create-p', roles: ['admin', 'owner', 'moderator'] }, { _id: 'delete-team-channel', roles: ['admin', 'owner', 'moderator'] }, { _id: 'delete-team-group', roles: ['admin', 'owner', 'moderator'] }, ],