[MM-59069] Make sure OTP are actual One Time Password (#28074)

Automatic Merge
Этот коммит содержится в:
Julien Tant
2024-09-16 15:44:32 -07:00
коммит произвёл GitHub
родитель d1ecea4c84
Коммит 1909206e16
21 изменённых файлов: 466 добавлений и 59 удалений

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

@@ -755,7 +755,7 @@ vet: ## Run mattermost go vet specific checks
## Note that it is pinned to a specific commit, rather than a branch. This is to prevent ## Note that it is pinned to a specific commit, rather than a branch. This is to prevent
## having to backport the fix to multiple release branches for any new change. ## having to backport the fix to multiple release branches for any new change.
$(GO) install github.com/mattermost/mattermost-govet/v2@72e7752bcf59b57688fcb41173a6a1583c405adf $(GO) install github.com/mattermost/mattermost-govet/v2@72e7752bcf59b57688fcb41173a6a1583c405adf
$(GO) vet -vettool=$(GOBIN)/mattermost-govet -structuredLogging -inconsistentReceiverName -emptyStrCmp -tFatal -configtelemetry -errorAssertions -license -inconsistentReceiverName.ignore=session_serial_gen.go,team_member_serial_gen.go,user_serial_gen.go ./... $(GO) vet -vettool=$(GOBIN)/mattermost-govet -structuredLogging -inconsistentReceiverName -emptyStrCmp -tFatal -configtelemetry -errorAssertions -license -inconsistentReceiverName.ignore=session_serial_gen.go,team_member_serial_gen.go,user_serial_gen.go,utils_serial_gen.go ./...
ifeq ($(BUILD_ENTERPRISE_READY),true) ifeq ($(BUILD_ENTERPRISE_READY),true)
ifneq ($(MM_NO_ENTERPRISE_LINT),true) ifneq ($(MM_NO_ENTERPRISE_LINT),true)
$(GO) vet -vettool=$(GOBIN)/mattermost-govet -structuredLogging -inconsistentReceiverName -emptyStrCmp -tFatal -configtelemetry -errorAssertions -enterpriseLicense $(BUILD_ENTERPRISE_DIR)/... $(GO) vet -vettool=$(GOBIN)/mattermost-govet -structuredLogging -inconsistentReceiverName -emptyStrCmp -tFatal -configtelemetry -errorAssertions -enterpriseLicense $(BUILD_ENTERPRISE_DIR)/...
@@ -780,6 +780,10 @@ gen-serialized: ## Generates serialization methods for hot structs
# would be to temporarily move all the structs to the same file, # would be to temporarily move all the structs to the same file,
# but that involves a lot of manual work. # but that involves a lot of manual work.
$(GO) install github.com/tinylib/msgp@v1.1.6 $(GO) install github.com/tinylib/msgp@v1.1.6
$(GOBIN)/msgp -file=./public/model/utils.go -tests=false -o=./public/model/utils_serial_gen.go
@echo "$$LICENSE_HEADER" > tmp.go
@cat ./public/model/utils_serial_gen.go >> tmp.go
@mv tmp.go ./public/model/utils_serial_gen.go
$(GOBIN)/msgp -file=./public/model/session.go -tests=false -o=./public/model/session_serial_gen.go $(GOBIN)/msgp -file=./public/model/session.go -tests=false -o=./public/model/session_serial_gen.go
@echo "$$LICENSE_HEADER" > tmp.go @echo "$$LICENSE_HEADER" > tmp.go
@cat ./public/model/session_serial_gen.go >> tmp.go @cat ./public/model/session_serial_gen.go >> tmp.go

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

@@ -212,7 +212,7 @@ func (a *App) CheckUserMfa(rctx request.CTX, user *model.User, token string) *mo
return model.NewAppError("CheckUserMfa", "mfa.mfa_disabled.app_error", nil, "", http.StatusNotImplemented) return model.NewAppError("CheckUserMfa", "mfa.mfa_disabled.app_error", nil, "", http.StatusNotImplemented)
} }
ok, err := mfa.New(a.Srv().Store().User()).ValidateToken(user.MfaSecret, token) ok, err := mfa.New(a.Srv().Store().User()).ValidateToken(user, token)
if err != nil { if err != nil {
return model.NewAppError("CheckUserMfa", "mfa.validate_token.authenticate.app_error", nil, "", http.StatusBadRequest).Wrap(err) return model.NewAppError("CheckUserMfa", "mfa.validate_token.authenticate.app_error", nil, "", http.StatusBadRequest).Wrap(err)
} }

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

@@ -249,6 +249,8 @@ channels/db/migrations/mysql/000125_remoteclusters_add_default_team_id.down.sql
channels/db/migrations/mysql/000125_remoteclusters_add_default_team_id.up.sql channels/db/migrations/mysql/000125_remoteclusters_add_default_team_id.up.sql
channels/db/migrations/mysql/000126_sharedchannels_remotes_add_deleteat.down.sql channels/db/migrations/mysql/000126_sharedchannels_remotes_add_deleteat.down.sql
channels/db/migrations/mysql/000126_sharedchannels_remotes_add_deleteat.up.sql channels/db/migrations/mysql/000126_sharedchannels_remotes_add_deleteat.up.sql
channels/db/migrations/mysql/000127_add_mfa_used_ts_to_users.down.sql
channels/db/migrations/mysql/000127_add_mfa_used_ts_to_users.up.sql
channels/db/migrations/postgres/000001_create_teams.down.sql channels/db/migrations/postgres/000001_create_teams.down.sql
channels/db/migrations/postgres/000001_create_teams.up.sql channels/db/migrations/postgres/000001_create_teams.up.sql
channels/db/migrations/postgres/000002_create_team_members.down.sql channels/db/migrations/postgres/000002_create_team_members.down.sql
@@ -499,3 +501,5 @@ channels/db/migrations/postgres/000125_remoteclusters_add_default_team_id.down.s
channels/db/migrations/postgres/000125_remoteclusters_add_default_team_id.up.sql channels/db/migrations/postgres/000125_remoteclusters_add_default_team_id.up.sql
channels/db/migrations/postgres/000126_sharedchannels_remotes_add_deleteat.down.sql channels/db/migrations/postgres/000126_sharedchannels_remotes_add_deleteat.down.sql
channels/db/migrations/postgres/000126_sharedchannels_remotes_add_deleteat.up.sql channels/db/migrations/postgres/000126_sharedchannels_remotes_add_deleteat.up.sql
channels/db/migrations/postgres/000127_add_mfa_used_ts_to_users.down.sql
channels/db/migrations/postgres/000127_add_mfa_used_ts_to_users.up.sql

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

@@ -0,0 +1 @@
ALTER TABLE Users DROP COLUMN MfaUsedTimestamps;

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

@@ -0,0 +1 @@
ALTER TABLE Users ADD COLUMN MfaUsedTimestamps json NULL;

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

@@ -0,0 +1,2 @@
ALTER TABLE Users DROP COLUMN IF EXISTS MfaUsedTimestamps;

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

@@ -0,0 +1 @@
ALTER TABLE Users ADD COLUMN IF NOT EXISTS MfaUsedTimestamps jsonb NULL;

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

@@ -11843,6 +11843,24 @@ func (s *OpenTracingLayerUserStore) GetMany(ctx context.Context, ids []string) (
return result, err return result, err
} }
func (s *OpenTracingLayerUserStore) GetMfaUsedTimestamps(userID string) ([]int, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetMfaUsedTimestamps")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetMfaUsedTimestamps(userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetNewUsersForTeam(teamID string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) { func (s *OpenTracingLayerUserStore) GetNewUsersForTeam(teamID string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
origCtx := s.Root.Store.Context() origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetNewUsersForTeam") span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetNewUsersForTeam")
@@ -12530,6 +12548,24 @@ func (s *OpenTracingLayerUserStore) SearchWithoutTeam(term string, options *mode
return result, err return result, err
} }
func (s *OpenTracingLayerUserStore) StoreMfaUsedTimestamps(userID string, ts []int) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.StoreMfaUsedTimestamps")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.UserStore.StoreMfaUsedTimestamps(userID, ts)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerUserStore) Update(rctx request.CTX, user *model.User, allowRoleUpdate bool) (*model.UserUpdate, error) { func (s *OpenTracingLayerUserStore) Update(rctx request.CTX, user *model.User, allowRoleUpdate bool) (*model.UserUpdate, error) {
origCtx := s.Root.Store.Context() origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.Update") span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.Update")

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

@@ -13529,6 +13529,27 @@ func (s *RetryLayerUserStore) GetMany(ctx context.Context, ids []string) ([]*mod
} }
func (s *RetryLayerUserStore) GetMfaUsedTimestamps(userID string) ([]int, error) {
tries := 0
for {
result, err := s.UserStore.GetMfaUsedTimestamps(userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetNewUsersForTeam(teamID string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) { func (s *RetryLayerUserStore) GetNewUsersForTeam(teamID string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
tries := 0 tries := 0
@@ -14303,6 +14324,27 @@ func (s *RetryLayerUserStore) SearchWithoutTeam(term string, options *model.User
} }
func (s *RetryLayerUserStore) StoreMfaUsedTimestamps(userID string, ts []int) error {
tries := 0
for {
err := s.UserStore.StoreMfaUsedTimestamps(userID, ts)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) Update(rctx request.CTX, user *model.User, allowRoleUpdate bool) (*model.UserUpdate, error) { func (s *RetryLayerUserStore) Update(rctx request.CTX, user *model.User, allowRoleUpdate bool) (*model.UserUpdate, error) {
tries := 0 tries := 0

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

@@ -9,6 +9,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"sort" "sort"
"strconv"
"strings" "strings"
"time" "time"
"unicode/utf8" "unicode/utf8"
@@ -55,7 +56,7 @@ func newSqlUserStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) s
// note: we are providing field names explicitly here to maintain order of columns (needed when using raw queries) // note: we are providing field names explicitly here to maintain order of columns (needed when using raw queries)
us.usersQuery = us.getQueryBuilder(). us.usersQuery = us.getQueryBuilder().
Select("u.Id", "u.CreateAt", "u.UpdateAt", "u.DeleteAt", "u.Username", "u.Password", "u.AuthData", "u.AuthService", "u.Email", "u.EmailVerified", "u.Nickname", "u.FirstName", "u.LastName", "u.Position", "u.Roles", "u.AllowMarketing", "u.Props", "u.NotifyProps", "u.LastPasswordUpdate", "u.LastPictureUpdate", "u.FailedAttempts", "u.Locale", "u.Timezone", "u.MfaActive", "u.MfaSecret", Select("u.Id", "u.CreateAt", "u.UpdateAt", "u.DeleteAt", "u.Username", "u.Password", "u.AuthData", "u.AuthService", "u.Email", "u.EmailVerified", "u.Nickname", "u.FirstName", "u.LastName", "u.Position", "u.Roles", "u.AllowMarketing", "u.Props", "u.NotifyProps", "u.LastPasswordUpdate", "u.LastPictureUpdate", "u.FailedAttempts", "u.Locale", "u.Timezone", "u.MfaActive", "u.MfaSecret", "u.MfaUsedTimestamps",
"b.UserId IS NOT NULL AS IsBot", "COALESCE(b.Description, '') AS BotDescription", "COALESCE(b.LastIconUpdate, 0) AS BotLastIconUpdate", "u.RemoteId", "u.LastLogin"). "b.UserId IS NOT NULL AS IsBot", "COALESCE(b.Description, '') AS BotDescription", "COALESCE(b.LastIconUpdate, 0) AS BotLastIconUpdate", "u.RemoteId", "u.LastLogin").
From("Users u"). From("Users u").
LeftJoin("Bots b ON ( b.UserId = u.Id )") LeftJoin("Bots b ON ( b.UserId = u.Id )")
@@ -85,12 +86,12 @@ func (us SqlUserStore) insert(user *model.User) (sql.Result, error) {
(Id, CreateAt, UpdateAt, DeleteAt, Username, Password, AuthData, AuthService, (Id, CreateAt, UpdateAt, DeleteAt, Username, Password, AuthData, AuthService,
Email, EmailVerified, Nickname, FirstName, LastName, Position, Roles, AllowMarketing, Email, EmailVerified, Nickname, FirstName, LastName, Position, Roles, AllowMarketing,
Props, NotifyProps, LastPasswordUpdate, LastPictureUpdate, FailedAttempts, Props, NotifyProps, LastPasswordUpdate, LastPictureUpdate, FailedAttempts,
Locale, Timezone, MfaActive, MfaSecret, RemoteId) Locale, Timezone, MfaActive, MfaSecret, RemoteId, MfaUsedTimestamps)
VALUES VALUES
(:Id, :CreateAt, :UpdateAt, :DeleteAt, :Username, :Password, :AuthData, :AuthService, (:Id, :CreateAt, :UpdateAt, :DeleteAt, :Username, :Password, :AuthData, :AuthService,
:Email, :EmailVerified, :Nickname, :FirstName, :LastName, :Position, :Roles, :AllowMarketing, :Email, :EmailVerified, :Nickname, :FirstName, :LastName, :Position, :Roles, :AllowMarketing,
:Props, :NotifyProps, :LastPasswordUpdate, :LastPictureUpdate, :FailedAttempts, :Props, :NotifyProps, :LastPasswordUpdate, :LastPictureUpdate, :FailedAttempts,
:Locale, :Timezone, :MfaActive, :MfaSecret, :RemoteId)` :Locale, :Timezone, :MfaActive, :MfaSecret, :RemoteId, :MfaUsedTimestamps)`
user.Props = wrapBinaryParamStringMap(us.IsBinaryParamEnabled(), user.Props) user.Props = wrapBinaryParamStringMap(us.IsBinaryParamEnabled(), user.Props)
return us.GetMasterX().NamedExec(query, user) return us.GetMasterX().NamedExec(query, user)
@@ -197,6 +198,7 @@ func (us SqlUserStore) Update(rctx request.CTX, user *model.User, trustedUpdateD
user.FailedAttempts = oldUser.FailedAttempts user.FailedAttempts = oldUser.FailedAttempts
user.MfaSecret = oldUser.MfaSecret user.MfaSecret = oldUser.MfaSecret
user.MfaActive = oldUser.MfaActive user.MfaActive = oldUser.MfaActive
user.MfaUsedTimestamps = oldUser.MfaUsedTimestamps
user.LastLogin = oldUser.LastLogin user.LastLogin = oldUser.LastLogin
if !trustedUpdateData { if !trustedUpdateData {
@@ -227,7 +229,7 @@ func (us SqlUserStore) Update(rctx request.CTX, user *model.User, trustedUpdateD
AllowMarketing=:AllowMarketing, Props=:Props, NotifyProps=:NotifyProps, AllowMarketing=:AllowMarketing, Props=:Props, NotifyProps=:NotifyProps,
LastPasswordUpdate=:LastPasswordUpdate, LastPictureUpdate=:LastPictureUpdate, LastPasswordUpdate=:LastPasswordUpdate, LastPictureUpdate=:LastPictureUpdate,
FailedAttempts=:FailedAttempts,Locale=:Locale, Timezone=:Timezone, MfaActive=:MfaActive, FailedAttempts=:FailedAttempts,Locale=:Locale, Timezone=:Timezone, MfaActive=:MfaActive,
MfaSecret=:MfaSecret, RemoteId=:RemoteId, LastLogin=:LastLogin MfaSecret=:MfaSecret, RemoteId=:RemoteId, LastLogin=:LastLogin, MfaUsedTimestamps=:MfaUsedTimestamps
WHERE Id=:Id` WHERE Id=:Id`
user.Props = wrapBinaryParamStringMap(us.IsBinaryParamEnabled(), user.Props) user.Props = wrapBinaryParamStringMap(us.IsBinaryParamEnabled(), user.Props)
@@ -343,7 +345,8 @@ func (us SqlUserStore) UpdateAuthData(userId string, service string, authData *s
if resetMfa { if resetMfa {
updateQuery = updateQuery.Set("MfaActive", false). updateQuery = updateQuery.Set("MfaActive", false).
Set("MfaSecret", "") Set("MfaSecret", "").
Set("MfaUsedTimestamps", model.StringArray{})
} }
queryString, args, err := updateQuery.ToSql() queryString, args, err := updateQuery.ToSql()
@@ -427,7 +430,7 @@ func (us SqlUserStore) ResetAuthDataToEmailForUsers(service string, userIDs []st
func (us SqlUserStore) UpdateMfaSecret(userId, secret string) error { func (us SqlUserStore) UpdateMfaSecret(userId, secret string) error {
updateAt := model.GetMillis() updateAt := model.GetMillis()
if _, err := us.GetMasterX().Exec("UPDATE Users SET MfaSecret = ?, UpdateAt = ? WHERE Id = ?", secret, updateAt, userId); err != nil { if _, err := us.GetMasterX().Exec("UPDATE Users SET MfaSecret = ?, MfaUsedTimestamps = ?, UpdateAt = ? WHERE Id = ?", secret, model.StringArray{}, updateAt, userId); err != nil {
return errors.Wrapf(err, "failed to update User with userId=%s", userId) return errors.Wrapf(err, "failed to update User with userId=%s", userId)
} }
@@ -444,6 +447,37 @@ func (us SqlUserStore) UpdateMfaActive(userId string, active bool) error {
return nil return nil
} }
func (us SqlUserStore) StoreMfaUsedTimestamps(userId string, ts []int) error {
tSStrArray := model.StringArray{}
for _, t := range ts {
tSStrArray = append(tSStrArray, fmt.Sprintf("%d", t))
}
updateAt := model.GetMillis()
if _, err := us.GetMasterX().Exec("UPDATE Users SET MfaUsedTimestamps = ?, UpdateAt = ? WHERE Id = ?", tSStrArray, updateAt, userId); err != nil {
return errors.Wrapf(err, "failed to update User with userId=%s", userId)
}
return nil
}
func (us SqlUserStore) GetMfaUsedTimestamps(userId string) ([]int, error) {
tsStrArray := model.StringArray{}
err := us.GetReplicaX().Get(&tsStrArray, "SELECT MfaUsedTimestamps FROM Users WHERE Id = ?", userId)
if err != nil {
return nil, errors.Wrapf(err, "failed to get MFA used timestamps for user with ID %s", userId)
}
ts := make([]int, len(tsStrArray))
for i, t := range tsStrArray {
ts[i], err = strconv.Atoi(t)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse MFA used timestamp %s for user with ID %s", t, userId)
}
}
return ts, nil
}
// GetMany returns a list of users for the provided list of ids // GetMany returns a list of users for the provided list of ids
func (us SqlUserStore) GetMany(ctx context.Context, ids []string) ([]*model.User, error) { func (us SqlUserStore) GetMany(ctx context.Context, ids []string) ([]*model.User, error) {
query := us.usersQuery.Where(sq.Eq{"Id": ids}) query := us.usersQuery.Where(sq.Eq{"Id": ids})
@@ -474,7 +508,7 @@ func (us SqlUserStore) Get(ctx context.Context, id string) (*model.User, error)
&user.Password, &user.AuthData, &user.AuthService, &user.Email, &user.EmailVerified, &user.Password, &user.AuthData, &user.AuthService, &user.Email, &user.EmailVerified,
&user.Nickname, &user.FirstName, &user.LastName, &user.Position, &user.Roles, &user.Nickname, &user.FirstName, &user.LastName, &user.Position, &user.Roles,
&user.AllowMarketing, &props, &notifyProps, &user.LastPasswordUpdate, &user.LastPictureUpdate, &user.AllowMarketing, &props, &notifyProps, &user.LastPasswordUpdate, &user.LastPictureUpdate,
&user.FailedAttempts, &user.Locale, &timezone, &user.MfaActive, &user.MfaSecret, &user.FailedAttempts, &user.Locale, &timezone, &user.MfaActive, &user.MfaSecret, &user.MfaUsedTimestamps,
&user.IsBot, &user.BotDescription, &user.BotLastIconUpdate, &user.RemoteId, &user.LastLogin) &user.IsBot, &user.BotDescription, &user.BotLastIconUpdate, &user.RemoteId, &user.LastLogin)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
@@ -877,7 +911,7 @@ func (us SqlUserStore) GetAllProfilesInChannel(ctx context.Context, channelID st
for rows.Next() { for rows.Next() {
var user model.User var user model.User
var props, notifyProps, timezone []byte var props, notifyProps, timezone []byte
if err = rows.Scan(&user.Id, &user.CreateAt, &user.UpdateAt, &user.DeleteAt, &user.Username, &user.Password, &user.AuthData, &user.AuthService, &user.Email, &user.EmailVerified, &user.Nickname, &user.FirstName, &user.LastName, &user.Position, &user.Roles, &user.AllowMarketing, &props, &notifyProps, &user.LastPasswordUpdate, &user.LastPictureUpdate, &user.FailedAttempts, &user.Locale, &timezone, &user.MfaActive, &user.MfaSecret, &user.IsBot, &user.BotDescription, &user.BotLastIconUpdate, &user.RemoteId, &user.LastLogin); err != nil { if err = rows.Scan(&user.Id, &user.CreateAt, &user.UpdateAt, &user.DeleteAt, &user.Username, &user.Password, &user.AuthData, &user.AuthService, &user.Email, &user.EmailVerified, &user.Nickname, &user.FirstName, &user.LastName, &user.Position, &user.Roles, &user.AllowMarketing, &props, &notifyProps, &user.LastPasswordUpdate, &user.LastPictureUpdate, &user.FailedAttempts, &user.Locale, &timezone, &user.MfaActive, &user.MfaSecret, &user.MfaUsedTimestamps, &user.IsBot, &user.BotDescription, &user.BotLastIconUpdate, &user.RemoteId, &user.LastLogin); err != nil {
return nil, errors.Wrap(err, "failed to scan values from rows into User entity") return nil, errors.Wrap(err, "failed to scan values from rows into User entity")
} }
if err = json.Unmarshal(props, &user.Props); err != nil { if err = json.Unmarshal(props, &user.Props); err != nil {

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

@@ -417,6 +417,8 @@ type UserStore interface {
ResetAuthDataToEmailForUsers(service string, userIDs []string, includeDeleted bool, dryRun bool) (int, error) ResetAuthDataToEmailForUsers(service string, userIDs []string, includeDeleted bool, dryRun bool) (int, error)
UpdateMfaSecret(userID, secret string) error UpdateMfaSecret(userID, secret string) error
UpdateMfaActive(userID string, active bool) error UpdateMfaActive(userID string, active bool) error
StoreMfaUsedTimestamps(userID string, ts []int) error
GetMfaUsedTimestamps(userID string) ([]int, error)
Get(ctx context.Context, id string) (*model.User, error) Get(ctx context.Context, id string) (*model.User, error)
GetMany(ctx context.Context, ids []string) ([]*model.User, error) GetMany(ctx context.Context, ids []string) ([]*model.User, error)
GetAll() ([]*model.User, error) GetAll() ([]*model.User, error)

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

@@ -861,6 +861,36 @@ func (_m *UserStore) GetMany(ctx context.Context, ids []string) ([]*model.User,
return r0, r1 return r0, r1
} }
// GetMfaUsedTimestamps provides a mock function with given fields: userID
func (_m *UserStore) GetMfaUsedTimestamps(userID string) ([]int, error) {
ret := _m.Called(userID)
if len(ret) == 0 {
panic("no return value specified for GetMfaUsedTimestamps")
}
var r0 []int
var r1 error
if rf, ok := ret.Get(0).(func(string) ([]int, error)); ok {
return rf(userID)
}
if rf, ok := ret.Get(0).(func(string) []int); ok {
r0 = rf(userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]int)
}
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetNewUsersForTeam provides a mock function with given fields: teamID, offset, limit, viewRestrictions // GetNewUsersForTeam provides a mock function with given fields: teamID, offset, limit, viewRestrictions
func (_m *UserStore) GetNewUsersForTeam(teamID string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) { func (_m *UserStore) GetNewUsersForTeam(teamID string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
ret := _m.Called(teamID, offset, limit, viewRestrictions) ret := _m.Called(teamID, offset, limit, viewRestrictions)
@@ -1884,6 +1914,24 @@ func (_m *UserStore) SearchWithoutTeam(term string, options *model.UserSearchOpt
return r0, r1 return r0, r1
} }
// StoreMfaUsedTimestamps provides a mock function with given fields: userID, ts
func (_m *UserStore) StoreMfaUsedTimestamps(userID string, ts []int) error {
ret := _m.Called(userID, ts)
if len(ret) == 0 {
panic("no return value specified for StoreMfaUsedTimestamps")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, []int) error); ok {
r0 = rf(userID, ts)
} else {
r0 = ret.Error(0)
}
return r0
}
// Update provides a mock function with given fields: rctx, user, allowRoleUpdate // Update provides a mock function with given fields: rctx, user, allowRoleUpdate
func (_m *UserStore) Update(rctx request.CTX, user *model.User, allowRoleUpdate bool) (*model.UserUpdate, error) { func (_m *UserStore) Update(rctx request.CTX, user *model.User, allowRoleUpdate bool) (*model.UserUpdate, error) {
ret := _m.Called(rctx, user, allowRoleUpdate) ret := _m.Called(rctx, user, allowRoleUpdate)

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

@@ -101,6 +101,7 @@ func TestUserStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
t.Run("GetUsersWithInvalidEmails", func(t *testing.T) { testGetUsersWithInvalidEmails(t, rctx, ss) }) t.Run("GetUsersWithInvalidEmails", func(t *testing.T) { testGetUsersWithInvalidEmails(t, rctx, ss) })
t.Run("UpdateLastLogin", func(t *testing.T) { testUpdateLastLogin(t, rctx, ss) }) t.Run("UpdateLastLogin", func(t *testing.T) { testUpdateLastLogin(t, rctx, ss) })
t.Run("GetUserReport", func(t *testing.T) { testGetUserReport(t, rctx, ss, s) }) t.Run("GetUserReport", func(t *testing.T) { testGetUserReport(t, rctx, ss, s) })
t.Run("MfaUsedTimestamps", func(t *testing.T) { testMfaUsedTimestamps(t, rctx, ss) })
} }
func testUserStoreSave(t *testing.T, rctx request.CTX, ss store.Store) { func testUserStoreSave(t *testing.T, rctx request.CTX, ss store.Store) {
@@ -6613,3 +6614,24 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store, s SqlStor
require.Len(t, userReport, 11) require.Len(t, userReport, 11)
}) })
} }
func testMfaUsedTimestamps(t *testing.T, rctx request.CTX, ss store.Store) {
u1, err := ss.User().Save(rctx, &model.User{
Email: "ben@invalid.mattermost.com",
Username: "u1" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(rctx, u1.Id)) }()
tss, err := ss.User().GetMfaUsedTimestamps(u1.Id)
require.NoError(t, err)
require.Empty(t, tss)
err = ss.User().StoreMfaUsedTimestamps(u1.Id, []int{1, 2, 3})
require.NoError(t, err)
tss, err = ss.User().GetMfaUsedTimestamps(u1.Id)
require.NoError(t, err)
require.Equal(t, []int{1, 2, 3}, tss)
}

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

@@ -10661,6 +10661,22 @@ func (s *TimerLayerUserStore) GetMany(ctx context.Context, ids []string) ([]*mod
return result, err return result, err
} }
func (s *TimerLayerUserStore) GetMfaUsedTimestamps(userID string) ([]int, error) {
start := time.Now()
result, err := s.UserStore.GetMfaUsedTimestamps(userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetMfaUsedTimestamps", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetNewUsersForTeam(teamID string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) { func (s *TimerLayerUserStore) GetNewUsersForTeam(teamID string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) {
start := time.Now() start := time.Now()
@@ -11282,6 +11298,22 @@ func (s *TimerLayerUserStore) SearchWithoutTeam(term string, options *model.User
return result, err return result, err
} }
func (s *TimerLayerUserStore) StoreMfaUsedTimestamps(userID string, ts []int) error {
start := time.Now()
err := s.UserStore.StoreMfaUsedTimestamps(userID, ts)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.StoreMfaUsedTimestamps", success, elapsed)
}
return err
}
func (s *TimerLayerUserStore) Update(rctx request.CTX, user *model.User, allowRoleUpdate bool) (*model.UserUpdate, error) { func (s *TimerLayerUserStore) Update(rctx request.CTX, user *model.User, allowRoleUpdate bool) (*model.UserUpdate, error) {
start := time.Now() start := time.Now()

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

@@ -11,6 +11,7 @@ import (
"strings" "strings"
"github.com/dgryski/dgoogauth" "github.com/dgryski/dgoogauth"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/rsc/qr" "github.com/mattermost/rsc/qr"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@@ -26,6 +27,8 @@ const (
type Store interface { type Store interface {
UpdateMfaActive(userId string, active bool) error UpdateMfaActive(userId string, active bool) error
UpdateMfaSecret(userId, secret string) error UpdateMfaSecret(userId, secret string) error
StoreMfaUsedTimestamps(userId string, ts []int) error
GetMfaUsedTimestamps(userId string) ([]int, error)
} }
type MFA struct { type MFA struct {
@@ -120,11 +123,17 @@ func (m *MFA) Deactivate(userId string) error {
} }
// Validate the provide token using the secret provided // Validate the provide token using the secret provided
func (m *MFA) ValidateToken(secret, token string) (bool, error) { func (m *MFA) ValidateToken(user *model.User, token string) (bool, error) {
usedTs, err := m.store.GetMfaUsedTimestamps(user.Id)
if err != nil {
return false, errors.Wrap(err, "unable to retrieve the DisallowReuse slice")
}
otpConfig := &dgoogauth.OTPConfig{ otpConfig := &dgoogauth.OTPConfig{
Secret: secret, Secret: user.MfaSecret,
WindowSize: 3, WindowSize: 3,
HotpCounter: 0, HotpCounter: 0,
DisallowReuse: usedTs,
} }
trimmedToken := strings.TrimSpace(token) trimmedToken := strings.TrimSpace(token)
@@ -132,6 +141,14 @@ func (m *MFA) ValidateToken(secret, token string) (bool, error) {
if err != nil { if err != nil {
return false, errors.Wrap(err, "unable to parse the token") return false, errors.Wrap(err, "unable to parse the token")
} }
if !ok {
return false, nil
}
return ok, nil err = m.store.StoreMfaUsedTimestamps(user.Id, otpConfig.DisallowReuse)
if err != nil {
return true, errors.Wrap(err, "unable to store the DisallowReuse slice")
}
return true, nil
} }

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

@@ -15,6 +15,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin/plugintest/mock" "github.com/mattermost/mattermost/server/public/plugin/plugintest/mock"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks" "github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
) )
@@ -156,12 +157,49 @@ func TestDeactivate(t *testing.T) {
func TestValidateToken(t *testing.T) { func TestValidateToken(t *testing.T) {
t.Run("fail on wrongly formatted token", func(t *testing.T) { t.Run("fail on wrongly formatted token", func(t *testing.T) {
id := model.NewId()
secret := newRandomBase32String(mfaSecretSize) secret := newRandomBase32String(mfaSecretSize)
ok, err := New(nil).ValidateToken(secret, "invalid-token") u := &model.User{Id: id, MfaSecret: secret}
usMock := mocks.UserStore{}
usMock.On("GetMfaUsedTimestamps", u.Id).Return([]int{}, nil).Once()
ok, err := New(&usMock).ValidateToken(u, "invalid-token")
require.Error(t, err) require.Error(t, err)
require.False(t, ok) require.False(t, ok)
require.Contains(t, err.Error(), "unable to parse the token") require.Contains(t, err.Error(), "unable to parse the token")
}) })
t.Run("successful validation", func(t *testing.T) {
id := model.NewId()
secret := newRandomBase32String(mfaSecretSize)
u := &model.User{Id: id, MfaSecret: secret}
code := fmt.Sprintf("%06d", dgoogauth.ComputeCode(secret, time.Now().UTC().Unix()/30))
usMock := mocks.UserStore{}
usMock.On("GetMfaUsedTimestamps", u.Id).Return([]int{}, nil).Once()
usMock.On("StoreMfaUsedTimestamps", u.Id, mock.AnythingOfType("[]int")).Return(nil).Once()
ok, err := New(&usMock).ValidateToken(u, code)
require.NoError(t, err)
require.True(t, ok)
})
t.Run("disallow reuse of totp", func(t *testing.T) {
id := model.NewId()
secret := newRandomBase32String(mfaSecretSize)
u := &model.User{Id: id, MfaSecret: secret}
t0 := time.Now().UTC().Unix() / 30
code := fmt.Sprintf("%06d", dgoogauth.ComputeCode(secret, t0))
usMock := mocks.UserStore{}
usMock.On("GetMfaUsedTimestamps", u.Id).Return([]int{int(t0)}, nil).Once()
ok, err := New(&usMock).ValidateToken(u, code)
require.False(t, ok)
require.NoError(t, err)
})
} }
func TestRandomBase32String(t *testing.T) { func TestRandomBase32String(t *testing.T) {

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

@@ -75,40 +75,41 @@ const (
// This struct's serializer methods are auto-generated. If a new field is added/removed, // This struct's serializer methods are auto-generated. If a new field is added/removed,
// please run make gen-serialized. // please run make gen-serialized.
type User struct { type User struct {
Id string `json:"id"` Id string `json:"id"`
CreateAt int64 `json:"create_at,omitempty"` CreateAt int64 `json:"create_at,omitempty"`
UpdateAt int64 `json:"update_at,omitempty"` UpdateAt int64 `json:"update_at,omitempty"`
DeleteAt int64 `json:"delete_at"` DeleteAt int64 `json:"delete_at"`
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password,omitempty"` Password string `json:"password,omitempty"`
AuthData *string `json:"auth_data,omitempty"` AuthData *string `json:"auth_data,omitempty"`
AuthService string `json:"auth_service"` AuthService string `json:"auth_service"`
Email string `json:"email"` Email string `json:"email"`
EmailVerified bool `json:"email_verified,omitempty"` EmailVerified bool `json:"email_verified,omitempty"`
Nickname string `json:"nickname"` Nickname string `json:"nickname"`
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name"` LastName string `json:"last_name"`
Position string `json:"position"` Position string `json:"position"`
Roles string `json:"roles"` Roles string `json:"roles"`
AllowMarketing bool `json:"allow_marketing,omitempty"` AllowMarketing bool `json:"allow_marketing,omitempty"`
Props StringMap `json:"props,omitempty"` Props StringMap `json:"props,omitempty"`
NotifyProps StringMap `json:"notify_props,omitempty"` NotifyProps StringMap `json:"notify_props,omitempty"`
LastPasswordUpdate int64 `json:"last_password_update,omitempty"` LastPasswordUpdate int64 `json:"last_password_update,omitempty"`
LastPictureUpdate int64 `json:"last_picture_update,omitempty"` LastPictureUpdate int64 `json:"last_picture_update,omitempty"`
FailedAttempts int `json:"failed_attempts,omitempty"` FailedAttempts int `json:"failed_attempts,omitempty"`
Locale string `json:"locale"` Locale string `json:"locale"`
Timezone StringMap `json:"timezone"` Timezone StringMap `json:"timezone"`
MfaActive bool `json:"mfa_active,omitempty"` MfaActive bool `json:"mfa_active,omitempty"`
MfaSecret string `json:"mfa_secret,omitempty"` MfaSecret string `json:"mfa_secret,omitempty"`
RemoteId *string `json:"remote_id,omitempty"` RemoteId *string `json:"remote_id,omitempty"`
LastActivityAt int64 `json:"last_activity_at,omitempty"` LastActivityAt int64 `json:"last_activity_at,omitempty"`
IsBot bool `json:"is_bot,omitempty"` IsBot bool `json:"is_bot,omitempty"`
BotDescription string `json:"bot_description,omitempty"` BotDescription string `json:"bot_description,omitempty"`
BotLastIconUpdate int64 `json:"bot_last_icon_update,omitempty"` BotLastIconUpdate int64 `json:"bot_last_icon_update,omitempty"`
TermsOfServiceId string `json:"terms_of_service_id,omitempty"` TermsOfServiceId string `json:"terms_of_service_id,omitempty"`
TermsOfServiceCreateAt int64 `json:"terms_of_service_create_at,omitempty"` TermsOfServiceCreateAt int64 `json:"terms_of_service_create_at,omitempty"`
DisableWelcomeEmail bool `json:"disable_welcome_email"` DisableWelcomeEmail bool `json:"disable_welcome_email"`
LastLogin int64 `json:"last_login,omitempty"` LastLogin int64 `json:"last_login,omitempty"`
MfaUsedTimestamps StringArray `json:"mfa_used_timestamps,omitempty"`
} }
func (u *User) Auditable() map[string]interface{} { func (u *User) Auditable() map[string]interface{} {
@@ -660,6 +661,7 @@ func (u *User) Sanitize(options map[string]bool) {
u.Password = "" u.Password = ""
u.AuthData = NewPointer("") u.AuthData = NewPointer("")
u.MfaSecret = "" u.MfaSecret = ""
u.MfaUsedTimestamps = nil
u.LastLogin = 0 u.LastLogin = 0
if len(options) != 0 && !options["email"] { if len(options) != 0 && !options["email"] {
@@ -694,6 +696,7 @@ func (u *User) SanitizeInput(isAdmin bool) {
u.FailedAttempts = 0 u.FailedAttempts = 0
u.MfaActive = false u.MfaActive = false
u.MfaSecret = "" u.MfaSecret = ""
u.MfaUsedTimestamps = StringArray{}
u.Email = strings.TrimSpace(u.Email) u.Email = strings.TrimSpace(u.Email)
u.LastActivityAt = 0 u.LastActivityAt = 0
} }
@@ -702,6 +705,7 @@ func (u *User) ClearNonProfileFields(asAdmin bool) {
u.Password = "" u.Password = ""
u.AuthData = NewPointer("") u.AuthData = NewPointer("")
u.MfaSecret = "" u.MfaSecret = ""
u.MfaUsedTimestamps = nil
u.EmailVerified = false u.EmailVerified = false
u.AllowMarketing = false u.AllowMarketing = false
u.LastPasswordUpdate = 0 u.LastPasswordUpdate = 0

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

@@ -17,8 +17,8 @@ func (z *User) DecodeMsg(dc *msgp.Reader) (err error) {
err = msgp.WrapError(err) err = msgp.WrapError(err)
return return
} }
if zb0001 != 34 { if zb0001 != 35 {
err = msgp.ArrayError{Wanted: 34, Got: zb0001} err = msgp.ArrayError{Wanted: 35, Got: zb0001}
return return
} }
z.Id, err = dc.ReadString() z.Id, err = dc.ReadString()
@@ -215,13 +215,18 @@ func (z *User) DecodeMsg(dc *msgp.Reader) (err error) {
err = msgp.WrapError(err, "LastLogin") err = msgp.WrapError(err, "LastLogin")
return return
} }
err = z.MfaUsedTimestamps.DecodeMsg(dc)
if err != nil {
err = msgp.WrapError(err, "MfaUsedTimestamps")
return
}
return return
} }
// EncodeMsg implements msgp.Encodable // EncodeMsg implements msgp.Encodable
func (z *User) EncodeMsg(en *msgp.Writer) (err error) { func (z *User) EncodeMsg(en *msgp.Writer) (err error) {
// array header, size 34 // array header, size 35
err = en.Append(0xdc, 0x0, 0x22) err = en.Append(0xdc, 0x0, 0x23)
if err != nil { if err != nil {
return return
} }
@@ -409,14 +414,19 @@ func (z *User) EncodeMsg(en *msgp.Writer) (err error) {
err = msgp.WrapError(err, "LastLogin") err = msgp.WrapError(err, "LastLogin")
return return
} }
err = z.MfaUsedTimestamps.EncodeMsg(en)
if err != nil {
err = msgp.WrapError(err, "MfaUsedTimestamps")
return
}
return return
} }
// MarshalMsg implements msgp.Marshaler // MarshalMsg implements msgp.Marshaler
func (z *User) MarshalMsg(b []byte) (o []byte, err error) { func (z *User) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.Require(b, z.Msgsize()) o = msgp.Require(b, z.Msgsize())
// array header, size 34 // array header, size 35
o = append(o, 0xdc, 0x0, 0x22) o = append(o, 0xdc, 0x0, 0x23)
o = msgp.AppendString(o, z.Id) o = msgp.AppendString(o, z.Id)
o = msgp.AppendInt64(o, z.CreateAt) o = msgp.AppendInt64(o, z.CreateAt)
o = msgp.AppendInt64(o, z.UpdateAt) o = msgp.AppendInt64(o, z.UpdateAt)
@@ -471,6 +481,11 @@ func (z *User) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.AppendInt64(o, z.TermsOfServiceCreateAt) o = msgp.AppendInt64(o, z.TermsOfServiceCreateAt)
o = msgp.AppendBool(o, z.DisableWelcomeEmail) o = msgp.AppendBool(o, z.DisableWelcomeEmail)
o = msgp.AppendInt64(o, z.LastLogin) o = msgp.AppendInt64(o, z.LastLogin)
o, err = z.MfaUsedTimestamps.MarshalMsg(o)
if err != nil {
err = msgp.WrapError(err, "MfaUsedTimestamps")
return
}
return return
} }
@@ -482,8 +497,8 @@ func (z *User) UnmarshalMsg(bts []byte) (o []byte, err error) {
err = msgp.WrapError(err) err = msgp.WrapError(err)
return return
} }
if zb0001 != 34 { if zb0001 != 35 {
err = msgp.ArrayError{Wanted: 34, Got: zb0001} err = msgp.ArrayError{Wanted: 35, Got: zb0001}
return return
} }
z.Id, bts, err = msgp.ReadStringBytes(bts) z.Id, bts, err = msgp.ReadStringBytes(bts)
@@ -678,6 +693,11 @@ func (z *User) UnmarshalMsg(bts []byte) (o []byte, err error) {
err = msgp.WrapError(err, "LastLogin") err = msgp.WrapError(err, "LastLogin")
return return
} }
bts, err = z.MfaUsedTimestamps.UnmarshalMsg(bts)
if err != nil {
err = msgp.WrapError(err, "MfaUsedTimestamps")
return
}
o = bts o = bts
return return
} }
@@ -696,7 +716,7 @@ func (z *User) Msgsize() (s int) {
} else { } else {
s += msgp.StringPrefixSize + len(*z.RemoteId) s += msgp.StringPrefixSize + len(*z.RemoteId)
} }
s += msgp.Int64Size + msgp.BoolSize + msgp.StringPrefixSize + len(z.BotDescription) + msgp.Int64Size + msgp.StringPrefixSize + len(z.TermsOfServiceId) + msgp.Int64Size + msgp.BoolSize + msgp.Int64Size s += msgp.Int64Size + msgp.BoolSize + msgp.StringPrefixSize + len(z.BotDescription) + msgp.Int64Size + msgp.StringPrefixSize + len(z.TermsOfServiceId) + msgp.Int64Size + msgp.BoolSize + msgp.Int64Size + z.MfaUsedTimestamps.Msgsize()
return return
} }

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

@@ -332,6 +332,7 @@ func TestUserSanitizeInput(t *testing.T) {
user.EmailVerified = true user.EmailVerified = true
user.FailedAttempts = 10 user.FailedAttempts = 10
user.LastActivityAt = GetMillis() user.LastActivityAt = GetMillis()
user.MfaUsedTimestamps = StringArray{"1234", "4566"}
user.SanitizeInput(false) user.SanitizeInput(false)
@@ -347,6 +348,7 @@ func TestUserSanitizeInput(t *testing.T) {
require.Equal(t, int64(0), user.LastPictureUpdate) require.Equal(t, int64(0), user.LastPictureUpdate)
require.Equal(t, int64(0), user.LastActivityAt) require.Equal(t, int64(0), user.LastActivityAt)
require.Equal(t, 0, user.FailedAttempts) require.Equal(t, 0, user.FailedAttempts)
require.Equal(t, StringArray{}, user.MfaUsedTimestamps)
// these fields should remain intact // these fields should remain intact
require.Equal(t, "user@example.com", user.Email) require.Equal(t, "user@example.com", user.Email)

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

@@ -41,8 +41,11 @@ const (
var ErrMaxPropSizeExceeded = fmt.Errorf("max prop size of %d exceeded", maxPropSizeBytes) var ErrMaxPropSizeExceeded = fmt.Errorf("max prop size of %d exceeded", maxPropSizeBytes)
//msgp:ignore StringInterface StringSet
type StringInterface map[string]any type StringInterface map[string]any
type StringSet map[string]struct{} type StringSet map[string]struct{}
//msgp:tuple StringArray
type StringArray []string type StringArray []string
func (ss StringSet) Has(val string) bool { func (ss StringSet) Has(val string) bool {
@@ -228,6 +231,7 @@ func AppErrorInit(t i18n.TranslateFunc) {
}) })
} }
//msgp:ignore AppError
type AppError struct { type AppError struct {
Id string `json:"id"` Id string `json:"id"`
Message string `json:"message"` // Message to be display to the end user without debugging information Message string `json:"message"` // Message to be display to the end user without debugging information

93
server/public/model/utils_serial_gen.go Обычный файл
Просмотреть файл

@@ -0,0 +1,93 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
// Code generated by github.com/tinylib/msgp DO NOT EDIT.
import (
"github.com/tinylib/msgp/msgp"
)
// DecodeMsg implements msgp.Decodable
func (z *StringArray) DecodeMsg(dc *msgp.Reader) (err error) {
var zb0002 uint32
zb0002, err = dc.ReadArrayHeader()
if err != nil {
err = msgp.WrapError(err)
return
}
if cap((*z)) >= int(zb0002) {
(*z) = (*z)[:zb0002]
} else {
(*z) = make(StringArray, zb0002)
}
for zb0001 := range *z {
(*z)[zb0001], err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, zb0001)
return
}
}
return
}
// EncodeMsg implements msgp.Encodable
func (z StringArray) EncodeMsg(en *msgp.Writer) (err error) {
err = en.WriteArrayHeader(uint32(len(z)))
if err != nil {
err = msgp.WrapError(err)
return
}
for zb0003 := range z {
err = en.WriteString(z[zb0003])
if err != nil {
err = msgp.WrapError(err, zb0003)
return
}
}
return
}
// MarshalMsg implements msgp.Marshaler
func (z StringArray) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.Require(b, z.Msgsize())
o = msgp.AppendArrayHeader(o, uint32(len(z)))
for zb0003 := range z {
o = msgp.AppendString(o, z[zb0003])
}
return
}
// UnmarshalMsg implements msgp.Unmarshaler
func (z *StringArray) UnmarshalMsg(bts []byte) (o []byte, err error) {
var zb0002 uint32
zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
if cap((*z)) >= int(zb0002) {
(*z) = (*z)[:zb0002]
} else {
(*z) = make(StringArray, zb0002)
}
for zb0001 := range *z {
(*z)[zb0001], bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, zb0001)
return
}
}
o = bts
return
}
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
func (z StringArray) Msgsize() (s int) {
s = msgp.ArrayHeaderSize
for zb0003 := range z {
s += msgp.StringPrefixSize + len(z[zb0003])
}
return
}