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
Этот коммит содержится в:
Doug Lauder
2021-04-13 10:40:12 -04:00
коммит произвёл GitHub
родитель 869da7a78b
Коммит f69cb38249
28 изменённых файлов: 544 добавлений и 147 удалений

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

@@ -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) {