MM-64428 - user tag invite filtering (#31226)

* MM-64428 - user tag invite filtering

* fix lint issues

* remove unnecesary line

* update translations and skip mysql tests

* simplify the solution so in abac channels the invitation link is never shown

* finish clean up of unnecessary code

* clean up and remove no longer necessary translations

* remove leftover props and remove no longer needed tests after simplification

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Pablo Vélez
2025-07-02 14:55:02 +02:00
коммит произвёл GitHub
родитель f1893e4837
Коммит e99aa4e430
5 изменённых файлов: 151 добавлений и 43 удалений

Просмотреть файл

@@ -1260,6 +1260,7 @@ func (a *App) filterOutOfChannelMentions(c request.CTX, sender *model.User, post
// Differentiate between mentionedUsersInTheTeam who can and can't be added to the channel // Differentiate between mentionedUsersInTheTeam who can and can't be added to the channel
var outOfChannelUsers model.UserSlice var outOfChannelUsers model.UserSlice
var outOfGroupsUsers model.UserSlice var outOfGroupsUsers model.UserSlice
if channel.IsGroupConstrained() { if channel.IsGroupConstrained() {
nonMemberIDs, err := a.FilterNonGroupChannelMembers(teamUsers.IDs(), channel) nonMemberIDs, err := a.FilterNonGroupChannelMembers(teamUsers.IDs(), channel)
if err != nil { if err != nil {
@@ -1289,6 +1290,8 @@ func makeOutOfChannelMentionPost(sender *model.User, post *model.Post, outOfChan
ephemeralPostId := model.NewId() ephemeralPostId := model.NewId()
var message string var message string
// Generate message for users who can be invited
if len(outOfChannelUsers) == 1 { if len(outOfChannelUsers) == 1 {
message = T("api.post.check_for_out_of_channel_mentions.message.one", map[string]any{ message = T("api.post.check_for_out_of_channel_mentions.message.one", map[string]any{
"Username": ocUsernames[0], "Username": ocUsernames[0],

Просмотреть файл

@@ -6,7 +6,9 @@ exports[`components/post_view/PostAddChannelMember should match snapshot, empty
exports[`components/post_view/PostAddChannelMember should match snapshot, more than 3 users 1`] = ` exports[`components/post_view/PostAddChannelMember should match snapshot, more than 3 users 1`] = `
<Fragment> <Fragment>
<p> <p
key="invitable"
>
<span> <span>
<Connect(Component) <Connect(Component)
channelId="channel_id" channelId="channel_id"
@@ -67,7 +69,9 @@ exports[`components/post_view/PostAddChannelMember should match snapshot, more t
exports[`components/post_view/PostAddChannelMember should match snapshot, more than 3 users 2`] = ` exports[`components/post_view/PostAddChannelMember should match snapshot, more than 3 users 2`] = `
<Fragment> <Fragment>
<p> <p
key="invitable"
>
<span> <span>
<Connect(Component) <Connect(Component)
channelId="channel_id" channelId="channel_id"
@@ -129,7 +133,9 @@ exports[`components/post_view/PostAddChannelMember should match snapshot, more t
exports[`components/post_view/PostAddChannelMember should match snapshot, private channel 1`] = ` exports[`components/post_view/PostAddChannelMember should match snapshot, private channel 1`] = `
<Fragment> <Fragment>
<p> <p
key="invitable"
>
<Connect(Component) <Connect(Component)
channelId="channel_id" channelId="channel_id"
mentionName="username_1" mentionName="username_1"
@@ -158,7 +164,9 @@ exports[`components/post_view/PostAddChannelMember should match snapshot, privat
exports[`components/post_view/PostAddChannelMember should match snapshot, public channel 1`] = ` exports[`components/post_view/PostAddChannelMember should match snapshot, public channel 1`] = `
<Fragment> <Fragment>
<p> <p
key="invitable"
>
<Connect(Component) <Connect(Component)
channelId="channel_id" channelId="channel_id"
mentionName="username_1" mentionName="username_1"
@@ -185,9 +193,45 @@ exports[`components/post_view/PostAddChannelMember should match snapshot, public
</Fragment> </Fragment>
`; `;
exports[`components/post_view/PostAddChannelMember should match snapshot, with ABAC policy enforced 1`] = `
<p>
<span>
<Connect(Component)
channelId="channel_id"
key="username_1"
mentionName="username_1"
/>
<span
key="1"
>
,
</span>
<Connect(Component)
channelId="channel_id"
key="username_2"
mentionName="username_2"
/>
<MemoizedFormattedMessage
defaultMessage=" and "
id="post_body.check_for_out_of_channel_mentions.link.and"
key="2"
/>
<Connect(Component)
channelId="channel_id"
key="username_3"
mentionName="username_3"
/>
</span>
did not get notified by this mention because they are not in the channel.
</p>
`;
exports[`components/post_view/PostAddChannelMember should match snapshot, with no-groups usernames 1`] = ` exports[`components/post_view/PostAddChannelMember should match snapshot, with no-groups usernames 1`] = `
<Fragment> <Fragment>
<p> <p
key="invitable"
>
<Connect(Component) <Connect(Component)
channelId="channel_id" channelId="channel_id"
mentionName="username_1" mentionName="username_1"
@@ -211,7 +255,9 @@ exports[`components/post_view/PostAddChannelMember should match snapshot, with n
id="post_body.check_for_out_of_channel_mentions.message_last" id="post_body.check_for_out_of_channel_mentions.message_last"
/> />
</p> </p>
<p> <p
key="out-of-groups"
>
<Connect(Component) <Connect(Component)
channelId="channel_id" channelId="channel_id"
mentionName="user_id_2" mentionName="user_id_2"

Просмотреть файл

@@ -17,15 +17,18 @@ import PostAddChannelMember from './post_add_channel_member';
type OwnProps = { type OwnProps = {
postId: string; postId: string;
userIds: string[];
} }
function mapStateToProps(state: GlobalState, ownProps: OwnProps) { function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
const post = getPost(state, ownProps.postId) || {}; const post = getPost(state, ownProps.postId) || {};
let channelType = ''; let channelType = '';
let isPolicyEnforced = false;
if (post && post.channel_id) { if (post && post.channel_id) {
const channel = getChannel(state, post.channel_id); const channel = getChannel(state, post.channel_id);
if (channel && channel.type) { if (channel && channel.type) {
channelType = channel.type; channelType = channel.type;
isPolicyEnforced = Boolean(channel.policy_enforced);
} }
} }
@@ -33,6 +36,7 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
channelType, channelType,
currentUser: getCurrentUser(state), currentUser: getCurrentUser(state),
post, post,
isPolicyEnforced,
}; };
} }

Просмотреть файл

@@ -43,6 +43,7 @@ describe('components/post_view/PostAddChannelMember', () => {
addChannelMember: jest.fn(), addChannelMember: jest.fn(),
}, },
noGroupsUsernames: [], noGroupsUsernames: [],
isPolicyEnforced: false,
}; };
test('should match snapshot, empty postId', () => { test('should match snapshot, empty postId', () => {
@@ -140,4 +141,39 @@ describe('components/post_view/PostAddChannelMember', () => {
const wrapper = shallow(<PostAddChannelMember {...props}/>); const wrapper = shallow(<PostAddChannelMember {...props}/>);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
test('should match snapshot, with ABAC policy enforced', () => {
const props: Props = {
...requiredProps,
usernames: ['username_1', 'username_2', 'username_3'],
isPolicyEnforced: true,
};
const wrapper = shallow(<PostAddChannelMember {...props}/>);
expect(wrapper).toMatchSnapshot();
});
test('should never show invite links when policy is enforced (ABAC channels)', () => {
const props: Props = {
...requiredProps,
usernames: ['username_1', 'username_2'],
noGroupsUsernames: [],
isPolicyEnforced: true,
};
const wrapper = shallow(<PostAddChannelMember {...props}/>);
expect(wrapper.find('.PostBody_addChannelMemberLink')).toHaveLength(0);
});
test('should show single consolidated message for ABAC channels regardless of user types', () => {
const props: Props = {
...requiredProps,
usernames: ['user1', 'user2'],
noGroupsUsernames: ['user3'],
isPolicyEnforced: true,
};
const wrapper = shallow(<PostAddChannelMember {...props}/>);
// Should render only one consolidated message with no invite links
expect(wrapper.find('p')).toHaveLength(1);
expect(wrapper.find('.PostBody_addChannelMemberLink')).toHaveLength(0);
});
}); });

Просмотреть файл

@@ -26,6 +26,7 @@ export interface Props {
userIds: string[]; userIds: string[];
usernames: string[]; usernames: string[];
noGroupsUsernames: string[]; noGroupsUsernames: string[];
isPolicyEnforced: boolean;
actions: Actions; actions: Actions;
} }
@@ -146,11 +147,33 @@ export default class PostAddChannelMember extends React.PureComponent<Props, Sta
} }
render() { render() {
const {channelType, postId, usernames, noGroupsUsernames} = this.props; const {channelType, postId, usernames, noGroupsUsernames, isPolicyEnforced} = this.props;
if (!postId || !channelType) { if (!postId || !channelType) {
return null; return null;
} }
// For ABAC channels (policy enforced), NEVER show invite links - only show notification message
if (isPolicyEnforced) {
// Combine all users into a single message without any invite functionality
const allUsers = [...usernames, ...noGroupsUsernames];
const allUsersAtMentions = this.generateAtMentions(allUsers);
if (allUsers.length === 0) {
return null;
}
const messageText = 'did not get notified by this mention because they are not in the channel.';
return (
<p>
{allUsersAtMentions}
{' '}
{messageText}
</p>
);
}
// Regular flow for non-ABAC channels
let link; let link;
if (channelType === Constants.PRIVATE_CHANNEL) { if (channelType === Constants.PRIVATE_CHANNEL) {
link = ( link = (
@@ -168,44 +191,32 @@ export default class PostAddChannelMember extends React.PureComponent<Props, Sta
); );
} }
let outOfChannelMessagePart; // Separate invitable users from group-constrained users
const outOfChannelAtMentions = this.generateAtMentions(usernames); const invitableUsers = usernames.filter((username) => !noGroupsUsernames.includes(username));
if (usernames.length === 1) { const outOfGroupsUsers = noGroupsUsernames;
outOfChannelMessagePart = (
const messages = [];
// Handle invitable users with invite functionality
if (invitableUsers.length > 0) {
const invitableAtMentions = this.generateAtMentions(invitableUsers);
const invitableMessagePart = invitableUsers.length === 1 ? (
<FormattedMessage <FormattedMessage
id='post_body.check_for_out_of_channel_mentions.message.one' id='post_body.check_for_out_of_channel_mentions.message.one'
defaultMessage='did not get notified by this mention because they are not in the channel. Would you like to ' defaultMessage='did not get notified by this mention because they are not in the channel. Would you like to '
/> />
); ) : (
} else if (usernames.length > 1) {
outOfChannelMessagePart = (
<FormattedMessage <FormattedMessage
id='post_body.check_for_out_of_channel_mentions.message.multiple' id='post_body.check_for_out_of_channel_mentions.message.multiple'
defaultMessage='did not get notified by this mention because they are not in the channel. Would you like to ' defaultMessage='did not get notified by this mention because they are not in the channel. Would you like to '
/> />
); );
}
let outOfGroupsMessagePart; messages.push(
const outOfGroupsAtMentions = this.generateAtMentions(noGroupsUsernames); <p key='invitable'>
if (noGroupsUsernames.length) { {invitableAtMentions}
outOfGroupsMessagePart = (
<FormattedMessage
id='post_body.check_for_out_of_channel_groups_mentions.message'
defaultMessage='did not get notified by this mention because they are not in the channel. They cannot be added to the channel because they are not a member of the linked groups. To add them to this channel, they must be added to the linked groups.'
/>
);
}
let outOfChannelMessage = null;
let outOfGroupsMessage = null;
if (usernames.length) {
outOfChannelMessage = (
<p>
{outOfChannelAtMentions}
{' '} {' '}
{outOfChannelMessagePart} {invitableMessagePart}
<a <a
className='PostBody_addChannelMemberLink' className='PostBody_addChannelMemberLink'
onClick={this.handleAddChannelMember} onClick={this.handleAddChannelMember}
@@ -213,27 +224,35 @@ export default class PostAddChannelMember extends React.PureComponent<Props, Sta
{link} {link}
</a> </a>
<FormattedMessage <FormattedMessage
id={'post_body.check_for_out_of_channel_mentions.message_last'} id='post_body.check_for_out_of_channel_mentions.message_last'
defaultMessage={'? They will have access to all message history.'} defaultMessage='? They will have access to all message history.'
/> />
</p> </p>,
); );
} }
if (noGroupsUsernames.length) { // Handle users not in required groups with specific messaging
outOfGroupsMessage = ( if (outOfGroupsUsers.length > 0) {
<p> const outOfGroupsAtMentions = this.generateAtMentions(outOfGroupsUsers);
const outOfGroupsMessagePart = (
<FormattedMessage
id='post_body.check_for_out_of_channel_groups_mentions.message'
defaultMessage='did not get notified by this mention because they are not in the channel. They cannot be added to the channel because they are not a member of the linked groups. To add them to this channel, they must be added to the linked groups.'
/>
);
messages.push(
<p key='out-of-groups'>
{outOfGroupsAtMentions} {outOfGroupsAtMentions}
{' '} {' '}
{outOfGroupsMessagePart} {outOfGroupsMessagePart}
</p> </p>,
); );
} }
return ( return (
<> <>
{outOfChannelMessage} {messages}
{outOfGroupsMessage}
</> </>
); );
} }