Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/loud-chefs-complain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Fixes `create-p` and `create-c` permissions not being applyed in teams creation
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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]);
Expand All @@ -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,
Expand Down Expand Up @@ -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}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -76,6 +77,8 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => {
return new RegExp(`^${namesValidation}$`);
}, [allowSpecialNames, namesValidation]);

const canOnlyCreateOneType = useCreateChannelTypePermission();

const validateTeamName = async (name: string): Promise<string | undefined> => {
if (!name) {
return;
Expand All @@ -100,7 +103,7 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => {
formState: { errors, isSubmitting },
} = useForm<CreateTeamModalInputs>({
defaultValues: {
isPrivate: true,
isPrivate: canOnlyCreateOneType ? canOnlyCreateOneType === 'p' : true,
readOnly: false,
encrypted: (e2eEnabledForPrivateByDefault as boolean) ?? false,
broadcast: false,
Expand Down Expand Up @@ -244,7 +247,14 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => {
control={control}
name='isPrivate'
render={({ field: { onChange, value, ref } }): ReactElement => (
<ToggleSwitch id={privateId} aria-describedby={`${privateId}-hint`} onChange={onChange} checked={value} ref={ref} />
<ToggleSwitch
id={privateId}
aria-describedby={`${privateId}-hint`}
onChange={onChange}
checked={canOnlyCreateOneType ? canOnlyCreateOneType === 'p' : value}
disabled={!!canOnlyCreateOneType}
ref={ref}
/>
)}
/>
</FieldRow>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,6 @@ export const useCreateNewItems = (): GenericMenuItemProps[] => {
...(canCreateDirectMessages ? [createDirectMessageItem] : []),
...(canCreateDiscussion && discussionEnabled ? [createDiscussionItem] : []),
...(canCreateChannel ? [createChannelItem] : []),
...(canCreateTeam ? [createTeamItem] : []),
...(canCreateTeam && canCreateChannel ? [createTeamItem] : []),
];
};
42 changes: 42 additions & 0 deletions apps/meteor/client/hooks/useCreateChannelTypePermission.ts
Original file line number Diff line number Diff line change
@@ -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]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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]);
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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<string | undefined> => {
if (!name) {
return;
Expand All @@ -92,7 +95,7 @@ const CreateTeamModal = ({ onClose }: { onClose: () => void }): ReactElement =>
formState: { errors, isSubmitting },
} = useForm<CreateTeamModalInputs>({
defaultValues: {
isPrivate: true,
isPrivate: canOnlyCreateOneType ? canOnlyCreateOneType === 'p' : true,
readOnly: false,
encrypted: (e2eEnabledForPrivateByDefault as boolean) ?? false,
broadcast: false,
Expand Down Expand Up @@ -228,7 +231,14 @@ const CreateTeamModal = ({ onClose }: { onClose: () => void }): ReactElement =>
control={control}
name='isPrivate'
render={({ field: { onChange, value, ref } }): ReactElement => (
<ToggleSwitch id={privateId} aria-describedby={`${privateId}-hint`} onChange={onChange} checked={value} ref={ref} />
<ToggleSwitch
id={privateId}
aria-describedby={`${privateId}-hint`}
onChange={onChange}
checked={canOnlyCreateOneType ? canOnlyCreateOneType === 'p' : value}
disabled={!!canOnlyCreateOneType}
ref={ref}
/>
)}
/>
</FieldRow>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,6 @@ export const useCreateRoomItems = (): GenericMenuItemProps[] => {
...(canCreateDirectMessages ? [createDirectMessageItem] : []),
...(canCreateDiscussion && discussionEnabled ? [createDiscussionItem] : []),
...(canCreateChannel ? [createChannelItem] : []),
...(canCreateTeam ? [createTeamItem] : []),
...(canCreateTeam && canCreateChannel ? [createTeamItem] : []),
];
};
67 changes: 67 additions & 0 deletions apps/meteor/tests/e2e/team-management.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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'] },
],
Expand Down
Loading