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

This commit is contained in:
upstream-mirror
2026-09-16 00:29:55 +05:00
commit 240db4e2a7
15309 changed files with 2173406 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View 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

View 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
)

View 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
}

View 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
}

View 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)
}

View 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
}

View 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())
}
})
}
}

View 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)
}

View 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")
}

View 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
}

View 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)
}

View 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)
}

View 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)
}

View 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
}