user service: add oauth and profile images (#17784)
* users: add cache to service * reflect review comments * add oauth * move profile picture genaration * reflect review comments * move default bot image to users package * add missing wraps and apply suggestions * add comment for app.GetSession
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
79d4e9e9a9
Коммит
f69b28610a
@@ -328,8 +328,6 @@ type AppIface interface {
|
||||
// the member's group memberships and the configuration of those groups to the syncable. This method should only
|
||||
// be invoked on group-synced (aka group-constrained) syncables.
|
||||
SyncSyncableRoles(syncableID string, syncableType model.GroupSyncableType) *model.AppError
|
||||
// TODO: migrate this after the user service implementation is completed
|
||||
GetSanitizeOptions(asAdmin bool) map[string]bool
|
||||
// TeamMembersMinusGroupMembers returns the set of users on the given team minus the set of users in the given
|
||||
// groups.
|
||||
//
|
||||
@@ -721,6 +719,7 @@ type AppIface interface {
|
||||
GetSamlCertificateStatus() *model.SamlCertificateStatus
|
||||
GetSamlMetadata() (string, *model.AppError)
|
||||
GetSamlMetadataFromIdp(idpMetadataUrl string) (*model.SamlMetadataResponse, *model.AppError)
|
||||
GetSanitizeOptions(asAdmin bool) map[string]bool
|
||||
GetScheme(id string) (*model.Scheme, *model.AppError)
|
||||
GetSchemeByName(name string) (*model.Scheme, *model.AppError)
|
||||
GetSchemeRolesForTeam(teamID string) (string, string, string, *model.AppError)
|
||||
@@ -1072,7 +1071,6 @@ type AppIface interface {
|
||||
UpdateRemoteClusterTopics(remoteClusterId string, topics string) (*model.RemoteCluster, *model.AppError)
|
||||
UpdateRole(role *model.Role) (*model.Role, *model.AppError)
|
||||
UpdateScheme(scheme *model.Scheme) (*model.Scheme, *model.AppError)
|
||||
UpdateSessionsIsGuest(userID string, isGuest bool)
|
||||
UpdateSharedChannel(sc *model.SharedChannel) (*model.SharedChannel, error)
|
||||
UpdateSharedChannelRemoteCursor(id string, cursor model.GetPostsSinceForSyncCursor) error
|
||||
UpdateSidebarCategories(userID, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError)
|
||||
|
||||
@@ -1970,10 +1970,12 @@ func TestMarkChannelsAsViewedPanic(t *testing.T) {
|
||||
}
|
||||
mockChannelStore.On("UpdateLastViewedAt", []string{"channelID"}, "userID", false).Return(times, nil)
|
||||
mockSessionStore := mocks.SessionStore{}
|
||||
mockOAuthStore := mocks.OAuthStore{}
|
||||
var err error
|
||||
th.App.srv.userService, err = users.New(users.ServiceInitializer{
|
||||
th.App.srv.userService, err = users.New(users.ServiceConfig{
|
||||
UserStore: &mockUserStore,
|
||||
SessionStore: &mockSessionStore,
|
||||
OAuthStore: &mockOAuthStore,
|
||||
ConfigFn: th.App.srv.Config,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -177,7 +177,7 @@ func (a *App) DoLogin(c *request.Context, w http.ResponseWriter, r *http.Request
|
||||
session.GenerateCSRF()
|
||||
|
||||
if deviceID != "" {
|
||||
a.SetSessionExpireInDays(session, *a.Config().ServiceSettings.SessionLengthMobileInDays)
|
||||
a.srv.userService.SetSessionExpireInDays(session, *a.Config().ServiceSettings.SessionLengthMobileInDays)
|
||||
|
||||
// A special case where we logout of all other sessions with the same Id
|
||||
if err := a.RevokeSessionsForDeviceId(user.Id, deviceID, ""); err != nil {
|
||||
@@ -185,11 +185,11 @@ func (a *App) DoLogin(c *request.Context, w http.ResponseWriter, r *http.Request
|
||||
return err
|
||||
}
|
||||
} else if isMobile {
|
||||
a.SetSessionExpireInDays(session, *a.Config().ServiceSettings.SessionLengthMobileInDays)
|
||||
a.srv.userService.SetSessionExpireInDays(session, *a.Config().ServiceSettings.SessionLengthMobileInDays)
|
||||
} else if isOAuthUser || isSaml {
|
||||
a.SetSessionExpireInDays(session, *a.Config().ServiceSettings.SessionLengthSSOInDays)
|
||||
a.srv.userService.SetSessionExpireInDays(session, *a.Config().ServiceSettings.SessionLengthSSOInDays)
|
||||
} else {
|
||||
a.SetSessionExpireInDays(session, *a.Config().ServiceSettings.SessionLengthWebInDays)
|
||||
a.srv.userService.SetSessionExpireInDays(session, *a.Config().ServiceSettings.SessionLengthWebInDays)
|
||||
}
|
||||
|
||||
ua := uasurfer.Parse(r.UserAgent())
|
||||
|
||||
36
app/oauth.go
36
app/oauth.go
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/mattermost/mattermost-server/v5/app/request"
|
||||
"github.com/mattermost/mattermost-server/v5/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/services/users"
|
||||
"github.com/mattermost/mattermost-server/v5/shared/i18n"
|
||||
"github.com/mattermost/mattermost-server/v5/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v5/store"
|
||||
@@ -369,7 +370,7 @@ func (a *App) newSession(appName string, user *model.User) (*model.Session, *mod
|
||||
// Set new token an session
|
||||
session := &model.Session{UserId: user.Id, Roles: user.Roles, IsOAuth: true}
|
||||
session.GenerateCSRF()
|
||||
a.SetSessionExpireInDays(session, *a.Config().ServiceSettings.SessionLengthSSOInDays)
|
||||
a.srv.userService.SetSessionExpireInDays(session, *a.Config().ServiceSettings.SessionLengthSSOInDays)
|
||||
session.AddProp(model.SESSION_PROP_PLATFORM, appName)
|
||||
session.AddProp(model.SESSION_PROP_OS, "OAuth2")
|
||||
session.AddProp(model.SESSION_PROP_BROWSER, "OAuth2")
|
||||
@@ -518,30 +519,15 @@ func (a *App) RegenerateOAuthAppSecret(app *model.OAuthApp) (*model.OAuthApp, *m
|
||||
}
|
||||
|
||||
func (a *App) RevokeAccessToken(token string) *model.AppError {
|
||||
session, _ := a.GetSession(token)
|
||||
|
||||
defer a.srv.userService.ReturnSessionToPool(session)
|
||||
|
||||
schan := make(chan error, 1)
|
||||
go func() {
|
||||
schan <- a.Srv().Store.Session().Remove(token)
|
||||
close(schan)
|
||||
}()
|
||||
|
||||
if _, err := a.Srv().Store.OAuth().GetAccessData(token); err != nil {
|
||||
return model.NewAppError("RevokeAccessToken", "api.oauth.revoke_access_token.get.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if err := a.Srv().Store.OAuth().RemoveAccessData(token); err != nil {
|
||||
return model.NewAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_token.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if err := <-schan; err != nil {
|
||||
return model.NewAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_session.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if session != nil {
|
||||
a.ClearSessionCacheForUser(session.UserId)
|
||||
if err := a.srv.userService.RevokeAccessToken(token); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, users.GetTokenError):
|
||||
return model.NewAppError("RevokeAccessToken", "api.oauth.revoke_access_token.get.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
case errors.Is(err, users.DeleteTokenError):
|
||||
return model.NewAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_token.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
case errors.Is(err, users.DeleteSessionError):
|
||||
return model.NewAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_session.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -70,9 +70,6 @@ func TestOAuthRevokeAccessToken(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
err := th.App.RevokeAccessToken(model.NewRandomString(16))
|
||||
require.NotNil(t, err, "Should have failed bad token")
|
||||
|
||||
session := &model.Session{}
|
||||
session.CreateAt = model.GetMillis()
|
||||
session.UserId = model.NewId()
|
||||
@@ -80,22 +77,12 @@ func TestOAuthRevokeAccessToken(t *testing.T) {
|
||||
session.Roles = model.SYSTEM_USER_ROLE_ID
|
||||
th.App.SetSessionExpireInDays(session, 1)
|
||||
|
||||
session, _ = th.App.CreateSession(session)
|
||||
var err *model.AppError
|
||||
session, err = th.App.CreateSession(session)
|
||||
require.Nil(t, err)
|
||||
err = th.App.RevokeAccessToken(session.Token)
|
||||
require.NotNil(t, err, "Should have failed does not have an access token")
|
||||
|
||||
accessData := &model.AccessData{}
|
||||
accessData.Token = session.Token
|
||||
accessData.UserId = session.UserId
|
||||
accessData.RedirectUri = "http://example.com"
|
||||
accessData.ClientId = model.NewId()
|
||||
accessData.ExpiresAt = session.ExpiresAt
|
||||
|
||||
_, nErr := th.App.Srv().Store.OAuth().SaveAccessData(accessData)
|
||||
require.NoError(t, nErr)
|
||||
|
||||
err = th.App.RevokeAccessToken(accessData.Token)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, http.StatusBadRequest, err.StatusCode)
|
||||
}
|
||||
|
||||
func TestOAuthDeleteApp(t *testing.T) {
|
||||
@@ -120,7 +107,7 @@ func TestOAuthDeleteApp(t *testing.T) {
|
||||
session.Token = model.NewId()
|
||||
session.Roles = model.SYSTEM_USER_ROLE_ID
|
||||
session.IsOAuth = true
|
||||
th.App.SetSessionExpireInDays(session, 1)
|
||||
th.App.srv.userService.SetSessionExpireInDays(session, 1)
|
||||
|
||||
session, _ = th.App.CreateSession(session)
|
||||
|
||||
|
||||
@@ -16469,21 +16469,6 @@ func (a *OpenTracingAppLayer) UpdateScheme(scheme *model.Scheme) (*model.Scheme,
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpdateSessionsIsGuest(userID string, isGuest bool) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateSessionsIsGuest")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store.SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
a.app.UpdateSessionsIsGuest(userID, isGuest)
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpdateSharedChannel(sc *model.SharedChannel) (*model.SharedChannel, error) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateSharedChannel")
|
||||
|
||||
@@ -409,9 +409,10 @@ func NewServer(options ...Option) (*Server, error) {
|
||||
return nil, errors.Wrap(err, "cannot create store")
|
||||
}
|
||||
|
||||
s.userService, err = users.New(users.ServiceInitializer{
|
||||
s.userService, err = users.New(users.ServiceConfig{
|
||||
UserStore: s.Store.User(),
|
||||
SessionStore: s.Store.Session(),
|
||||
OAuthStore: s.Store.OAuth(),
|
||||
ConfigFn: s.Config,
|
||||
Metrics: s.Metrics,
|
||||
Cluster: s.Cluster,
|
||||
@@ -2264,18 +2265,18 @@ func (s *Server) GetProfileImage(user *model.User) ([]byte, bool, *model.AppErro
|
||||
}
|
||||
|
||||
func (s *Server) GetDefaultProfileImage(user *model.User) ([]byte, *model.AppError) {
|
||||
var img []byte
|
||||
var appErr *model.AppError
|
||||
img, err := s.userService.GetDefaultProfileImage(user)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, users.DefaultFontError):
|
||||
return nil, model.NewAppError("GetDefaultProfileImage", "api.user.create_profile_image.default_font.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
case errors.Is(err, users.UserInitialsError):
|
||||
return nil, model.NewAppError("GetDefaultProfileImage", "api.user.create_profile_image.initial.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
default:
|
||||
return nil, model.NewAppError("GetDefaultProfileImage", "api.user.create_profile_image.encode.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
if user.IsBot {
|
||||
img = model.BotDefaultImage
|
||||
appErr = nil
|
||||
} else {
|
||||
img, appErr = CreateProfileImage(user.Username, user.Id, *s.Config().FileSettings.InitialFont)
|
||||
}
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
|
||||
|
||||
132
app/session.go
132
app/session.go
@@ -6,16 +6,15 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/audit"
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/services/users"
|
||||
"github.com/mattermost/mattermost-server/v5/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v5/store"
|
||||
"github.com/mattermost/mattermost-server/v5/store/sqlstore"
|
||||
)
|
||||
|
||||
func (a *App) CreateSession(session *model.Session) (*model.Session, *model.AppError) {
|
||||
@@ -64,25 +63,16 @@ func (a *App) GetRemoteClusterSession(token string, remoteId string) (*model.Ses
|
||||
}
|
||||
|
||||
func (a *App) GetSession(token string) (*model.Session, *model.AppError) {
|
||||
session, err := a.srv.userService.GetSession(token)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetSession", "app.session.get.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
var session *model.Session
|
||||
// We intentionally skip the error check here, we only want to check if the token is valid.
|
||||
// If we don't have the session we are going to create one with the token eventually.
|
||||
if session, _ = a.srv.userService.GetSession(token); session != nil {
|
||||
if session.Token != token {
|
||||
return nil, model.NewAppError("GetSession", "api.context.invalid_token.error", map[string]interface{}{"Token": token, "Error": ""}, "session token is different from the one in DB", http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
if session.Id == "" {
|
||||
var nErr error
|
||||
if session, nErr = a.Srv().Store.Session().Get(sqlstore.WithMaster(context.Background()), token); nErr == nil {
|
||||
if session != nil {
|
||||
if session.Token != token {
|
||||
return nil, model.NewAppError("GetSession", "api.context.invalid_token.error", map[string]interface{}{"Token": token, "Error": ""}, "session token is different from the one in DB", http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
if !session.IsExpired() {
|
||||
a.srv.userService.AddSessionToCache(session)
|
||||
}
|
||||
}
|
||||
} else if nfErr := new(store.ErrNotFound); !errors.As(nErr, &nfErr) {
|
||||
return nil, model.NewAppError("GetSession", "app.session.get.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
||||
if !session.IsExpired() {
|
||||
a.srv.userService.AddSessionToCache(session)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +124,7 @@ func (a *App) GetSession(token string) (*model.Session, *model.AppError) {
|
||||
}
|
||||
|
||||
func (a *App) GetSessions(userID string) ([]*model.Session, *model.AppError) {
|
||||
sessions, err := a.Srv().Store.Session().GetSessions(userID)
|
||||
sessions, err := a.srv.userService.GetSessions(userID)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetSessions", "app.session.get_sessions.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
@@ -142,41 +132,18 @@ func (a *App) GetSessions(userID string) ([]*model.Session, *model.AppError) {
|
||||
return sessions, nil
|
||||
}
|
||||
|
||||
func (a *App) UpdateSessionsIsGuest(userID string, isGuest bool) {
|
||||
sessions, err := a.Srv().Store.Session().GetSessions(userID)
|
||||
if err != nil {
|
||||
mlog.Error("Unable to get user sessions", mlog.String("user_id", userID), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, session := range sessions {
|
||||
session.AddProp(model.SESSION_PROP_IS_GUEST, fmt.Sprintf("%t", isGuest))
|
||||
err := a.Srv().Store.Session().UpdateProps(session)
|
||||
if err != nil {
|
||||
mlog.Warn("Unable to update isGuest session", mlog.Err(err))
|
||||
continue
|
||||
}
|
||||
a.srv.userService.AddSessionToCache(session)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) RevokeAllSessions(userID string) *model.AppError {
|
||||
sessions, err := a.Srv().Store.Session().GetSessions(userID)
|
||||
if err != nil {
|
||||
return model.NewAppError("RevokeAllSessions", "app.session.get_sessions.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
for _, session := range sessions {
|
||||
if session.IsOAuth {
|
||||
a.RevokeAccessToken(session.Token)
|
||||
} else {
|
||||
if err := a.Srv().Store.Session().Remove(session.Id); err != nil {
|
||||
return model.NewAppError("RevokeAllSessions", "app.session.remove.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
if err := a.srv.userService.RevokeAllSessions(userID); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, users.GetSessionError):
|
||||
return model.NewAppError("RevokeAllSessions", "app.session.get_sessions.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
case errors.Is(err, users.DeleteSessionError):
|
||||
return model.NewAppError("RevokeAllSessions", "app.session.remove.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
default:
|
||||
return model.NewAppError("RevokeAllSessions", "app.session.remove.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
a.ClearSessionCacheForUser(userID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -187,16 +154,14 @@ func (a *App) AddSessionToCache(session *model.Session) {
|
||||
// RevokeSessionsFromAllUsers will go through all the sessions active
|
||||
// in the server and revoke them
|
||||
func (a *App) RevokeSessionsFromAllUsers() *model.AppError {
|
||||
// revoke tokens before sessions so they can't be used to relogin
|
||||
nErr := a.Srv().Store.OAuth().RemoveAllAccessData()
|
||||
if nErr != nil {
|
||||
return model.NewAppError("RevokeSessionsFromAllUsers", "app.oauth.remove_access_data.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
||||
if err := a.srv.userService.RevokeSessionsFromAllUsers(); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, users.DeleteAllAccessDataError):
|
||||
return model.NewAppError("RevokeSessionsFromAllUsers", "app.oauth.remove_access_data.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
default:
|
||||
return model.NewAppError("RevokeSessionsFromAllUsers", "app.session.remove_all_sessions_for_team.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
err := a.Srv().Store.Session().RemoveAllSessions()
|
||||
if err != nil {
|
||||
return model.NewAppError("RevokeSessionsFromAllUsers", "app.session.remove_all_sessions_for_team.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
a.ClearSessionCacheForAllUsers()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -222,24 +187,15 @@ func (a *App) ClearSessionCacheForAllUsersSkipClusterSend() {
|
||||
}
|
||||
|
||||
func (a *App) RevokeSessionsForDeviceId(userID string, deviceID string, currentSessionId string) *model.AppError {
|
||||
sessions, err := a.Srv().Store.Session().GetSessions(userID)
|
||||
if err != nil {
|
||||
if err := a.srv.userService.RevokeSessionsForDeviceId(userID, deviceID, currentSessionId); err != nil {
|
||||
return model.NewAppError("RevokeSessionsForDeviceId", "app.session.get_sessions.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
for _, session := range sessions {
|
||||
if session.DeviceId == deviceID && session.Id != currentSessionId {
|
||||
mlog.Debug("Revoking sessionId for userId. Re-login with the same device Id", mlog.String("session_id", session.Id), mlog.String("user_id", userID))
|
||||
if err := a.RevokeSession(session); err != nil {
|
||||
mlog.Warn("Could not revoke session for device", mlog.String("device_id", deviceID), mlog.Err(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) GetSessionById(sessionID string) (*model.Session, *model.AppError) {
|
||||
session, err := a.Srv().Store.Session().Get(context.Background(), sessionID)
|
||||
session, err := a.srv.userService.GetSessionByID(sessionID)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetSessionById", "app.session.get.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
@@ -248,7 +204,7 @@ func (a *App) GetSessionById(sessionID string) (*model.Session, *model.AppError)
|
||||
}
|
||||
|
||||
func (a *App) RevokeSessionById(sessionID string) *model.AppError {
|
||||
session, err := a.Srv().Store.Session().Get(context.Background(), sessionID)
|
||||
session, err := a.GetSessionById(sessionID)
|
||||
if err != nil {
|
||||
return model.NewAppError("RevokeSessionById", "app.session.get.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
@@ -257,18 +213,15 @@ func (a *App) RevokeSessionById(sessionID string) *model.AppError {
|
||||
}
|
||||
|
||||
func (a *App) RevokeSession(session *model.Session) *model.AppError {
|
||||
if session.IsOAuth {
|
||||
if err := a.RevokeAccessToken(session.Token); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := a.Srv().Store.Session().Remove(session.Id); err != nil {
|
||||
if err := a.srv.userService.RevokeSession(session); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, users.DeleteSessionError):
|
||||
return model.NewAppError("RevokeSession", "app.session.remove.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
default:
|
||||
return model.NewAppError("RevokeSession", "app.session.remove.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
a.ClearSessionCacheForUser(session.UserId)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -375,16 +328,11 @@ func (a *App) GetSessionLengthInMillis(session *model.Session) int64 {
|
||||
// relative to either the session creation date or the current time, depending
|
||||
// on the `ExtendSessionOnActivity` config setting.
|
||||
func (a *App) SetSessionExpireInDays(session *model.Session, days int) {
|
||||
if session.CreateAt == 0 || *a.Config().ServiceSettings.ExtendSessionLengthWithActivity {
|
||||
session.ExpiresAt = model.GetMillis() + (1000 * 60 * 60 * 24 * int64(days))
|
||||
} else {
|
||||
session.ExpiresAt = session.CreateAt + (1000 * 60 * 60 * 24 * int64(days))
|
||||
}
|
||||
a.srv.userService.SetSessionExpireInDays(session, days)
|
||||
}
|
||||
|
||||
func (a *App) CreateUserAccessToken(token *model.UserAccessToken) (*model.UserAccessToken, *model.AppError) {
|
||||
|
||||
user, nErr := a.Srv().Store.User().Get(context.Background(), token.UserId)
|
||||
user, nErr := a.srv.userService.GetUser(token.UserId)
|
||||
if nErr != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
@@ -469,7 +417,7 @@ func (a *App) createSessionForUserAccessToken(tokenString string) (*model.Sessio
|
||||
} else {
|
||||
session.AddProp(model.SESSION_PROP_IS_GUEST, "false")
|
||||
}
|
||||
a.SetSessionExpireInDays(session, model.SESSION_USER_ACCESS_TOKEN_EXPIRY)
|
||||
a.srv.userService.SetSessionExpireInDays(session, model.SESSION_USER_ACCESS_TOKEN_EXPIRY)
|
||||
|
||||
session, nErr = a.Srv().Store.Session().Save(session)
|
||||
if nErr != nil {
|
||||
@@ -490,7 +438,7 @@ func (a *App) createSessionForUserAccessToken(tokenString string) (*model.Sessio
|
||||
|
||||
func (a *App) RevokeUserAccessToken(token *model.UserAccessToken) *model.AppError {
|
||||
var session *model.Session
|
||||
session, _ = a.Srv().Store.Session().Get(context.Background(), token.Token)
|
||||
session, _ = a.srv.userService.GetSessionContext(context.Background(), token.Token)
|
||||
|
||||
if err := a.Srv().Store.UserAccessToken().Delete(token.Id); err != nil {
|
||||
return model.NewAppError("RevokeUserAccessToken", "app.user_access_token.delete.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
@@ -505,7 +453,7 @@ func (a *App) RevokeUserAccessToken(token *model.UserAccessToken) *model.AppErro
|
||||
|
||||
func (a *App) DisableUserAccessToken(token *model.UserAccessToken) *model.AppError {
|
||||
var session *model.Session
|
||||
session, _ = a.Srv().Store.Session().Get(context.Background(), token.Token)
|
||||
session, _ = a.srv.userService.GetSessionContext(context.Background(), token.Token)
|
||||
|
||||
if err := a.Srv().Store.UserAccessToken().UpdateTokenDisable(token.Id); err != nil {
|
||||
return model.NewAppError("DisableUserAccessToken", "app.user_access_token.update_token_disable.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
@@ -520,7 +468,7 @@ func (a *App) DisableUserAccessToken(token *model.UserAccessToken) *model.AppErr
|
||||
|
||||
func (a *App) EnableUserAccessToken(token *model.UserAccessToken) *model.AppError {
|
||||
var session *model.Session
|
||||
session, _ = a.Srv().Store.Session().Get(context.Background(), token.Token)
|
||||
session, _ = a.srv.userService.GetSessionContext(context.Background(), token.Token)
|
||||
|
||||
err := a.Srv().Store.UserAccessToken().UpdateTokenEnable(token.Id)
|
||||
if err != nil {
|
||||
|
||||
@@ -329,58 +329,6 @@ func TestApp_ExtendExpiryIfNeeded(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
dayInMillis = 86400000
|
||||
grace = 5 * 1000
|
||||
thirtyDays = dayInMillis * 30
|
||||
)
|
||||
|
||||
func TestApp_SetSessionExpireInDays(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
now := model.GetMillis()
|
||||
createAt := now - (dayInMillis * 20)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
extend bool
|
||||
create bool
|
||||
days int
|
||||
want int64
|
||||
}{
|
||||
{name: "zero days, extend", extend: true, create: true, days: 0, want: now},
|
||||
{name: "zero days, extend", extend: true, create: false, days: 0, want: now},
|
||||
{name: "zero days, no extend", extend: false, create: true, days: 0, want: createAt},
|
||||
{name: "zero days, no extend", extend: false, create: false, days: 0, want: now},
|
||||
{name: "thirty days, extend", extend: true, create: true, days: 30, want: now + thirtyDays},
|
||||
{name: "thirty days, extend", extend: true, create: false, days: 30, want: now + thirtyDays},
|
||||
{name: "thirty days, no extend", extend: false, create: true, days: 30, want: createAt + thirtyDays},
|
||||
{name: "thirty days, no extend", extend: false, create: false, days: 30, want: now + thirtyDays},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.ExtendSessionLengthWithActivity = tt.extend
|
||||
})
|
||||
var create int64
|
||||
if tt.create {
|
||||
create = createAt
|
||||
}
|
||||
|
||||
session := &model.Session{
|
||||
CreateAt: create,
|
||||
ExpiresAt: model.GetMillis() + dayInMillis,
|
||||
}
|
||||
th.App.SetSessionExpireInDays(session, tt.days)
|
||||
|
||||
// must be within 5 seconds of expected time.
|
||||
require.GreaterOrEqual(t, session.ExpiresAt, tt.want-grace)
|
||||
require.LessOrEqual(t, session.ExpiresAt, tt.want+grace)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCloudSession(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer func() {
|
||||
|
||||
162
app/user.go
162
app/user.go
@@ -6,26 +6,15 @@ package app
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
b64 "encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/freetype"
|
||||
"github.com/golang/freetype/truetype"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/app/imaging"
|
||||
"github.com/mattermost/mattermost-server/v5/app/request"
|
||||
"github.com/mattermost/mattermost-server/v5/einterfaces"
|
||||
@@ -36,7 +25,6 @@ import (
|
||||
"github.com/mattermost/mattermost-server/v5/shared/mfa"
|
||||
"github.com/mattermost/mattermost-server/v5/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v5/store"
|
||||
"github.com/mattermost/mattermost-server/v5/utils/fileutils"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -320,12 +308,12 @@ func (a *App) CreateOAuthUser(c *request.Context, service string, userData io.Re
|
||||
}
|
||||
}
|
||||
|
||||
userByAuth, _ := a.Srv().Store.User().GetByAuth(user.AuthData, service)
|
||||
userByAuth, _ := a.srv.userService.GetUserByAuth(user.AuthData, service)
|
||||
if userByAuth != nil {
|
||||
return userByAuth, nil
|
||||
}
|
||||
|
||||
userByEmail, _ := a.Srv().Store.User().GetByEmail(user.Email)
|
||||
userByEmail, _ := a.srv.userService.GetUserByEmail(user.Email)
|
||||
if userByEmail != nil {
|
||||
if userByEmail.AuthService == "" {
|
||||
return nil, model.NewAppError("CreateOAuthUser", "api.user.create_oauth_user.already_attached.app_error", map[string]interface{}{"Service": service, "Auth": model.USER_AUTH_SERVICE_EMAIL}, "email="+user.Email, http.StatusBadRequest)
|
||||
@@ -633,7 +621,7 @@ func (a *App) GetUsersByGroupChannelIds(c *request.Context, channelIDs []string,
|
||||
}
|
||||
|
||||
func (a *App) GetUsersByUsernames(usernames []string, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
|
||||
users, err := a.Srv().Store.User().GetProfilesByUsernames(usernames, viewRestrictions)
|
||||
users, err := a.srv.userService.GetUsersByUsernames(usernames, &model.UserGetOptions{ViewRestrictions: viewRestrictions})
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetUsersByUsernames", "app.user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
@@ -658,28 +646,18 @@ func (a *App) GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppErro
|
||||
return nil, model.NewAppError("GenerateMfaSecret", "mfa.mfa_disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
secret, img, err := mfa.New(a.Srv().Store.User()).GenerateSecret(*a.Config().ServiceSettings.SiteURL, user.Email, user.Id)
|
||||
mfaSecret, err := a.srv.userService.GenerateMfaSecret(user)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GenerateMfaSecret", "mfa.generate_qr_code.create_code.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Make sure the old secret is not cached on any cluster nodes.
|
||||
a.InvalidateCacheForUser(user.Id)
|
||||
|
||||
mfaSecret := &model.MfaSecret{Secret: secret, QRCode: b64.StdEncoding.EncodeToString(img)}
|
||||
return mfaSecret, nil
|
||||
}
|
||||
|
||||
func (a *App) ActivateMfa(userID, token string) *model.AppError {
|
||||
user, err := a.Srv().Store.User().Get(context.Background(), userID)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return model.NewAppError("ActivateMfa", MissingAccountError, nil, nfErr.Error(), http.StatusNotFound)
|
||||
default:
|
||||
return model.NewAppError("ActivateMfa", "app.user.get.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
user, appErr := a.GetUser(userID)
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
if user.AuthService != "" && user.AuthService != model.USER_AUTH_SERVICE_LDAP {
|
||||
@@ -690,7 +668,7 @@ func (a *App) ActivateMfa(userID, token string) *model.AppError {
|
||||
return model.NewAppError("ActivateMfa", "mfa.mfa_disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
if err := mfa.New(a.Srv().Store.User()).Activate(user.MfaSecret, user.Id, token); err != nil {
|
||||
if err := a.srv.userService.ActivateMfa(user, token); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, mfa.InvalidToken):
|
||||
return model.NewAppError("ActivateMfa", "mfa.activate.bad_token.app_error", nil, "", http.StatusUnauthorized)
|
||||
@@ -706,7 +684,12 @@ func (a *App) ActivateMfa(userID, token string) *model.AppError {
|
||||
}
|
||||
|
||||
func (a *App) DeactivateMfa(userID string) *model.AppError {
|
||||
if err := mfa.New(a.Srv().Store.User()).Deactivate(userID); err != nil {
|
||||
user, appErr := a.GetUser(userID)
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
if err := a.srv.userService.DeactivateMfa(user); err != nil {
|
||||
return model.NewAppError("DeactivateMfa", "mfa.deactivate.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
@@ -716,89 +699,6 @@ func (a *App) DeactivateMfa(userID string) *model.AppError {
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateProfileImage(username string, userID string, initialFont string) ([]byte, *model.AppError) {
|
||||
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, model.NewAppError("CreateProfileImage", "api.user.create_profile_image.default_font.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
color := colors[int64(seed)%int64(len(colors))]
|
||||
dstImg := image.NewRGBA(image.Rect(0, 0, ImageProfilePixelDimension, ImageProfilePixelDimension))
|
||||
srcImg := image.White
|
||||
draw.Draw(dstImg, dstImg.Bounds(), &image.Uniform{color}, image.Point{}, draw.Src)
|
||||
size := float64(ImageProfilePixelDimension / 2)
|
||||
|
||||
c := freetype.NewContext()
|
||||
c.SetFont(font)
|
||||
c.SetFontSize(size)
|
||||
c.SetClip(dstImg.Bounds())
|
||||
c.SetDst(dstImg)
|
||||
c.SetSrc(srcImg)
|
||||
|
||||
pt := freetype.Pt(ImageProfilePixelDimension/5, ImageProfilePixelDimension*2/3)
|
||||
_, err = c.DrawString(initial, pt)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("CreateProfileImage", "api.user.create_profile_image.initial.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
if imgErr := png.Encode(buf, dstImg); imgErr != nil {
|
||||
return nil, model.NewAppError("CreateProfileImage", "api.user.create_profile_image.encode.app_error", nil, imgErr.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
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 := ioutil.ReadFile(filepath.Join(fontDir, initialFont))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return freetype.ParseFont(fontBytes)
|
||||
}
|
||||
|
||||
func (a *App) GetProfileImage(user *model.User) ([]byte, bool, *model.AppError) {
|
||||
return a.srv.GetProfileImage(user)
|
||||
}
|
||||
@@ -929,10 +829,6 @@ func (a *App) UpdatePasswordAsUser(userID, currentPassword, newPassword string)
|
||||
}
|
||||
|
||||
func (a *App) userDeactivated(c *request.Context, userID string) *model.AppError {
|
||||
if err := a.RevokeAllSessions(userID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.SetStatusOffline(userID, false)
|
||||
|
||||
user, err := a.GetUser(userID)
|
||||
@@ -982,7 +878,7 @@ func (a *App) UpdateActive(c *request.Context, user *model.User, active bool) (*
|
||||
user.DeleteAt = user.UpdateAt
|
||||
}
|
||||
|
||||
userUpdate, err := a.Srv().Store.User().Update(user, true)
|
||||
userUpdate, err := a.srv.userService.UpdateUser(user, true)
|
||||
if err != nil {
|
||||
var appErr *model.AppError
|
||||
var invErr *store.ErrInvalidInput
|
||||
@@ -998,6 +894,9 @@ func (a *App) UpdateActive(c *request.Context, user *model.User, active bool) (*
|
||||
ruser := userUpdate.New
|
||||
|
||||
if !active {
|
||||
if err := a.RevokeAllSessions(ruser.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := a.userDeactivated(c, ruser.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1012,7 +911,7 @@ func (a *App) UpdateActive(c *request.Context, user *model.User, active bool) (*
|
||||
}
|
||||
|
||||
func (a *App) DeactivateGuests(c *request.Context) *model.AppError {
|
||||
userIDs, err := a.Srv().Store.User().DeactivateGuests()
|
||||
userIDs, err := a.srv.userService.DeactivateAllGuests()
|
||||
if err != nil {
|
||||
return model.NewAppError("DeactivateGuests", "app.user.update_active_for_multiple_users.updating.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
@@ -1032,19 +931,12 @@ func (a *App) DeactivateGuests(c *request.Context) *model.AppError {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: migrate this after the user service implementation is completed
|
||||
func (a *App) GetSanitizeOptions(asAdmin bool) map[string]bool {
|
||||
options := a.Config().GetSanitizeOptions()
|
||||
if asAdmin {
|
||||
options["email"] = true
|
||||
options["fullname"] = true
|
||||
options["authservice"] = true
|
||||
}
|
||||
return options
|
||||
return a.srv.userService.GetSanitizeOptions(asAdmin)
|
||||
}
|
||||
|
||||
func (a *App) SanitizeProfile(user *model.User, asAdmin bool) {
|
||||
options := a.GetSanitizeOptions(asAdmin)
|
||||
options := a.srv.userService.GetSanitizeOptions(asAdmin)
|
||||
|
||||
user.SanitizeProfile(options)
|
||||
}
|
||||
@@ -1136,7 +1028,7 @@ func (a *App) sendUpdatedUserEvent(user model.User) {
|
||||
}
|
||||
|
||||
func (a *App) UpdateUser(user *model.User, sendNotifications bool) (*model.User, *model.AppError) {
|
||||
prev, err := a.Srv().Store.User().Get(context.Background(), user.Id)
|
||||
prev, err := a.srv.userService.GetUser(user.Id)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
@@ -1179,7 +1071,7 @@ func (a *App) UpdateUser(user *model.User, sendNotifications bool) (*model.User,
|
||||
}
|
||||
}
|
||||
|
||||
userUpdate, err := a.Srv().Store.User().Update(user, false)
|
||||
userUpdate, err := a.srv.userService.UpdateUser(user, false)
|
||||
if err != nil {
|
||||
var appErr *model.AppError
|
||||
var invErr *store.ErrInvalidInput
|
||||
@@ -2120,7 +2012,9 @@ func (a *App) PromoteGuestToUser(c *request.Context, user *model.User, requestor
|
||||
mlog.Warn("Failed to get user on promote guest to user", mlog.Err(err))
|
||||
} else {
|
||||
a.sendUpdatedUserEvent(*promotedUser)
|
||||
a.UpdateSessionsIsGuest(promotedUser.Id, promotedUser.IsGuest())
|
||||
if uErr := a.srv.userService.UpdateSessionsIsGuest(promotedUser.Id, promotedUser.IsGuest()); uErr != nil {
|
||||
mlog.Warn("Unable to update user sessions", mlog.String("user_id", promotedUser.Id), mlog.Err(uErr))
|
||||
}
|
||||
}
|
||||
|
||||
teamMembers, err := a.GetTeamMembersForUser(user.Id)
|
||||
@@ -2159,7 +2053,9 @@ func (a *App) DemoteUserToGuest(user *model.User) *model.AppError {
|
||||
}
|
||||
|
||||
a.sendUpdatedUserEvent(*demotedUser)
|
||||
a.UpdateSessionsIsGuest(demotedUser.Id, demotedUser.IsGuest())
|
||||
if uErr := a.srv.userService.UpdateSessionsIsGuest(demotedUser.Id, demotedUser.IsGuest()); uErr != nil {
|
||||
mlog.Warn("Unable to update user sessions", mlog.String("user_id", demotedUser.Id), mlog.Err(uErr))
|
||||
}
|
||||
|
||||
teamMembers, err := a.GetTeamMembersForUser(user.Id)
|
||||
if err != nil {
|
||||
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"image"
|
||||
"image/color"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -82,19 +80,6 @@ func TestCreateOAuthUser(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateProfileImage(t *testing.T) {
|
||||
b, err := CreateProfileImage("Corey Hulen", "eo1zkdr96pdj98pjmq8zy35wba", "nunito-bold.ttf")
|
||||
require.Nil(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")
|
||||
}
|
||||
|
||||
func TestSetDefaultProfileImage(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
@@ -153,15 +153,18 @@ func TestHubSessionRevokeRace(t *testing.T) {
|
||||
mockStatusStore.On("UpdateLastActivityAt", "user1", mock.Anything).Return(nil)
|
||||
mockStatusStore.On("SaveOrUpdate", mock.AnythingOfType("*model.Status")).Return(nil)
|
||||
|
||||
mockOAuthStore := mocks.OAuthStore{}
|
||||
mockStore.On("Session").Return(&mockSessionStore)
|
||||
mockStore.On("OAuth").Return(&mockOAuthStore)
|
||||
mockStore.On("Status").Return(&mockStatusStore)
|
||||
mockStore.On("User").Return(&mockUserStore)
|
||||
mockStore.On("Post").Return(&mockPostStore)
|
||||
mockStore.On("System").Return(&mockSystemStore)
|
||||
|
||||
userService, err := users.New(users.ServiceInitializer{
|
||||
userService, err := users.New(users.ServiceConfig{
|
||||
UserStore: &mockUserStore,
|
||||
SessionStore: &mockSessionStore,
|
||||
OAuthStore: &mockOAuthStore,
|
||||
ConfigFn: th.App.srv.Config,
|
||||
Metrics: th.App.Metrics(),
|
||||
Cluster: th.App.Cluster(),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package model
|
||||
package users
|
||||
|
||||
var BotDefaultImage = []byte{
|
||||
var botDefaultImage = []byte{
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
|
||||
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x7e,
|
||||
0x08, 0x06, 0x00, 0x00, 0x00, 0xec, 0xa6, 0x19, 0xa2, 0x00, 0x00, 0x00,
|
||||
@@ -6,9 +6,20 @@ 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.")
|
||||
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")
|
||||
|
||||
GetTokenError = errors.New("could not get token")
|
||||
GetSessionError = errors.New("could not get session")
|
||||
DeleteTokenError = errors.New("could not delete token")
|
||||
DeleteSessionError = errors.New("could not delete session")
|
||||
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")
|
||||
)
|
||||
|
||||
// ErrInvalidPassword indicates an error against the password settings
|
||||
|
||||
@@ -87,8 +87,14 @@ func setupTestHelper(s store.Store, includeCacheLayer bool, tb testing.TB) *Test
|
||||
service: &UserService{
|
||||
store: s.User(),
|
||||
sessionStore: s.Session(),
|
||||
oAuthStore: s.OAuth(),
|
||||
sessionCache: cache,
|
||||
config: configStore.Get,
|
||||
sessionPool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
return &model.Session{}
|
||||
},
|
||||
},
|
||||
},
|
||||
Context: &request.Context{},
|
||||
configStore: configStore,
|
||||
@@ -155,3 +161,15 @@ func (th *TestHelper) TearDown() {
|
||||
os.RemoveAll(th.workspace)
|
||||
}
|
||||
}
|
||||
|
||||
func (th *TestHelper) UpdateConfig(f func(*model.Config)) {
|
||||
if th.configStore.IsReadOnly() {
|
||||
return
|
||||
}
|
||||
old := th.configStore.Get()
|
||||
updated := old.Clone()
|
||||
f(updated)
|
||||
if _, _, err := th.configStore.Set(updated); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
117
services/users/profile_picture.go
Обычный файл
117
services/users/profile_picture.go
Обычный файл
@@ -0,0 +1,117 @@
|
||||
// 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/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/freetype"
|
||||
"github.com/golang/freetype/truetype"
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/utils/fileutils"
|
||||
)
|
||||
|
||||
const (
|
||||
imageProfilePixelDimension = 128
|
||||
)
|
||||
|
||||
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))
|
||||
srcImg := image.White
|
||||
draw.Draw(dstImg, dstImg.Bounds(), &image.Uniform{color}, image.Point{}, draw.Src)
|
||||
size := float64(imageProfilePixelDimension / 2)
|
||||
|
||||
c := freetype.NewContext()
|
||||
c.SetFont(font)
|
||||
c.SetFontSize(size)
|
||||
c.SetClip(dstImg.Bounds())
|
||||
c.SetDst(dstImg)
|
||||
c.SetSrc(srcImg)
|
||||
|
||||
pt := freetype.Pt(imageProfilePixelDimension/5, imageProfilePixelDimension*2/3)
|
||||
_, err = c.DrawString(initial, pt)
|
||||
if err != nil {
|
||||
return nil, UserInitialsError
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
if imgErr := png.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 := ioutil.ReadFile(filepath.Join(fontDir, initialFont))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return freetype.ParseFont(fontBytes)
|
||||
}
|
||||
26
services/users/profile_picture_test.go
Обычный файл
26
services/users/profile_picture_test.go
Обычный файл
@@ -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")
|
||||
}
|
||||
82
services/users/service.go
Обычный файл
82
services/users/service.go
Обычный файл
@@ -0,0 +1,82 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package users
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/services/cache"
|
||||
"github.com/mattermost/mattermost-server/v5/store"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
store store.UserStore
|
||||
sessionStore store.SessionStore
|
||||
oAuthStore store.OAuthStore
|
||||
sessionCache cache.Cache
|
||||
sessionPool sync.Pool
|
||||
metrics einterfaces.MetricsInterface
|
||||
cluster einterfaces.ClusterInterface
|
||||
config func() *model.Config
|
||||
}
|
||||
|
||||
// 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
|
||||
// Optional fields
|
||||
Metrics einterfaces.MetricsInterface
|
||||
Cluster einterfaces.ClusterInterface
|
||||
}
|
||||
|
||||
func New(c ServiceConfig) (*UserService, error) {
|
||||
if err := c.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cacheProvider := cache.NewProvider()
|
||||
if err := cacheProvider.Connect(); err != nil {
|
||||
return nil, fmt.Errorf("could not connect to cache provider: %w", err)
|
||||
}
|
||||
|
||||
sessionCache, err := cacheProvider.NewCache(&cache.CacheOptions{
|
||||
Size: model.SESSION_CACHE_SIZE,
|
||||
Striped: true,
|
||||
StripedBuckets: maxInt(runtime.NumCPU()-1, 1),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create session cache: %w", err)
|
||||
}
|
||||
|
||||
return &UserService{
|
||||
store: c.UserStore,
|
||||
sessionStore: c.SessionStore,
|
||||
oAuthStore: c.OAuthStore,
|
||||
config: c.ConfigFn,
|
||||
metrics: c.Metrics,
|
||||
cluster: c.Cluster,
|
||||
sessionCache: sessionCache,
|
||||
sessionPool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
return &model.Session{}
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *ServiceConfig) validate() error {
|
||||
if in := c; in.ConfigFn == nil || in.UserStore == nil || in.SessionStore == nil || in.OAuthStore == nil {
|
||||
return errors.New("required parameters are not provided")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
30
services/users/service_test.go
Обычный файл
30
services/users/service_test.go
Обычный файл
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package users
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
_, err := New(ServiceConfig{})
|
||||
require.Error(t, err)
|
||||
|
||||
dbStore := mainHelper.GetStore()
|
||||
|
||||
cfn := func() *model.Config {
|
||||
return &model.Config{}
|
||||
}
|
||||
|
||||
_, err = New(ServiceConfig{
|
||||
UserStore: dbStore.User(),
|
||||
SessionStore: dbStore.Session(),
|
||||
OAuthStore: dbStore.OAuth(),
|
||||
ConfigFn: cfn,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -4,9 +4,14 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v5/store/sqlstore"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (us *UserService) ReturnSessionToPool(session *model.Session) {
|
||||
@@ -40,7 +45,20 @@ func (us *UserService) GetSession(token string) (*model.Session, error) {
|
||||
us.metrics.IncrementMemCacheMissCounterSession()
|
||||
}
|
||||
}
|
||||
return session, nil
|
||||
|
||||
if session.Id != "" {
|
||||
return session, nil
|
||||
}
|
||||
|
||||
return us.GetSessionContext(sqlstore.WithMaster(context.Background()), token)
|
||||
}
|
||||
|
||||
func (us *UserService) GetSessionContext(ctx context.Context, token string) (*model.Session, error) {
|
||||
return us.sessionStore.Get(ctx, token)
|
||||
}
|
||||
|
||||
func (us *UserService) GetSessions(userID string) ([]*model.Session, error) {
|
||||
return us.sessionStore.GetSessions(userID)
|
||||
}
|
||||
|
||||
func (us *UserService) AddSessionToCache(session *model.Session) {
|
||||
@@ -98,3 +116,134 @@ func (us *UserService) ClearAllUsersSessionCache() {
|
||||
us.cluster.SendClusterMessage(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (us *UserService) GetSessionByID(sessionID string) (*model.Session, error) {
|
||||
return us.sessionStore.Get(context.Background(), sessionID)
|
||||
}
|
||||
|
||||
func (us *UserService) RevokeSessionsFromAllUsers() error {
|
||||
// revoke tokens before sessions so they can't be used to relogin
|
||||
nErr := us.oAuthStore.RemoveAllAccessData()
|
||||
if nErr != nil {
|
||||
return errors.Wrap(DeleteAllAccessDataError, nErr.Error())
|
||||
}
|
||||
err := us.sessionStore.RemoveAllSessions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
us.ClearAllUsersSessionCache()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (us *UserService) RevokeSessionsForDeviceId(userID string, deviceID string, currentSessionId string) error {
|
||||
sessions, err := us.sessionStore.GetSessions(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, session := range sessions {
|
||||
if session.DeviceId == deviceID && session.Id != currentSessionId {
|
||||
mlog.Debug("Revoking sessionId for userId. Re-login with the same device Id", mlog.String("session_id", session.Id), mlog.String("user_id", userID))
|
||||
if err := us.RevokeSession(session); err != nil {
|
||||
mlog.Warn("Could not revoke session for device", mlog.String("device_id", deviceID), mlog.Err(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (us *UserService) RevokeSession(session *model.Session) error {
|
||||
if session.IsOAuth {
|
||||
if err := us.RevokeAccessToken(session.Token); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := us.sessionStore.Remove(session.Id); err != nil {
|
||||
return errors.Wrap(DeleteSessionError, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
us.ClearUserSessionCache(session.UserId)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (us *UserService) RevokeAccessToken(token string) error {
|
||||
session, _ := us.GetSession(token)
|
||||
|
||||
defer us.ReturnSessionToPool(session)
|
||||
|
||||
schan := make(chan error, 1)
|
||||
go func() {
|
||||
schan <- us.sessionStore.Remove(token)
|
||||
close(schan)
|
||||
}()
|
||||
|
||||
if _, err := us.oAuthStore.GetAccessData(token); err != nil {
|
||||
return errors.Wrap(GetTokenError, err.Error())
|
||||
}
|
||||
|
||||
if err := us.oAuthStore.RemoveAccessData(token); err != nil {
|
||||
return errors.Wrap(DeleteTokenError, err.Error())
|
||||
}
|
||||
|
||||
if err := <-schan; err != nil {
|
||||
return errors.Wrap(DeleteSessionError, err.Error())
|
||||
}
|
||||
|
||||
if session != nil {
|
||||
us.ClearUserSessionCache(session.UserId)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetSessionExpireInDays sets the session's expiry the specified number of days
|
||||
// relative to either the session creation date or the current time, depending
|
||||
// on the `ExtendSessionOnActivity` config setting.
|
||||
func (us *UserService) SetSessionExpireInDays(session *model.Session, days int) {
|
||||
if session.CreateAt == 0 || *us.config().ServiceSettings.ExtendSessionLengthWithActivity {
|
||||
session.ExpiresAt = model.GetMillis() + (1000 * 60 * 60 * 24 * int64(days))
|
||||
} else {
|
||||
session.ExpiresAt = session.CreateAt + (1000 * 60 * 60 * 24 * int64(days))
|
||||
}
|
||||
}
|
||||
|
||||
func (us *UserService) UpdateSessionsIsGuest(userID string, isGuest bool) error {
|
||||
sessions, err := us.GetSessions(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, session := range sessions {
|
||||
session.AddProp(model.SESSION_PROP_IS_GUEST, fmt.Sprintf("%t", isGuest))
|
||||
err := us.sessionStore.UpdateProps(session)
|
||||
if err != nil {
|
||||
mlog.Warn("Unable to update isGuest session", mlog.Err(err))
|
||||
continue
|
||||
}
|
||||
us.AddSessionToCache(session)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (us *UserService) RevokeAllSessions(userID string) error {
|
||||
sessions, err := us.sessionStore.GetSessions(userID)
|
||||
if err != nil {
|
||||
return errors.Wrap(GetSessionError, err.Error())
|
||||
}
|
||||
for _, session := range sessions {
|
||||
if session.IsOAuth {
|
||||
us.RevokeAccessToken(session.Token)
|
||||
} else {
|
||||
if err := us.sessionStore.Remove(session.Id); err != nil {
|
||||
return errors.Wrap(DeleteSessionError, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
us.ClearUserSessionCache(userID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11,6 +11,12 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
dayInMillis = 86400000
|
||||
grace = 5 * 1000
|
||||
thirtyDays = dayInMillis * 30
|
||||
)
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
@@ -47,3 +53,81 @@ func TestCache(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, rkeys)
|
||||
}
|
||||
|
||||
func TestSetSessionExpireInDays(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
now := model.GetMillis()
|
||||
createAt := now - (dayInMillis * 20)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
extend bool
|
||||
create bool
|
||||
days int
|
||||
want int64
|
||||
}{
|
||||
{name: "zero days, extend", extend: true, create: true, days: 0, want: now},
|
||||
{name: "zero days, extend", extend: true, create: false, days: 0, want: now},
|
||||
{name: "zero days, no extend", extend: false, create: true, days: 0, want: createAt},
|
||||
{name: "zero days, no extend", extend: false, create: false, days: 0, want: now},
|
||||
{name: "thirty days, extend", extend: true, create: true, days: 30, want: now + thirtyDays},
|
||||
{name: "thirty days, extend", extend: true, create: false, days: 30, want: now + thirtyDays},
|
||||
{name: "thirty days, no extend", extend: false, create: true, days: 30, want: createAt + thirtyDays},
|
||||
{name: "thirty days, no extend", extend: false, create: false, days: 30, want: now + thirtyDays},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
th.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.ExtendSessionLengthWithActivity = tt.extend
|
||||
})
|
||||
var create int64
|
||||
if tt.create {
|
||||
create = createAt
|
||||
}
|
||||
|
||||
session := &model.Session{
|
||||
CreateAt: create,
|
||||
ExpiresAt: model.GetMillis() + dayInMillis,
|
||||
}
|
||||
th.service.SetSessionExpireInDays(session, tt.days)
|
||||
|
||||
// must be within 5 seconds of expected time.
|
||||
require.GreaterOrEqual(t, session.ExpiresAt, tt.want-grace)
|
||||
require.LessOrEqual(t, session.ExpiresAt, tt.want+grace)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOAuthRevokeAccessToken(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
err := th.service.RevokeAccessToken(model.NewRandomString(16))
|
||||
require.Error(t, err, "Should have failed due to an incorrect token")
|
||||
|
||||
session := &model.Session{}
|
||||
session.CreateAt = model.GetMillis()
|
||||
session.UserId = model.NewId()
|
||||
session.Token = model.NewId()
|
||||
session.Roles = model.SYSTEM_USER_ROLE_ID
|
||||
th.service.SetSessionExpireInDays(session, 1)
|
||||
|
||||
session, _ = th.service.CreateSession(session)
|
||||
err = th.service.RevokeAccessToken(session.Token)
|
||||
require.Error(t, err, "Should have failed does not have an access token")
|
||||
|
||||
accessData := &model.AccessData{}
|
||||
accessData.Token = session.Token
|
||||
accessData.UserId = session.UserId
|
||||
accessData.RedirectUri = "http://example.com"
|
||||
accessData.ClientId = model.NewId()
|
||||
accessData.ExpiresAt = session.ExpiresAt
|
||||
|
||||
_, nErr := th.service.oAuthStore.SaveAccessData(accessData)
|
||||
require.NoError(t, nErr)
|
||||
|
||||
err = th.service.RevokeAccessToken(accessData.Token)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -5,79 +5,21 @@ package users
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/services/cache"
|
||||
"github.com/mattermost/mattermost-server/v5/shared/i18n"
|
||||
"github.com/mattermost/mattermost-server/v5/shared/mfa"
|
||||
"github.com/mattermost/mattermost-server/v5/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v5/store"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
store store.UserStore
|
||||
sessionStore store.SessionStore
|
||||
sessionCache cache.Cache
|
||||
sessionPool sync.Pool
|
||||
metrics einterfaces.MetricsInterface
|
||||
cluster einterfaces.ClusterInterface
|
||||
config func() *model.Config
|
||||
}
|
||||
|
||||
type UserCreateOptions struct {
|
||||
Guest bool
|
||||
FromImport bool
|
||||
}
|
||||
|
||||
// ServiceInitializer is used to initialize the UserService.
|
||||
type ServiceInitializer struct {
|
||||
// Mandatory fields
|
||||
UserStore store.UserStore
|
||||
SessionStore store.SessionStore
|
||||
ConfigFn func() *model.Config
|
||||
// Optional fields
|
||||
Metrics einterfaces.MetricsInterface
|
||||
Cluster einterfaces.ClusterInterface
|
||||
}
|
||||
|
||||
func New(initializer ServiceInitializer) (*UserService, error) {
|
||||
cacheProvider := cache.NewProvider()
|
||||
if err := cacheProvider.Connect(); err != nil {
|
||||
return nil, fmt.Errorf("could not create cache provider: %w", err)
|
||||
}
|
||||
|
||||
sessionCache, err := cacheProvider.NewCache(&cache.CacheOptions{
|
||||
Size: model.SESSION_CACHE_SIZE,
|
||||
Striped: true,
|
||||
StripedBuckets: maxInt(runtime.NumCPU()-1, 1),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create session cache: %w", err)
|
||||
}
|
||||
|
||||
if initializer.ConfigFn == nil || initializer.UserStore == nil || initializer.SessionStore == nil {
|
||||
return nil, errors.New("required parameters are not provided")
|
||||
}
|
||||
|
||||
return &UserService{
|
||||
store: initializer.UserStore,
|
||||
sessionStore: initializer.SessionStore,
|
||||
config: initializer.ConfigFn,
|
||||
metrics: initializer.Metrics,
|
||||
cluster: initializer.Cluster,
|
||||
sessionCache: sessionCache,
|
||||
sessionPool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
return &model.Session{}
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateUser creates a user
|
||||
func (us *UserService) CreateUser(user *model.User, opts UserCreateOptions) (*model.User, error) {
|
||||
if opts.FromImport {
|
||||
@@ -167,6 +109,10 @@ func (us *UserService) GetUsers(options *model.UserGetOptions) ([]*model.User, e
|
||||
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.GetUsers(options)
|
||||
if err != nil {
|
||||
@@ -243,6 +189,25 @@ func (us *UserService) GetUsersWithoutTeam(options *model.UserGetOptions) ([]*mo
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (us *UserService) UpdateUser(user *model.User, allowRoleUpdate bool) (*model.UserUpdate, error) {
|
||||
return us.store.Update(user, allowRoleUpdate)
|
||||
}
|
||||
|
||||
func (us *UserService) DeactivateAllGuests() ([]string, error) {
|
||||
users, err := us.store.DeactivateGuests()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, userID := range users {
|
||||
if err := us.RevokeAllSessions(userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (us *UserService) InvalidateCacheForUser(userID string) {
|
||||
us.store.InvalidateProfilesInChannelCacheByUser(userID)
|
||||
us.store.InvalidateProfileCacheForUser(userID)
|
||||
@@ -256,3 +221,24 @@ func (us *UserService) InvalidateCacheForUser(userID string) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ func GetMockStoreForSetupFunctions() *mocks.Store {
|
||||
roleStore.On("GetAll").Return([]*model.Role{}, nil)
|
||||
|
||||
sessionStore := mocks.SessionStore{}
|
||||
oAuthStore := mocks.OAuthStore{}
|
||||
|
||||
mockStore.On("System").Return(&systemStore)
|
||||
mockStore.On("User").Return(&userStore)
|
||||
@@ -101,5 +102,6 @@ func GetMockStoreForSetupFunctions() *mocks.Store {
|
||||
mockStore.On("DropAllTables").Return(nil)
|
||||
mockStore.On("MarkSystemRanUnitTests").Return(nil)
|
||||
mockStore.On("Session").Return(&sessionStore)
|
||||
mockStore.On("OAuth").Return(&oAuthStore)
|
||||
return &mockStore
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user