From 5f7cb8cfbf879aa0b0d43a7b7068688368fda9fc Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Wed, 6 Jul 2016 08:23:24 -0400 Subject: PLT-3346/PLT-3342/PLT-3360 EE: Add the ability to restrict channel management permissions (#3453) * EE: Add the ability to restrict channel management permissions * Always allow last user in a channel to delete that channel --- .../components/admin_console/policy_settings.jsx | 56 +++++++- webapp/components/channel_header.jsx | 157 +++++++++++++-------- webapp/components/more_channels.jsx | 56 +++++--- webapp/components/navbar_dropdown.jsx | 4 +- webapp/components/new_channel_modal.jsx | 65 ++++++--- webapp/components/sidebar.jsx | 77 ++++++---- webapp/components/sidebar_right_menu.jsx | 4 +- .../components/tutorial/tutorial_intro_screens.jsx | 2 +- webapp/i18n/en.json | 12 +- webapp/stores/channel_store.jsx | 43 ++++++ webapp/utils/channel_intro_messages.jsx | 4 +- webapp/utils/constants.jsx | 6 +- 12 files changed, 350 insertions(+), 136 deletions(-) (limited to 'webapp') diff --git a/webapp/components/admin_console/policy_settings.jsx b/webapp/components/admin_console/policy_settings.jsx index 7fe8e9460..c7031af7b 100644 --- a/webapp/components/admin_console/policy_settings.jsx +++ b/webapp/components/admin_console/policy_settings.jsx @@ -21,12 +21,16 @@ export default class PolicySettings extends AdminSettings { this.renderSettings = this.renderSettings.bind(this); this.state = Object.assign(this.state, { - restrictTeamInvite: props.config.TeamSettings.RestrictTeamInvite + restrictTeamInvite: props.config.TeamSettings.RestrictTeamInvite, + restrictPublicChannelManagement: props.config.TeamSettings.RestrictPublicChannelManagement, + restrictPrivateChannelManagement: props.config.TeamSettings.RestrictPrivateChannelManagement }); } getConfigFromState(config) { config.TeamSettings.RestrictTeamInvite = this.state.restrictTeamInvite; + config.TeamSettings.RestrictPublicChannelManagement = this.state.restrictPublicChannelManagement; + config.TeamSettings.RestrictPrivateChannelManagement = this.state.restrictPrivateChannelManagement; return config; } @@ -48,9 +52,9 @@ export default class PolicySettings extends AdminSettings { } /> + + } + value={this.state.restrictPublicChannelManagement} + onChange={this.handleChange} + helpText={ + + } + /> + + } + value={this.state.restrictPrivateChannelManagement} + onChange={this.handleChange} + helpText={ + + } + /> ); } diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx index 3449a0fd6..2b9b1e1cc 100644 --- a/webapp/components/channel_header.jsx +++ b/webapp/components/channel_header.jsx @@ -56,6 +56,7 @@ export default class ChannelHeader extends React.Component { state.showRenameChannelModal = false; this.state = state; } + getStateFromStores() { const extraInfo = ChannelStore.getExtraInfo(this.props.channelId); @@ -67,6 +68,7 @@ export default class ChannelHeader extends React.Component { currentUser: UserStore.getCurrentUser() }; } + validState() { if (!this.state.channel || !this.state.memberChannel || @@ -77,6 +79,7 @@ export default class ChannelHeader extends React.Component { } return true; } + componentDidMount() { ChannelStore.addChangeListener(this.onListenerChange); ChannelStore.addExtraInfoChangeListener(this.onListenerChange); @@ -87,6 +90,7 @@ export default class ChannelHeader extends React.Component { $('.sidebar--left .dropdown-menu').perfectScrollbar(); document.addEventListener('keydown', this.openRecentMentions); } + componentWillUnmount() { ChannelStore.removeChangeListener(this.onListenerChange); ChannelStore.removeExtraInfoChangeListener(this.onListenerChange); @@ -96,6 +100,7 @@ export default class ChannelHeader extends React.Component { UserStore.removeStatusesChangeListener(this.onListenerChange); document.removeEventListener('keydown', this.openRecentMentions); } + onListenerChange() { const newState = this.getStateFromStores(); if (!Utils.areObjectsEqual(newState, this.state)) { @@ -103,6 +108,7 @@ export default class ChannelHeader extends React.Component { } $('.channel-header__info .description').popover({placement: 'bottom', trigger: 'hover', html: true, delay: {show: 500, hide: 500}}); } + handleLeave() { Client.leaveChannel(this.state.channel.id, () => { @@ -119,6 +125,7 @@ export default class ChannelHeader extends React.Component { } ); } + searchMentions(e) { e.preventDefault(); @@ -146,12 +153,14 @@ export default class ChannelHeader extends React.Component { is_mention_search: true }); } + openRecentMentions(e) { if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.keyCode === Constants.KeyCodes.M) { e.preventDefault(); this.searchMentions(e); } } + showRenameChannelModal(e) { e.preventDefault(); @@ -159,6 +168,7 @@ export default class ChannelHeader extends React.Component { showRenameChannelModal: true }); } + hideRenameChannelModal() { this.setState({ showRenameChannelModal: false @@ -179,6 +189,30 @@ export default class ChannelHeader extends React.Component { return null; } + showManagementOptions(channel, isAdmin, isSystemAdmin) { + if (global.window.mm_license.IsLicensed !== 'true') { + return true; + } + + if (channel.type === Constants.OPEN_CHANNEL) { + if (global.window.mm_config.RestrictPublicChannelManagement === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { + return false; + } + if (global.window.mm_config.RestrictPublicChannelManagement === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { + return false; + } + } else if (channel.type === Constants.PRIVATE_CHANNEL) { + if (global.window.mm_config.RestrictPrivateChannelManagement === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { + return false; + } + if (global.window.mm_config.RestrictPrivateChannelManagement === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { + return false; + } + } + + return true; + } + render() { if (!this.validState()) { return null; @@ -210,7 +244,8 @@ export default class ChannelHeader extends React.Component { ); let channelTitle = channel.display_name; const currentId = this.state.currentUser.id; - const isAdmin = Utils.isAdmin(this.state.memberChannel.roles) || TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser(); + const isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser(); + const isSystemAdmin = UserStore.isSystemAdminForCurrentUser(); const isDirect = (this.state.channel.type === 'D'); if (isDirect) { @@ -331,67 +366,90 @@ export default class ChannelHeader extends React.Component { dropdownContents.push(
  • ); - dropdownContents.push( + + const deleteOption = (
  • - this.setState({showEditChannelPurposeModal: true})} + dialogType={DeleteChannelModal} + dialogProps={{channel}} > - -
  • - ); - dropdownContents.push( -
  • - -
  • ); - if (isAdmin) { + if (this.showManagementOptions(channel, isAdmin, isSystemAdmin)) { + dropdownContents.push( +
  • + + + +
  • + ); + + dropdownContents.push( +
  • + this.setState({showEditChannelPurposeModal: true})} + > + + +
  • + ); + dropdownContents.push(
  • - - - -
  • - ); + dropdownContents.push(deleteOption); } + } else if (this.state.userCount === 1) { + dropdownContents.push(deleteOption); } const canLeave = channel.type === Constants.PRIVATE_CHANNEL ? this.state.userCount > 1 : true; diff --git a/webapp/components/more_channels.jsx b/webapp/components/more_channels.jsx index 54a06d0ae..b7ffff712 100644 --- a/webapp/components/more_channels.jsx +++ b/webapp/components/more_channels.jsx @@ -6,8 +6,11 @@ import LoadingScreen from './loading_screen.jsx'; import NewChannelFlow from './new_channel_flow.jsx'; import ChannelStore from 'stores/channel_store.jsx'; +import UserStore from 'stores/user_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; import * as Utils from 'utils/utils.jsx'; +import Constants from 'utils/constants.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; @@ -132,6 +135,41 @@ export default class MoreChannels extends React.Component { serverError =
    ; } + let createNewChannelButton = ( + + ); + + let createChannelHelpText = ( +

    + +

    + ); + + const isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser(); + const isSystemAdmin = UserStore.isSystemAdminForCurrentUser(); + + if (global.window.mm_license.IsLicensed === 'true') { + if (global.window.mm_config.RestrictPublicChannelManagement === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { + createNewChannelButton = null; + createChannelHelpText = null; + } else if (global.window.mm_config.RestrictPublicChannelManagement === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { + createNewChannelButton = null; + createChannelHelpText = null; + } + } + var moreChannels; if (this.state.channels != null) { @@ -153,12 +191,7 @@ export default class MoreChannels extends React.Component { defaultMessage='No more channels to join' />

    -

    - -

    + {createChannelHelpText} ); } @@ -195,16 +228,7 @@ export default class MoreChannels extends React.Component { defaultMessage='More Channels' /> - + {createNewChannelButton}

    {this.props.serverError}

    ; } + let createPublicChannelLink = ( + + + + ); + + let createPrivateChannelLink = ( + + + + ); + + const isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser(); + const isSystemAdmin = UserStore.isSystemAdminForCurrentUser(); + + if (global.window.mm_license.IsLicensed === 'true') { + if (global.window.mm_config.RestrictPublicChannelManagement === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { + createPublicChannelLink = null; + } else if (global.window.mm_config.RestrictPublicChannelManagement === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { + createPublicChannelLink = null; + } + + if (global.window.mm_config.RestrictPrivateChannelManagement === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { + createPrivateChannelLink = null; + } else if (global.window.mm_config.RestrictPrivateChannelManagement === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { + createPrivateChannelLink = null; + } + } + var channelTerm = ''; var channelSwitchText = ''; switch (this.props.channelType) { @@ -129,15 +174,7 @@ class NewChannelModal extends React.Component { id='channel_modal.privateGroup1' defaultMessage='Create a new private group with restricted membership. ' /> - - - + {createPublicChannelLink} ); break; @@ -154,15 +191,7 @@ class NewChannelModal extends React.Component { id='channel_modal.publicChannel2' defaultMessage='Create a new public channel anyone can join. ' /> - - - + {createPrivateChannelLink} ); break; diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx index 4f678274d..fdcae1dff 100644 --- a/webapp/components/sidebar.jsx +++ b/webapp/components/sidebar.jsx @@ -682,6 +682,55 @@ export default class Sidebar extends React.Component { /> ); + const isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser(); + const isSystemAdmin = UserStore.isSystemAdminForCurrentUser(); + + let createPublicChannelIcon = ( + + + {'+'} + + + ); + + let createPrivateChannelIcon = ( + + + {'+'} + + + ); + + if (global.window.mm_license.IsLicensed === 'true') { + if (global.window.mm_config.RestrictPublicChannelManagement === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { + createPublicChannelIcon = null; + } else if (global.window.mm_config.RestrictPublicChannelManagement === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { + createPublicChannelIcon = null; + } + + if (global.window.mm_config.RestrictPrivateChannelManagement === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { + createPrivateChannelIcon = null; + } else if (global.window.mm_config.RestrictPrivateChannelManagement === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { + createPrivateChannelIcon = null; + } + } + return (
    - - - {'+'} - - + {createPublicChannelIcon} {publicChannelItems} @@ -765,19 +802,7 @@ export default class Sidebar extends React.Component { id='sidebar.pg' defaultMessage='Private Groups' /> - - - {'+'} - - + {createPrivateChannelIcon} {privateChannelItems} diff --git a/webapp/components/sidebar_right_menu.jsx b/webapp/components/sidebar_right_menu.jsx index 2cf758f00..25136e8bc 100644 --- a/webapp/components/sidebar_right_menu.jsx +++ b/webapp/components/sidebar_right_menu.jsx @@ -186,10 +186,10 @@ export default class SidebarRightMenu extends React.Component { } if (global.window.mm_license.IsLicensed === 'true') { - if (global.window.mm_config.RestrictTeamInvite === Constants.TEAM_INVITE_SYSTEM_ADMIN && !isSystemAdmin) { + if (global.window.mm_config.RestrictTeamInvite === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { teamLink = null; inviteLink = null; - } else if (global.window.mm_config.RestrictTeamInvite === Constants.TEAM_INVITE_TEAM_ADMIN && !isAdmin) { + } else if (global.window.mm_config.RestrictTeamInvite === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { teamLink = null; inviteLink = null; } diff --git a/webapp/components/tutorial/tutorial_intro_screens.jsx b/webapp/components/tutorial/tutorial_intro_screens.jsx index b0d831d96..639fa07b2 100644 --- a/webapp/components/tutorial/tutorial_intro_screens.jsx +++ b/webapp/components/tutorial/tutorial_intro_screens.jsx @@ -108,7 +108,7 @@ export default class TutorialIntroScreens extends React.Component { let inviteModalLink; let inviteText; - if (global.window.mm_license.IsLicensed !== 'true' || global.window.mm_config.RestrictTeamInvite === Constants.TEAM_INVITE_ALL) { + if (global.window.mm_license.IsLicensed !== 'true' || global.window.mm_config.RestrictTeamInvite === Constants.PERMISSIONS_ALL) { if (team.type === Constants.INVITE_TEAM) { inviteModalLink = (
    Selecting \"Team and System Admins\" hides the email invitation and team invite link in the Main Menu from users who are not Team or System Admins. Note: If \"Get Team Invite Link\" is used to share a link, it will need to be regenerated after the desired users joined the team.

    Selecting \"System Admins\" hides the email invitation and team invite link in the Main Menu from users who are not System Admins. Note: If \"Get Team Invite Link\" is used to share a link, it will need to be regenerated after the desired users joined the team.", - "admin.general.policy.teamInviteSystemAdmin": "System Admins", + "admin.general.policy.permissionsAll": "All team members", + "admin.general.policy.permissionsAdmin": "Team and System Admins", + "admin.general.policy.permissionsSystemAdmin": "System Admins", "admin.general.policy.teamInviteTitle": "Enable sending team invites from:", + "admin.general.policy.teamInviteDescription": "Selecting \"All team members\" allows any team member to invite others using an email invitation or team invite link.

    Selecting \"Team and System Admins\" hides the email invitation and team invite link in the Main Menu from users who are not Team or System Admins. Note: If \"Get Team Invite Link\" is used to share a link, it will need to be regenerated after the desired users joined the team.

    Selecting \"System Admins\" hides the email invitation and team invite link in the Main Menu from users who are not System Admins. Note: If \"Get Team Invite Link\" is used to share a link, it will need to be regenerated after the desired users joined the team.", + "admin.general.policy.restrictPublicChannelManagementTitle": "Enable public channel management permissions for:", + "admin.general.policy.restrictPublicChannelManagementDescription": "Selecting \"All team members\" allows any team members to create, delete, rename, and set the header or purpose for public channels.

    Selecting \"Team and System Admins\" restricts channel management permissions for public channels to Team and System Admins, including creating, deleting, renaming, and setting the channel header or purpose.

    Selecting \"System Admins\" restricts channel management permissions for public channels to System Admins, including creating, deleting, renaming, and setting the channel header or purpose.", + "admin.general.policy.restrictPrivateChannelManagementTitle": "Enable private group management permissions for:", + "admin.general.policy.restrictPrivateChannelManagementDescription": "Selecting \"All team members\" allows any team members to create, delete, rename, and set the header or purpose for private groups.

    Selecting \"Team and System Admins\" restricts group management permissions for private groups to Team and System Admins, including creating, deleting, renaming, and setting the group header or purpose.

    Selecting \"System Admins\" restricts group management permissions for private groups to System Admins, including creating, deleting, renaming, and setting the group header or purpose.", "admin.general.privacy": "Privacy", "admin.general.usersAndTeams": "Users and Teams", "admin.gitab.clientSecretDescription": "Obtain this value via the instructions above for logging into GitLab.", diff --git a/webapp/stores/channel_store.jsx b/webapp/stores/channel_store.jsx index b65ec330c..dc2577811 100644 --- a/webapp/stores/channel_store.jsx +++ b/webapp/stores/channel_store.jsx @@ -53,54 +53,70 @@ class ChannelStoreClass extends EventEmitter { this.extraInfos = {}; this.unreadCounts = {}; } + get POST_MODE_CHANNEL() { return 1; } + get POST_MODE_FOCUS() { return 2; } + emitChange() { this.emit(CHANGE_EVENT); } + addChangeListener(callback) { this.on(CHANGE_EVENT, callback); } + removeChangeListener(callback) { this.removeListener(CHANGE_EVENT, callback); } + emitMoreChange() { this.emit(MORE_CHANGE_EVENT); } + addMoreChangeListener(callback) { this.on(MORE_CHANGE_EVENT, callback); } + removeMoreChangeListener(callback) { this.removeListener(MORE_CHANGE_EVENT, callback); } + emitExtraInfoChange() { this.emit(EXTRA_INFO_EVENT); } + addExtraInfoChangeListener(callback) { this.on(EXTRA_INFO_EVENT, callback); } + removeExtraInfoChangeListener(callback) { this.removeListener(EXTRA_INFO_EVENT, callback); } emitLeave(id) { this.emit(LEAVE_EVENT, id); } + addLeaveListener(callback) { this.on(LEAVE_EVENT, callback); } + removeLeaveListener(callback) { this.removeListener(LEAVE_EVENT, callback); } + findFirstBy(field, value) { return this.doFindFirst(field, value, this.getChannels()); } + findFirstMoreBy(field, value) { return this.doFindFirst(field, value, this.getMoreChannels()); } + doFindFirst(field, value, channels) { for (var i = 0; i < channels.length; i++) { if (channels[i][field] === value) { @@ -110,33 +126,43 @@ class ChannelStoreClass extends EventEmitter { return null; } + get(id) { return this.findFirstBy('id', id); } + getMember(id) { return this.getAllMembers()[id]; } + getByName(name) { return this.findFirstBy('name', name); } + getByDisplayName(displayName) { return this.findFirstBy('display_name', displayName); } + getMoreByName(name) { return this.findFirstMoreBy('name', name); } + getAll() { return this.getChannels(); } + getAllMembers() { return this.getChannelMembers(); } + getMoreAll() { return this.getMoreChannels(); } + setCurrentId(id) { this.currentId = id; } + resetCounts(id) { const cm = this.channelMembers; for (var cmid in cm) { @@ -151,9 +177,11 @@ class ChannelStoreClass extends EventEmitter { } } } + getCurrentId() { return this.currentId; } + getCurrent() { var currentId = this.getCurrentId(); @@ -163,6 +191,7 @@ class ChannelStoreClass extends EventEmitter { return null; } + getCurrentMember() { var currentId = this.getCurrentId(); @@ -172,15 +201,18 @@ class ChannelStoreClass extends EventEmitter { return null; } + setChannelMember(member) { var members = this.getChannelMembers(); members[member.channel_id] = member; this.storeChannelMembers(members); this.emitChange(); } + getCurrentExtraInfo() { return this.getExtraInfo(this.getCurrentId()); } + getExtraInfo(channelId) { var extra = null; @@ -197,6 +229,7 @@ class ChannelStoreClass extends EventEmitter { return extra; } + pStoreChannel(channel) { var channels = this.getChannels(); var found; @@ -220,35 +253,45 @@ class ChannelStoreClass extends EventEmitter { channels.sort(Utils.sortByDisplayName); this.storeChannels(channels); } + storeChannels(channels) { this.channels = channels; } + getChannels() { return this.channels; } + pStoreChannelMember(channelMember) { var members = this.getChannelMembers(); members[channelMember.channel_id] = channelMember; this.storeChannelMembers(members); } + storeChannelMembers(channelMembers) { this.channelMembers = channelMembers; } + getChannelMembers() { return this.channelMembers; } + storeMoreChannels(channels) { this.moreChannels = channels; } + getMoreChannels() { return this.moreChannels; } + storeExtraInfos(extraInfos) { this.extraInfos = extraInfos; } + getExtraInfos() { return this.extraInfos; } + isDefault(channel) { return channel.name === Constants.DEFAULT_CHANNEL; } diff --git a/webapp/utils/channel_intro_messages.jsx b/webapp/utils/channel_intro_messages.jsx index 043894b7b..50d12ed42 100644 --- a/webapp/utils/channel_intro_messages.jsx +++ b/webapp/utils/channel_intro_messages.jsx @@ -114,9 +114,9 @@ export function createDefaultIntroMessage(channel) { const isSystemAdmin = UserStore.isSystemAdminForCurrentUser(); if (global.window.mm_license.IsLicensed === 'true') { - if (global.window.mm_config.RestrictTeamInvite === Constants.TEAM_INVITE_SYSTEM_ADMIN && !isSystemAdmin) { + if (global.window.mm_config.RestrictTeamInvite === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { inviteModalLink = null; - } else if (global.window.mm_config.RestrictTeamInvite === Constants.TEAM_INVITE_TEAM_ADMIN && !isAdmin) { + } else if (global.window.mm_config.RestrictTeamInvite === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { inviteModalLink = null; } } diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx index 1b0fa6374..0191edcf0 100644 --- a/webapp/utils/constants.jsx +++ b/webapp/utils/constants.jsx @@ -762,7 +762,7 @@ export default { POST_COLLAPSE_TIMEOUT: 1000 * 60 * 5, // five minutes LICENSE_EXPIRY_NOTIFICATION: 1000 * 60 * 60 * 24 * 15, // 15 days LICENSE_GRACE_PERIOD: 1000 * 60 * 60 * 24 * 15, // 15 days - TEAM_INVITE_ALL: 'all', - TEAM_INVITE_TEAM_ADMIN: 'team_admin', - TEAM_INVITE_SYSTEM_ADMIN: 'system_admin' + PERMISSIONS_ALL: 'all', + PERMISSIONS_TEAM_ADMIN: 'team_admin', + PERMISSIONS_SYSTEM_ADMIN: 'system_admin' }; -- cgit v1.2.3-1-g7c22