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
Этот коммит содержится в:
Ibrahim Serdar Acikgoz
2021-06-18 19:25:03 +03:00
коммит произвёл GitHub
родитель 79d4e9e9a9
Коммит f69b28610a
23 изменённых файлов: 681 добавлений и 437 удалений

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

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

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

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

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

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

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

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

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