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
Этот коммит содержится в:
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
var atMentionRegexp = regexp.MustCompile(`\B@[[:alnum:]][[:alnum:]\.\-_]*`)
|
||||
var atMentionRegexp = regexp.MustCompile(`\B@[[:alnum:]][[:alnum:]\.\-_:]*`)
|
||||
|
||||
const usernameSpecialChars = ".-_"
|
||||
|
||||
@@ -24,7 +24,7 @@ func PossibleAtMentions(message string) []string {
|
||||
alreadyMentioned := make(map[string]bool)
|
||||
for _, match := range atMentionRegexp.FindAllString(message, -1) {
|
||||
name := NormalizeUsername(match[1:])
|
||||
if !alreadyMentioned[name] && IsValidUsername(name) {
|
||||
if !alreadyMentioned[name] && IsValidUsernameAllowRemote(name) {
|
||||
names = append(names, name)
|
||||
alreadyMentioned[name] = true
|
||||
}
|
||||
|
||||
@@ -673,6 +673,7 @@ func (h auditOutgoingWebhook) IsNil() bool {
|
||||
type auditRemoteCluster struct {
|
||||
RemoteId string
|
||||
RemoteTeamId string
|
||||
Name string
|
||||
DisplayName string
|
||||
SiteURL string
|
||||
CreateAt int64
|
||||
@@ -686,6 +687,7 @@ func newRemoteCluster(r *RemoteCluster) auditRemoteCluster {
|
||||
if r != nil {
|
||||
rc.RemoteId = r.RemoteId
|
||||
rc.RemoteTeamId = r.RemoteTeamId
|
||||
rc.Name = r.Name
|
||||
rc.DisplayName = r.DisplayName
|
||||
rc.SiteURL = r.SiteURL
|
||||
rc.CreateAt = r.CreateAt
|
||||
@@ -698,6 +700,7 @@ func newRemoteCluster(r *RemoteCluster) auditRemoteCluster {
|
||||
func (r auditRemoteCluster) MarshalJSONObject(enc *gojay.Encoder) {
|
||||
enc.StringKey("remote_id", r.RemoteId)
|
||||
enc.StringKey("remote_team_id", r.RemoteTeamId)
|
||||
enc.StringKey("name", r.Name)
|
||||
enc.StringKey("display_name", r.DisplayName)
|
||||
enc.StringKey("site_url", r.SiteURL)
|
||||
enc.Int64Key("create_at", r.CreateAt)
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/ldap"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/shared/filestore"
|
||||
"github.com/mattermost/mattermost-server/v5/shared/mlog"
|
||||
)
|
||||
|
||||
@@ -11,16 +11,24 @@ import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
RemoteOfflineAfterMillis = 1000 * 60 * 5 // 5 minutes
|
||||
RemoteNameMinLength = 1
|
||||
RemoteNameMaxLength = 64
|
||||
)
|
||||
|
||||
var (
|
||||
validRemoteNameChars = regexp.MustCompile(`^[a-zA-Z0-9\.\-\_]+$`)
|
||||
)
|
||||
|
||||
type RemoteCluster struct {
|
||||
RemoteId string `json:"remote_id"`
|
||||
RemoteTeamId string `json:"remote_team_id"`
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
SiteURL string `json:"site_url"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
@@ -36,6 +44,14 @@ func (rc *RemoteCluster) PreSave() {
|
||||
rc.RemoteId = NewId()
|
||||
}
|
||||
|
||||
if rc.DisplayName == "" {
|
||||
rc.DisplayName = rc.Name
|
||||
}
|
||||
|
||||
rc.Name = SanitizeUnicode(rc.Name)
|
||||
rc.DisplayName = SanitizeUnicode(rc.DisplayName)
|
||||
rc.Name = NormalizeRemoteName(rc.Name)
|
||||
|
||||
if rc.Token == "" {
|
||||
rc.Token = NewId()
|
||||
}
|
||||
@@ -51,8 +67,8 @@ func (rc *RemoteCluster) IsValid() *AppError {
|
||||
return NewAppError("RemoteCluster.IsValid", "model.cluster.is_valid.id.app_error", nil, "id="+rc.RemoteId, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if rc.DisplayName == "" {
|
||||
return NewAppError("RemoteCluster.IsValid", "model.cluster.is_valid.name.app_error", nil, "display_name empty", http.StatusBadRequest)
|
||||
if !IsValidRemoteName(rc.Name) {
|
||||
return NewAppError("RemoteCluster.IsValid", "model.cluster.is_valid.name.app_error", nil, "name="+rc.Name, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if rc.CreateAt == 0 {
|
||||
@@ -65,7 +81,21 @@ func (rc *RemoteCluster) IsValid() *AppError {
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsValidRemoteName(s string) bool {
|
||||
if len(s) < RemoteNameMinLength || len(s) > RemoteNameMaxLength {
|
||||
return false
|
||||
}
|
||||
return validRemoteNameChars.MatchString(s)
|
||||
}
|
||||
|
||||
func (rc *RemoteCluster) PreUpdate() {
|
||||
if rc.DisplayName == "" {
|
||||
rc.DisplayName = rc.Name
|
||||
}
|
||||
|
||||
rc.Name = SanitizeUnicode(rc.Name)
|
||||
rc.DisplayName = SanitizeUnicode(rc.DisplayName)
|
||||
rc.Name = NormalizeRemoteName(rc.Name)
|
||||
rc.fixTopics()
|
||||
}
|
||||
|
||||
@@ -105,12 +135,17 @@ func (rc *RemoteCluster) ToJSON() (string, error) {
|
||||
|
||||
func (rc *RemoteCluster) ToRemoteClusterInfo() RemoteClusterInfo {
|
||||
return RemoteClusterInfo{
|
||||
Name: rc.Name,
|
||||
DisplayName: rc.DisplayName,
|
||||
CreateAt: rc.CreateAt,
|
||||
LastPingAt: rc.LastPingAt,
|
||||
}
|
||||
}
|
||||
|
||||
func NormalizeRemoteName(name string) string {
|
||||
return strings.ToLower(name)
|
||||
}
|
||||
|
||||
func RemoteClusterFromJSON(data io.Reader) (*RemoteCluster, *AppError) {
|
||||
var rc RemoteCluster
|
||||
err := json.NewDecoder(data).Decode(&rc)
|
||||
@@ -122,6 +157,7 @@ func RemoteClusterFromJSON(data io.Reader) (*RemoteCluster, *AppError) {
|
||||
|
||||
// RemoteClusterInfo provides a subset of RemoteCluster fields suitable for sending to clients.
|
||||
type RemoteClusterInfo struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
LastPingAt int64 `json:"last_ping_at"`
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
func TestRemoteClusterJson(t *testing.T) {
|
||||
o := RemoteCluster{RemoteId: NewId(), DisplayName: "test"}
|
||||
o := RemoteCluster{RemoteId: NewId(), Name: "test"}
|
||||
|
||||
json, err := o.ToJSON()
|
||||
require.NoError(t, err)
|
||||
@@ -24,7 +24,7 @@ func TestRemoteClusterJson(t *testing.T) {
|
||||
require.Nil(t, appErr)
|
||||
|
||||
require.Equal(t, o.RemoteId, ro.RemoteId)
|
||||
require.Equal(t, o.DisplayName, ro.DisplayName)
|
||||
require.Equal(t, o.Name, ro.Name)
|
||||
}
|
||||
|
||||
func TestRemoteClusterIsValid(t *testing.T) {
|
||||
@@ -38,13 +38,13 @@ func TestRemoteClusterIsValid(t *testing.T) {
|
||||
}{
|
||||
{name: "Zero value", rc: &RemoteCluster{}, valid: false},
|
||||
{name: "Missing cluster_name", rc: &RemoteCluster{RemoteId: id}, valid: false},
|
||||
{name: "Missing host_name", rc: &RemoteCluster{RemoteId: id, DisplayName: "test cluster"}, valid: false},
|
||||
{name: "Missing create_at", rc: &RemoteCluster{RemoteId: id, DisplayName: "test cluster", SiteURL: "example.com"}, valid: false},
|
||||
{name: "Missing last_ping_at", rc: &RemoteCluster{RemoteId: id, DisplayName: "test cluster", SiteURL: "example.com", CreatorId: creator, CreateAt: now}, valid: true},
|
||||
{name: "Missing creator", rc: &RemoteCluster{RemoteId: id, DisplayName: "test cluster", SiteURL: "example.com", CreateAt: now, LastPingAt: now}, valid: false},
|
||||
{name: "RemoteCluster valid", rc: &RemoteCluster{RemoteId: id, DisplayName: "test cluster", SiteURL: "example.com", CreateAt: now, LastPingAt: now, CreatorId: creator}, valid: true},
|
||||
{name: "Include protocol", rc: &RemoteCluster{RemoteId: id, DisplayName: "test cluster", SiteURL: "http://example.com", CreateAt: now, LastPingAt: now, CreatorId: creator}, valid: true},
|
||||
{name: "Include protocol & port", rc: &RemoteCluster{RemoteId: id, DisplayName: "test cluster", SiteURL: "http://example.com:8065", CreateAt: now, LastPingAt: now, CreatorId: creator}, valid: true},
|
||||
{name: "Missing host_name", rc: &RemoteCluster{RemoteId: id, Name: NewId()}, valid: false},
|
||||
{name: "Missing create_at", rc: &RemoteCluster{RemoteId: id, Name: NewId(), SiteURL: "example.com"}, valid: false},
|
||||
{name: "Missing last_ping_at", rc: &RemoteCluster{RemoteId: id, Name: NewId(), SiteURL: "example.com", CreatorId: creator, CreateAt: now}, valid: true},
|
||||
{name: "Missing creator", rc: &RemoteCluster{RemoteId: id, Name: NewId(), SiteURL: "example.com", CreateAt: now, LastPingAt: now}, valid: false},
|
||||
{name: "RemoteCluster valid", rc: &RemoteCluster{RemoteId: id, Name: NewId(), SiteURL: "example.com", CreateAt: now, LastPingAt: now, CreatorId: creator}, valid: true},
|
||||
{name: "Include protocol", rc: &RemoteCluster{RemoteId: id, Name: NewId(), SiteURL: "http://example.com", CreateAt: now, LastPingAt: now, CreatorId: creator}, valid: true},
|
||||
{name: "Include protocol & port", rc: &RemoteCluster{RemoteId: id, Name: NewId(), SiteURL: "http://example.com:8065", CreateAt: now, LastPingAt: now, CreatorId: creator}, valid: true},
|
||||
}
|
||||
|
||||
for _, item := range data {
|
||||
@@ -60,7 +60,7 @@ func TestRemoteClusterIsValid(t *testing.T) {
|
||||
func TestRemoteClusterPreSave(t *testing.T) {
|
||||
now := GetMillis()
|
||||
|
||||
o := RemoteCluster{RemoteId: NewId(), DisplayName: "test"}
|
||||
o := RemoteCluster{RemoteId: NewId(), Name: NewId()}
|
||||
o.PreSave()
|
||||
|
||||
require.GreaterOrEqual(t, o.CreateAt, now)
|
||||
|
||||
@@ -272,8 +272,14 @@ func (u *User) IsValid() *AppError {
|
||||
return InvalidUserError("update_at", u.Id)
|
||||
}
|
||||
|
||||
if !IsValidUsername(u.Username) {
|
||||
return InvalidUserError("username", u.Id)
|
||||
if u.IsRemote() {
|
||||
if !IsValidUsernameAllowRemote(u.Username) {
|
||||
return InvalidUserError("username", u.Id)
|
||||
}
|
||||
} else {
|
||||
if !IsValidUsername(u.Username) {
|
||||
return InvalidUserError("username", u.Id)
|
||||
}
|
||||
}
|
||||
|
||||
if len(u.Email) > USER_EMAIL_MAX_LENGTH || u.Email == "" || !IsValidEmail(u.Email) {
|
||||
@@ -746,6 +752,21 @@ func (u *User) IsRemote() bool {
|
||||
return u.RemoteId != nil && *u.RemoteId != ""
|
||||
}
|
||||
|
||||
// GetProp fetches a prop value by name.
|
||||
func (u *User) GetProp(name string) (string, bool) {
|
||||
val, ok := u.Props[name]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
// SetProp sets a prop value by name, creating the map if nil.
|
||||
// Not thread safe.
|
||||
func (u *User) SetProp(name string, value string) {
|
||||
if u.Props == nil {
|
||||
u.Props = make(map[string]string)
|
||||
}
|
||||
u.Props[name] = value
|
||||
}
|
||||
|
||||
func (u *User) ToPatch() *UserPatch {
|
||||
return &UserPatch{
|
||||
Username: &u.Username, Password: &u.Password,
|
||||
@@ -836,12 +857,13 @@ func ComparePassword(hash string, password string) bool {
|
||||
}
|
||||
|
||||
var validUsernameChars = regexp.MustCompile(`^[a-z0-9\.\-_]+$`)
|
||||
var validUsernameCharsForRemote = regexp.MustCompile(`^[a-z0-9\.\-_:]+$`)
|
||||
|
||||
var restrictedUsernames = []string{
|
||||
"all",
|
||||
"channel",
|
||||
"matterbot",
|
||||
"system",
|
||||
var restrictedUsernames = map[string]struct{}{
|
||||
"all": {},
|
||||
"channel": {},
|
||||
"matterbot": {},
|
||||
"system": {},
|
||||
}
|
||||
|
||||
func IsValidUsername(s string) bool {
|
||||
@@ -853,13 +875,21 @@ func IsValidUsername(s string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, restrictedUsername := range restrictedUsernames {
|
||||
if s == restrictedUsername {
|
||||
return false
|
||||
}
|
||||
_, found := restrictedUsernames[s]
|
||||
return !found
|
||||
}
|
||||
|
||||
func IsValidUsernameAllowRemote(s string) bool {
|
||||
if len(s) < USER_NAME_MIN_LENGTH || len(s) > USER_NAME_MAX_LENGTH {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
if !validUsernameCharsForRemote.MatchString(s) {
|
||||
return false
|
||||
}
|
||||
|
||||
_, found := restrictedUsernames[s]
|
||||
return !found
|
||||
}
|
||||
|
||||
func CleanUsername(username string) string {
|
||||
|
||||
@@ -211,26 +211,30 @@ func TestUserGetDisplayNameWithPrefix(t *testing.T) {
|
||||
assert.Equal(t, user.GetDisplayNameWithPrefix(SHOW_NICKNAME_FULLNAME, "@"), "nickname", "Display name should be nickname")
|
||||
}
|
||||
|
||||
var usernames = []struct {
|
||||
value string
|
||||
expected bool
|
||||
}{
|
||||
{"spin-punch", true},
|
||||
{"sp", true},
|
||||
{"s", true},
|
||||
{"1spin-punch", true},
|
||||
{"-spin-punch", true},
|
||||
{".spin-punch", true},
|
||||
{"Spin-punch", false},
|
||||
{"spin punch-", false},
|
||||
{"spin_punch", true},
|
||||
{"spin", true},
|
||||
{"PUNCH", false},
|
||||
{"spin.punch", true},
|
||||
{"spin'punch", false},
|
||||
{"spin*punch", false},
|
||||
{"all", false},
|
||||
{"system", false},
|
||||
type usernamesTest struct {
|
||||
value string
|
||||
expected bool
|
||||
expectedWhenRemote bool
|
||||
}
|
||||
|
||||
var usernames = []usernamesTest{
|
||||
{"spin-punch", true, true},
|
||||
{"sp", true, true},
|
||||
{"s", true, true},
|
||||
{"1spin-punch", true, true},
|
||||
{"-spin-punch", true, true},
|
||||
{".spin-punch", true, true},
|
||||
{"Spin-punch", false, false},
|
||||
{"spin punch-", false, false},
|
||||
{"spin_punch", true, true},
|
||||
{"spin", true, true},
|
||||
{"PUNCH", false, false},
|
||||
{"spin.punch", true, true},
|
||||
{"spin'punch", false, false},
|
||||
{"spin*punch", false, false},
|
||||
{"all", false, false},
|
||||
{"system", false, false},
|
||||
{"spin:punch", false, true},
|
||||
}
|
||||
|
||||
func TestValidUsername(t *testing.T) {
|
||||
@@ -239,6 +243,11 @@ func TestValidUsername(t *testing.T) {
|
||||
t.Errorf("expect %v as %v", v.value, v.expected)
|
||||
}
|
||||
}
|
||||
for _, v := range usernames {
|
||||
if IsValidUsernameAllowRemote(v.value) != v.expectedWhenRemote {
|
||||
t.Errorf("expect %v as %v", v.value, v.expectedWhenRemote)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeUsername(t *testing.T) {
|
||||
|
||||
Ссылка в новой задаче
Block a user