MM-32133 shared channel username collisions (#17347)
Support for handling username collisions between remote clusters. Users belonging to remote clusters have their username changed to include the remote name e.g. wiggin becomes wiggin:mattermost. @mentions are also modified so the munged username is replaced with the original username when the post is sync'd with the remote the user belongs to. When adding remote users: - append the remote name to the username with colon separator - append the remote name to the email address with colon separator - store the original username and email address in user props - when resolving @mentions replace with the stored original username
Этот коммит содержится в:
@@ -12,11 +12,12 @@ import (
|
||||
)
|
||||
|
||||
// AcceptInvitation is called when accepting an invitation to connect with a remote cluster.
|
||||
func (rcs *Service) AcceptInvitation(invite *model.RemoteClusterInvite, name string, creatorId string, teamId string, siteURL string) (*model.RemoteCluster, error) {
|
||||
func (rcs *Service) AcceptInvitation(invite *model.RemoteClusterInvite, name string, displayName, creatorId string, teamId string, siteURL string) (*model.RemoteCluster, error) {
|
||||
rc := &model.RemoteCluster{
|
||||
RemoteId: invite.RemoteId,
|
||||
RemoteTeamId: invite.RemoteTeamId,
|
||||
DisplayName: name,
|
||||
Name: name,
|
||||
DisplayName: displayName,
|
||||
Token: model.NewId(),
|
||||
RemoteToken: invite.Token,
|
||||
SiteURL: invite.SiteURL,
|
||||
|
||||
@@ -177,14 +177,14 @@ func makeRemoteClusters(num int, siteURL string) []*model.RemoteCluster {
|
||||
|
||||
func makeRemoteCluster(name string, siteURL string, topics string) *model.RemoteCluster {
|
||||
return &model.RemoteCluster{
|
||||
RemoteId: model.NewId(),
|
||||
DisplayName: name,
|
||||
SiteURL: siteURL,
|
||||
Token: model.NewId(),
|
||||
Topics: topics,
|
||||
CreateAt: model.GetMillis(),
|
||||
LastPingAt: model.GetMillis(),
|
||||
CreatorId: model.NewId(),
|
||||
RemoteId: model.NewId(),
|
||||
Name: name,
|
||||
SiteURL: siteURL,
|
||||
Token: model.NewId(),
|
||||
Topics: topics,
|
||||
CreateAt: model.GetMillis(),
|
||||
LastPingAt: model.GetMillis(),
|
||||
CreatorId: model.NewId(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,9 @@ func (rcs *Service) sendMsg(task sendMsgTask) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
rcs.server.GetLogger().Log(mlog.LvlRemoteClusterServiceError, "Remote Cluster sendMsg panic",
|
||||
mlog.String("remote", task.rc.DisplayName), mlog.String("msgId", task.msg.Id), mlog.Any("panic", r))
|
||||
mlog.String("remote", task.rc.DisplayName),
|
||||
mlog.String("msgId", task.msg.Id), mlog.Any("panic", r),
|
||||
)
|
||||
}
|
||||
|
||||
if errResp != nil {
|
||||
|
||||
@@ -63,7 +63,7 @@ type RemoteClusterServiceIFace interface {
|
||||
RemoveConnectionStateListener(listenerId string)
|
||||
SendMsg(ctx context.Context, msg model.RemoteClusterMsg, rc *model.RemoteCluster, f SendMsgResultFunc) error
|
||||
SendFile(ctx context.Context, us *model.UploadSession, fi *model.FileInfo, rc *model.RemoteCluster, rp ReaderProvider, f SendFileResultFunc) error
|
||||
AcceptInvitation(invite *model.RemoteClusterInvite, name string, creatorId string, teamId string, siteURL string) (*model.RemoteCluster, error)
|
||||
AcceptInvitation(invite *model.RemoteClusterInvite, name string, displayName string, creatorId string, teamId string, siteURL string) (*model.RemoteCluster, error)
|
||||
ReceiveIncomingMsg(rc *model.RemoteCluster, msg model.RemoteClusterMsg) Response
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestOnReceiveChannelInvite(t *testing.T) {
|
||||
}
|
||||
|
||||
mockStore := &mocks.Store{}
|
||||
remoteCluster := &model.RemoteCluster{DisplayName: "test"}
|
||||
remoteCluster := &model.RemoteCluster{Name: "test"}
|
||||
invitation := channelInviteMsg{
|
||||
ChannelId: model.NewId(),
|
||||
TeamId: model.NewId(),
|
||||
@@ -119,7 +119,7 @@ func TestOnReceiveChannelInvite(t *testing.T) {
|
||||
}
|
||||
|
||||
mockStore := &mocks.Store{}
|
||||
remoteCluster := &model.RemoteCluster{DisplayName: "test"}
|
||||
remoteCluster := &model.RemoteCluster{Name: "test2"}
|
||||
invitation := channelInviteMsg{
|
||||
ChannelId: model.NewId(),
|
||||
TeamId: model.NewId(),
|
||||
@@ -161,7 +161,7 @@ func TestOnReceiveChannelInvite(t *testing.T) {
|
||||
}
|
||||
|
||||
mockStore := &mocks.Store{}
|
||||
remoteCluster := &model.RemoteCluster{DisplayName: "test", CreatorId: model.NewId()}
|
||||
remoteCluster := &model.RemoteCluster{Name: "test3", CreatorId: model.NewId()}
|
||||
invitation := channelInviteMsg{
|
||||
ChannelId: model.NewId(),
|
||||
TeamId: model.NewId(),
|
||||
|
||||
@@ -230,6 +230,22 @@ func (_m *MockAppIface) GetOrCreateDirectChannel(userId string, otherUserId stri
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MentionsToTeamMembers provides a mock function with given fields: message, teamID
|
||||
func (_m *MockAppIface) MentionsToTeamMembers(message string, teamID string) model.UserMentionMap {
|
||||
ret := _m.Called(message, teamID)
|
||||
|
||||
var r0 model.UserMentionMap
|
||||
if rf, ok := ret.Get(0).(func(string, string) model.UserMentionMap); ok {
|
||||
r0 = rf(message, teamID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(model.UserMentionMap)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// PatchChannelModerationsForChannel provides a mock function with given fields: channel, channelModerationsPatch
|
||||
func (_m *MockAppIface) PatchChannelModerationsForChannel(channel *model.Channel, channelModerationsPatch []*model.ChannelModerationPatch) ([]*model.ChannelModeration, *model.AppError) {
|
||||
ret := _m.Called(channel, channelModerationsPatch)
|
||||
|
||||
@@ -6,6 +6,7 @@ package sharedchannel
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/shared/mlog"
|
||||
@@ -54,6 +55,7 @@ func (u userCache) Add(id string) {
|
||||
func (scs *Service) postsToSyncMessages(posts []*model.Post, rc *model.RemoteCluster, nextSyncAt int64) ([]syncMsg, error) {
|
||||
syncMessages := make([]syncMsg, 0, len(posts))
|
||||
|
||||
var teamId string
|
||||
uCache := make(userCache)
|
||||
|
||||
for _, p := range posts {
|
||||
@@ -61,6 +63,19 @@ func (scs *Service) postsToSyncMessages(posts []*model.Post, rc *model.RemoteClu
|
||||
continue
|
||||
}
|
||||
|
||||
// lookup team id once
|
||||
if teamId == "" {
|
||||
sc, err := scs.server.GetStore().SharedChannel().Get(p.ChannelId)
|
||||
if err != nil {
|
||||
scs.server.GetLogger().Log(mlog.LvlSharedChannelServiceError, "Could not get shared channel for post",
|
||||
mlog.String("post_id", p.Id),
|
||||
mlog.Err(err),
|
||||
)
|
||||
continue
|
||||
}
|
||||
teamId = sc.TeamId
|
||||
}
|
||||
|
||||
// any reactions originating from the remote cluster are filtered out
|
||||
reactions, err := scs.server.GetStore().Reaction().GetForPostSince(p.Id, nextSyncAt, rc.RemoteId, true)
|
||||
if err != nil {
|
||||
@@ -104,7 +119,7 @@ func (scs *Service) postsToSyncMessages(posts []*model.Post, rc *model.RemoteClu
|
||||
}
|
||||
|
||||
// any users originating from the remote cluster are filtered out
|
||||
users := scs.usersForPost(postSync, reactions, rc, uCache)
|
||||
users := scs.usersForPost(postSync, reactions, teamId, rc, uCache)
|
||||
|
||||
// if everything was filtered out then don't send an empty message.
|
||||
if postSync == nil && len(reactions) == 0 && len(users) == 0 {
|
||||
@@ -127,8 +142,9 @@ func (scs *Service) postsToSyncMessages(posts []*model.Post, rc *model.RemoteClu
|
||||
// usersForPost provides a list of Users associated with the post that need to be synchronized.
|
||||
// The user cache ensures the same user is not synchronized redundantly if they appear in multiple
|
||||
// posts for this sync batch.
|
||||
func (scs *Service) usersForPost(post *model.Post, reactions []*model.Reaction, rc *model.RemoteCluster, uCache userCache) []*model.User {
|
||||
func (scs *Service) usersForPost(post *model.Post, reactions []*model.Reaction, teamID string, rc *model.RemoteCluster, uCache userCache) []*model.User {
|
||||
userIds := make([]string, 0)
|
||||
var mentionMap model.UserMentionMap
|
||||
|
||||
if post != nil && !uCache.Has(post.UserId) {
|
||||
userIds = append(userIds, post.UserId)
|
||||
@@ -142,7 +158,20 @@ func (scs *Service) usersForPost(post *model.Post, reactions []*model.Reaction,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: extract @mentions to local users and sync those as well?
|
||||
// get mentions and userids for each mention
|
||||
if post != nil {
|
||||
mentionMap = scs.app.MentionsToTeamMembers(post.Message, teamID)
|
||||
for mention, id := range mentionMap {
|
||||
if !uCache.Has(id) {
|
||||
userIds = append(userIds, id)
|
||||
uCache.Add(id)
|
||||
scs.server.GetLogger().Log(mlog.LvlSharedChannelServiceDebug, "Found mention",
|
||||
mlog.String("mention", mention),
|
||||
mlog.String("user_id", id),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
users := make([]*model.User, 0)
|
||||
|
||||
@@ -152,27 +181,52 @@ func (scs *Service) usersForPost(post *model.Post, reactions []*model.Reaction,
|
||||
if sync, err2 := scs.shouldUserSync(user, rc); err2 != nil {
|
||||
scs.server.GetLogger().Log(mlog.LvlSharedChannelServiceError, "Could not find user for post",
|
||||
mlog.String("user_id", id),
|
||||
mlog.Err(err2))
|
||||
mlog.Err(err2),
|
||||
)
|
||||
continue
|
||||
} else if sync {
|
||||
users = append(users, sanitizeUserForSync(user))
|
||||
}
|
||||
// if this was a mention then put the real username in place of the username+remotename, but only
|
||||
// when sending to the remote that the user belongs to.
|
||||
if user.RemoteId != nil && *user.RemoteId == rc.RemoteId {
|
||||
fixMention(post, mentionMap, user)
|
||||
}
|
||||
} else {
|
||||
scs.server.GetLogger().Log(mlog.LvlSharedChannelServiceError, "Error checking if user should sync",
|
||||
mlog.String("user_id", id),
|
||||
mlog.Err(err))
|
||||
mlog.Err(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
// fixMention replaces any mentions in a post for the user with the user's real username.
|
||||
func fixMention(post *model.Post, mentionMap model.UserMentionMap, user *model.User) {
|
||||
if post == nil || len(mentionMap) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
realUsername, ok := user.GetProp(KeyRemoteUsername)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// there may be more than one mention for each user so we have to walk the whole map.
|
||||
for mention, id := range mentionMap {
|
||||
if id == user.Id && strings.Contains(mention, ":") {
|
||||
post.Message = strings.ReplaceAll(post.Message, "@"+mention, "@"+realUsername)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeUserForSync(user *model.User) *model.User {
|
||||
user.Password = model.NewId()
|
||||
user.AuthData = nil
|
||||
user.AuthService = ""
|
||||
user.Roles = "system_user"
|
||||
user.AllowMarketing = false
|
||||
user.Props = model.StringMap{}
|
||||
user.NotifyProps = model.StringMap{}
|
||||
user.LastPasswordUpdate = 0
|
||||
user.LastPictureUpdate = 0
|
||||
|
||||
@@ -25,6 +25,9 @@ const (
|
||||
MaxPostsPerSync = 12 // a bit more than one typical screenfull of posts
|
||||
NotifyRemoteOfflineThreshold = time.Second * 10
|
||||
NotifyMinimumDelay = time.Second * 2
|
||||
MaxUpsertRetries = 25
|
||||
KeyRemoteUsername = "RemoteUsername"
|
||||
KeyRemoteEmail = "RemoteEmail"
|
||||
)
|
||||
|
||||
// Mocks can be re-generated with `make sharedchannel-mocks`.
|
||||
@@ -53,6 +56,7 @@ type AppIface interface {
|
||||
PatchChannelModerationsForChannel(channel *model.Channel, channelModerationsPatch []*model.ChannelModerationPatch) ([]*model.ChannelModeration, *model.AppError)
|
||||
CreateUploadSession(us *model.UploadSession) (*model.UploadSession, *model.AppError)
|
||||
FileReader(path string) (filestore.ReadCloseSeeker, *model.AppError)
|
||||
MentionsToTeamMembers(message, teamID string) model.UserMentionMap
|
||||
}
|
||||
|
||||
// errNotFound allows checking against Store.ErrNotFound errors without making Store a dependency.
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/services/remotecluster"
|
||||
@@ -117,7 +118,7 @@ func (scs *Service) processSyncMessages(syncMessages []syncMsg, rc *model.Remote
|
||||
sm.Post.Message = scs.processPermalinkFromRemote(sm.Post, team)
|
||||
}
|
||||
|
||||
// add/update post (may be nil if only reactions changed)
|
||||
// add/update post
|
||||
rpost, err := scs.upsertSyncPost(sm.Post, channel, rc)
|
||||
if err != nil {
|
||||
postErrors = append(postErrors, sm.Post.Id)
|
||||
@@ -169,11 +170,11 @@ func (scs *Service) processSyncMessages(syncMessages []syncMsg, rc *model.Remote
|
||||
|
||||
func (scs *Service) upsertSyncUser(user *model.User, channel *model.Channel, rc *model.RemoteCluster) (*model.User, error) {
|
||||
var err error
|
||||
var userSaved *model.User
|
||||
if user.RemoteId == nil || *user.RemoteId == "" {
|
||||
user.RemoteId = model.NewString(rc.RemoteId)
|
||||
}
|
||||
|
||||
user.RemoteId = model.NewString(rc.RemoteId)
|
||||
|
||||
// does the user already exist?
|
||||
// Check if user already exists
|
||||
euser, err := scs.server.GetStore().User().Get(context.Background(), user.Id)
|
||||
if err != nil {
|
||||
if _, ok := err.(errNotFound); !ok {
|
||||
@@ -181,41 +182,33 @@ func (scs *Service) upsertSyncUser(user *model.User, channel *model.Channel, rc
|
||||
}
|
||||
}
|
||||
|
||||
var userSaved *model.User
|
||||
if euser == nil {
|
||||
if userSaved, err = scs.server.GetStore().User().Save(user); err != nil {
|
||||
if e, ok := err.(errInvalidInput); ok {
|
||||
_, field, value := e.InvalidInputInfo()
|
||||
if field == "email" || field == "username" {
|
||||
// username or email collision
|
||||
// TODO: handle collision by modifying username/email (MM-32133)
|
||||
return nil, fmt.Errorf("collision inserting sync user (%s=%s): %w", field, value, err)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("error inserting sync user: %w", err)
|
||||
if userSaved, err = scs.insertSyncUser(user, channel, rc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
patch := &model.UserPatch{
|
||||
Username: &user.Username,
|
||||
Nickname: &user.Nickname,
|
||||
FirstName: &user.FirstName,
|
||||
LastName: &user.LastName,
|
||||
Email: &user.Email,
|
||||
Position: &user.Position,
|
||||
Locale: &user.Locale,
|
||||
Timezone: user.Timezone,
|
||||
RemoteId: user.RemoteId,
|
||||
}
|
||||
euser.Patch(patch)
|
||||
userUpdated, err := scs.server.GetStore().User().Update(euser, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error updating sync user: %w", err)
|
||||
if userSaved, err = scs.updateSyncUser(patch, euser, channel, rc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userSaved = userUpdated.New
|
||||
}
|
||||
|
||||
// add user to team. We do this here regardless of whether the user was
|
||||
// Add user to team. We do this here regardless of whether the user was
|
||||
// just created or patched since there are three steps to adding a user
|
||||
// (insert rec, add to team, add to channel) and any one could fail.
|
||||
// Instead of undoing what succeeded on any failure we simply do all steps each
|
||||
// time. AddUserToChannel & AddUserToTeamByTeamId do not error if user already
|
||||
// time. AddUserToChannel & AddUserToTeamByTeamId do not error if user was already
|
||||
// added and exit quickly.
|
||||
if err := scs.app.AddUserToTeamByTeamId(channel.TeamId, userSaved); err != nil {
|
||||
return nil, fmt.Errorf("error adding sync user to Team: %w", err)
|
||||
@@ -228,6 +221,97 @@ func (scs *Service) upsertSyncUser(user *model.User, channel *model.Channel, rc
|
||||
return userSaved, nil
|
||||
}
|
||||
|
||||
func (scs *Service) insertSyncUser(user *model.User, channel *model.Channel, rc *model.RemoteCluster) (*model.User, error) {
|
||||
var err error
|
||||
var userSaved *model.User
|
||||
var suffix string
|
||||
|
||||
// save the originals in props (if not already done by another remote)
|
||||
if _, ok := user.GetProp(KeyRemoteUsername); !ok {
|
||||
user.SetProp(KeyRemoteUsername, user.Username)
|
||||
}
|
||||
if _, ok := user.GetProp(KeyRemoteEmail); !ok {
|
||||
user.SetProp(KeyRemoteEmail, user.Email)
|
||||
}
|
||||
|
||||
// Apply a suffix to the username until it is unique. Collisions will be quite
|
||||
// rare since we are joining a username that is unique at a remote site with a unique
|
||||
// name for that site. However we need to truncate the combined name to 64 chars and
|
||||
// that might introduce a collision.
|
||||
for i := 1; i <= MaxUpsertRetries; i++ {
|
||||
if i > 1 {
|
||||
suffix = strconv.FormatInt(int64(i), 10)
|
||||
}
|
||||
|
||||
user.Username = mungUsername(user.Username, rc.Name, suffix, model.USER_NAME_MAX_LENGTH)
|
||||
user.Email = mungEmail(rc.Name, model.USER_EMAIL_MAX_LENGTH)
|
||||
|
||||
if userSaved, err = scs.server.GetStore().User().Save(user); err != nil {
|
||||
e, ok := err.(errInvalidInput)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
_, field, value := e.InvalidInputInfo()
|
||||
if field == "email" || field == "username" {
|
||||
// username or email collision; try again with different suffix
|
||||
scs.server.GetLogger().Log(mlog.LvlSharedChannelServiceWarn, "Collision inserting sync user",
|
||||
mlog.String("field", field),
|
||||
mlog.Any("value", value),
|
||||
mlog.Int("attempt", i),
|
||||
mlog.Err(err),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return userSaved, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("error inserting sync user %s: %w", user.Id, err)
|
||||
}
|
||||
|
||||
func (scs *Service) updateSyncUser(patch *model.UserPatch, user *model.User, channel *model.Channel, rc *model.RemoteCluster) (*model.User, error) {
|
||||
var err error
|
||||
var update *model.UserUpdate
|
||||
var suffix string
|
||||
|
||||
if patch.Username != nil {
|
||||
user.SetProp(KeyRemoteUsername, *patch.Username)
|
||||
}
|
||||
if patch.Email != nil {
|
||||
user.SetProp(KeyRemoteEmail, *patch.Email)
|
||||
}
|
||||
|
||||
user.Patch(patch)
|
||||
|
||||
// Apply a suffix to the username until it is unique.
|
||||
for i := 1; i <= MaxUpsertRetries; i++ {
|
||||
if i > 1 {
|
||||
suffix = strconv.FormatInt(int64(i), 10)
|
||||
}
|
||||
user.Username = mungUsername(user.Username, rc.Name, suffix, model.USER_NAME_MAX_LENGTH)
|
||||
user.Email = mungEmail(rc.Name, model.USER_EMAIL_MAX_LENGTH)
|
||||
|
||||
if update, err = scs.server.GetStore().User().Update(user, false); err != nil {
|
||||
e, ok := err.(errInvalidInput)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
_, field, value := e.InvalidInputInfo()
|
||||
if field == "email" || field == "username" {
|
||||
// username or email collision; try again with different suffix
|
||||
scs.server.GetLogger().Log(mlog.LvlSharedChannelServiceWarn, "Collision updating sync user",
|
||||
mlog.String("field", field),
|
||||
mlog.Any("value", value),
|
||||
mlog.Int("attempt", i),
|
||||
mlog.Err(err),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return update.New, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("error updating sync user %s: %w", user.Id, err)
|
||||
}
|
||||
|
||||
func (scs *Service) upsertSyncPost(post *model.Post, channel *model.Channel, rc *model.RemoteCluster) (*model.Post, error) {
|
||||
var appErr *model.AppError
|
||||
|
||||
|
||||
@@ -315,7 +315,7 @@ func (scs *Service) updateForRemote(task syncTask, rc *model.RemoteCluster) erro
|
||||
// All posts were filtered out, meaning no need to send them. Fast forward SharedChannelRemote's NextSyncAt.
|
||||
scs.updateNextSyncForRemote(scr.Id, rc, nextSince)
|
||||
|
||||
// everything was filtered out, nothing to send.
|
||||
// if there are more posts eligible to sync then schedule another sync
|
||||
if repeat {
|
||||
scs.addTask(newSyncTask(task.channelId, task.remoteId, nil))
|
||||
}
|
||||
|
||||
66
services/sharedchannel/util.go
Обычный файл
66
services/sharedchannel/util.go
Обычный файл
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sharedchannel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
)
|
||||
|
||||
// mungUsername creates a new username by combining username and remote cluster name, plus
|
||||
// a suffix to create uniqueness. If the resulting username exceeds the max length then
|
||||
// it is truncated and ellipses added.
|
||||
func mungUsername(username string, remotename string, suffix string, maxLen int) string {
|
||||
if suffix != "" {
|
||||
suffix = "~" + suffix
|
||||
}
|
||||
|
||||
// If the username already contains a colon then another server already munged it.
|
||||
// In that case we can split on the colon and use the existing remote name.
|
||||
// We still need to re-mung with suffix in case of collision.
|
||||
comps := strings.Split(username, ":")
|
||||
if len(comps) >= 2 {
|
||||
username = comps[0]
|
||||
remotename = strings.Join(comps[1:], "")
|
||||
}
|
||||
|
||||
var userEllipses string
|
||||
var remoteEllipses string
|
||||
|
||||
// The remotename is allowed to use up to half the maxLen, and the username gets the remaining space.
|
||||
// Username might have a suffix to account for, and remotename always has a preceding colon.
|
||||
half := maxLen / 2
|
||||
|
||||
// If the remotename is less than half the maxLen, then the left over space can be given to
|
||||
// the username.
|
||||
extra := half - (len(remotename) + 1)
|
||||
if extra < 0 {
|
||||
extra = 0
|
||||
}
|
||||
|
||||
truncUser := (len(username) + len(suffix)) - (half + extra)
|
||||
if truncUser > 0 {
|
||||
username = username[:len(username)-truncUser-3]
|
||||
userEllipses = "..."
|
||||
}
|
||||
|
||||
truncRemote := (len(remotename) + 1) - (maxLen - (len(username) + len(userEllipses) + len(suffix)))
|
||||
if truncRemote > 0 {
|
||||
remotename = remotename[:len(remotename)-truncRemote-3]
|
||||
remoteEllipses = "..."
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s%s%s:%s%s", username, suffix, userEllipses, remotename, remoteEllipses)
|
||||
}
|
||||
|
||||
// mungEmail creates a unique email address using a UID and remote name.
|
||||
func mungEmail(remotename string, maxLen int) string {
|
||||
s := fmt.Sprintf("%s@%s", model.NewId(), remotename)
|
||||
if len(s) > maxLen {
|
||||
s = s[:maxLen]
|
||||
}
|
||||
return s
|
||||
}
|
||||
76
services/sharedchannel/util_test.go
Обычный файл
76
services/sharedchannel/util_test.go
Обычный файл
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sharedchannel
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_mungUsername(t *testing.T) {
|
||||
type args struct {
|
||||
username string
|
||||
remotename string
|
||||
suffix string
|
||||
maxLen int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{"everything empty", args{username: "", remotename: "", suffix: "", maxLen: 64}, ":"},
|
||||
|
||||
{"no trunc, no suffix", args{username: "bart", remotename: "example.com", suffix: "", maxLen: 64}, "bart:example.com"},
|
||||
{"no trunc, suffix", args{username: "bart", remotename: "example.com", suffix: "2", maxLen: 64}, "bart~2:example.com"},
|
||||
|
||||
{"trunc remote, no suffix", args{username: "bart", remotename: "example1234567890.com", suffix: "", maxLen: 24}, "bart:example123456789..."},
|
||||
{"trunc remote, suffix", args{username: "bart", remotename: "example1234567890.com", suffix: "2", maxLen: 24}, "bart~2:example1234567..."},
|
||||
|
||||
{"trunc both, no suffix", args{username: R(24, "A"), remotename: R(24, "B"), suffix: "", maxLen: 24}, "AAAAAAAAA...:BBBBBBBB..."},
|
||||
{"trunc both, suffix", args{username: R(24, "A"), remotename: R(24, "B"), suffix: "10", maxLen: 24}, "AAAAAA~10...:BBBBBBBB..."},
|
||||
|
||||
{"trunc user, no suffix", args{username: R(40, "A"), remotename: "abc", suffix: "", maxLen: 24}, "AAAAAAAAAAAAAAAAA...:abc"},
|
||||
{"trunc user, suffix", args{username: R(40, "A"), remotename: "abc", suffix: "11", maxLen: 24}, "AAAAAAAAAAAAAA~11...:abc"},
|
||||
|
||||
{"trunc user, remote, no suffix", args{username: R(40, "A"), remotename: "abcdefghijk", suffix: "", maxLen: 24}, "AAAAAAAAA...:abcdefghijk"},
|
||||
{"trunc user, remote, suffix", args{username: R(40, "A"), remotename: "abcdefghijk", suffix: "19", maxLen: 24}, "AAAAAA~19...:abcdefghijk"},
|
||||
|
||||
{"short user, long remote, no suffix", args{username: "bart", remotename: R(40, "B"), suffix: "", maxLen: 24}, "bart:BBBBBBBBBBBBBBBB..."},
|
||||
{"long user, short remote, no suffix", args{username: R(40, "A"), remotename: "abc.com", suffix: "", maxLen: 24}, "AAAAAAAAAAAAA...:abc.com"},
|
||||
|
||||
{"short user, long remote, suffix", args{username: "bart", remotename: R(40, "B"), suffix: "12", maxLen: 24}, "bart~12:BBBBBBBBBBBBB..."},
|
||||
{"long user, short remote, suffix", args{username: R(40, "A"), remotename: "abc.com", suffix: "12", maxLen: 24}, "AAAAAAAAAA~12...:abc.com"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := mungUsername(tt.args.username, tt.args.remotename, tt.args.suffix, tt.args.maxLen); got != tt.want {
|
||||
t.Errorf("mungUsername() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_mungUsernameFuzz(t *testing.T) {
|
||||
// ensure no index out of bounds panic for any combination
|
||||
for i := 0; i < 70; i++ {
|
||||
for j := 0; j < 70; j++ {
|
||||
for k := 0; k < 3; k++ {
|
||||
username := R(i, "A")
|
||||
remotename := R(j, "B")
|
||||
suffix := R(k, "1")
|
||||
|
||||
result := mungUsername(username, remotename, suffix, 64)
|
||||
require.LessOrEqual(t, len(result), 64)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// R returns a string with the specified string repeated `count` times.
|
||||
func R(count int, s string) string {
|
||||
return strings.Repeat(s, count)
|
||||
}
|
||||
Ссылка в новой задаче
Block a user