diff --git a/e2e-tests/playwright/support/server/default_config.ts b/e2e-tests/playwright/support/server/default_config.ts index 6f2252317c..d260375ca0 100644 --- a/e2e-tests/playwright/support/server/default_config.ts +++ b/e2e-tests/playwright/support/server/default_config.ts @@ -639,6 +639,7 @@ const defaultServerConfig: AdminConfig = { }, GuestAccountsSettings: { Enable: false, + HideTags: false, AllowEmailAccounts: true, EnforceMultifactorAuthentication: false, RestrictCreationToDomains: '', diff --git a/server/config/client.go b/server/config/client.go index e0ddd6f75c..60593731b1 100644 --- a/server/config/client.go +++ b/server/config/client.go @@ -322,6 +322,7 @@ func GenerateLimitedClientConfig(c *model.Config, telemetryID string, license *m props["EnableMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnableMultifactorAuthentication) props["EnforceMultifactorAuthentication"] = "false" props["EnableGuestAccounts"] = strconv.FormatBool(*c.GuestAccountsSettings.Enable) + props["HideGuestTags"] = strconv.FormatBool(*c.GuestAccountsSettings.HideTags) props["GuestAccountsEnforceMultifactorAuthentication"] = strconv.FormatBool(*c.GuestAccountsSettings.EnforceMultifactorAuthentication) if license != nil { diff --git a/server/platform/services/telemetry/telemetry.go b/server/platform/services/telemetry/telemetry.go index ccbed22744..b609f7892a 100644 --- a/server/platform/services/telemetry/telemetry.go +++ b/server/platform/services/telemetry/telemetry.go @@ -860,6 +860,7 @@ func (ts *TelemetryService) trackConfig() { ts.SendTelemetry(TrackConfigGuestAccounts, map[string]any{ "enable": *cfg.GuestAccountsSettings.Enable, + "hide_tag": *cfg.GuestAccountsSettings.HideTags, "allow_email_accounts": *cfg.GuestAccountsSettings.AllowEmailAccounts, "enforce_multifactor_authentication": *cfg.GuestAccountsSettings.EnforceMultifactorAuthentication, "isdefault_restrict_creation_to_domains": isDefault(*cfg.GuestAccountsSettings.RestrictCreationToDomains, ""), diff --git a/server/public/model/config.go b/server/public/model/config.go index b68c4086f2..d663826574 100644 --- a/server/public/model/config.go +++ b/server/public/model/config.go @@ -3055,6 +3055,7 @@ func (s *DisplaySettings) SetDefaults() { type GuestAccountsSettings struct { Enable *bool `access:"authentication_guest_access"` + HideTags *bool `access:"authentication_guest_access"` AllowEmailAccounts *bool `access:"authentication_guest_access"` EnforceMultifactorAuthentication *bool `access:"authentication_guest_access"` RestrictCreationToDomains *string `access:"authentication_guest_access"` @@ -3065,6 +3066,10 @@ func (s *GuestAccountsSettings) SetDefaults() { s.Enable = NewBool(false) } + if s.HideTags == nil { + s.HideTags = NewBool(false) + } + if s.AllowEmailAccounts == nil { s.AllowEmailAccounts = NewBool(true) } diff --git a/webapp/channels/src/components/admin_console/admin_definition.jsx b/webapp/channels/src/components/admin_console/admin_definition.jsx index f409c41920..cfd7756ae7 100644 --- a/webapp/channels/src/components/admin_console/admin_definition.jsx +++ b/webapp/channels/src/components/admin_console/admin_definition.jsx @@ -5727,6 +5727,16 @@ const AdminDefinition = { key: 'GuestAccountsSettings.Enable', isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.AUTHENTICATION.GUEST_ACCESS)), }, + { + type: Constants.SettingsTypes.TYPE_BOOL, + key: 'GuestAccountsSettings.HideTags', + label: t('admin.guest_access.hideTags'), + label_default: 'Hide guest tag', + help_text: t('admin.guest_access.hideTagsDescription'), + help_text_default: 'When true, the "guest" tag will not be shown next to the name of all guest users in the Mattermost chat interface.', + help_text_markdown: false, + isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.AUTHENTICATION.GUEST_ACCESS)), + }, { type: Constants.SettingsTypes.TYPE_TEXT, key: 'GuestAccountsSettings.RestrictCreationToDomains', diff --git a/webapp/channels/src/components/channel_header/channel_header.test.tsx b/webapp/channels/src/components/channel_header/channel_header.test.tsx index eb245bbee0..49c3fe55be 100644 --- a/webapp/channels/src/components/channel_header/channel_header.test.tsx +++ b/webapp/channels/src/components/channel_header/channel_header.test.tsx @@ -46,6 +46,7 @@ describe('components/ChannelHeader', () => { 'minute', 'hour', ], + hideGuestTags: false, }; const populatedProps = { diff --git a/webapp/channels/src/components/channel_header/channel_header.tsx b/webapp/channels/src/components/channel_header/channel_header.tsx index 268b832655..d60a6343ea 100644 --- a/webapp/channels/src/components/channel_header/channel_header.tsx +++ b/webapp/channels/src/components/channel_header/channel_header.tsx @@ -95,6 +95,7 @@ export type Props = { isLastActiveEnabled: boolean; timestampUnits?: string[]; lastActivityTimestamp?: number; + hideGuestTags: boolean; }; type State = { @@ -292,12 +293,13 @@ class ChannelHeader extends React.PureComponent { rhsState, hasGuests, teammateNameDisplaySetting, + hideGuestTags, } = this.props; const {formatMessage} = this.props.intl; const ariaLabelChannelHeader = localizeMessage('accessibility.sections.channelHeader', 'channel header region'); let hasGuestsText: ReactNode = ''; - if (hasGuests) { + if (hasGuests && !hideGuestTags) { hasGuestsText = ( @@ -399,7 +401,7 @@ class ChannelHeader extends React.PureComponent { ); }); - if (hasGuests) { + if (hasGuests && !hideGuestTags) { hasGuestsText = ( { channelId: 'channel-id', channel: TestHelper.getChannelMock(), currentTeam: TestHelper.getTeamMock(), + hideGuestTags: false, }; const state = {entities: { diff --git a/webapp/channels/src/components/post_markdown/post_markdown.tsx b/webapp/channels/src/components/post_markdown/post_markdown.tsx index 29df662f3f..4826b18b0c 100644 --- a/webapp/channels/src/components/post_markdown/post_markdown.tsx +++ b/webapp/channels/src/components/post_markdown/post_markdown.tsx @@ -66,6 +66,8 @@ type Props = { */ isMilitaryTime?: boolean; timezone?: string; + + hideGuestTags: boolean; } export default class PostMarkdown extends React.PureComponent { @@ -94,6 +96,7 @@ export default class PostMarkdown extends React.PureComponent { const renderedSystemMessage = renderSystemMessage(post, this.props.currentTeam, this.props.channel, + this.props.hideGuestTags, this.props.isUserCanManageMembers, this.props.isMilitaryTime, this.props.timezone); diff --git a/webapp/channels/src/components/post_markdown/system_message_helpers.tsx b/webapp/channels/src/components/post_markdown/system_message_helpers.tsx index 2de53dbfc2..1dc74bd009 100644 --- a/webapp/channels/src/components/post_markdown/system_message_helpers.tsx +++ b/webapp/channels/src/components/post_markdown/system_message_helpers.tsx @@ -50,7 +50,10 @@ function renderJoinChannelMessage(post: Post): ReactNode { ); } -function renderGuestJoinChannelMessage(post: Post): ReactNode { +function renderGuestJoinChannelMessage(post: Post, hideGuestTags: boolean): ReactNode { + if (hideGuestTags) { + return renderJoinChannelMessage(post); + } const username = renderUsername(post.props.username); return ( @@ -90,7 +93,10 @@ function renderAddToChannelMessage(post: Post): ReactNode { ); } -function renderAddGuestToChannelMessage(post: Post): ReactNode { +function renderAddGuestToChannelMessage(post: Post, hideGuestTags: boolean): ReactNode { + if (hideGuestTags) { + return renderAddToChannelMessage(post); + } const username = renderUsername(post.props.username); const addedUsername = renderUsername(post.props.addedUsername); @@ -363,10 +369,9 @@ function renderMeMessage(post: Post): ReactNode { const systemMessageRenderers = { [Posts.POST_TYPES.JOIN_CHANNEL]: renderJoinChannelMessage, - [Posts.POST_TYPES.GUEST_JOIN_CHANNEL]: renderGuestJoinChannelMessage, [Posts.POST_TYPES.LEAVE_CHANNEL]: renderLeaveChannelMessage, [Posts.POST_TYPES.ADD_TO_CHANNEL]: renderAddToChannelMessage, - [Posts.POST_TYPES.ADD_GUEST_TO_CHANNEL]: renderAddGuestToChannelMessage, + [Posts.POST_TYPES.EPHEMERAL_ADD_TO_CHANNEL]: renderAddToChannelMessage, [Posts.POST_TYPES.REMOVE_FROM_CHANNEL]: renderRemoveFromChannelMessage, [Posts.POST_TYPES.JOIN_TEAM]: renderJoinTeamMessage, [Posts.POST_TYPES.LEAVE_TEAM]: renderLeaveTeamMessage, @@ -381,7 +386,7 @@ const systemMessageRenderers = { [Posts.POST_TYPES.ME]: renderMeMessage, }; -export function renderSystemMessage(post: Post, currentTeam: Team, channel: Channel, isUserCanManageMembers?: boolean, isMilitaryTime?: boolean, timezone?: string): ReactNode { +export function renderSystemMessage(post: Post, currentTeam: Team, channel: Channel, hideGuestTags: boolean, isUserCanManageMembers?: boolean, isMilitaryTime?: boolean, timezone?: string): ReactNode { const isEphemeral = Utils.isPostEphemeral(post); if (isEphemeral && post.props?.type === Posts.POST_TYPES.REMINDER) { return renderReminderACKMessage(post, currentTeam, Boolean(isMilitaryTime), timezone); @@ -405,8 +410,10 @@ export function renderSystemMessage(post: Post, currentTeam: Team, channel: Chan return null; } else if (systemMessageRenderers[post.type]) { return systemMessageRenderers[post.type](post); - } else if (post.type === Posts.POST_TYPES.EPHEMERAL_ADD_TO_CHANNEL) { - return renderAddToChannelMessage(post); + } else if (post.type === Posts.POST_TYPES.GUEST_JOIN_CHANNEL) { + return renderGuestJoinChannelMessage(post, hideGuestTags); + } else if (post.type === Posts.POST_TYPES.ADD_GUEST_TO_CHANNEL) { + return renderAddGuestToChannelMessage(post, hideGuestTags); } else if (post.type === Posts.POST_TYPES.COMBINED_USER_ACTIVITY) { const {allUserIds, allUsernames, messageData} = post.props.user_activity; diff --git a/webapp/channels/src/components/widgets/tag/__snapshots__/guest_tag.test.tsx.snap b/webapp/channels/src/components/widgets/tag/__snapshots__/guest_tag.test.tsx.snap deleted file mode 100644 index 3d0099a5a5..0000000000 --- a/webapp/channels/src/components/widgets/tag/__snapshots__/guest_tag.test.tsx.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`components/widgets/tag/GuestTag should match the snapshot 1`] = ` - -`; diff --git a/webapp/channels/src/components/widgets/tag/guest_tag.test.tsx b/webapp/channels/src/components/widgets/tag/guest_tag.test.tsx index 4335b36010..640095da6e 100644 --- a/webapp/channels/src/components/widgets/tag/guest_tag.test.tsx +++ b/webapp/channels/src/components/widgets/tag/guest_tag.test.tsx @@ -2,13 +2,18 @@ // See LICENSE.txt for license information. import React from 'react'; -import {shallow} from 'enzyme'; import GuestTag from './guest_tag'; +import {renderWithIntlAndStore, screen} from 'tests/react_testing_utils'; describe('components/widgets/tag/GuestTag', () => { test('should match the snapshot', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); + renderWithIntlAndStore(); + screen.getByText('GUEST'); + }); + + test('should not render when hideTags is true', () => { + renderWithIntlAndStore(, {entities: {general: {config: {HideGuestTags: 'true'}}}}); + expect(() => screen.getByText('GUEST')).toThrow(); }); }); diff --git a/webapp/channels/src/components/widgets/tag/guest_tag.tsx b/webapp/channels/src/components/widgets/tag/guest_tag.tsx index b3053867a4..06a8e32da7 100644 --- a/webapp/channels/src/components/widgets/tag/guest_tag.tsx +++ b/webapp/channels/src/components/widgets/tag/guest_tag.tsx @@ -6,6 +6,9 @@ import classNames from 'classnames'; import {useIntl} from 'react-intl'; import Tag, {TagSize} from './tag'; +import {useSelector} from 'react-redux'; +import {getConfig} from 'mattermost-redux/selectors/entities/general'; +import {GlobalState} from '@mattermost/types/store'; type Props = { className?: string; @@ -14,6 +17,12 @@ type Props = { const GuestTag = ({className = '', size = 'xs'}: Props) => { const {formatMessage} = useIntl(); + const shouldHideTag = useSelector((state: GlobalState) => getConfig(state).HideGuestTags === 'true'); + + if (shouldHideTag) { + return null; + } + return ( multi-factor authentication for guests is required for login. New guest users will be required to configure MFA on signup. Logged in guest users without MFA configured are redirected to the MFA setup page until configuration is complete.\n \nIf your system has guest users with login methods other than AD/LDAP and email, MFA must be enforced with the authentication provider outside of Mattermost.", "admin.guest_access.mfaDescriptionMFANotEnabled": "[Multi-factor authentication](./mfa) is currently not enabled.", "admin.guest_access.mfaDescriptionMFANotEnforced": "[Multi-factor authentication](./mfa) is currently not enforced.", diff --git a/webapp/channels/src/tests/react_testing_utils.tsx b/webapp/channels/src/tests/react_testing_utils.tsx index 23df2bb752..4e9329afbb 100644 --- a/webapp/channels/src/tests/react_testing_utils.tsx +++ b/webapp/channels/src/tests/react_testing_utils.tsx @@ -26,7 +26,7 @@ export const renderWithIntl = (component: React.ReactNode | React.ReactNodeArray return render({component}); }; -export const renderWithIntlAndStore = (component: React.ReactNode | React.ReactNodeArray, initialState: DeepPartial, locale = 'en') => { +export const renderWithIntlAndStore = (component: React.ReactNode | React.ReactNodeArray, initialState: DeepPartial = {}, locale = 'en') => { // We use a redux-mock-store store for testing, but we set up a real store to ensure the initial state is complete const realStore = configureStore(initialState); diff --git a/webapp/platform/types/src/config.ts b/webapp/platform/types/src/config.ts index 2ba61f6084..0fbbd09b11 100644 --- a/webapp/platform/types/src/config.ts +++ b/webapp/platform/types/src/config.ts @@ -127,6 +127,7 @@ export type ClientConfig = { GuestAccountsEnforceMultifactorAuthentication: string; HasImageProxy: string; HelpLink: string; + HideGuestTags: string; IosAppDownloadLink: string; IosLatestVersion: string; IosMinVersion: string; @@ -846,6 +847,7 @@ export type DisplaySettings = { export type GuestAccountsSettings = { Enable: boolean; + HideTags: boolean; AllowEmailAccounts: boolean; EnforceMultifactorAuthentication: boolean; RestrictCreationToDomains: string;