diff --git a/.changeset/gentle-kings-greet.md b/.changeset/gentle-kings-greet.md new file mode 100644 index 0000000000000..eef3a1538a81f --- /dev/null +++ b/.changeset/gentle-kings-greet.md @@ -0,0 +1,11 @@ +--- +"@rocket.chat/meteor": major +--- + +Adds a new set of permissions to provide a more granular control for the creation and deletion of rooms within teams + - `create-team-channel`: controls the creations of public rooms within teams, it is checked within the team's main room scope and overrides the global `create-c` permission check. That is, granting this permission to a role allows users to create channels in teams even if they do not have the permission to create channels globally; + - `create-team-group`: controls the creations of private rooms within teams, it is checked within the team's main room scope and overrides the global `create-p` permission check. That is, granting this permission to a role allows users to create groups in teams even if they do not have the permission to create groups globally; + - `delete-team-channel`: controls the deletion of public rooms within teams, it is checked within the team's main room scope and complements the global `delete-c` permission check. That is, users must have both permissions (`delete-c` in the channel scope and `delete-team-channel` in its team scope) in order to be able to delete a channel in a team; + - `delete-team-group`: controls the deletion of private rooms within teams, it is checked within the team's main room scope and complements the global `delete-p` permission check. That is, users must have both permissions (`delete-p` in the group scope and `delete-team-group` in its team scope) in order to be able to delete a group in a team;; + +Renames `add-team-channel` permission (used for adding existing rooms to teams) to `move-room-to-team`, since it is applied to groups and channels. \ No newline at end of file diff --git a/apps/meteor/app/api/server/api.ts b/apps/meteor/app/api/server/api.ts index fef2f2165ef22..76f9cc44de7a2 100644 --- a/apps/meteor/app/api/server/api.ts +++ b/apps/meteor/app/api/server/api.ts @@ -1021,6 +1021,7 @@ export const API: { members?: { key: string; value?: string[] }; customFields?: { key: string; value?: string }; teams?: { key: string; value?: string[] }; + teamId?: { key: string; value?: string }; }) => Promise; execute: ( userId: string, diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts index de74cbba503ae..006e721c62e4f 100644 --- a/apps/meteor/app/api/server/v1/channels.ts +++ b/apps/meteor/app/api/server/v1/channels.ts @@ -643,8 +643,15 @@ async function createChannelValidator(params: { members?: { key: string; value?: string[] }; customFields?: { key: string; value?: string }; teams?: { key: string; value?: string[] }; + teamId?: { key: string; value?: string }; }) { - if (!(await hasPermissionAsync(params.user.value, 'create-c'))) { + const teamId = params.teamId?.value; + + const team = teamId && (await Team.getInfoById(teamId)); + if ( + (!teamId && !(await hasPermissionAsync(params.user.value, 'create-c'))) || + (teamId && team && !(await hasPermissionAsync(params.user.value, 'create-team-channel', team.roomId))) + ) { throw new Error('unauthorized'); } @@ -725,6 +732,10 @@ API.v1.addRoute( value: bodyParams.teams, key: 'teams', }, + teamId: { + value: bodyParams.extraData?.teamId, + key: 'teamId', + }, }); } catch (e: any) { if (e.message === 'unauthorized') { diff --git a/apps/meteor/app/api/server/v1/teams.ts b/apps/meteor/app/api/server/v1/teams.ts index 63132d811b1c6..f5a9b0c52d5e0 100644 --- a/apps/meteor/app/api/server/v1/teams.ts +++ b/apps/meteor/app/api/server/v1/teams.ts @@ -173,7 +173,7 @@ API.v1.addRoute( return API.v1.failure('team-does-not-exist'); } - if (!(await hasPermissionAsync(this.userId, 'add-team-channel', team.roomId))) { + if (!(await hasPermissionAsync(this.userId, 'move-room-to-team', team.roomId))) { return API.v1.unauthorized('error-no-permission-team-channel'); } diff --git a/apps/meteor/app/authorization/server/constant/permissions.ts b/apps/meteor/app/authorization/server/constant/permissions.ts index 8ae0001609ae7..12ed3eb8d06c7 100644 --- a/apps/meteor/app/authorization/server/constant/permissions.ts +++ b/apps/meteor/app/authorization/server/constant/permissions.ts @@ -193,7 +193,11 @@ export const permissions = [ { _id: 'edit-team', roles: ['admin', 'owner'] }, { _id: 'add-team-member', roles: ['admin', 'owner', 'moderator'] }, { _id: 'edit-team-member', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'add-team-channel', roles: ['admin', 'owner', 'moderator'] }, + { _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: 'delete-team-channel', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'delete-team-group', roles: ['admin', 'owner', 'moderator'] }, { _id: 'edit-team-channel', roles: ['admin', 'owner', 'moderator'] }, { _id: 'remove-team-channel', roles: ['admin', 'owner', 'moderator'] }, { _id: 'view-all-team-channels', roles: ['admin', 'owner'] }, diff --git a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts index 04e8fdbaf186d..42adc75cb563a 100644 --- a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts +++ b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts @@ -102,19 +102,38 @@ const validators: RoomSettingsValidators = { return; } - if (value === 'c' && !(await hasPermissionAsync(userId, 'create-c'))) { + if (value === 'c' && !room.teamId && !(await hasPermissionAsync(userId, 'create-c'))) { throw new Meteor.Error('error-action-not-allowed', 'Changing a private group to a public channel is not allowed', { method: 'saveRoomSettings', action: 'Change_Room_Type', }); } - if (value === 'p' && !(await hasPermissionAsync(userId, 'create-p'))) { + if (value === 'p' && !room.teamId && !(await hasPermissionAsync(userId, 'create-p'))) { throw new Meteor.Error('error-action-not-allowed', 'Changing a public channel to a private room is not allowed', { method: 'saveRoomSettings', action: 'Change_Room_Type', }); } + + if (!room.teamId) { + return; + } + const team = await Team.getInfoById(room.teamId); + + if (value === 'c' && !(await hasPermissionAsync(userId, 'create-team-channel', team?.roomId))) { + throw new Meteor.Error('error-action-not-allowed', `Changing a team's private group to a public channel is not allowed`, { + method: 'saveRoomSettings', + action: 'Change_Room_Type', + }); + } + + if (value === 'p' && !(await hasPermissionAsync(userId, 'create-team-group', team?.roomId))) { + throw new Meteor.Error('error-action-not-allowed', `Changing a team's public channel to a private room is not allowed`, { + method: 'saveRoomSettings', + action: 'Change_Room_Type', + }); + } }, async encrypted({ userId, value, room, rid }) { if (value !== room.encrypted) { diff --git a/apps/meteor/app/lib/server/methods/createChannel.ts b/apps/meteor/app/lib/server/methods/createChannel.ts index 8903583fb2423..3b106ad5acff4 100644 --- a/apps/meteor/app/lib/server/methods/createChannel.ts +++ b/apps/meteor/app/lib/server/methods/createChannel.ts @@ -1,6 +1,6 @@ -import type { ICreatedRoom } from '@rocket.chat/core-typings'; +import type { ICreatedRoom, ITeam } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Users } from '@rocket.chat/models'; +import { Users, Team } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -40,9 +40,18 @@ export const createChannelMethod = async ( throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'createChannel' }); } - if (!(await hasPermissionAsync(userId, 'create-c'))) { + if (extraData.teamId) { + const team = await Team.findOneById>(extraData.teamId, { projection: { roomId: 1 } }); + if (!team) { + throw new Meteor.Error('error-team-not-found', 'The "teamId" param provided does not match any team', { method: 'createChannel' }); + } + if (!(await hasPermissionAsync(userId, 'create-team-channel', team.roomId))) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'createChannel' }); + } + } else if (!(await hasPermissionAsync(userId, 'create-c'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'createChannel' }); } + return createRoom('c', name, user, members, excludeSelf, readOnly, { customFields, ...extraData, diff --git a/apps/meteor/app/lib/server/methods/createPrivateGroup.ts b/apps/meteor/app/lib/server/methods/createPrivateGroup.ts index 1efa98948ebf6..f07e37109901c 100644 --- a/apps/meteor/app/lib/server/methods/createPrivateGroup.ts +++ b/apps/meteor/app/lib/server/methods/createPrivateGroup.ts @@ -1,6 +1,6 @@ -import type { ICreatedRoom, IUser } from '@rocket.chat/core-typings'; +import type { ICreatedRoom, IUser, ITeam } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Users } from '@rocket.chat/models'; +import { Users, Team } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -25,8 +25,8 @@ export const createPrivateGroupMethod = async ( name: string, members: string[], readOnly = false, - customFields = {}, - extraData = {}, + customFields: Record = {}, + extraData: Record = {}, excludeSelf = false, ): Promise< ICreatedRoom & { @@ -36,7 +36,17 @@ export const createPrivateGroupMethod = async ( check(name, String); check(members, Match.Optional([String])); - if (!(await hasPermissionAsync(user._id, 'create-p'))) { + if (extraData.teamId) { + const team = await Team.findOneById>(extraData.teamId, { projection: { roomId: 1 } }); + if (!team) { + throw new Meteor.Error('error-team-not-found', 'The "teamId" param provided does not match any team', { + method: 'createPrivateGroup', + }); + } + if (!(await hasPermissionAsync(user._id, 'create-team-group', team.roomId))) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'createPrivateGroup' }); + } + } else if (!(await hasPermissionAsync(user._id, 'create-p'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'createPrivateGroup' }); } diff --git a/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx b/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx index 03e1122da7739..50b3c85ea6345 100644 --- a/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx +++ b/apps/meteor/client/sidebar/header/CreateChannel/CreateChannelModal.tsx @@ -1,3 +1,4 @@ +import type { IRoom } from '@rocket.chat/core-typings'; import { Box, Modal, @@ -36,6 +37,7 @@ import { useEncryptedRoomDescription } from '../hooks/useEncryptedRoomDescriptio type CreateChannelModalProps = { teamId?: string; + mainRoom?: IRoom; onClose: () => void; reload?: () => void; }; @@ -61,7 +63,7 @@ const getFederationHintKey = (licenseModule: ReturnType { +const CreateChannelModal = ({ teamId = '', mainRoom, onClose, reload }: CreateChannelModalProps): ReactElement => { const t = useTranslation(); const canSetReadOnly = usePermissionWithScopedRoles('set-readonly', ['owner']); const e2eEnabled = useSetting('E2E_Enable'); @@ -71,7 +73,7 @@ const CreateChannelModal = ({ teamId = '', onClose, reload }: CreateChannelModal const e2eEnabledForPrivateByDefault = useSetting('E2E_Enabled_Default_PrivateRooms') && e2eEnabled; const canCreateChannel = usePermission('create-c'); - const canCreatePrivateChannel = usePermission('create-p'); + const canCreateGroup = usePermission('create-p'); const getEncryptedHint = useEncryptedRoomDescription('channel'); const channelNameRegex = useMemo(() => new RegExp(`^${namesValidation}$`), [namesValidation]); @@ -82,17 +84,20 @@ const CreateChannelModal = ({ teamId = '', onClose, reload }: CreateChannelModal 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 (!canCreateChannel && canCreatePrivateChannel) { + if ((!teamId && !canCreateChannel && canCreateGroup) || (teamId && !canCreateTeamChannel && canCreateTeamGroup)) { return 'p'; } - if (canCreateChannel && !canCreatePrivateChannel) { + if ((!teamId && canCreateChannel && !canCreateGroup) || (teamId && canCreateTeamChannel && !canCreateTeamGroup)) { return 'c'; } return false; - }, [canCreateChannel, canCreatePrivateChannel]); + }, [canCreateChannel, canCreateGroup, canCreateTeamChannel, canCreateTeamGroup, teamId]); const { register, @@ -267,7 +272,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/views/hooks/roomActions/useDeleteRoom.tsx b/apps/meteor/client/views/hooks/roomActions/useDeleteRoom.tsx index 48b6507a331d6..ef46a0f68cedf 100644 --- a/apps/meteor/client/views/hooks/roomActions/useDeleteRoom.tsx +++ b/apps/meteor/client/views/hooks/roomActions/useDeleteRoom.tsx @@ -2,7 +2,7 @@ import type { IRoom, RoomAdminFieldsType } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useSetModal, useToastMessageDispatch, useRouter, usePermission, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQuery } from '@tanstack/react-query'; import React from 'react'; import GenericModal from '../../../components/GenericModal'; @@ -13,14 +13,25 @@ export const useDeleteRoom = (room: IRoom | Pick, { const router = useRouter(); const setModal = useSetModal(); const dispatchToastMessage = useToastMessageDispatch(); - const hasPermissionToDelete = usePermission(`delete-${room.t}`, room._id); - const canDeleteRoom = isRoomFederated(room) ? false : hasPermissionToDelete; // eslint-disable-next-line no-nested-ternary const roomType = 'prid' in room ? 'discussion' : room.teamId && room.teamMain ? 'team' : 'channel'; const isAdminRoute = router.getRouteName() === 'admin-rooms'; const deleteRoomEndpoint = useEndpoint('POST', '/v1/rooms.delete'); const deleteTeamEndpoint = useEndpoint('POST', '/v1/teams.delete'); + const teamsInfoEndpoint = useEndpoint('GET', '/v1/teams.info'); + + const teamId = room.teamId || ''; + const { data: teamInfoData } = useQuery(['teamId', teamId], async () => teamsInfoEndpoint({ teamId }), { + keepPreviousData: true, + retry: false, + enabled: room.teamId !== '', + }); + + const hasPermissionToDeleteRoom = usePermission(`delete-${room.t}`, room._id); + const hasPermissionToDeleteTeamRoom = usePermission(`delete-team-${room.t === 'c' ? 'channel' : 'group'}`, teamInfoData?.teamInfo.roomId); + const isTeamRoom = room.teamId; + const canDeleteRoom = isRoomFederated(room) ? false : hasPermissionToDeleteRoom && (!isTeamRoom || hasPermissionToDeleteTeamRoom); const deleteRoomMutation = useMutation({ mutationFn: deleteRoomEndpoint, diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomPermissions.ts b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomPermissions.ts index 87640d1689c11..7f9043bfb8243 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomPermissions.ts +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomPermissions.ts @@ -1,5 +1,6 @@ import type { IRoom, IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; -import { usePermission, useAtLeastOnePermission, useRole } from '@rocket.chat/ui-contexts'; +import { usePermission, useAtLeastOnePermission, useRole, useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; import { useMemo } from 'react'; import { E2EEState } from '../../../../../../app/e2e/client/E2EEState'; @@ -12,11 +13,28 @@ const getCanChangeType = (room: IRoom | IRoomWithRetentionPolicy, canCreateChann export const useEditRoomPermissions = (room: IRoom | IRoomWithRetentionPolicy) => { const isAdmin = useRole('admin'); - const canCreateChannel = usePermission('create-c'); - const canCreateGroup = usePermission('create-p'); const e2eeState = useE2EEState(); const isE2EEReady = e2eeState === E2EEState.READY || e2eeState === E2EEState.SAVE_PASSWORD; - const canChangeType = getCanChangeType(room, canCreateChannel, canCreateGroup, isAdmin); + const canCreateChannel = usePermission('create-c'); + const canCreateGroup = usePermission('create-p'); + const teamsInfoEndpoint = useEndpoint('GET', '/v1/teams.info'); + + const teamId = room.teamId || ''; + const { data: teamInfoData } = useQuery(['teamId', teamId], async () => teamsInfoEndpoint({ teamId }), { + keepPreviousData: true, + retry: false, + enabled: room.teamId !== '', + }); + + const canCreateTeamChannel = usePermission('create-team-channel', teamInfoData?.teamInfo.roomId); + const canCreateTeamGroup = usePermission('create-team-group', teamInfoData?.teamInfo.roomId); + + const canChangeType = getCanChangeType( + room, + teamId ? canCreateTeamChannel : canCreateChannel, + teamId ? canCreateTeamGroup : canCreateGroup, + isAdmin, + ); const canSetReadOnly = usePermission('set-readonly', room._id); const canSetReactWhenReadOnly = usePermission('set-react-when-readonly', room._id); const canEditRoomRetentionPolicy = usePermission('edit-room-retention-policy', room._id); diff --git a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItem.tsx b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItem.tsx index 01f92d38488cc..5ed3056420cfa 100644 --- a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItem.tsx +++ b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItem.tsx @@ -34,9 +34,11 @@ const TeamsChannelItem = ({ room, mainRoom, onClickView, reload }: TeamsChannelI const [showButton, setShowButton] = useState(); - const canRemoveTeamChannel = usePermission('remove-team-channel', rid); - const canEditTeamChannel = usePermission('edit-team-channel', rid); - const canDeleteTeamChannel = usePermission(type === 'c' ? 'delete-c' : 'delete-p', rid); + const canRemoveTeamChannel = usePermission('remove-team-channel', mainRoom._id); + const canEditTeamChannel = usePermission('edit-team-channel', mainRoom._id); + const canDeleteChannel = usePermission(`delete-${type}`, rid); + const canDeleteTeamChannel = usePermission(`delete-team-${type === 'c' ? 'channel' : 'group'}`, mainRoom._id); + const canDelete = canDeleteChannel && canDeleteTeamChannel; const isReduceMotionEnabled = usePrefersReducedMotion(); const handleMenuEvent = { @@ -67,7 +69,7 @@ const TeamsChannelItem = ({ room, mainRoom, onClickView, reload }: TeamsChannelI )} - {(canRemoveTeamChannel || canEditTeamChannel || canDeleteTeamChannel) && ( + {(canRemoveTeamChannel || canEditTeamChannel || canDelete) && ( {showButton ? : } diff --git a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelsWithData.tsx b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelsWithData.tsx index a886479b535b0..731a4fbd1a6e5 100644 --- a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelsWithData.tsx +++ b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelsWithData.tsx @@ -1,6 +1,6 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { useLocalStorage, useDebouncedValue, useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, usePermission } from '@rocket.chat/ui-contexts'; +import { useSetModal, usePermission, useAtLeastOnePermission } from '@rocket.chat/ui-contexts'; import React, { useCallback, useMemo, useState } from 'react'; import { useRecordList } from '../../../../hooks/lists/useRecordList'; @@ -17,7 +17,8 @@ const TeamsChannelsWithData = () => { const room = useRoom(); const setModal = useSetModal(); const { closeTab } = useRoomToolbox(); - const canAddExistingTeam = usePermission('add-team-channel', room._id); + const canAddExistingRoomToTeam = usePermission('move-room-to-team', room._id); + const canCreateRoomInTeam = useAtLeastOnePermission(['create-team-channel', 'create-team-group'], room._id); const { teamId } = room; @@ -44,7 +45,7 @@ const TeamsChannelsWithData = () => { }); const handleCreateNew = useEffectEvent(() => { - setModal( setModal(null)} reload={reload} />); + setModal( setModal(null)} reload={reload} />); }); const goToRoom = useEffectEvent((room: IRoom) => { @@ -62,8 +63,8 @@ const TeamsChannelsWithData = () => { channels={items} total={total} onClickClose={closeTab} - onClickAddExisting={canAddExistingTeam && handleAddExisting} - onClickCreateNew={canAddExistingTeam && handleCreateNew} + onClickAddExisting={canAddExistingRoomToTeam && handleAddExisting} + onClickCreateNew={canCreateRoomInTeam && handleCreateNew} onClickView={goToRoom} loadMoreItems={loadMoreItems} reload={reload} diff --git a/apps/meteor/server/lib/eraseRoom.ts b/apps/meteor/server/lib/eraseRoom.ts index d67de3325eea9..143c0a31977dd 100644 --- a/apps/meteor/server/lib/eraseRoom.ts +++ b/apps/meteor/server/lib/eraseRoom.ts @@ -32,6 +32,13 @@ export async function eraseRoom(rid: string, uid: string): Promise { }); } + const team = room.teamId && (await Team.getOneById(room.teamId, { projection: { roomId: 1 } })); + if (team && !(await hasPermissionAsync(uid, `delete-team-${room.t === 'c' ? 'channel' : 'group'}`, team.roomId))) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { + method: 'eraseRoom', + }); + } + if (Apps.self?.isLoaded()) { const prevent = await Apps.getBridges()?.getListenerBridge().roomEvent(AppEvents.IPreRoomDeletePrevent, room); if (prevent) { @@ -41,8 +48,6 @@ export async function eraseRoom(rid: string, uid: string): Promise { await deleteRoom(rid); - const team = room.teamId && (await Team.getOneById(room.teamId)); - if (team) { const user = await Meteor.userAsync(); if (user) { diff --git a/apps/meteor/server/services/team/service.ts b/apps/meteor/server/services/team/service.ts index 4be96bff9866d..bdca222b246cd 100644 --- a/apps/meteor/server/services/team/service.ts +++ b/apps/meteor/server/services/team/service.ts @@ -593,9 +593,13 @@ export class TeamService extends ServiceClassInternal implements ITeamService { let teamRoomIds: string[]; if (showCanDeleteOnly) { + const canDeleteTeamChannel = await Authorization.hasPermission(userId, 'delete-team-channel', team.roomId); + const canDeleteTeamGroup = await Authorization.hasPermission(userId, 'delete-team-group', team.roomId); for await (const room of teamRooms) { - const roomType = room.t; - const canDeleteRoom = await Authorization.hasPermission(userId, roomType === 'c' ? 'delete-c' : 'delete-p', room._id); + const isPublicRoom = room.t === 'c'; + const canDeleteTeamRoom = isPublicRoom ? canDeleteTeamChannel : canDeleteTeamGroup; + const canDeleteRoom = + canDeleteTeamRoom && (await Authorization.hasPermission(userId, isPublicRoom ? 'delete-c' : 'delete-p', room._id)); room.userCanDelete = canDeleteRoom; } diff --git a/apps/meteor/server/startup/migrations/index.ts b/apps/meteor/server/startup/migrations/index.ts index c77d750b25c44..bf1cd59dbd0de 100644 --- a/apps/meteor/server/startup/migrations/index.ts +++ b/apps/meteor/server/startup/migrations/index.ts @@ -47,5 +47,6 @@ import './v311'; import './v312'; import './v313'; import './v314'; +import './v315'; export * from './xrun'; diff --git a/apps/meteor/server/startup/migrations/v315.ts b/apps/meteor/server/startup/migrations/v315.ts new file mode 100644 index 0000000000000..799d80079d931 --- /dev/null +++ b/apps/meteor/server/startup/migrations/v315.ts @@ -0,0 +1,27 @@ +import type { IPermission } from '@rocket.chat/core-typings'; +import { Permissions } from '@rocket.chat/models'; + +import { upsertPermissions } from '../../../app/authorization/server/functions/upsertPermissions'; +import { addMigration } from '../../lib/migrations'; + +addMigration({ + version: 315, + name: 'Copy roles from add-team-channel permission to new create-team-channel, create-team-group and move-room-to-team permissions', + async up() { + // Calling upsertPermissions on purpose so that the new permissions are added before the migration runs + await upsertPermissions(); + + const addTeamChannelPermission = await Permissions.findOneById>('add-team-channel', { + projection: { roles: 1 }, + }); + + if (addTeamChannelPermission) { + await Permissions.updateMany( + { _id: { $in: ['create-team-channel', 'create-team-group', 'move-room-to-team'] } }, + { $set: { roles: addTeamChannelPermission.roles } }, + ); + + await Permissions.deleteOne({ _id: 'add-team-channel' }); + } + }, +}); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-channels.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-channels.ts index 98d864598834d..b7040c9bd2794 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-channels.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-channels.ts @@ -15,6 +15,10 @@ export class HomeFlextabChannels { return this.page.locator('button >> text="Add Existing"'); } + get btnCreateNew(): Locator { + return this.page.locator('button >> text="Create new"'); + } + get inputChannels(): Locator { return this.page.locator('#modal-root input').first(); } @@ -42,4 +46,8 @@ export class HomeFlextabChannels { .getByRole('button', { name: 'Remove', exact: true }) .click(); } + + async confirmDeleteRoom() { + return this.page.getByRole('button', { name: 'Yes, delete', exact: true }).click(); + } } diff --git a/apps/meteor/tests/e2e/team-management.spec.ts b/apps/meteor/tests/e2e/team-management.spec.ts index 690fa2b5b5dd2..8f365a84558c8 100644 --- a/apps/meteor/tests/e2e/team-management.spec.ts +++ b/apps/meteor/tests/e2e/team-management.spec.ts @@ -13,11 +13,25 @@ test.describe.serial('teams-management', () => { const targetTeam = faker.string.uuid(); const targetTeamNonPrivate = faker.string.uuid(); const targetTeamReadOnly = faker.string.uuid(); + const targetGroupNameInTeam = faker.string.uuid(); + const targetChannelNameInTeam = faker.string.uuid(); test.beforeAll(async ({ api }) => { targetChannel = await createTargetChannel(api); }); + test.afterAll(async ({ api }) => { + await api.post('/permissions.update', { + permissions: [ + { _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: 'delete-team-channel', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'delete-team-group', roles: ['admin', 'owner', 'moderator'] }, + ], + }); + }); + test.beforeEach(async ({ page }) => { poHomeTeam = new HomeTeam(page); @@ -84,7 +98,90 @@ test.describe.serial('teams-management', () => { await expect(poHomeTeam.content.getSystemMessageByText('set room to read only')).toBeVisible(); }); - test('should insert targetChannel inside targetTeam', async ({ page }) => { + test('should not allow moving room to team if move-room-to-team permission has not been granted', async ({ api }) => { + expect((await api.post('/permissions.update', { permissions: [{ _id: 'move-room-to-team', roles: ['moderator'] }] })).status()).toBe( + 200, + ); + + await poHomeTeam.sidenav.openChat(targetTeam); + await poHomeTeam.tabs.btnChannels.click(); + await expect(poHomeTeam.tabs.channels.btnAddExisting).not.toBeVisible(); + }); + + test('should not allow creating a room in a team if both create-team-channel and create-team-group permissions have not been granted', async ({ + api, + }) => { + expect( + ( + await api.post('/permissions.update', { + permissions: [ + { _id: 'create-team-channel', roles: ['moderator'] }, + { _id: 'create-team-group', roles: ['moderator'] }, + ], + }) + ).status(), + ).toBe(200); + + await poHomeTeam.sidenav.openChat(targetTeam); + await poHomeTeam.tabs.btnChannels.click(); + await expect(poHomeTeam.tabs.channels.btnCreateNew).not.toBeVisible(); + }); + + test('should allow creating a channel in a team if user has the create-team-channel permission, but not the create-team-group permission', async ({ + api, + }) => { + expect( + ( + await api.post('/permissions.update', { + permissions: [ + { _id: 'create-team-channel', roles: ['admin'] }, + { _id: 'create-team-group', roles: ['moderator'] }, + ], + }) + ).status(), + ).toBe(200); + + await poHomeTeam.sidenav.openChat(targetTeam); + await poHomeTeam.tabs.btnChannels.click(); + await expect(poHomeTeam.tabs.channels.btnCreateNew).toBeVisible(); + await poHomeTeam.tabs.channels.btnCreateNew.click(); + await poHomeTeam.sidenav.inputChannelName.type(targetChannelNameInTeam); + await expect(poHomeTeam.sidenav.checkboxPrivateChannel).not.toBeChecked(); + await expect(poHomeTeam.sidenav.checkboxPrivateChannel).toBeDisabled(); + await poHomeTeam.sidenav.btnCreate.click(); + + await expect(poHomeTeam.tabs.channels.channelsList).toContainText(targetChannelNameInTeam); + }); + + test('should allow creating a group in a team if user has the create-team-group permission, but not the create-team-channel permission', async ({ + api, + }) => { + expect( + ( + await api.post('/permissions.update', { + permissions: [ + { _id: 'create-team-group', roles: ['admin'] }, + { _id: 'create-team-channel', roles: ['moderator'] }, + ], + }) + ).status(), + ).toBe(200); + + await poHomeTeam.sidenav.openChat(targetTeam); + await poHomeTeam.tabs.btnChannels.click(); + await expect(poHomeTeam.tabs.channels.btnCreateNew).toBeVisible(); + await poHomeTeam.tabs.channels.btnCreateNew.click(); + await poHomeTeam.sidenav.inputChannelName.type(targetGroupNameInTeam); + await expect(poHomeTeam.sidenav.checkboxPrivateChannel).toBeChecked(); + await expect(poHomeTeam.sidenav.checkboxPrivateChannel).toBeDisabled(); + await poHomeTeam.sidenav.btnCreate.click(); + + await expect(poHomeTeam.tabs.channels.channelsList).toContainText(targetGroupNameInTeam); + }); + + test('should move targetChannel to targetTeam', async ({ page, api }) => { + expect((await api.post('/permissions.update', { permissions: [{ _id: 'move-room-to-team', roles: ['owner'] }] })).status()).toBe(200); + await poHomeTeam.sidenav.openChat(targetTeam); await poHomeTeam.tabs.btnChannels.click(); await poHomeTeam.tabs.channels.btnAddExisting.click(); @@ -105,7 +202,157 @@ test.describe.serial('teams-management', () => { await expect(page).toHaveURL(`/group/${targetTeam}`); }); - test('should remove targetChannel from targetTeam', async ({ page }) => { + test('should not allow removing a targetGroup from targetTeam if user does not have the remove-team-channel permission', async ({ + page, + api, + }) => { + expect((await api.post('/permissions.update', { permissions: [{ _id: 'remove-team-channel', roles: ['moderator'] }] })).status()).toBe( + 200, + ); + + await poHomeTeam.sidenav.openChat(targetTeam); + await poHomeTeam.tabs.btnChannels.click(); + await poHomeTeam.tabs.channels.openChannelOptionMoreActions(targetGroupNameInTeam); + await expect(page.getByRole('menu', { exact: true }).getByRole('menuitem', { name: 'Remove from team' })).not.toBeVisible(); + }); + + test('should allow removing a targetGroup from targetTeam if user has the remove-team-channel permission', async ({ page, api }) => { + expect((await api.post('/permissions.update', { permissions: [{ _id: 'remove-team-channel', roles: ['owner'] }] })).status()).toBe(200); + + await poHomeTeam.sidenav.openChat(targetTeam); + await poHomeTeam.tabs.btnChannels.click(); + await poHomeTeam.tabs.channels.openChannelOptionMoreActions(targetGroupNameInTeam); + await expect(page.getByRole('menu', { exact: true }).getByRole('menuitem', { name: 'Remove from team' })).toBeVisible(); + await page.getByRole('menu', { exact: true }).getByRole('menuitem', { name: 'Remove from team' }).click(); + await poHomeTeam.tabs.channels.confirmRemoveChannel(); + + await expect(poHomeTeam.tabs.channels.channelsList).not.toContainText(targetGroupNameInTeam); + }); + + test('should not allow deleting a targetGroup from targetTeam if the group owner does not have the delete-team-group permission', async ({ + page, + api, + }) => { + expect( + ( + await api.post('/permissions.update', { + permissions: [ + { _id: 'delete-team-group', roles: ['moderator'] }, + { _id: 'move-room-to-team', roles: ['owner'] }, + ], + }) + ).status(), + ).toBe(200); + + // re-add channel to team + await poHomeTeam.sidenav.openChat(targetTeam); + await poHomeTeam.tabs.btnChannels.click(); + await poHomeTeam.tabs.channels.btnAddExisting.click(); + await poHomeTeam.tabs.channels.inputChannels.fill(targetGroupNameInTeam); + await page.locator(`.rcx-option__content:has-text("${targetGroupNameInTeam}")`).click(); + await poHomeTeam.tabs.channels.btnAdd.click(); + await expect(poHomeTeam.tabs.channels.channelsList).toContainText(targetGroupNameInTeam); + + // try to delete group in team + await poHomeTeam.tabs.channels.openChannelOptionMoreActions(targetGroupNameInTeam); + await expect(page.getByRole('menu', { exact: true }).getByRole('menuitem', { name: 'Delete' })).not.toBeVisible(); + }); + + test('should allow deleting a targetGroup from targetTeam if the group owner also has the delete-team-group permission', async ({ + page, + api, + }) => { + expect((await api.post('/permissions.update', { permissions: [{ _id: 'delete-team-group', roles: ['owner'] }] })).status()).toBe(200); + + await poHomeTeam.sidenav.openChat(targetTeam); + await poHomeTeam.tabs.btnChannels.click(); + await poHomeTeam.tabs.channels.openChannelOptionMoreActions(targetGroupNameInTeam); + await expect(page.getByRole('menu', { exact: true }).getByRole('menuitem', { name: 'Delete' })).toBeVisible(); + await page.getByRole('menu', { exact: true }).getByRole('menuitem', { name: 'Delete' }).click(); + await poHomeTeam.tabs.channels.confirmDeleteRoom(); + + await poHomeTeam.sidenav.openChat(targetTeam); + await poHomeTeam.tabs.btnChannels.click(); + await expect(poHomeTeam.tabs.channels.channelsList).not.toContainText(targetGroupNameInTeam); + }); + + test('should not allow removing a targetChannel from targetTeam if user does not have the remove-team-channel permission', async ({ + page, + api, + }) => { + expect((await api.post('/permissions.update', { permissions: [{ _id: 'remove-team-channel', roles: ['moderator'] }] })).status()).toBe( + 200, + ); + + await poHomeTeam.sidenav.openChat(targetTeam); + await poHomeTeam.tabs.btnChannels.click(); + await poHomeTeam.tabs.channels.openChannelOptionMoreActions(targetChannelNameInTeam); + await expect(page.getByRole('menu', { exact: true }).getByRole('menuitem', { name: 'Remove from team' })).not.toBeVisible(); + }); + + test('should allow removing a targetChannel from targetTeam if user has the remove-team-channel permission', async ({ page, api }) => { + expect((await api.post('/permissions.update', { permissions: [{ _id: 'remove-team-channel', roles: ['owner'] }] })).status()).toBe(200); + + await poHomeTeam.sidenav.openChat(targetTeam); + await poHomeTeam.tabs.btnChannels.click(); + await poHomeTeam.tabs.channels.openChannelOptionMoreActions(targetChannelNameInTeam); + await expect(page.getByRole('menu', { exact: true }).getByRole('menuitem', { name: 'Remove from team' })).toBeVisible(); + await page.getByRole('menu', { exact: true }).getByRole('menuitem', { name: 'Remove from team' }).click(); + await poHomeTeam.tabs.channels.confirmRemoveChannel(); + + await expect(poHomeTeam.tabs.channels.channelsList).not.toContainText(targetChannelNameInTeam); + }); + + test('should not allow deleting a targetChannel from targetTeam if the channel owner does not have the delete-team-channel permission', async ({ + page, + api, + }) => { + expect( + ( + await api.post('/permissions.update', { + permissions: [ + { _id: 'delete-team-channel', roles: ['moderator'] }, + { _id: 'move-room-to-team', roles: ['owner'] }, + ], + }) + ).status(), + ).toBe(200); + + // re-add channel to team + await poHomeTeam.sidenav.openChat(targetTeam); + await poHomeTeam.tabs.btnChannels.click(); + await poHomeTeam.tabs.channels.btnAddExisting.click(); + await poHomeTeam.tabs.channels.inputChannels.fill(targetChannelNameInTeam); + await page.locator(`.rcx-option__content:has-text("${targetChannelNameInTeam}")`).click(); + await poHomeTeam.tabs.channels.btnAdd.click(); + await expect(poHomeTeam.tabs.channels.channelsList).toContainText(targetChannelNameInTeam); + + // try to delete channel in team + await poHomeTeam.tabs.channels.openChannelOptionMoreActions(targetChannelNameInTeam); + await expect(page.getByRole('menu', { exact: true }).getByRole('menuitem', { name: 'Delete' })).not.toBeVisible(); + }); + + test('should allow deleting a targetChannel from targetTeam if the channel owner also has the delete-team-channel permission', async ({ + page, + api, + }) => { + expect((await api.post('/permissions.update', { permissions: [{ _id: 'delete-team-channel', roles: ['owner'] }] })).status()).toBe(200); + + await poHomeTeam.sidenav.openChat(targetTeam); + await poHomeTeam.tabs.btnChannels.click(); + await poHomeTeam.tabs.channels.openChannelOptionMoreActions(targetChannelNameInTeam); + await expect(page.getByRole('menu', { exact: true }).getByRole('menuitem', { name: 'Delete' })).toBeVisible(); + await page.getByRole('menu', { exact: true }).getByRole('menuitem', { name: 'Delete' }).click(); + await poHomeTeam.tabs.channels.confirmDeleteRoom(); + + await poHomeTeam.sidenav.openChat(targetTeam); + await poHomeTeam.tabs.btnChannels.click(); + await expect(poHomeTeam.tabs.channels.channelsList).not.toContainText(targetChannelNameInTeam); + }); + + test('should remove targetChannel from targetTeam', async ({ page, api }) => { + expect((await api.post('/permissions.update', { permissions: [{ _id: 'remove-team-channel', roles: ['owner'] }] })).status()).toBe(200); + await poHomeTeam.sidenav.openChat(targetTeam); await poHomeTeam.tabs.btnChannels.click(); await poHomeTeam.tabs.channels.openChannelOptionMoreActions(targetChannel); diff --git a/apps/meteor/tests/end-to-end/api/channels.ts b/apps/meteor/tests/end-to-end/api/channels.ts index 7d4e6f3fa0c60..4747ed6242440 100644 --- a/apps/meteor/tests/end-to-end/api/channels.ts +++ b/apps/meteor/tests/end-to-end/api/channels.ts @@ -1,5 +1,5 @@ import type { Credentials } from '@rocket.chat/api-client'; -import type { IIntegration, IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import type { IIntegration, IMessage, IRoom, ITeam, IUser } from '@rocket.chat/core-typings'; import { expect, assert } from 'chai'; import { after, before, describe, it } from 'mocha'; @@ -628,13 +628,33 @@ describe('[Channels]', () => { describe('[/channels.create]', () => { let guestUser: TestUser; + let invitedUser: TestUser; + let invitedUserCredentials: Credentials; let room: IRoom; + let teamId: ITeam['_id']; before(async () => { guestUser = await createUser({ roles: ['guest'] }); + invitedUser = await createUser(); + invitedUserCredentials = await login(invitedUser.username, password); + + await updatePermission('create-team', ['admin', 'user']); + const teamCreateRes = await request + .post(api('teams.create')) + .set(credentials) + .send({ + name: `team-${Date.now()}`, + type: 0, + members: [invitedUser.username], + }); + + teamId = teamCreateRes.body.team._id; + await updatePermission('create-team-channel', ['owner']); }); after(async () => { await deleteUser(guestUser); + await deleteUser(invitedUser); + await updatePermission('create-team-channel', ['admin', 'owner', 'moderator']); }); it(`should fail when trying to use an existing room's name`, async () => { @@ -702,6 +722,37 @@ describe('[Channels]', () => { await Promise.all(channelIds.map((id) => deleteRoom({ type: 'c', roomId: id }))); }); + + it('should successfully create a channel in a team', async () => { + await request + .post(api('channels.create')) + .set(credentials) + .send({ + name: `team-channel-${Date.now()}`, + extraData: { teamId }, + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('channel'); + expect(res.body.channel).to.have.property('teamId', teamId); + }); + }); + + it('should fail creating a channel in a team when member does not have the necessary permission', async () => { + await request + .post(api('channels.create')) + .set(invitedUserCredentials) + .send({ + name: `team-channel-${Date.now()}`, + extraData: { teamId }, + }) + .expect(403) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'unauthorized'); + }); + }); }); describe('[/channels.info]', () => { const testChannelName = `api-channel-test-${Date.now()}`; @@ -1830,19 +1881,74 @@ describe('[Channels]', () => { }); }); - describe('/channels.delete:', () => { + describe('/channels.delete', () => { let testChannel: IRoom; + let testTeamChannel: IRoom; + let testModeratorTeamChannel: IRoom; + let invitedUser: TestUser; + let moderatorUser: TestUser; + let invitedUserCredentials: Credentials; + let moderatorUserCredentials: Credentials; + let teamId: ITeam['_id']; + let teamMainRoomId: IRoom['_id']; before(async () => { - testChannel = (await createRoom({ type: 'c', name: `channel.test.${Date.now()}` })).body.channel; - }); + testChannel = (await createRoom({ name: `channel.test.${Date.now()}`, type: 'c' })).body.channel; + invitedUser = await createUser(); + moderatorUser = await createUser(); + invitedUserCredentials = await login(invitedUser.username, password); + moderatorUserCredentials = await login(moderatorUser.username, password); + + await updatePermission('create-team', ['admin', 'user']); + const teamCreateRes = await request + .post(api('teams.create')) + .set(credentials) + .send({ + name: `team-${Date.now()}`, + type: 0, + members: [invitedUser.username, moderatorUser.username], + }); + teamId = teamCreateRes.body.team._id; + teamMainRoomId = teamCreateRes.body.team.roomId; + + await updatePermission('delete-team-channel', ['owner', 'moderator']); + await updatePermission('create-team-channel', ['admin', 'owner', 'moderator', 'user']); + const teamChannelResponse = await createRoom({ + name: `channel.test.${Date.now()}`, + type: 'c', + extraData: { teamId }, + credentials: invitedUserCredentials, + }); + testTeamChannel = teamChannelResponse.body.channel; + await request + .post(api('channels.addModerator')) + .set(credentials) + .send({ + userId: moderatorUser._id, + roomId: teamMainRoomId, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }); + const teamModeratorChannelResponse = await createRoom({ + name: `channel.test.moderator.${Date.now()}`, + type: 'c', + extraData: { teamId }, + credentials: moderatorUserCredentials, + }); + testModeratorTeamChannel = teamModeratorChannelResponse.body.channel; + }); after(async () => { - await deleteRoom({ type: 'c', roomId: testChannel._id }); + await deleteUser(invitedUser); + await deleteUser(moderatorUser); + await updatePermission('create-team-channel', ['admin', 'owner', 'moderator']); + await updatePermission('delete-team-channel', ['admin', 'owner', 'moderator']); }); - - it('/channels.delete', (done) => { - void request + it('should succesfully delete a channel', async () => { + await request .post(api('channels.delete')) .set(credentials) .send({ @@ -1852,11 +1958,10 @@ describe('[Channels]', () => { .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); - }) - .end(done); + }); }); - it('/channels.info', (done) => { - void request + it(`should fail retrieving a channel's info after it's been deleted`, async () => { + await request .get(api('channels.info')) .set(credentials) .query({ @@ -1867,8 +1972,52 @@ describe('[Channels]', () => { .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('errorType', 'error-room-not-found'); + }); + }); + it(`should fail deleting a team's channel when member does not have the necessary permission in the team`, async () => { + await request + .post(api('channels.delete')) + .set(invitedUserCredentials) + .send({ + roomName: testTeamChannel.name, }) - .end(done); + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.a.property('error'); + expect(res.body).to.have.a.property('errorType'); + expect(res.body.errorType).to.be.equal('error-not-allowed'); + }); + }); + it(`should fail deleting a team's channel when member has the necessary permission in the team, but not in the deleted room`, async () => { + await request + .post(api('channels.delete')) + .set(moderatorUserCredentials) + .send({ + roomName: testTeamChannel.name, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.a.property('error'); + expect(res.body).to.have.a.property('errorType'); + expect(res.body.errorType).to.be.equal('error-not-allowed'); + }); + }); + it(`should successfully delete a team's channel when member has both team and channel permissions`, async () => { + await request + .post(api('channels.delete')) + .set(moderatorUserCredentials) + .send({ + roomId: testModeratorTeamChannel._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }); }); }); diff --git a/apps/meteor/tests/end-to-end/api/groups.ts b/apps/meteor/tests/end-to-end/api/groups.ts index af22c65c2a84c..3e4ed0d86c268 100644 --- a/apps/meteor/tests/end-to-end/api/groups.ts +++ b/apps/meteor/tests/end-to-end/api/groups.ts @@ -1,5 +1,5 @@ import type { Credentials } from '@rocket.chat/api-client'; -import type { IIntegration, IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import type { IIntegration, IMessage, IRoom, ITeam, IUser } from '@rocket.chat/core-typings'; import { assert, expect } from 'chai'; import { after, before, describe, it } from 'mocha'; @@ -71,14 +71,34 @@ describe('[Groups]', () => { describe('/groups.create', () => { let guestUser: TestUser; + let invitedUser: TestUser; + let invitedUserCredentials: Credentials; let room: IRoom; + let teamId: ITeam['_id']; before(async () => { guestUser = await createUser({ roles: ['guest'] }); + invitedUser = await createUser(); + invitedUserCredentials = await login(invitedUser.username, password); + + await updatePermission('create-team', ['admin', 'user']); + const teamCreateRes = await request + .post(api('teams.create')) + .set(credentials) + .send({ + name: `team-${Date.now()}`, + type: 0, + members: [invitedUser.username], + }); + + teamId = teamCreateRes.body.team._id; + await updatePermission('create-team-group', ['owner']); }); after(async () => { await deleteUser(guestUser); + await deleteUser(invitedUser); + await updatePermission('create-team-group', ['admin', 'owner', 'moderator']); }); describe('guest users', () => { @@ -268,6 +288,37 @@ describe('[Groups]', () => { expect(res.body).to.have.nested.property('errorType', 'error-duplicate-channel-name'); }); }); + + it('should successfully create a group in a team', async () => { + await request + .post(api('groups.create')) + .set(credentials) + .send({ + name: `team-group-${Date.now()}`, + extraData: { teamId }, + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('group'); + expect(res.body.group).to.have.property('teamId', teamId); + }); + }); + + it('should fail creating a group in a team when member does not have the necessary permission', async () => { + await request + .post(api('groups.create')) + .set(invitedUserCredentials) + .send({ + name: `team-group-${Date.now()}`, + extraData: { teamId }, + }) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'error-not-allowed'); + }); + }); }); describe('/groups.info', () => { @@ -1656,22 +1707,71 @@ describe('[Groups]', () => { describe('/groups.delete', () => { let testGroup: IRoom; + let testTeamGroup: IRoom; + let testModeratorTeamGroup: IRoom; + let invitedUser: TestUser; + let moderatorUser: TestUser; + let invitedUserCredentials: Credentials; + let moderatorUserCredentials: Credentials; + let teamId: ITeam['_id']; + let teamMainRoomId: IRoom['_id']; + before(async () => { + testGroup = (await createRoom({ name: `group.test.${Date.now()}`, type: 'p' })).body.group; + invitedUser = await createUser(); + moderatorUser = await createUser(); + invitedUserCredentials = await login(invitedUser.username, password); + moderatorUserCredentials = await login(moderatorUser.username, password); + + const teamCreateRes = await request + .post(api('teams.create')) + .set(credentials) + .send({ + name: `team-${Date.now()}`, + type: 1, + members: [invitedUser.username, moderatorUser.username], + }); + teamId = teamCreateRes.body.team._id; + teamMainRoomId = teamCreateRes.body.team.roomId; + + await updatePermission('delete-team-group', ['owner', 'moderator']); + await updatePermission('create-team-group', ['admin', 'owner', 'moderator', 'user']); + const teamGroupResponse = await createRoom({ + name: `group.test.${Date.now()}`, + type: 'p', + extraData: { teamId }, + credentials: invitedUserCredentials, + }); + testTeamGroup = teamGroupResponse.body.group; + await request - .post(api('groups.create')) + .post(api('groups.addModerator')) .set(credentials) .send({ - name: `group.test.${Date.now()}`, + userId: moderatorUser._id, + roomId: teamMainRoomId, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { - testGroup = res.body.group; + expect(res.body).to.have.property('success', true); }); + const teamModeratorGroupResponse = await createRoom({ + name: `group.test.moderator.${Date.now()}`, + type: 'p', + extraData: { teamId }, + credentials: moderatorUserCredentials, + }); + testModeratorTeamGroup = teamModeratorGroupResponse.body.group; }); - - it('should delete group', (done) => { - void request + after(async () => { + await deleteUser(invitedUser); + await deleteUser(moderatorUser); + await updatePermission('create-team-group', ['admin', 'owner', 'moderator']); + await updatePermission('delete-team-group', ['admin', 'owner', 'moderator']); + }); + it('should succesfully delete a group', async () => { + await request .post(api('groups.delete')) .set(credentials) .send({ @@ -1681,12 +1781,10 @@ describe('[Groups]', () => { .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); - }) - .end(done); + }); }); - - it('should return group not found', (done) => { - void request + it(`should fail retrieving a group's info after it's been deleted`, async () => { + await request .get(api('groups.info')) .set(credentials) .query({ @@ -1697,8 +1795,50 @@ describe('[Groups]', () => { .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('errorType', 'error-room-not-found'); + }); + }); + it(`should fail deleting a team's group when member does not have the necessary permission in the team`, async () => { + await request + .post(api('groups.delete')) + .set(invitedUserCredentials) + .send({ + roomName: testTeamGroup.name, }) - .end(done); + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.a.property('error'); + expect(res.body).to.have.a.property('errorType', 'error-not-allowed'); + }); + }); + it(`should fail deleting a team's group when member has the necessary permission in the team, but not in the deleted room`, async () => { + await request + .post(api('groups.delete')) + .set(moderatorUserCredentials) + .send({ + roomName: testTeamGroup.name, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.a.property('error'); + expect(res.body).to.have.a.property('errorType', 'error-room-not-found'); + }); + }); + it(`should successfully delete a team's group when member has both team and group permissions`, async () => { + await request + .post(api('groups.delete')) + .set(moderatorUserCredentials) + .send({ + roomId: testModeratorTeamGroup._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }); }); }); diff --git a/apps/meteor/tests/end-to-end/api/teams.ts b/apps/meteor/tests/end-to-end/api/teams.ts index 5aa455ac985b3..71d9f63e3458c 100644 --- a/apps/meteor/tests/end-to-end/api/teams.ts +++ b/apps/meteor/tests/end-to-end/api/teams.ts @@ -1519,14 +1519,14 @@ describe('/teams.addRooms', () => { after(async () => { await Promise.all([deleteTeam(credentials, publicTeam.name), deleteTeam(credentials, privateTeam.name)]); await Promise.all([ - updatePermission('add-team-channel', ['admin', 'owner', 'moderator']), + updatePermission('move-room-to-team', ['admin', 'owner', 'moderator']), ...[privateRoom, privateRoom2, privateRoom3, publicRoom, publicRoom2].map((room) => deleteRoom({ type: room.t, roomId: room._id })), deleteUser(testUser), ]); }); it('should throw an error if no permission', (done) => { - void updatePermission('add-team-channel', []).then(() => { + void updatePermission('move-room-to-team', []).then(() => { void request .post(api('teams.addRooms')) .set(credentials) @@ -1546,7 +1546,7 @@ describe('/teams.addRooms', () => { }); it('should add public and private rooms to team', (done) => { - void updatePermission('add-team-channel', ['admin']).then(() => { + void updatePermission('move-room-to-team', ['admin']).then(() => { void request .post(api('teams.addRooms')) .set(credentials) @@ -1575,7 +1575,7 @@ describe('/teams.addRooms', () => { }); it('should add public room to private team', (done) => { - void updatePermission('add-team-channel', ['admin']).then(() => { + void updatePermission('move-room-to-team', ['admin']).then(() => { void request .post(api('teams.addRooms')) .set(credentials) @@ -1596,7 +1596,7 @@ describe('/teams.addRooms', () => { }); it('should add private room to team', (done) => { - void updatePermission('add-team-channel', ['admin']).then(() => { + void updatePermission('move-room-to-team', ['admin']).then(() => { void request .post(api('teams.addRooms')) .set(credentials) @@ -1617,7 +1617,7 @@ describe('/teams.addRooms', () => { }); it('should fail if the user cannot access the channel', (done) => { - void updatePermission('add-team-channel', ['admin', 'user']) + void updatePermission('move-room-to-team', ['admin', 'user']) .then(() => { void request .post(api('teams.addRooms')) diff --git a/packages/i18n/src/locales/da.i18n.json b/packages/i18n/src/locales/da.i18n.json index 48b5fdf0f69e5..3167d718bbc43 100644 --- a/packages/i18n/src/locales/da.i18n.json +++ b/packages/i18n/src/locales/da.i18n.json @@ -303,8 +303,6 @@ "add-livechat-department-agents": "Tilføj omni-kanal-agenter til afdelinger", "add-oauth-service": "Tilføj OAuth-tjeneste", "add-oauth-service_description": "Tilladelse til at tilføje nye OAuth-tjenester", - "add-team-channel": "Tilføj teamkanal", - "add-team-channel_description": "Tilladelse til at tilføje en kanal til et team", "add-team-member": "Tilføj teammedlem", "add-user": "Tilføj bruger", "add-user_description": "Tilladelse til at tilføje nye brugere til serveren via brugere-menuen", diff --git a/packages/i18n/src/locales/de.i18n.json b/packages/i18n/src/locales/de.i18n.json index 3fe08dbce3d36..9b1881dffc6f3 100644 --- a/packages/i18n/src/locales/de.i18n.json +++ b/packages/i18n/src/locales/de.i18n.json @@ -305,8 +305,6 @@ "add-oauth-service_description": "Berechtigung, einen neuen OAuth-Dienst hinzuzufügen", "bypass-time-limit-edit-and-delete": "Zeitlimit umgehen", "bypass-time-limit-edit-and-delete_description": "Erlaubnis, das Zeitlimit für das Bearbeiten und Löschen von Nachrichten zu umgehen", - "add-team-channel": "Team Channel hinzufügen", - "add-team-channel_description": "Erlaubnis zum Hinzufügen eines Kanals zu einem Team", "add-team-member": "Teammitglied hinzufügen", "add-team-member_description": "Erlaubnis zum Hinzufügen von Mitgliedern zu einem Team", "add-user": "Benutzer erstellen", diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 5abf0d8a69b23..c871ce50e773c 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -343,8 +343,14 @@ "add-oauth-service_description": "Permission to add a new OAuth service", "bypass-time-limit-edit-and-delete": "Bypass time limit", "bypass-time-limit-edit-and-delete_description": "Permission to Bypass time limit for editing and deleting messages", - "add-team-channel": "Add Team Channel", - "add-team-channel_description": "Permission to add a channel to a team", + "create-team-channel": "Create channel within team", + "create-team-channel_description": "Permission to create a channel in a team (Overrides global permission)", + "create-team-group": "Create group within team", + "create-team-group_description": "Permission to create a group in a team (Overrides global permission)", + "delete-team-channel": "Delete channel within team", + "delete-team-channel_description": "Permission to delete a channel in a team (when delete public channels is already granted)", + "delete-team-group": "Delete group within team", + "delete-team-group_description": "Permission to delete a group in a team (when delete groups is already granted)", "add-team-member": "Add Team Member", "add-team-member_description": "Permission to add members to a team", "Add_them": "Add them", @@ -3816,6 +3822,8 @@ "Move_beginning_message": "`%s` - Move to the beginning of the message", "Move_end_message": "`%s` - Move to the end of the message", "Move_queue": "Move to the queue", + "move-room-to-team": "Move room within team", + "move-room-to-team_description": "Permission to add an existing room to a team", "Msgs": "Msgs", "multi": "multi", "Multi_line": "Multi line", diff --git a/packages/i18n/src/locales/fi.i18n.json b/packages/i18n/src/locales/fi.i18n.json index 001d1617ebe2b..d7015832ae2c7 100644 --- a/packages/i18n/src/locales/fi.i18n.json +++ b/packages/i18n/src/locales/fi.i18n.json @@ -306,8 +306,6 @@ "add-oauth-service_description": "Oikeus lisätä uusi OAuth-palvelu", "bypass-time-limit-edit-and-delete": "Ohita rajoitus", "bypass-time-limit-edit-and-delete_description": "Oikeus ohittaa viestien muokkauksen ja poistamisen aikaraja", - "add-team-channel": "Lisää tiimin kanava", - "add-team-channel_description": "Oikeus lisätä kanava tiimiin", "add-team-member": "Lisää tiimin jäsen", "add-team-member_description": "Oikeus lisätä jäseniä tiimiin", "add-user": "Lisää käyttäjä", diff --git a/packages/i18n/src/locales/hi-IN.i18n.json b/packages/i18n/src/locales/hi-IN.i18n.json index a1380493fe0cd..ceb6e47b0b500 100644 --- a/packages/i18n/src/locales/hi-IN.i18n.json +++ b/packages/i18n/src/locales/hi-IN.i18n.json @@ -322,8 +322,6 @@ "add-oauth-service_description": "नई OAuth सेवा जोड़ने की अनुमति", "bypass-time-limit-edit-and-delete": "समय सीमा को बायपास करें", "bypass-time-limit-edit-and-delete_description": "संदेशों को संपादित करने और हटाने के लिए समय सीमा को बायपास करने की अनुमति", - "add-team-channel": "टीम चैनल जोड़ें", - "add-team-channel_description": "किसी टीम में चैनल जोड़ने की अनुमति", "add-team-member": "टीम सदस्य जोड़ें", "add-team-member_description": "किसी टीम में सदस्यों को जोड़ने की अनुमति", "add-user": "उपयोगकर्ता जोड़ें", diff --git a/packages/i18n/src/locales/hu.i18n.json b/packages/i18n/src/locales/hu.i18n.json index 9727daa29c776..d0b0fbd76c19b 100644 --- a/packages/i18n/src/locales/hu.i18n.json +++ b/packages/i18n/src/locales/hu.i18n.json @@ -298,8 +298,6 @@ "add-livechat-department-agents_description": "Jogosultság az összcsatornás ügyintézők részlegekhez való hozzáadásához", "add-oauth-service": "OAuth-szolgáltatás hozzáadása", "add-oauth-service_description": "Jogosultság új OAuth-szolgáltatás hozzáadásához", - "add-team-channel": "Csapatcsatorna hozzáadása", - "add-team-channel_description": "Jogosultság egy csatornának egy csapathoz való hozzáadásához", "add-team-member": "Csapattag hozzáadása", "add-team-member_description": "Jogosultság a tagoknak egy csapathoz való hozzáadásához", "add-user": "Felhasználó hozzáadása", diff --git a/packages/i18n/src/locales/nn.i18n.json b/packages/i18n/src/locales/nn.i18n.json index ddcba4eb3d009..c920f409ecfe0 100644 --- a/packages/i18n/src/locales/nn.i18n.json +++ b/packages/i18n/src/locales/nn.i18n.json @@ -330,8 +330,6 @@ "add-oauth-service_description": "Tillatelse til å legge til en ny Oauth-tjeneste", "bypass-time-limit-edit-and-delete": "Omgå tidsbegrensning", "bypass-time-limit-edit-and-delete_description": "Tillatelse til å omgå tidsgrense for redigering og sletting av meldinger", - "add-team-channel": "Legg til Team Channel", - "add-team-channel_description": "Tillatelse til å legge til en kanal i et team", "add-team-member": "Legg til et teammedlem", "add-team-member_description": "Tillatelse til å legge til medlemmer i et team", "Add_them": "Legg dem til", diff --git a/packages/i18n/src/locales/no.i18n.json b/packages/i18n/src/locales/no.i18n.json index 4186115e9c352..9582f1beea48a 100644 --- a/packages/i18n/src/locales/no.i18n.json +++ b/packages/i18n/src/locales/no.i18n.json @@ -330,8 +330,6 @@ "add-oauth-service_description": "Tillatelse til å legge til en ny Oauth-tjeneste", "bypass-time-limit-edit-and-delete": "Omgå tidsbegrensning", "bypass-time-limit-edit-and-delete_description": "Tillatelse til å omgå tidsgrense for redigering og sletting av meldinger", - "add-team-channel": "Legg til Team Channel", - "add-team-channel_description": "Tillatelse til å legge til en kanal i et team", "add-team-member": "Legg til et teammedlem", "add-team-member_description": "Tillatelse til å legge til medlemmer i et team", "Add_them": "Legg dem til", diff --git a/packages/i18n/src/locales/pl.i18n.json b/packages/i18n/src/locales/pl.i18n.json index 2fbf024c4b5f8..c1d7c935b6475 100644 --- a/packages/i18n/src/locales/pl.i18n.json +++ b/packages/i18n/src/locales/pl.i18n.json @@ -328,8 +328,6 @@ "add-livechat-department-agents_description": "Uprawnienie do dodawania agentów omnichannel do działów", "add-oauth-service": "Dodaj usługę Oauth", "add-oauth-service_description": "Uprawnienie do dodawania nowej usługi Oauth", - "add-team-channel": "Dodaj zespół Channel", - "add-team-channel_description": "Zezwolenie na dodanie kanału do zespołu", "add-team-member": "Dodaj członka zespołu", "add-team-member_description": "Uprawnienie do dodawania członków do zespołu", "Add_them": "Dodaj ich", diff --git a/packages/i18n/src/locales/pt-BR.i18n.json b/packages/i18n/src/locales/pt-BR.i18n.json index e0f71c0ff44c6..5980a6bb51c3d 100644 --- a/packages/i18n/src/locales/pt-BR.i18n.json +++ b/packages/i18n/src/locales/pt-BR.i18n.json @@ -301,8 +301,10 @@ "add-livechat-department-agents_description": "Permissão para incluir agentes omnichannel aos departamentos", "add-oauth-service": "Adicionar Serviço OAuth", "add-oauth-service_description": "Permissão para adicionar um novo serviço OAuth", - "add-team-channel": "Adicionar Time ao Canal", - "add-team-channel_description": "Permissão para adicionar um canal a um time", + "create-team-channel": "Criar canal em um time", + "create-team-channel_description": "Permissão para criar um canal em um time (sobrepõe a permissão global)", + "create-team-group": "Criar grupo em um time", + "create-team-group_description": "Permissão para criar um grupo em um time (sobrepõe a permissão global)", "add-team-member": "Adicionar membro ao Team", "add-team-member_description": "Permissão para adicionar membros ao time", "add-user": "Adicionar Usuário", @@ -3079,6 +3081,8 @@ "Move_beginning_message": "`%s` - Mover para o início da mensagem", "Move_end_message": "`%s` - Mover para o final da mensagem", "Move_queue": "Mover para a fila", + "move-room-to-team": "Mover sala a um time", + "move-room-to-team_description": "Permissão para adicionar uma sala existente a um time", "Msgs": "Msgs", "multi": "multi", "Mute": "Silenciar", diff --git a/packages/i18n/src/locales/ru.i18n.json b/packages/i18n/src/locales/ru.i18n.json index 76e84acd36a4c..1b5bc0f258d18 100644 --- a/packages/i18n/src/locales/ru.i18n.json +++ b/packages/i18n/src/locales/ru.i18n.json @@ -314,8 +314,6 @@ "bypass-time-limit-edit-and-delete": "Обход ограничения по времени", "bypass-time-limit-edit-and-delete_description": "Разрешение на обход ограничения по времени для редактирования и удаления сообщений", "Private_Apps_Count_Enabled_few": "{{count}} приватных приложений включено", - "add-team-channel": "Добавить Channel Команды", - "add-team-channel_description": "Разрешение на добавление канала в Команду", "add-team-member": "Добавить участника Команды", "add-team-member_description": "Разрешение на добавление участников в Команду", "add-user": "Добавить пользователя", diff --git a/packages/i18n/src/locales/se.i18n.json b/packages/i18n/src/locales/se.i18n.json index a0cd0cda6f989..09bc4e6e525a6 100644 --- a/packages/i18n/src/locales/se.i18n.json +++ b/packages/i18n/src/locales/se.i18n.json @@ -345,8 +345,6 @@ "add-oauth-service_description": "Permission to add a new OAuth service", "bypass-time-limit-edit-and-delete": "Bypass time limit", "bypass-time-limit-edit-and-delete_description": "Permission to Bypass time limit for editing and deleting messages", - "add-team-channel": "Add Team Channel", - "add-team-channel_description": "Permission to add a channel to a team", "add-team-member": "Add Team Member", "add-team-member_description": "Permission to add members to a team", "Add_them": "Add them", diff --git a/packages/i18n/src/locales/sv.i18n.json b/packages/i18n/src/locales/sv.i18n.json index a46173af84d5b..d1f28648824e1 100644 --- a/packages/i18n/src/locales/sv.i18n.json +++ b/packages/i18n/src/locales/sv.i18n.json @@ -308,8 +308,6 @@ "add-oauth-service_description": "Tillstånd att lägga till en ny Oauth-tjänst", "bypass-time-limit-edit-and-delete": "Överskrid tidsbegränsning", "bypass-time-limit-edit-and-delete_description": "Behörighet att överskrida gränsen för att ändra och radera meddelanden", - "add-team-channel": "Lägg till team Channel", - "add-team-channel_description": "Behörighet att lägga till en kanal till ett team", "add-team-member": "Lägg till teammedlem", "add-team-member_description": "Behörighet att lägga till medlemmar i ett team", "add-user": "Lägg till användare",