snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
This commit is contained in:
BIN
server/channels/app/users/bot_default_icon.png
Normal file
BIN
server/channels/app/users/bot_default_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
11
server/channels/app/users/bot_default_image.go
Normal file
11
server/channels/app/users/bot_default_image.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package users
|
||||
|
||||
import _ "embed"
|
||||
|
||||
// This embed duplicates ../../../../webapp/channels/src/images/bot_default_icon.png
|
||||
//
|
||||
//go:embed bot_default_icon.png
|
||||
var botDefaultImage []byte
|
||||
12
server/channels/app/users/constants.go
Normal file
12
server/channels/app/users/constants.go
Normal file
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package users
|
||||
|
||||
const (
|
||||
TokenTypePasswordRecovery = "password_recovery"
|
||||
TokenTypeVerifyEmail = "verify_email"
|
||||
TokenTypeTeamInvitation = "team_invitation"
|
||||
TokenTypeGuestInvitation = "guest_invitation"
|
||||
InvitationExpiryTime = 1000 * 60 * 60 * 48 // 48 hours
|
||||
)
|
||||
42
server/channels/app/users/errors.go
Normal file
42
server/channels/app/users/errors.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package users
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
AcceptedDomainError = errors.New("the email provided does not belong to an accepted domain")
|
||||
VerifyUserError = errors.New("could not update verify email field")
|
||||
UserCountError = errors.New("could not get the total number of the users.")
|
||||
UserCreationDisabledError = errors.New("user creation is not allowed")
|
||||
UserStoreIsEmptyError = errors.New("could not check if the user store is empty")
|
||||
|
||||
DeleteAllAccessDataError = errors.New("could not delete all access data")
|
||||
|
||||
DefaultFontError = errors.New("could not get default font")
|
||||
UserInitialsError = errors.New("could not get user initials")
|
||||
ImageEncodingError = errors.New("could not encode image")
|
||||
GlyphError = errors.New("could not get glyph")
|
||||
|
||||
InvalidPasswordError = NewErrInvalidPassword("")
|
||||
)
|
||||
|
||||
// ErrInvalidPassword indicates an error against the password settings
|
||||
type ErrInvalidPassword struct {
|
||||
id string
|
||||
}
|
||||
|
||||
func NewErrInvalidPassword(id string) *ErrInvalidPassword {
|
||||
return &ErrInvalidPassword{
|
||||
id: id,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ErrInvalidPassword) Error() string {
|
||||
return "invalid password"
|
||||
}
|
||||
|
||||
func (e *ErrInvalidPassword) Id() string {
|
||||
return e.id
|
||||
}
|
||||
140
server/channels/app/users/helper_test.go
Normal file
140
server/channels/app/users/helper_test.go
Normal file
@@ -0,0 +1,140 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package users
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store"
|
||||
"github.com/mattermost/mattermost/server/v8/config"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type TestHelper struct {
|
||||
service *UserService
|
||||
configStore *config.Store
|
||||
dbStore store.Store
|
||||
workspace string
|
||||
|
||||
Context *request.Context
|
||||
BasicUser *model.User
|
||||
BasicUser2 *model.User
|
||||
|
||||
SystemAdminUser *model.User
|
||||
LogBuffer *bytes.Buffer
|
||||
}
|
||||
|
||||
func Setup(tb testing.TB) *TestHelper {
|
||||
if testing.Short() {
|
||||
tb.SkipNow()
|
||||
}
|
||||
dbStore := mainHelper.GetStore()
|
||||
dbStore.DropAllTables()
|
||||
dbStore.MarkSystemRanUnitTests()
|
||||
mainHelper.PreloadMigrations()
|
||||
|
||||
return setupTestHelper(dbStore, false, tb)
|
||||
}
|
||||
|
||||
func setupTestHelper(s store.Store, _ bool, tb testing.TB) *TestHelper {
|
||||
tempWorkspace, err := os.MkdirTemp("", "userservicetest")
|
||||
require.NoError(tb, err)
|
||||
|
||||
configStore := config.NewTestMemoryStore()
|
||||
|
||||
config := configStore.Get()
|
||||
*config.PluginSettings.Directory = filepath.Join(tempWorkspace, "plugins")
|
||||
*config.PluginSettings.ClientDirectory = filepath.Join(tempWorkspace, "webapp")
|
||||
*config.PluginSettings.AutomaticPrepackagedPlugins = false
|
||||
*config.LogSettings.EnableSentry = false // disable error reporting during tests
|
||||
*config.AnnouncementSettings.AdminNoticesEnabled = false
|
||||
*config.AnnouncementSettings.UserNoticesEnabled = false
|
||||
*config.TeamSettings.MaxUsersPerTeam = 50
|
||||
*config.RateLimitSettings.Enable = false
|
||||
*config.TeamSettings.EnableOpenServer = true
|
||||
_, _, err = configStore.Set(config)
|
||||
require.NoError(tb, err)
|
||||
|
||||
buffer := &bytes.Buffer{}
|
||||
|
||||
tb.Cleanup(func() {
|
||||
err := configStore.Close()
|
||||
require.NoError(tb, err)
|
||||
|
||||
s.Close()
|
||||
|
||||
if tempWorkspace != "" {
|
||||
os.RemoveAll(tempWorkspace)
|
||||
}
|
||||
})
|
||||
|
||||
return &TestHelper{
|
||||
service: &UserService{
|
||||
store: s.User(),
|
||||
sessionStore: s.Session(),
|
||||
oAuthStore: s.OAuth(),
|
||||
config: configStore.Get,
|
||||
},
|
||||
Context: request.EmptyContext(mlog.CreateConsoleTestLogger(tb)),
|
||||
configStore: configStore,
|
||||
dbStore: s,
|
||||
LogBuffer: buffer,
|
||||
workspace: tempWorkspace,
|
||||
}
|
||||
}
|
||||
|
||||
func (th *TestHelper) InitBasic(tb testing.TB) *TestHelper {
|
||||
var err error
|
||||
|
||||
th.SystemAdminUser = th.CreateUser(tb)
|
||||
th.SystemAdminUser, err = th.service.GetUser(th.Context, th.SystemAdminUser.Id)
|
||||
require.NoError(tb, err)
|
||||
|
||||
th.BasicUser = th.CreateUser(tb)
|
||||
th.BasicUser, err = th.service.GetUser(th.Context, th.BasicUser.Id)
|
||||
require.NoError(tb, err)
|
||||
|
||||
th.BasicUser2 = th.CreateUser(tb)
|
||||
th.BasicUser2, err = th.service.GetUser(th.Context, th.BasicUser2.Id)
|
||||
require.NoError(tb, err)
|
||||
|
||||
return th
|
||||
}
|
||||
|
||||
func (th *TestHelper) CreateUser(tb testing.TB) *model.User {
|
||||
return th.CreateUserOrGuest(tb, false)
|
||||
}
|
||||
|
||||
func (th *TestHelper) CreateGuest(tb testing.TB) *model.User {
|
||||
return th.CreateUserOrGuest(tb, true)
|
||||
}
|
||||
|
||||
func (th *TestHelper) CreateUserOrGuest(tb testing.TB, guest bool) *model.User {
|
||||
id := model.NewId()
|
||||
|
||||
user := &model.User{
|
||||
Email: "success+" + id + "@simulator.amazonses.com",
|
||||
Username: "un_" + id,
|
||||
Nickname: "nn_" + id,
|
||||
Password: model.NewTestPassword(),
|
||||
EmailVerified: true,
|
||||
}
|
||||
|
||||
var err error
|
||||
if guest {
|
||||
user, err = th.service.CreateUser(th.Context, user, UserCreateOptions{Guest: true})
|
||||
require.NoError(tb, err)
|
||||
} else {
|
||||
user, err = th.service.CreateUser(th.Context, user, UserCreateOptions{})
|
||||
require.NoError(tb, err)
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
24
server/channels/app/users/main_test.go
Normal file
24
server/channels/app/users/main_test.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package users
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/channels/testlib"
|
||||
)
|
||||
|
||||
var mainHelper *testlib.MainHelper
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
var options = testlib.HelperOptions{
|
||||
EnableStore: true,
|
||||
EnableResources: true,
|
||||
}
|
||||
|
||||
mainHelper = testlib.NewMainHelperWithOptions(&options)
|
||||
defer mainHelper.Close()
|
||||
|
||||
mainHelper.Main(m)
|
||||
}
|
||||
70
server/channels/app/users/password.go
Normal file
70
server/channels/app/users/password.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package users
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
func (us *UserService) isPasswordValid(password string) error {
|
||||
return IsPasswordValidWithSettings(password, &us.config().PasswordSettings)
|
||||
}
|
||||
|
||||
// IsPasswordValidWithSettings is a utility functions that checks if the given password
|
||||
// conforms to the password settings. It returns the error id as error value.
|
||||
func IsPasswordValidWithSettings(password string, settings *model.PasswordSettings) error {
|
||||
id := "model.user.is_valid.pwd"
|
||||
isError := false
|
||||
isMinMaxError := false
|
||||
|
||||
if len(password) < *settings.MinimumLength {
|
||||
isError = true
|
||||
isMinMaxError = true
|
||||
id = id + "_min_length"
|
||||
}
|
||||
|
||||
if len(password) > model.PasswordMaximumLength {
|
||||
isError = true
|
||||
isMinMaxError = true
|
||||
id = id + "_max_length"
|
||||
}
|
||||
|
||||
if !isMinMaxError {
|
||||
if *settings.Lowercase {
|
||||
if !strings.ContainsAny(password, model.LowercaseLetters) {
|
||||
isError = true
|
||||
id = id + "_lowercase"
|
||||
}
|
||||
}
|
||||
|
||||
if *settings.Uppercase {
|
||||
if !strings.ContainsAny(password, model.UppercaseLetters) {
|
||||
isError = true
|
||||
id = id + "_uppercase"
|
||||
}
|
||||
}
|
||||
|
||||
if *settings.Number {
|
||||
if !strings.ContainsAny(password, model.NUMBERS) {
|
||||
isError = true
|
||||
id = id + "_number"
|
||||
}
|
||||
}
|
||||
|
||||
if *settings.Symbol {
|
||||
if !strings.ContainsAny(password, model.SYMBOLS) {
|
||||
isError = true
|
||||
id = id + "_symbol"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isError {
|
||||
return NewErrInvalidPassword(id + ".app_error")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
133
server/channels/app/users/password_test.go
Normal file
133
server/channels/app/users/password_test.go
Normal file
@@ -0,0 +1,133 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package users
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
func TestIsPasswordValidWithSettings(t *testing.T) {
|
||||
for name, tc := range map[string]struct {
|
||||
Password string
|
||||
Settings *model.PasswordSettings
|
||||
ExpectedError string
|
||||
}{
|
||||
"Short": {
|
||||
Password: strings.Repeat("x", model.PasswordFIPSMinimumLength),
|
||||
Settings: &model.PasswordSettings{
|
||||
MinimumLength: model.NewPointer(model.PasswordFIPSMinimumLength),
|
||||
Lowercase: new(false),
|
||||
Uppercase: new(false),
|
||||
Number: new(false),
|
||||
Symbol: new(false),
|
||||
},
|
||||
},
|
||||
"Long": {
|
||||
Password: strings.Repeat("x", model.PasswordMaximumLength),
|
||||
Settings: &model.PasswordSettings{
|
||||
Lowercase: new(false),
|
||||
Uppercase: new(false),
|
||||
Number: new(false),
|
||||
Symbol: new(false),
|
||||
},
|
||||
},
|
||||
"TooShort": {
|
||||
Password: strings.Repeat("x", 2),
|
||||
Settings: &model.PasswordSettings{
|
||||
MinimumLength: model.NewPointer(model.PasswordFIPSMinimumLength),
|
||||
Lowercase: new(false),
|
||||
Uppercase: new(false),
|
||||
Number: new(false),
|
||||
Symbol: new(false),
|
||||
},
|
||||
ExpectedError: "model.user.is_valid.pwd_min_length.app_error",
|
||||
},
|
||||
"TooLong": {
|
||||
Password: strings.Repeat("x", model.PasswordMaximumLength+1),
|
||||
Settings: &model.PasswordSettings{
|
||||
Lowercase: new(false),
|
||||
Uppercase: new(false),
|
||||
Number: new(false),
|
||||
Symbol: new(false),
|
||||
},
|
||||
ExpectedError: "model.user.is_valid.pwd_max_length.app_error",
|
||||
},
|
||||
"MissingLower": {
|
||||
Password: "AAAAAAAAAAASD123!@#",
|
||||
Settings: &model.PasswordSettings{
|
||||
Lowercase: new(true),
|
||||
Uppercase: new(false),
|
||||
Number: new(false),
|
||||
Symbol: new(false),
|
||||
},
|
||||
ExpectedError: "model.user.is_valid.pwd_lowercase.app_error",
|
||||
},
|
||||
"MissingUpper": {
|
||||
Password: "aaaaaaaaaaaaasd123!@#",
|
||||
Settings: &model.PasswordSettings{
|
||||
Uppercase: new(true),
|
||||
Lowercase: new(false),
|
||||
Number: new(false),
|
||||
Symbol: new(false),
|
||||
},
|
||||
ExpectedError: "model.user.is_valid.pwd_uppercase.app_error",
|
||||
},
|
||||
"MissingNumber": {
|
||||
Password: "asasdasdsadASD!@#",
|
||||
Settings: &model.PasswordSettings{
|
||||
Number: new(true),
|
||||
Lowercase: new(false),
|
||||
Uppercase: new(false),
|
||||
Symbol: new(false),
|
||||
},
|
||||
ExpectedError: "model.user.is_valid.pwd_number.app_error",
|
||||
},
|
||||
"MissingSymbol": {
|
||||
Password: "asdasdasdasdasdASD123",
|
||||
Settings: &model.PasswordSettings{
|
||||
Symbol: new(true),
|
||||
Lowercase: new(false),
|
||||
Uppercase: new(false),
|
||||
Number: new(false),
|
||||
},
|
||||
ExpectedError: "model.user.is_valid.pwd_symbol.app_error",
|
||||
},
|
||||
"MissingMultiple": {
|
||||
Password: "asdasdasdasdasdasd",
|
||||
Settings: &model.PasswordSettings{
|
||||
Lowercase: new(true),
|
||||
Uppercase: new(true),
|
||||
Number: new(true),
|
||||
Symbol: new(true),
|
||||
},
|
||||
ExpectedError: "model.user.is_valid.pwd_uppercase_number_symbol.app_error",
|
||||
},
|
||||
"Everything": {
|
||||
Password: "asdASDasd!@#123",
|
||||
Settings: &model.PasswordSettings{
|
||||
Lowercase: new(true),
|
||||
Uppercase: new(true),
|
||||
Number: new(true),
|
||||
Symbol: new(true),
|
||||
},
|
||||
},
|
||||
} {
|
||||
tc.Settings.SetDefaults()
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if err := IsPasswordValidWithSettings(tc.Password, tc.Settings); tc.ExpectedError == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
invErr, ok := err.(*ErrInvalidPassword)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, tc.ExpectedError, invErr.Id())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
204
server/channels/app/users/profile_picture.go
Normal file
204
server/channels/app/users/profile_picture.go
Normal file
@@ -0,0 +1,204 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package users
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"hash/fnv"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/freetype"
|
||||
"github.com/golang/freetype/truetype"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils/fileutils"
|
||||
"github.com/mattermost/mattermost/server/v8/platform/shared/filestore"
|
||||
xfont "golang.org/x/image/font"
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
const (
|
||||
imageProfilePixelDimension = 128
|
||||
)
|
||||
|
||||
func (us *UserService) GetProfileImage(user *model.User) ([]byte, bool, error) {
|
||||
if *us.config().FileSettings.DriverName == "" {
|
||||
img, err := us.GetDefaultProfileImage(user)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return img, false, nil
|
||||
}
|
||||
|
||||
path := path.Join("users", user.Id, "profile.png")
|
||||
data, err := us.ReadFile(path)
|
||||
if err != nil {
|
||||
img, appErr := us.GetDefaultProfileImage(user)
|
||||
if appErr != nil {
|
||||
return nil, false, appErr
|
||||
}
|
||||
|
||||
if user.LastPictureUpdate == 0 {
|
||||
if _, err := us.writeFile(bytes.NewReader(img), path); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
return img, true, nil
|
||||
}
|
||||
|
||||
return data, false, nil
|
||||
}
|
||||
|
||||
func (us *UserService) FileBackend() (filestore.FileBackend, error) {
|
||||
license := us.license()
|
||||
insecure := us.config().ServiceSettings.EnableInsecureOutgoingConnections
|
||||
allowedUntrustedInternalConnections := model.SafeDereference(us.config().ServiceSettings.AllowedUntrustedInternalConnections)
|
||||
backend, err := filestore.NewFileBackend(filestore.NewFileBackendSettingsFromConfig(&us.config().FileSettings, license != nil && *license.Features.Compliance, insecure != nil && *insecure, allowedUntrustedInternalConnections))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return backend, nil
|
||||
}
|
||||
|
||||
func (us *UserService) ReadFile(path string) ([]byte, error) {
|
||||
backend, err := us.FileBackend()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, nErr := backend.ReadFile(path)
|
||||
if nErr != nil {
|
||||
return nil, nErr
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (us *UserService) writeFile(fr io.Reader, path string) (int64, error) {
|
||||
backend, err := us.FileBackend()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
result, nErr := backend.WriteFile(fr, path)
|
||||
if nErr != nil {
|
||||
return result, nErr
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (us *UserService) GetDefaultProfileImage(user *model.User) ([]byte, error) {
|
||||
if user.IsBot {
|
||||
return botDefaultImage, nil
|
||||
}
|
||||
|
||||
return createProfileImage(user.Username, user.Id, *us.config().FileSettings.InitialFont)
|
||||
}
|
||||
|
||||
func createProfileImage(username string, userID string, initialFont string) ([]byte, error) {
|
||||
colors := []color.NRGBA{
|
||||
{197, 8, 126, 255},
|
||||
{227, 207, 18, 255},
|
||||
{28, 181, 105, 255},
|
||||
{35, 188, 224, 255},
|
||||
{116, 49, 196, 255},
|
||||
{197, 8, 126, 255},
|
||||
{197, 19, 19, 255},
|
||||
{250, 134, 6, 255},
|
||||
{227, 207, 18, 255},
|
||||
{123, 201, 71, 255},
|
||||
{28, 181, 105, 255},
|
||||
{35, 188, 224, 255},
|
||||
{116, 49, 196, 255},
|
||||
{197, 8, 126, 255},
|
||||
{197, 19, 19, 255},
|
||||
{250, 134, 6, 255},
|
||||
{227, 207, 18, 255},
|
||||
{123, 201, 71, 255},
|
||||
{28, 181, 105, 255},
|
||||
{35, 188, 224, 255},
|
||||
{116, 49, 196, 255},
|
||||
{197, 8, 126, 255},
|
||||
{197, 19, 19, 255},
|
||||
{250, 134, 6, 255},
|
||||
{227, 207, 18, 255},
|
||||
{123, 201, 71, 255},
|
||||
}
|
||||
|
||||
h := fnv.New32a()
|
||||
h.Write([]byte(userID))
|
||||
seed := h.Sum32()
|
||||
|
||||
initial := string(strings.ToUpper(username)[0])
|
||||
|
||||
font, err := getFont(initialFont)
|
||||
if err != nil {
|
||||
return nil, DefaultFontError
|
||||
}
|
||||
|
||||
color := colors[int64(seed)%int64(len(colors))]
|
||||
|
||||
dstImg := image.NewRGBA(image.Rect(0, 0, imageProfilePixelDimension, imageProfilePixelDimension))
|
||||
|
||||
draw.Draw(dstImg, dstImg.Bounds(), &image.Uniform{color}, image.Point{}, draw.Src)
|
||||
|
||||
size := float64(imageProfilePixelDimension / 2)
|
||||
|
||||
opts := truetype.Options{}
|
||||
opts.Size = size
|
||||
face := truetype.NewFace(font, &opts)
|
||||
|
||||
d := &xfont.Drawer{
|
||||
Dst: dstImg,
|
||||
Src: image.White,
|
||||
Face: face,
|
||||
}
|
||||
|
||||
bounds, advance, ok := face.GlyphBounds([]rune(initial)[0])
|
||||
|
||||
if !ok {
|
||||
return nil, GlyphError
|
||||
}
|
||||
|
||||
x := (fixed.I(imageProfilePixelDimension) - advance) / 2
|
||||
y := (fixed.I(imageProfilePixelDimension) + (bounds.Max.Y - bounds.Min.Y)) / 2
|
||||
|
||||
d.Dot = fixed.Point26_6{
|
||||
X: x,
|
||||
Y: y,
|
||||
}
|
||||
d.DrawString(initial)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
enc := png.Encoder{
|
||||
CompressionLevel: png.BestCompression,
|
||||
}
|
||||
if imgErr := enc.Encode(buf, dstImg); imgErr != nil {
|
||||
return nil, ImageEncodingError
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func getFont(initialFont string) (*truetype.Font, error) {
|
||||
// Some people have the old default font still set, so just treat that as if they're using the new default
|
||||
if initialFont == "luximbi.ttf" {
|
||||
initialFont = "nunito-bold.ttf"
|
||||
}
|
||||
|
||||
fontDir, _ := fileutils.FindDir("fonts")
|
||||
fontBytes, err := os.ReadFile(filepath.Join(fontDir, initialFont))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return freetype.ParseFont(fontBytes)
|
||||
}
|
||||
26
server/channels/app/users/profile_picture_test.go
Normal file
26
server/channels/app/users/profile_picture_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package users
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCreateProfileImage(t *testing.T) {
|
||||
b, err := createProfileImage("Corey Hulen", "eo1zkdr96pdj98pjmq8zy35wba", "nunito-bold.ttf")
|
||||
require.NoError(t, err)
|
||||
|
||||
rdr := bytes.NewReader(b)
|
||||
img, _, err2 := image.Decode(rdr)
|
||||
require.NoError(t, err2)
|
||||
|
||||
colorful := color.RGBA{116, 49, 196, 255}
|
||||
|
||||
require.Equal(t, colorful, img.At(1, 1), "Failed to create correct color")
|
||||
}
|
||||
59
server/channels/app/users/service.go
Normal file
59
server/channels/app/users/service.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package users
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store"
|
||||
"github.com/mattermost/mattermost/server/v8/einterfaces"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
store store.UserStore
|
||||
sessionStore store.SessionStore
|
||||
oAuthStore store.OAuthStore
|
||||
metrics einterfaces.MetricsInterface
|
||||
cluster einterfaces.ClusterInterface
|
||||
config func() *model.Config
|
||||
license func() *model.License
|
||||
}
|
||||
|
||||
// ServiceConfig is used to initialize the UserService.
|
||||
type ServiceConfig struct {
|
||||
// Mandatory fields
|
||||
UserStore store.UserStore
|
||||
SessionStore store.SessionStore
|
||||
OAuthStore store.OAuthStore
|
||||
ConfigFn func() *model.Config
|
||||
LicenseFn func() *model.License
|
||||
// Optional fields
|
||||
Metrics einterfaces.MetricsInterface
|
||||
Cluster einterfaces.ClusterInterface
|
||||
}
|
||||
|
||||
func New(c ServiceConfig) (*UserService, error) {
|
||||
if err := c.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &UserService{
|
||||
store: c.UserStore,
|
||||
sessionStore: c.SessionStore,
|
||||
oAuthStore: c.OAuthStore,
|
||||
config: c.ConfigFn,
|
||||
license: c.LicenseFn,
|
||||
metrics: c.Metrics,
|
||||
cluster: c.Cluster,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *ServiceConfig) validate() error {
|
||||
if c.ConfigFn == nil || c.UserStore == nil || c.SessionStore == nil || c.OAuthStore == nil || c.LicenseFn == nil {
|
||||
return errors.New("required parameters are not provided")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
39
server/channels/app/users/service_test.go
Normal file
39
server/channels/app/users/service_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package users
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
}
|
||||
_, err := New(ServiceConfig{})
|
||||
require.Error(t, err)
|
||||
|
||||
dbStore := mainHelper.GetStore()
|
||||
|
||||
cfn := func() *model.Config {
|
||||
return &model.Config{}
|
||||
}
|
||||
|
||||
lfn := func() *model.License {
|
||||
return model.NewTestLicense()
|
||||
}
|
||||
|
||||
_, err = New(ServiceConfig{
|
||||
UserStore: dbStore.User(),
|
||||
SessionStore: dbStore.Session(),
|
||||
OAuthStore: dbStore.OAuth(),
|
||||
ConfigFn: cfn,
|
||||
LicenseFn: lfn,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
276
server/channels/app/users/users.go
Normal file
276
server/channels/app/users/users.go
Normal file
@@ -0,0 +1,276 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package users
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/i18n"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store"
|
||||
"github.com/mattermost/mattermost/server/v8/platform/shared/mfa"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type UserCreateOptions struct {
|
||||
Guest bool
|
||||
FromImport bool
|
||||
}
|
||||
|
||||
// CreateUser creates a user
|
||||
func (us *UserService) CreateUser(rctx request.CTX, user *model.User, opts UserCreateOptions) (*model.User, error) {
|
||||
if opts.FromImport {
|
||||
return us.createUser(rctx, user)
|
||||
}
|
||||
|
||||
user.Roles = model.SystemUserRoleId
|
||||
if opts.Guest {
|
||||
user.Roles = model.SystemGuestRoleId
|
||||
}
|
||||
|
||||
if !user.IsLDAPUser() && !user.IsSAMLUser() && !user.IsGuest() && !CheckUserDomain(user, *us.config().TeamSettings.RestrictCreationToDomains) {
|
||||
return nil, AcceptedDomainError
|
||||
}
|
||||
|
||||
if !user.IsLDAPUser() && !user.IsSAMLUser() && user.IsGuest() && !CheckUserDomain(user, *us.config().GuestAccountsSettings.RestrictCreationToDomains) {
|
||||
return nil, AcceptedDomainError
|
||||
}
|
||||
|
||||
// Below is a special case where the first user in the entire
|
||||
// system is granted the system_admin role. Bot users must never be
|
||||
// granted this role: on a fresh install the bots created by plugins
|
||||
// can be the only accounts present, and they should not be treated
|
||||
// as the "first user".
|
||||
if !user.IsBot {
|
||||
if ok, err := us.store.IsEmpty(true); err != nil {
|
||||
return nil, errors.Wrap(UserStoreIsEmptyError, err.Error())
|
||||
} else if ok {
|
||||
user.Roles = model.SystemAdminRoleId + " " + model.SystemUserRoleId
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := i18n.GetSupportedLocales()[user.Locale]; !ok {
|
||||
user.Locale = *us.config().LocalizationSettings.DefaultClientLocale
|
||||
}
|
||||
|
||||
return us.createUser(rctx, user)
|
||||
}
|
||||
|
||||
func (us *UserService) createUser(rctx request.CTX, user *model.User) (*model.User, error) {
|
||||
user.MakeNonNil()
|
||||
|
||||
if err := us.isPasswordValid(user.Password); user.AuthService == "" && err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ruser, err := us.store.Save(rctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.EmailVerified {
|
||||
if err := us.verifyUserEmail(ruser.Id, user.Email); err != nil {
|
||||
mlog.Warn("Failed to set email verified", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
// Determine whether to send the created user a welcome email
|
||||
ruser.DisableWelcomeEmail = user.DisableWelcomeEmail
|
||||
ruser.Sanitize(map[string]bool{})
|
||||
|
||||
return ruser, nil
|
||||
}
|
||||
|
||||
func (us *UserService) verifyUserEmail(userID, email string) error {
|
||||
if _, err := us.store.VerifyEmail(userID, email); err != nil {
|
||||
return VerifyUserError
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (us *UserService) GetUser(rctx request.CTX, userID string) (*model.User, error) {
|
||||
return us.store.Get(rctx, userID)
|
||||
}
|
||||
|
||||
func (us *UserService) GetUsers(rctx request.CTX, userIDs []string) ([]*model.User, error) {
|
||||
return us.store.GetMany(rctx, userIDs)
|
||||
}
|
||||
|
||||
func (us *UserService) GetUserByUsername(username string) (*model.User, error) {
|
||||
return us.store.GetByUsername(username)
|
||||
}
|
||||
|
||||
func (us *UserService) GetUserByEmail(email string) (*model.User, error) {
|
||||
return us.store.GetByEmail(email)
|
||||
}
|
||||
|
||||
func (us *UserService) GetUserByRemoteID(remoteID string) (*model.User, error) {
|
||||
return us.store.GetByRemoteID(remoteID)
|
||||
}
|
||||
|
||||
func (us *UserService) GetUserByAuth(authData *string, authService string) (*model.User, error) {
|
||||
return us.store.GetByAuth(authData, authService)
|
||||
}
|
||||
|
||||
func (us *UserService) GetUserByAuthData(authData *string) (*model.User, error) {
|
||||
return us.store.GetByAuthData(authData)
|
||||
}
|
||||
|
||||
func (us *UserService) GetUsersFromProfiles(options *model.UserGetOptions) ([]*model.User, error) {
|
||||
return us.store.GetAllProfiles(options)
|
||||
}
|
||||
|
||||
func (us *UserService) GetUsersByUsernames(usernames []string, options *model.UserGetOptions) ([]*model.User, error) {
|
||||
return us.store.GetProfilesByUsernames(usernames, options.ViewRestrictions)
|
||||
}
|
||||
|
||||
func (us *UserService) GetUsersPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, error) {
|
||||
users, err := us.GetUsersFromProfiles(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return us.sanitizeProfiles(users, asAdmin), nil
|
||||
}
|
||||
|
||||
func (us *UserService) GetUsersEtag(restrictionsHash string) string {
|
||||
return fmt.Sprintf("%v.%v.%v.%v", us.store.GetEtagForAllProfiles(), us.config().PrivacySettings.ShowFullName, us.config().PrivacySettings.ShowEmailAddress, restrictionsHash)
|
||||
}
|
||||
|
||||
func (us *UserService) GetUsersByIds(rctx request.CTX, userIDs []string, options *store.UserGetByIdsOpts) ([]*model.User, error) {
|
||||
allowFromCache := options.ViewRestrictions == nil
|
||||
|
||||
users, err := us.store.GetProfileByIds(rctx, userIDs, options, allowFromCache)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return us.sanitizeProfiles(users, options.IsAdmin), nil
|
||||
}
|
||||
|
||||
func (us *UserService) GetUsersInTeam(options *model.UserGetOptions) ([]*model.User, error) {
|
||||
return us.store.GetProfiles(options)
|
||||
}
|
||||
|
||||
func (us *UserService) GetUsersNotInTeam(teamID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
|
||||
return us.store.GetProfilesNotInTeam(teamID, groupConstrained, offset, limit, viewRestrictions)
|
||||
}
|
||||
|
||||
func (us *UserService) GetUsersInTeamPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, error) {
|
||||
users, err := us.GetUsersInTeam(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return us.sanitizeProfiles(users, asAdmin), nil
|
||||
}
|
||||
|
||||
func (us *UserService) GetUsersNotInTeamPage(teamID string, groupConstrained bool, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
|
||||
users, err := us.GetUsersNotInTeam(teamID, groupConstrained, page*perPage, perPage, viewRestrictions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return us.sanitizeProfiles(users, asAdmin), nil
|
||||
}
|
||||
|
||||
func (us *UserService) GetUsersInTeamEtag(teamID string, restrictionsHash string) string {
|
||||
return fmt.Sprintf("%v.%v.%v.%v", us.store.GetEtagForProfiles(teamID), us.config().PrivacySettings.ShowFullName, us.config().PrivacySettings.ShowEmailAddress, restrictionsHash)
|
||||
}
|
||||
|
||||
func (us *UserService) GetUsersNotInTeamEtag(teamID string, restrictionsHash string) string {
|
||||
return fmt.Sprintf("%v.%v.%v.%v", us.store.GetEtagForProfilesNotInTeam(teamID), us.config().PrivacySettings.ShowFullName, us.config().PrivacySettings.ShowEmailAddress, restrictionsHash)
|
||||
}
|
||||
|
||||
func (us *UserService) GetUsersWithoutTeamPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, error) {
|
||||
users, err := us.GetUsersWithoutTeam(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return us.sanitizeProfiles(users, asAdmin), nil
|
||||
}
|
||||
|
||||
func (us *UserService) GetUsersWithoutTeam(options *model.UserGetOptions) ([]*model.User, error) {
|
||||
users, err := us.store.GetProfilesWithoutTeam(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (us *UserService) UpdateUser(rctx request.CTX, user *model.User, allowRoleUpdate bool) (*model.UserUpdate, error) {
|
||||
return us.store.Update(rctx, user, allowRoleUpdate)
|
||||
}
|
||||
|
||||
func (us *UserService) UpdateUserNotifyProps(userID string, props map[string]string) error {
|
||||
return us.store.UpdateNotifyProps(userID, props)
|
||||
}
|
||||
|
||||
func (us *UserService) DeactivateAllGuests() ([]string, error) {
|
||||
users, err := us.store.DeactivateGuests()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (us *UserService) DeactivateMagicLinkGuests() ([]string, error) {
|
||||
users, err := us.store.DeactivateMagicLinkGuests()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (us *UserService) InvalidateCacheForUser(userID string) {
|
||||
us.store.InvalidateProfilesInChannelCacheByUser(userID)
|
||||
us.store.InvalidateProfileCacheForUser(userID)
|
||||
|
||||
if us.cluster != nil {
|
||||
msg := &model.ClusterMessage{
|
||||
Event: model.ClusterEventInvalidateCacheForUser,
|
||||
SendType: model.ClusterSendBestEffort,
|
||||
Data: []byte(userID),
|
||||
}
|
||||
us.cluster.SendClusterMessage(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (us *UserService) GenerateMfaSecret(user *model.User) (*model.MfaSecret, error) {
|
||||
secret, img, err := mfa.New(us.store).GenerateSecret(*us.config().ServiceSettings.SiteURL, user.Email, user.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure the old secret is not cached on any cluster nodes.
|
||||
us.InvalidateCacheForUser(user.Id)
|
||||
|
||||
mfaSecret := &model.MfaSecret{Secret: secret, QRCode: base64.StdEncoding.EncodeToString(img)}
|
||||
return mfaSecret, nil
|
||||
}
|
||||
|
||||
func (us *UserService) ActivateMfa(user *model.User, token string) error {
|
||||
return mfa.New(us.store).Activate(user.MfaSecret, user.Id, token)
|
||||
}
|
||||
|
||||
func (us *UserService) DeactivateMfa(user *model.User) error {
|
||||
return mfa.New(us.store).Deactivate(user.Id)
|
||||
}
|
||||
|
||||
func (us *UserService) PromoteGuestToUser(rctx request.CTX, user *model.User) error {
|
||||
return us.store.PromoteGuestToUser(rctx, user.Id)
|
||||
}
|
||||
|
||||
func (us *UserService) DemoteUserToGuest(rctx request.CTX, user *model.User) (*model.User, error) {
|
||||
return us.store.DemoteUserToGuest(rctx, user.Id)
|
||||
}
|
||||
122
server/channels/app/users/users_test.go
Normal file
122
server/channels/app/users/users_test.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package users
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIsUsernameTaken(t *testing.T) {
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
user := th.BasicUser
|
||||
taken := th.service.IsUsernameTaken(user.Username)
|
||||
|
||||
if !taken {
|
||||
t.Logf("the username '%v' should be taken", user.Username)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
newUsername := "randomUsername"
|
||||
taken = th.service.IsUsernameTaken(newUsername)
|
||||
|
||||
if taken {
|
||||
t.Logf("the username '%v' should not be taken", newUsername)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirstUserPromoted(t *testing.T) {
|
||||
th := Setup(t)
|
||||
|
||||
user, err := th.service.CreateUser(th.Context, &model.User{
|
||||
Username: model.NewUsername(),
|
||||
Password: model.NewId(),
|
||||
Email: "user@example.com",
|
||||
}, UserCreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, user)
|
||||
|
||||
require.Equal(t, model.SystemAdminRoleId+" "+model.SystemUserRoleId, user.Roles)
|
||||
|
||||
user2, err := th.service.CreateUser(th.Context, &model.User{
|
||||
Username: model.NewUsername(),
|
||||
Password: model.NewId(),
|
||||
Email: "user2@example.com",
|
||||
}, UserCreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, user2)
|
||||
|
||||
require.Equal(t, model.SystemUserRoleId, user2.Roles)
|
||||
|
||||
err = th.dbStore.User().PermanentDelete(th.Context, user.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
b := &model.Bot{
|
||||
UserId: user2.Id,
|
||||
OwnerId: model.NewId(),
|
||||
Username: model.NewUsername(),
|
||||
}
|
||||
|
||||
_, err = th.dbStore.Bot().Save(b)
|
||||
require.NoError(t, err)
|
||||
|
||||
user3, err := th.service.CreateUser(th.Context, &model.User{
|
||||
Username: model.NewUsername(),
|
||||
Password: model.NewId(),
|
||||
Email: "user3@example.com",
|
||||
}, UserCreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, user3)
|
||||
|
||||
require.Equal(t, model.SystemAdminRoleId+" "+model.SystemUserRoleId, user3.Roles)
|
||||
|
||||
user4, err := th.service.CreateUser(th.Context, &model.User{
|
||||
Username: model.NewUsername(),
|
||||
Password: model.NewId(),
|
||||
Email: "user4@example.com",
|
||||
}, UserCreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, user4)
|
||||
|
||||
require.Equal(t, model.SystemUserRoleId, user4.Roles)
|
||||
}
|
||||
|
||||
func TestFirstUserNotPromotedIfBot(t *testing.T) {
|
||||
th := Setup(t)
|
||||
|
||||
// On a completely fresh install the store is empty, but a bot user
|
||||
// (one created by a plugin) must never become the first system admin.
|
||||
botUser, err := th.service.CreateUser(th.Context, &model.User{
|
||||
Username: model.NewUsername(),
|
||||
Password: model.NewId(),
|
||||
Email: "bot@example.com",
|
||||
IsBot: true,
|
||||
}, UserCreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, botUser)
|
||||
require.Equal(t, model.SystemUserRoleId, botUser.Roles)
|
||||
|
||||
// Record the corresponding bot so the store correctly excludes it.
|
||||
_, err = th.dbStore.Bot().Save(&model.Bot{
|
||||
UserId: botUser.Id,
|
||||
OwnerId: model.NewId(),
|
||||
Username: botUser.Username,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// The first non-bot user is still promoted to system admin.
|
||||
user, err := th.service.CreateUser(th.Context, &model.User{
|
||||
Username: model.NewUsername(),
|
||||
Password: model.NewId(),
|
||||
Email: "user@example.com",
|
||||
}, UserCreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, user)
|
||||
require.Equal(t, model.SystemAdminRoleId+" "+model.SystemUserRoleId, user.Roles)
|
||||
}
|
||||
70
server/channels/app/users/utils.go
Normal file
70
server/channels/app/users/utils.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package users
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
// CheckUserDomain checks that a user's email domain matches a list of space-delimited domains as a string.
|
||||
func CheckUserDomain(user *model.User, domains string) bool {
|
||||
return CheckEmailDomain(user.Email, domains)
|
||||
}
|
||||
|
||||
// CheckEmailDomain checks that an email domain matches a list of space-delimited domains as a string.
|
||||
func CheckEmailDomain(email string, domains string) bool {
|
||||
if domains == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
domainArray := strings.FieldsSeq(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(domains, "@", " ", -1), ",", " ", -1))))
|
||||
|
||||
for d := range domainArray {
|
||||
if strings.HasSuffix(strings.ToLower(email), "@"+d) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (us *UserService) sanitizeProfiles(users []*model.User, asAdmin bool) []*model.User {
|
||||
for _, u := range users {
|
||||
us.SanitizeProfile(u, asAdmin)
|
||||
}
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
func (us *UserService) SanitizeProfile(user *model.User, asAdmin bool) {
|
||||
options := us.GetSanitizeOptions(asAdmin)
|
||||
|
||||
user.SanitizeProfile(options, asAdmin)
|
||||
}
|
||||
|
||||
func (us *UserService) GetSanitizeOptions(asAdmin bool) map[string]bool {
|
||||
options := us.config().GetSanitizeOptions()
|
||||
if asAdmin {
|
||||
options["email"] = true
|
||||
options["fullname"] = true
|
||||
options["authservice"] = true
|
||||
options["authdata"] = true
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
// IsUsernameTaken checks if the username is already used by another user. Return false if the username is invalid.
|
||||
func (us *UserService) IsUsernameTaken(name string) bool {
|
||||
if !model.IsValidUsername(name) {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, err := us.store.GetByUsername(name); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user