diff --git a/server/channels/api4/team.go b/server/channels/api4/team.go index b6d991d7081..325d6acb8ce 100644 --- a/server/channels/api4/team.go +++ b/server/channels/api4/team.go @@ -251,6 +251,12 @@ func patchTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } + // if changing "AllowOpenInvite" or "AllowedDomains", user must have InviteUser permission + if (team.AllowOpenInvite != nil || team.AllowedDomains != nil) && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionInviteUser) { + c.SetPermissionError(model.PermissionInviteUser) + return + } + if oldTeam, err := c.App.GetTeam(c.Params.TeamId); err == nil { auditRec.AddEventPriorState(oldTeam) auditRec.AddEventObjectType("team") diff --git a/server/channels/api4/team_test.go b/server/channels/api4/team_test.go index 170f43aa8fa..604f73241f7 100644 --- a/server/channels/api4/team_test.go +++ b/server/channels/api4/team_test.go @@ -618,6 +618,39 @@ func TestPatchTeam(t *testing.T) { _, _, err = client.PatchTeam(context.Background(), th.BasicTeam.Id, patch) require.NoError(t, err) }) + + t.Run("Changing AllowOpenInvite requires InviteUser permission", func(t *testing.T) { + th.LoginTeamAdmin() + team2 := &model.Team{DisplayName: "Name", Name: GenerateTestTeamName(), Email: th.GenerateTestEmail(), Type: model.TeamOpen, AllowOpenInvite: true} + team2, _, _ = th.Client.CreateTeam(context.Background(), team2) + + patch2 := &model.TeamPatch{ + AllowOpenInvite: model.NewPointer(false), + AllowedDomains: model.NewPointer("test.com"), + } + + rteam2, _, err3 := th.Client.PatchTeam(context.Background(), team2.Id, patch2) + require.NoError(t, err3) + require.Equal(t, team2.Id, rteam2.Id) + require.False(t, rteam2.AllowOpenInvite) + + // remove invite user permission from team admin and user roles + th.RemovePermissionFromRole(model.PermissionInviteUser.Id, model.TeamAdminRoleId) + th.RemovePermissionFromRole(model.PermissionInviteUser.Id, model.TeamUserRoleId) + + patch2 = &model.TeamPatch{ + AllowOpenInvite: model.NewPointer(true), + } + + _, _, err3 = th.Client.PatchTeam(context.Background(), rteam2.Id, patch2) + require.Error(t, err3) + + patch2 = &model.TeamPatch{ + AllowedDomains: model.NewPointer("testDomain.com"), + } + _, _, err3 = th.Client.PatchTeam(context.Background(), rteam2.Id, patch2) + require.Error(t, err3) + }) } func TestRestoreTeam(t *testing.T) { diff --git a/webapp/channels/src/components/team_settings_modal/index.ts b/webapp/channels/src/components/team_settings_modal/index.ts index ecec6c650f7..46e51c41736 100644 --- a/webapp/channels/src/components/team_settings_modal/index.ts +++ b/webapp/channels/src/components/team_settings_modal/index.ts @@ -3,6 +3,10 @@ import {connect} from 'react-redux'; +import {Permissions} from 'mattermost-redux/constants'; +import {haveITeamPermission} from 'mattermost-redux/selectors/entities/roles'; +import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; + import {isModalOpen} from 'selectors/views/modals'; import {ModalIdentifiers} from 'utils/constants'; @@ -12,9 +16,12 @@ import type {GlobalState} from 'types/store'; import TeamSettingsModal from './team_settings_modal'; function mapStateToProps(state: GlobalState) { + const teamId = getCurrentTeamId(state); + const canInviteUsers = haveITeamPermission(state, teamId, Permissions.INVITE_USER); const modalId = ModalIdentifiers.TEAM_SETTINGS; return { show: isModalOpen(state, modalId), + canInviteUsers, }; } diff --git a/webapp/channels/src/components/team_settings_modal/team_settings_modal.test.tsx b/webapp/channels/src/components/team_settings_modal/team_settings_modal.test.tsx index 3adb852d37a..6e3a3cb8a40 100644 --- a/webapp/channels/src/components/team_settings_modal/team_settings_modal.test.tsx +++ b/webapp/channels/src/components/team_settings_modal/team_settings_modal.test.tsx @@ -10,8 +10,8 @@ import {renderWithContext} from 'tests/react_testing_utils'; describe('components/team_settings_modal', () => { const baseProps = { - isCloud: false, onExited: jest.fn(), + canInviteUsers: true, }; test('should hide the modal when the close button is clicked', async () => { @@ -25,5 +25,31 @@ describe('components/team_settings_modal', () => { fireEvent.click(screen.getByText('Close')); expect(modal.className).toBe('fade modal'); }); + + test('should display access tab when can invite users', async () => { + const props = {...baseProps, canInviteUsers: true}; + renderWithContext( + , + ); + const infoButton = screen.getByRole('tab', {name: 'info'}); + expect(infoButton).toBeDefined(); + const accessButton = screen.getByRole('tab', {name: 'access'}); + expect(accessButton).toBeDefined(); + }); + + test('should not display access tab when can not invite users', async () => { + const props = {...baseProps, canInviteUsers: false}; + renderWithContext( + , + ); + const tabs = screen.getAllByRole('tab'); + expect(tabs.length).toEqual(1); + const infoButton = screen.getByRole('tab', {name: 'info'}); + expect(infoButton).toBeDefined(); + }); }); diff --git a/webapp/channels/src/components/team_settings_modal/team_settings_modal.tsx b/webapp/channels/src/components/team_settings_modal/team_settings_modal.tsx index 5a4bc487008..0aa75da164f 100644 --- a/webapp/channels/src/components/team_settings_modal/team_settings_modal.tsx +++ b/webapp/channels/src/components/team_settings_modal/team_settings_modal.tsx @@ -12,9 +12,10 @@ const SettingsSidebar = React.lazy(() => import('components/settings_sidebar')); type Props = { onExited: () => void; + canInviteUsers: boolean; } -const TeamSettingsModal = ({onExited}: Props) => { +const TeamSettingsModal = ({onExited, canInviteUsers}: Props) => { const [activeTab, setActiveTab] = useState('info'); const [show, setShow] = useState(true); const [hasChanges, setHasChanges] = useState(false); @@ -49,8 +50,10 @@ const TeamSettingsModal = ({onExited}: Props) => { const tabs = [ {name: 'info', uiName: formatMessage({id: 'team_settings_modal.infoTab', defaultMessage: 'Info'}), icon: 'icon icon-information-outline', iconTitle: formatMessage({id: 'generic_icons.info', defaultMessage: 'Info Icon'})}, - {name: 'access', uiName: formatMessage({id: 'team_settings_modal.accessTab', defaultMessage: 'Access'}), icon: 'icon icon-account-multiple-outline', iconTitle: formatMessage({id: 'generic_icons.member', defaultMessage: 'Member Icon'})}, ]; + if (canInviteUsers) { + tabs.push({name: 'access', uiName: formatMessage({id: 'team_settings_modal.accessTab', defaultMessage: 'Access'}), icon: 'icon icon-account-multiple-outline', iconTitle: formatMessage({id: 'generic_icons.member', defaultMessage: 'Member Icon'})}); + } return (