mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
[MM-37984] Allow Desktop App to authenticate via external providers outside of the app on supported servers (#24140)
* [MM-37984] Allow Desktop App to authenticate via external providers outside of the app on supported servers * PR feedback * Add support for mattermost-dev protocol for development use * Update server/channels/db/migrations/postgres/000110_create_desktop_tokens.up.sql * Fix silly typo * Update server/channels/db/migrations/postgres/000110_create_desktop_tokens.up.sql * Remove storage of client token, only validate it on the client * Update migrations * Add concurrently create index * Remove CONCURRENTLY for now * Fix issue with changing history * Remove old migration * Use idempotent statement to drop old index * Remove reference to old table
This commit is contained in:
@@ -9,6 +9,8 @@ import (
|
||||
"github.com/mattermost/gziphandler"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/web"
|
||||
)
|
||||
|
||||
@@ -200,6 +202,17 @@ func (api *API) APILocal(h handlerFunc) http.Handler {
|
||||
return handler
|
||||
}
|
||||
|
||||
func (api *API) RateLimitedHandler(apiHandler http.Handler, settings model.RateLimitSettings) http.Handler {
|
||||
settings.SetDefaults()
|
||||
|
||||
rateLimiter, err := app.NewRateLimiter(&settings, []string{})
|
||||
if err != nil {
|
||||
mlog.Error("getRateLimitedHandler", mlog.Err(err))
|
||||
return nil
|
||||
}
|
||||
return rateLimiter.RateLimitHandler(apiHandler)
|
||||
}
|
||||
|
||||
func requireLicense(c *Context) *model.AppError {
|
||||
if c.App.Channels().License() == nil {
|
||||
err := model.NewAppError("", "api.license_error", nil, "", http.StatusNotImplemented)
|
||||
|
||||
@@ -61,6 +61,7 @@ func (api *API) InitUser() {
|
||||
api.BaseRoutes.User.Handle("/mfa/generate", api.APISessionRequiredMfa(generateMfaSecret)).Methods("POST")
|
||||
|
||||
api.BaseRoutes.Users.Handle("/login", api.APIHandler(login)).Methods("POST")
|
||||
api.BaseRoutes.Users.Handle("/login/desktop_token", api.RateLimitedHandler(api.APIHandler(loginWithDesktopToken), model.RateLimitSettings{PerSec: model.NewInt(2), MaxBurst: model.NewInt(1)})).Methods("POST")
|
||||
api.BaseRoutes.Users.Handle("/login/switch", api.APIHandler(switchAccountType)).Methods("POST")
|
||||
api.BaseRoutes.Users.Handle("/login/cws", api.APIHandlerTrustRequester(loginCWS)).Methods("POST")
|
||||
api.BaseRoutes.Users.Handle("/logout", api.APIHandler(logout)).Methods("POST")
|
||||
@@ -1978,6 +1979,30 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func loginWithDesktopToken(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
props := model.MapFromJSON(r.Body)
|
||||
token := props["token"]
|
||||
deviceId := props["device_id"]
|
||||
|
||||
user, err := c.App.ValidateDesktopToken(token, time.Now().Add(-model.DesktopTokenTTL).Unix())
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
err = c.App.DoLogin(c.AppContext, w, r, user, deviceId, false, false, false)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
c.App.AttachSessionCookies(c.AppContext, w, r)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(user); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func loginCWS(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
campaignToURL := map[string]string{
|
||||
"focalboard": "/boards",
|
||||
|
||||
@@ -577,6 +577,7 @@ type AppIface interface {
|
||||
FilterUsersByVisible(viewer *model.User, otherUsers []*model.User) ([]*model.User, *model.AppError)
|
||||
FindTeamByName(name string) bool
|
||||
FinishSendAdminNotifyPost(trial bool, now int64, pluginBasedData map[string][]*model.NotifyAdminData)
|
||||
GenerateAndSaveDesktopToken(expiryTime int64, user *model.User) (*string, *model.AppError)
|
||||
GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppError)
|
||||
GeneratePresignURLForExport(name string) (*model.PresignURLResponse, *model.AppError)
|
||||
GeneratePublicLink(siteURL string, info *model.FileInfo) string
|
||||
@@ -702,8 +703,8 @@ type AppIface interface {
|
||||
GetOAuthAppsByCreator(userID string, page, perPage int) ([]*model.OAuthApp, *model.AppError)
|
||||
GetOAuthCodeRedirect(userID string, authRequest *model.AuthorizeRequest) (string, *model.AppError)
|
||||
GetOAuthImplicitRedirect(userID string, authRequest *model.AuthorizeRequest) (string, *model.AppError)
|
||||
GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, service, teamID, action, redirectTo, loginHint string, isMobile bool) (string, *model.AppError)
|
||||
GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service, teamID string) (string, *model.AppError)
|
||||
GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, service, teamID, action, redirectTo, loginHint string, isMobile bool, desktopToken string) (string, *model.AppError)
|
||||
GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service, teamID string, desktopToken string) (string, *model.AppError)
|
||||
GetOAuthStateToken(token string) (*model.Token, *model.AppError)
|
||||
GetOnboarding() (*model.System, *model.AppError)
|
||||
GetOpenGraphMetadata(requestURL string) ([]byte, error)
|
||||
@@ -1169,6 +1170,7 @@ type AppIface interface {
|
||||
UserAlreadyNotifiedOnRequiredFeature(user string, feature model.MattermostFeature) bool
|
||||
UserCanSeeOtherUser(userID string, otherUserId string) (bool, *model.AppError)
|
||||
UserIsFirstAdmin(user *model.User) bool
|
||||
ValidateDesktopToken(token string, expiryTime int64) (*model.User, *model.AppError)
|
||||
VerifyEmailFromToken(c request.CTX, userSuppliedTokenString string) *model.AppError
|
||||
VerifyUserEmail(userID, email string) *model.AppError
|
||||
ViewChannel(c request.CTX, view *model.ChannelView, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError)
|
||||
|
||||
48
server/channels/app/desktop_login.go
Normal file
48
server/channels/app/desktop_login.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
func (a *App) GenerateAndSaveDesktopToken(expiryTime int64, user *model.User) (*string, *model.AppError) {
|
||||
token := model.NewRandomString(64)
|
||||
err := a.Srv().Store().DesktopTokens().Insert(token, expiryTime, user.Id)
|
||||
if err != nil {
|
||||
// Delete any other related tokens if there's an error
|
||||
a.Srv().Store().DesktopTokens().DeleteByUserId(user.Id)
|
||||
|
||||
return nil, model.NewAppError("GenerateAndSaveDesktopToken", "app.desktop_token.generateServerToken.invalid_or_expired", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return &token, nil
|
||||
}
|
||||
|
||||
func (a *App) ValidateDesktopToken(token string, expiryTime int64) (*model.User, *model.AppError) {
|
||||
// Check if token is valid
|
||||
userId, err := a.Srv().Store().DesktopTokens().GetUserId(token, expiryTime)
|
||||
if err != nil {
|
||||
// Delete the token if it is expired or invalid
|
||||
a.Srv().Store().DesktopTokens().Delete(token)
|
||||
|
||||
return nil, model.NewAppError("ValidateDesktopToken", "app.desktop_token.validate.invalid", nil, err.Error(), http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
// Get the user profile
|
||||
user, userErr := a.GetUser(*userId)
|
||||
if userErr != nil {
|
||||
// Delete the token if the user is invalid somehow
|
||||
a.Srv().Store().DesktopTokens().Delete(token)
|
||||
|
||||
return nil, model.NewAppError("ValidateDesktopToken", "app.desktop_token.validate.no_user", nil, userErr.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Clean up other tokens if they exist
|
||||
a.Srv().Store().DesktopTokens().DeleteByUserId(*userId)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
75
server/channels/app/desktop_login_test.go
Normal file
75
server/channels/app/desktop_login_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
TTL = time.Minute * 3
|
||||
ExpiredLength = time.Minute * 10
|
||||
)
|
||||
|
||||
func TestGenerateAndSaveDesktopToken(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("generate token", func(t *testing.T) {
|
||||
token, err := th.App.GenerateAndSaveDesktopToken(time.Now().Add(-TTL).Unix(), th.BasicUser)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, token)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateDesktopToken(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
authenticatedServerToken, err := th.App.GenerateAndSaveDesktopToken(time.Now().Add(-TTL).Unix(), th.BasicUser)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, authenticatedServerToken)
|
||||
|
||||
expiredServerToken, err := th.App.GenerateAndSaveDesktopToken(time.Now().Add(-ExpiredLength).Unix(), th.BasicUser2)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, expiredServerToken)
|
||||
|
||||
badUser := model.User{Id: "some_garbage_user_id"}
|
||||
badUserServerToken, err := th.App.GenerateAndSaveDesktopToken(time.Now().Add(-TTL).Unix(), &badUser)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, badUserServerToken)
|
||||
|
||||
t.Run("validate token", func(t *testing.T) {
|
||||
user, err := th.App.ValidateDesktopToken(*authenticatedServerToken, time.Now().Add(-TTL).Unix())
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, user)
|
||||
assert.Equal(t, th.BasicUser.Id, user.Id)
|
||||
})
|
||||
|
||||
t.Run("validate token - expired", func(t *testing.T) {
|
||||
user, err := th.App.ValidateDesktopToken(*expiredServerToken, time.Now().Add(-TTL).Unix())
|
||||
assert.NotNil(t, err)
|
||||
assert.Nil(t, user)
|
||||
assert.Equal(t, "app.desktop_token.validate.invalid", err.Id)
|
||||
})
|
||||
|
||||
t.Run("validate token - not authenticated", func(t *testing.T) {
|
||||
user, err := th.App.ValidateDesktopToken("not_real_token", time.Now().Add(-TTL).Unix())
|
||||
assert.NotNil(t, err)
|
||||
assert.Nil(t, user)
|
||||
assert.Equal(t, "app.desktop_token.validate.invalid", err.Id)
|
||||
})
|
||||
|
||||
t.Run("validate token - bad user id", func(t *testing.T) {
|
||||
user, err := th.App.ValidateDesktopToken(*badUserServerToken, time.Now().Add(-TTL).Unix())
|
||||
assert.NotNil(t, err)
|
||||
assert.Nil(t, user)
|
||||
assert.Equal(t, "app.desktop_token.validate.no_user", err.Id)
|
||||
})
|
||||
}
|
||||
@@ -429,7 +429,7 @@ func (a *App) newSessionUpdateToken(app *model.OAuthApp, accessData *model.Acces
|
||||
return accessRsp, nil
|
||||
}
|
||||
|
||||
func (a *App) GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, service, teamID, action, redirectTo, loginHint string, isMobile bool) (string, *model.AppError) {
|
||||
func (a *App) GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, service, teamID, action, redirectTo, loginHint string, isMobile bool, desktopToken string) (string, *model.AppError) {
|
||||
stateProps := map[string]string{}
|
||||
stateProps["action"] = action
|
||||
if teamID != "" {
|
||||
@@ -440,6 +440,10 @@ func (a *App) GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, serv
|
||||
stateProps["redirect_to"] = redirectTo
|
||||
}
|
||||
|
||||
if desktopToken != "" {
|
||||
stateProps["desktop_token"] = desktopToken
|
||||
}
|
||||
|
||||
stateProps[model.UserAuthServiceIsMobile] = strconv.FormatBool(isMobile)
|
||||
|
||||
authURL, err := a.GetAuthorizationCode(w, r, service, stateProps, loginHint)
|
||||
@@ -450,13 +454,17 @@ func (a *App) GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, serv
|
||||
return authURL, nil
|
||||
}
|
||||
|
||||
func (a *App) GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service, teamID string) (string, *model.AppError) {
|
||||
func (a *App) GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service, teamID string, desktopToken string) (string, *model.AppError) {
|
||||
stateProps := map[string]string{}
|
||||
stateProps["action"] = model.OAuthActionSignup
|
||||
if teamID != "" {
|
||||
stateProps["team_id"] = teamID
|
||||
}
|
||||
|
||||
if desktopToken != "" {
|
||||
stateProps["desktop_token"] = desktopToken
|
||||
}
|
||||
|
||||
authURL, err := a.GetAuthorizationCode(w, r, service, stateProps, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -4539,6 +4539,28 @@ func (a *OpenTracingAppLayer) FinishSendAdminNotifyPost(trial bool, now int64, p
|
||||
a.app.FinishSendAdminNotifyPost(trial, now, pluginBasedData)
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GenerateAndSaveDesktopToken(expiryTime int64, user *model.User) (*string, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GenerateAndSaveDesktopToken")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store().SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store().SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0, resultVar1 := a.app.GenerateAndSaveDesktopToken(expiryTime, user)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GenerateMfaSecret")
|
||||
@@ -7613,7 +7635,7 @@ func (a *OpenTracingAppLayer) GetOAuthImplicitRedirect(userID string, authReques
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, service string, teamID string, action string, redirectTo string, loginHint string, isMobile bool) (string, *model.AppError) {
|
||||
func (a *OpenTracingAppLayer) GetOAuthLoginEndpoint(w http.ResponseWriter, r *http.Request, service string, teamID string, action string, redirectTo string, loginHint string, isMobile bool, desktopToken string) (string, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthLoginEndpoint")
|
||||
|
||||
@@ -7625,7 +7647,7 @@ func (a *OpenTracingAppLayer) GetOAuthLoginEndpoint(w http.ResponseWriter, r *ht
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0, resultVar1 := a.app.GetOAuthLoginEndpoint(w, r, service, teamID, action, redirectTo, loginHint, isMobile)
|
||||
resultVar0, resultVar1 := a.app.GetOAuthLoginEndpoint(w, r, service, teamID, action, redirectTo, loginHint, isMobile, desktopToken)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
@@ -7635,7 +7657,7 @@ func (a *OpenTracingAppLayer) GetOAuthLoginEndpoint(w http.ResponseWriter, r *ht
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service string, teamID string) (string, *model.AppError) {
|
||||
func (a *OpenTracingAppLayer) GetOAuthSignupEndpoint(w http.ResponseWriter, r *http.Request, service string, teamID string, desktopToken string) (string, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOAuthSignupEndpoint")
|
||||
|
||||
@@ -7647,7 +7669,7 @@ func (a *OpenTracingAppLayer) GetOAuthSignupEndpoint(w http.ResponseWriter, r *h
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0, resultVar1 := a.app.GetOAuthSignupEndpoint(w, r, service, teamID)
|
||||
resultVar0, resultVar1 := a.app.GetOAuthSignupEndpoint(w, r, service, teamID, desktopToken)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
@@ -18583,6 +18605,28 @@ func (a *OpenTracingAppLayer) UserIsInAdminRoleGroup(userID string, syncableID s
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) ValidateDesktopToken(token string, expiryTime int64) (*model.User, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ValidateDesktopToken")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store().SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store().SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0, resultVar1 := a.app.ValidateDesktopToken(token, expiryTime)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) ValidateUserPermissionsOnChannels(c request.CTX, userId string, channelIds []string) []string {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ValidateUserPermissionsOnChannels")
|
||||
|
||||
@@ -39,6 +39,7 @@ import (
|
||||
"github.com/mattermost/mattermost/server/v8/channels/audit"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/jobs"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/jobs/active_users"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/jobs/cleanup_desktop_tokens"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/jobs/expirynotify"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/jobs/export_delete"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/jobs/export_process"
|
||||
@@ -1637,6 +1638,12 @@ func (s *Server) initJobs() {
|
||||
hosted_purchase_screening.MakeScheduler(s.Jobs, s.License()),
|
||||
)
|
||||
|
||||
s.Jobs.RegisterJobType(
|
||||
model.JobTypeCleanupDesktopTokens,
|
||||
cleanup_desktop_tokens.MakeWorker(s.Jobs, s.Store()),
|
||||
cleanup_desktop_tokens.MakeScheduler(s.Jobs),
|
||||
)
|
||||
|
||||
s.platform.Jobs = s.Jobs
|
||||
}
|
||||
|
||||
|
||||
@@ -220,6 +220,8 @@ channels/db/migrations/mysql/000109_create_persistent_notifications.down.sql
|
||||
channels/db/migrations/mysql/000109_create_persistent_notifications.up.sql
|
||||
channels/db/migrations/mysql/000111_update_vacuuming.down.sql
|
||||
channels/db/migrations/mysql/000111_update_vacuuming.up.sql
|
||||
channels/db/migrations/mysql/000112_rework_desktop_tokens.down.sql
|
||||
channels/db/migrations/mysql/000112_rework_desktop_tokens.up.sql
|
||||
channels/db/migrations/postgres/000001_create_teams.down.sql
|
||||
channels/db/migrations/postgres/000001_create_teams.up.sql
|
||||
channels/db/migrations/postgres/000002_create_team_members.down.sql
|
||||
@@ -440,3 +442,5 @@ channels/db/migrations/postgres/000109_create_persistent_notifications.down.sql
|
||||
channels/db/migrations/postgres/000109_create_persistent_notifications.up.sql
|
||||
channels/db/migrations/postgres/000111_update_vacuuming.down.sql
|
||||
channels/db/migrations/postgres/000111_update_vacuuming.up.sql
|
||||
channels/db/migrations/postgres/000112_rework_desktop_tokens.down.sql
|
||||
channels/db/migrations/postgres/000112_rework_desktop_tokens.up.sql
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE table_name = 'DesktopTokens'
|
||||
AND table_schema = DATABASE()
|
||||
AND index_name = 'idx_desktoptokens_createat'
|
||||
) > 0,
|
||||
'DROP INDEX idx_desktoptokens_createat ON DesktopTokens;',
|
||||
'SELECT 1'
|
||||
));
|
||||
|
||||
PREPARE removeIndexIfExists FROM @preparedStatement;
|
||||
EXECUTE removeIndexIfExists;
|
||||
DEALLOCATE PREPARE removeIndexIfExists;
|
||||
|
||||
DROP TABLE IF EXISTS DesktopTokens;
|
||||
@@ -0,0 +1,38 @@
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE table_name = 'DesktopTokens'
|
||||
AND table_schema = DATABASE()
|
||||
AND index_name = 'idx_desktoptokens_createat'
|
||||
) > 0,
|
||||
'DROP INDEX idx_desktoptokens_createat ON DesktopTokens;',
|
||||
'SELECT 1'
|
||||
));
|
||||
|
||||
PREPARE removeIndexIfExists FROM @preparedStatement;
|
||||
EXECUTE removeIndexIfExists;
|
||||
DEALLOCATE PREPARE removeIndexIfExists;
|
||||
|
||||
DROP TABLE IF EXISTS DesktopTokens;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS DesktopTokens (
|
||||
Token varchar(64) NOT NULL,
|
||||
CreateAt bigint NOT NULL,
|
||||
UserId varchar(26) NOT NULL,
|
||||
PRIMARY KEY (Token)
|
||||
);
|
||||
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE table_name = 'DesktopTokens'
|
||||
AND table_schema = DATABASE()
|
||||
AND index_name = 'idx_desktoptokens_token_createat'
|
||||
) > 0,
|
||||
'SELECT 1',
|
||||
'CREATE INDEX idx_desktoptokens_token_createat ON DesktopTokens(Token, CreateAt);'
|
||||
));
|
||||
|
||||
PREPARE createIndexIfNotExists FROM @preparedStatement;
|
||||
EXECUTE createIndexIfNotExists;
|
||||
DEALLOCATE PREPARE createIndexIfNotExists;
|
||||
@@ -0,0 +1,2 @@
|
||||
DROP INDEX IF EXISTS idx_desktoptokens_token_createat;
|
||||
DROP TABLE IF EXISTS desktoptokens;
|
||||
@@ -0,0 +1,11 @@
|
||||
DROP INDEX IF EXISTS idx_desktoptokens_createat;
|
||||
DROP TABLE IF EXISTS desktoptokens;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS desktoptokens (
|
||||
token VARCHAR(64) NOT NULL,
|
||||
createat BIGINT NOT NULL,
|
||||
userid VARCHAR(26) NOT NULL,
|
||||
PRIMARY KEY (token)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_desktoptokens_token_createat ON desktoptokens(token, createat)
|
||||
20
server/channels/jobs/cleanup_desktop_tokens/scheduler.go
Normal file
20
server/channels/jobs/cleanup_desktop_tokens/scheduler.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package cleanup_desktop_tokens
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/jobs"
|
||||
)
|
||||
|
||||
const schedFreq = 1 * time.Hour
|
||||
|
||||
func MakeScheduler(jobServer *jobs.JobServer) model.Scheduler {
|
||||
isEnabled := func(cfg *model.Config) bool {
|
||||
return true
|
||||
}
|
||||
return jobs.NewPeriodicScheduler(jobServer, model.JobTypeCleanupDesktopTokens, schedFreq, isEnabled)
|
||||
}
|
||||
36
server/channels/jobs/cleanup_desktop_tokens/worker.go
Normal file
36
server/channels/jobs/cleanup_desktop_tokens/worker.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package cleanup_desktop_tokens
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/jobs"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store"
|
||||
"github.com/mattermost/mattermost/server/v8/platform/services/configservice"
|
||||
)
|
||||
|
||||
const jobName = "CleanupDesktopTokens"
|
||||
const maxAge = 5 * time.Minute
|
||||
|
||||
type AppIface interface {
|
||||
configservice.ConfigService
|
||||
ListDirectory(path string) ([]string, *model.AppError)
|
||||
FileModTime(path string) (time.Time, *model.AppError)
|
||||
RemoveFile(path string) *model.AppError
|
||||
}
|
||||
|
||||
func MakeWorker(jobServer *jobs.JobServer, store store.Store) model.Worker {
|
||||
isEnabled := func(cfg *model.Config) bool {
|
||||
return true
|
||||
}
|
||||
execute := func(job *model.Job) error {
|
||||
defer jobServer.HandleJobPanic(job)
|
||||
|
||||
return store.DesktopTokens().DeleteOlderThan(time.Now().Add(-maxAge).Unix())
|
||||
}
|
||||
worker := jobs.NewSimpleWorker(jobName, jobServer, execute, isEnabled)
|
||||
return worker
|
||||
}
|
||||
@@ -26,6 +26,7 @@ type OpenTracingLayer struct {
|
||||
CommandStore store.CommandStore
|
||||
CommandWebhookStore store.CommandWebhookStore
|
||||
ComplianceStore store.ComplianceStore
|
||||
DesktopTokensStore store.DesktopTokensStore
|
||||
DraftStore store.DraftStore
|
||||
EmojiStore store.EmojiStore
|
||||
FileInfoStore store.FileInfoStore
|
||||
@@ -95,6 +96,10 @@ func (s *OpenTracingLayer) Compliance() store.ComplianceStore {
|
||||
return s.ComplianceStore
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayer) DesktopTokens() store.DesktopTokensStore {
|
||||
return s.DesktopTokensStore
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayer) Draft() store.DraftStore {
|
||||
return s.DraftStore
|
||||
}
|
||||
@@ -275,6 +280,11 @@ type OpenTracingLayerComplianceStore struct {
|
||||
Root *OpenTracingLayer
|
||||
}
|
||||
|
||||
type OpenTracingLayerDesktopTokensStore struct {
|
||||
store.DesktopTokensStore
|
||||
Root *OpenTracingLayer
|
||||
}
|
||||
|
||||
type OpenTracingLayerDraftStore struct {
|
||||
store.DraftStore
|
||||
Root *OpenTracingLayer
|
||||
@@ -3234,6 +3244,96 @@ func (s *OpenTracingLayerComplianceStore) Update(compliance *model.Compliance) (
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerDesktopTokensStore) Delete(token string) error {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DesktopTokensStore.Delete")
|
||||
s.Root.Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
s.Root.Store.SetContext(origCtx)
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
err := s.DesktopTokensStore.Delete(token)
|
||||
if err != nil {
|
||||
span.LogFields(spanlog.Error(err))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerDesktopTokensStore) DeleteByUserId(userId string) error {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DesktopTokensStore.DeleteByUserId")
|
||||
s.Root.Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
s.Root.Store.SetContext(origCtx)
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
err := s.DesktopTokensStore.DeleteByUserId(userId)
|
||||
if err != nil {
|
||||
span.LogFields(spanlog.Error(err))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerDesktopTokensStore) DeleteOlderThan(minCreatedAt int64) error {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DesktopTokensStore.DeleteOlderThan")
|
||||
s.Root.Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
s.Root.Store.SetContext(origCtx)
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
err := s.DesktopTokensStore.DeleteOlderThan(minCreatedAt)
|
||||
if err != nil {
|
||||
span.LogFields(spanlog.Error(err))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerDesktopTokensStore) GetUserId(token string, minCreatedAt int64) (*string, error) {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DesktopTokensStore.GetUserId")
|
||||
s.Root.Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
s.Root.Store.SetContext(origCtx)
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
result, err := s.DesktopTokensStore.GetUserId(token, minCreatedAt)
|
||||
if err != nil {
|
||||
span.LogFields(spanlog.Error(err))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerDesktopTokensStore) Insert(token string, createdAt int64, userId string) error {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DesktopTokensStore.Insert")
|
||||
s.Root.Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
s.Root.Store.SetContext(origCtx)
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
err := s.DesktopTokensStore.Insert(token, createdAt, userId)
|
||||
if err != nil {
|
||||
span.LogFields(spanlog.Error(err))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerDraftStore) Delete(userID string, channelID string, rootID string) error {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DraftStore.Delete")
|
||||
@@ -12885,6 +12985,7 @@ func New(childStore store.Store, ctx context.Context) *OpenTracingLayer {
|
||||
newStore.CommandStore = &OpenTracingLayerCommandStore{CommandStore: childStore.Command(), Root: &newStore}
|
||||
newStore.CommandWebhookStore = &OpenTracingLayerCommandWebhookStore{CommandWebhookStore: childStore.CommandWebhook(), Root: &newStore}
|
||||
newStore.ComplianceStore = &OpenTracingLayerComplianceStore{ComplianceStore: childStore.Compliance(), Root: &newStore}
|
||||
newStore.DesktopTokensStore = &OpenTracingLayerDesktopTokensStore{DesktopTokensStore: childStore.DesktopTokens(), Root: &newStore}
|
||||
newStore.DraftStore = &OpenTracingLayerDraftStore{DraftStore: childStore.Draft(), Root: &newStore}
|
||||
newStore.EmojiStore = &OpenTracingLayerEmojiStore{EmojiStore: childStore.Emoji(), Root: &newStore}
|
||||
newStore.FileInfoStore = &OpenTracingLayerFileInfoStore{FileInfoStore: childStore.FileInfo(), Root: &newStore}
|
||||
|
||||
@@ -29,6 +29,7 @@ type RetryLayer struct {
|
||||
CommandStore store.CommandStore
|
||||
CommandWebhookStore store.CommandWebhookStore
|
||||
ComplianceStore store.ComplianceStore
|
||||
DesktopTokensStore store.DesktopTokensStore
|
||||
DraftStore store.DraftStore
|
||||
EmojiStore store.EmojiStore
|
||||
FileInfoStore store.FileInfoStore
|
||||
@@ -98,6 +99,10 @@ func (s *RetryLayer) Compliance() store.ComplianceStore {
|
||||
return s.ComplianceStore
|
||||
}
|
||||
|
||||
func (s *RetryLayer) DesktopTokens() store.DesktopTokensStore {
|
||||
return s.DesktopTokensStore
|
||||
}
|
||||
|
||||
func (s *RetryLayer) Draft() store.DraftStore {
|
||||
return s.DraftStore
|
||||
}
|
||||
@@ -278,6 +283,11 @@ type RetryLayerComplianceStore struct {
|
||||
Root *RetryLayer
|
||||
}
|
||||
|
||||
type RetryLayerDesktopTokensStore struct {
|
||||
store.DesktopTokensStore
|
||||
Root *RetryLayer
|
||||
}
|
||||
|
||||
type RetryLayerDraftStore struct {
|
||||
store.DraftStore
|
||||
Root *RetryLayer
|
||||
@@ -3607,6 +3617,111 @@ func (s *RetryLayerComplianceStore) Update(compliance *model.Compliance) (*model
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerDesktopTokensStore) Delete(token string) error {
|
||||
|
||||
tries := 0
|
||||
for {
|
||||
err := s.DesktopTokensStore.Delete(token)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !isRepeatableError(err) {
|
||||
return err
|
||||
}
|
||||
tries++
|
||||
if tries >= 3 {
|
||||
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
||||
return err
|
||||
}
|
||||
timepkg.Sleep(100 * timepkg.Millisecond)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerDesktopTokensStore) DeleteByUserId(userId string) error {
|
||||
|
||||
tries := 0
|
||||
for {
|
||||
err := s.DesktopTokensStore.DeleteByUserId(userId)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !isRepeatableError(err) {
|
||||
return err
|
||||
}
|
||||
tries++
|
||||
if tries >= 3 {
|
||||
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
||||
return err
|
||||
}
|
||||
timepkg.Sleep(100 * timepkg.Millisecond)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerDesktopTokensStore) DeleteOlderThan(minCreatedAt int64) error {
|
||||
|
||||
tries := 0
|
||||
for {
|
||||
err := s.DesktopTokensStore.DeleteOlderThan(minCreatedAt)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !isRepeatableError(err) {
|
||||
return err
|
||||
}
|
||||
tries++
|
||||
if tries >= 3 {
|
||||
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
||||
return err
|
||||
}
|
||||
timepkg.Sleep(100 * timepkg.Millisecond)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerDesktopTokensStore) GetUserId(token string, minCreatedAt int64) (*string, error) {
|
||||
|
||||
tries := 0
|
||||
for {
|
||||
result, err := s.DesktopTokensStore.GetUserId(token, minCreatedAt)
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
if !isRepeatableError(err) {
|
||||
return result, err
|
||||
}
|
||||
tries++
|
||||
if tries >= 3 {
|
||||
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
||||
return result, err
|
||||
}
|
||||
timepkg.Sleep(100 * timepkg.Millisecond)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerDesktopTokensStore) Insert(token string, createdAt int64, userId string) error {
|
||||
|
||||
tries := 0
|
||||
for {
|
||||
err := s.DesktopTokensStore.Insert(token, createdAt, userId)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !isRepeatableError(err) {
|
||||
return err
|
||||
}
|
||||
tries++
|
||||
if tries >= 3 {
|
||||
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
||||
return err
|
||||
}
|
||||
timepkg.Sleep(100 * timepkg.Millisecond)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerDraftStore) Delete(userID string, channelID string, rootID string) error {
|
||||
|
||||
tries := 0
|
||||
@@ -14684,6 +14799,7 @@ func New(childStore store.Store) *RetryLayer {
|
||||
newStore.CommandStore = &RetryLayerCommandStore{CommandStore: childStore.Command(), Root: &newStore}
|
||||
newStore.CommandWebhookStore = &RetryLayerCommandWebhookStore{CommandWebhookStore: childStore.CommandWebhook(), Root: &newStore}
|
||||
newStore.ComplianceStore = &RetryLayerComplianceStore{ComplianceStore: childStore.Compliance(), Root: &newStore}
|
||||
newStore.DesktopTokensStore = &RetryLayerDesktopTokensStore{DesktopTokensStore: childStore.DesktopTokens(), Root: &newStore}
|
||||
newStore.DraftStore = &RetryLayerDraftStore{DraftStore: childStore.Draft(), Root: &newStore}
|
||||
newStore.EmojiStore = &RetryLayerEmojiStore{EmojiStore: childStore.Emoji(), Root: &newStore}
|
||||
newStore.FileInfoStore = &RetryLayerFileInfoStore{FileInfoStore: childStore.FileInfo(), Root: &newStore}
|
||||
|
||||
@@ -59,6 +59,7 @@ func genStore() *mocks.Store {
|
||||
mock.On("PostAcknowledgement").Return(&mocks.PostAcknowledgementStore{})
|
||||
mock.On("PostPersistentNotification").Return(&mocks.PostPersistentNotificationStore{})
|
||||
mock.On("TrueUpReview").Return(&mocks.TrueUpReviewStore{})
|
||||
mock.On("DesktopTokens").Return(&mocks.DesktopTokensStore{})
|
||||
return mock
|
||||
}
|
||||
|
||||
|
||||
123
server/channels/store/sqlstore/desktop_tokens_store.go
Normal file
123
server/channels/store/sqlstore/desktop_tokens_store.go
Normal file
@@ -0,0 +1,123 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store"
|
||||
"github.com/mattermost/mattermost/server/v8/einterfaces"
|
||||
sq "github.com/mattermost/squirrel"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type SqlDesktopTokensStore struct {
|
||||
*SqlStore
|
||||
metrics einterfaces.MetricsInterface
|
||||
}
|
||||
|
||||
func newSqlDesktopTokensStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterface) store.DesktopTokensStore {
|
||||
return &SqlDesktopTokensStore{
|
||||
SqlStore: sqlStore,
|
||||
metrics: metrics,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SqlDesktopTokensStore) GetUserId(token string, minCreateAt int64) (*string, error) {
|
||||
query := s.getQueryBuilder().
|
||||
Select("UserId").
|
||||
From("DesktopTokens").
|
||||
Where(sq.And{
|
||||
sq.Eq{"Token": token},
|
||||
sq.GtOrEq{"CreateAt": minCreateAt},
|
||||
})
|
||||
|
||||
dt := struct{ UserId string }{}
|
||||
err := s.GetReplicaX().GetBuilder(&dt, query)
|
||||
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, store.NewErrNotFound("DesktopTokens", token)
|
||||
}
|
||||
return nil, errors.Wrapf(err, "No token for %s", token)
|
||||
}
|
||||
|
||||
return &dt.UserId, nil
|
||||
}
|
||||
|
||||
func (s *SqlDesktopTokensStore) Insert(token string, createAt int64, userId string) error {
|
||||
builder := s.getQueryBuilder().
|
||||
Insert("DesktopTokens").
|
||||
Columns("Token", "CreateAt", "UserId").
|
||||
Values(token, createAt, userId)
|
||||
|
||||
query, args, err := builder.ToSql()
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "insert_desktoptokens_tosql")
|
||||
}
|
||||
|
||||
if _, err = s.GetMasterX().Exec(query, args...); err != nil {
|
||||
return errors.Wrap(err, "failed to insert token row")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SqlDesktopTokensStore) Delete(token string) error {
|
||||
builder := s.getQueryBuilder().
|
||||
Delete("DesktopTokens").
|
||||
Where(sq.Eq{
|
||||
"Token": token,
|
||||
})
|
||||
|
||||
query, args, err := builder.ToSql()
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "delete_desktoptokens_tosql")
|
||||
}
|
||||
|
||||
if _, err = s.GetMasterX().Exec(query, args...); err != nil {
|
||||
return errors.Wrap(err, "failed to delete token row")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SqlDesktopTokensStore) DeleteByUserId(userId string) error {
|
||||
builder := s.getQueryBuilder().
|
||||
Delete("DesktopTokens").
|
||||
Where(sq.Eq{
|
||||
"UserId": userId,
|
||||
})
|
||||
|
||||
query, args, err := builder.ToSql()
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "delete_by_userid_desktoptokens_tosql")
|
||||
}
|
||||
|
||||
if _, err = s.GetMasterX().Exec(query, args...); err != nil {
|
||||
return errors.Wrap(err, "failed to delete token row")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SqlDesktopTokensStore) DeleteOlderThan(minCreateAt int64) error {
|
||||
builder := s.getQueryBuilder().
|
||||
Delete("DesktopTokens").
|
||||
Where(sq.Lt{
|
||||
"CreateAt": minCreateAt,
|
||||
})
|
||||
|
||||
query, args, err := builder.ToSql()
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "delete_old_desktoptokens_tosql")
|
||||
}
|
||||
|
||||
if _, err = s.GetMasterX().Exec(query, args...); err != nil {
|
||||
return errors.Wrap(err, "failed to delete token row")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
14
server/channels/store/sqlstore/desktop_tokens_store_test.go
Normal file
14
server/channels/store/sqlstore/desktop_tokens_store_test.go
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
|
||||
)
|
||||
|
||||
func TestDesktopTokensStore(t *testing.T) {
|
||||
StoreTestWithSqlStore(t, storetest.TestDesktopTokensStore)
|
||||
}
|
||||
@@ -108,6 +108,7 @@ type SqlStoreStores struct {
|
||||
postAcknowledgement store.PostAcknowledgementStore
|
||||
postPersistentNotification store.PostPersistentNotificationStore
|
||||
trueUpReview store.TrueUpReviewStore
|
||||
desktopTokens store.DesktopTokensStore
|
||||
}
|
||||
|
||||
type SqlStore struct {
|
||||
@@ -229,6 +230,7 @@ func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) (*Sql
|
||||
store.stores.postAcknowledgement = newSqlPostAcknowledgementStore(store)
|
||||
store.stores.postPersistentNotification = newSqlPostPersistentNotificationStore(store)
|
||||
store.stores.trueUpReview = newSqlTrueUpReviewStore(store)
|
||||
store.stores.desktopTokens = newSqlDesktopTokensStore(store, metrics)
|
||||
|
||||
store.stores.preference.(*SqlPreferenceStore).deleteUnusedFeatures()
|
||||
|
||||
@@ -1080,6 +1082,10 @@ func (ss *SqlStore) TrueUpReview() store.TrueUpReviewStore {
|
||||
return ss.stores.trueUpReview
|
||||
}
|
||||
|
||||
func (ss *SqlStore) DesktopTokens() store.DesktopTokensStore {
|
||||
return ss.stores.desktopTokens
|
||||
}
|
||||
|
||||
func (ss *SqlStore) DropAllTables() {
|
||||
if ss.DriverName() == model.DatabaseDriverPostgres {
|
||||
ss.masterX.Exec(`DO
|
||||
|
||||
@@ -87,6 +87,7 @@ type Store interface {
|
||||
PostAcknowledgement() PostAcknowledgementStore
|
||||
PostPersistentNotification() PostPersistentNotificationStore
|
||||
TrueUpReview() TrueUpReviewStore
|
||||
DesktopTokens() DesktopTokensStore
|
||||
}
|
||||
|
||||
type RetentionPolicyStore interface {
|
||||
@@ -650,6 +651,14 @@ type TokenStore interface {
|
||||
RemoveAllTokensByType(tokenType string) error
|
||||
}
|
||||
|
||||
type DesktopTokensStore interface {
|
||||
GetUserId(token string, minCreatedAt int64) (*string, error)
|
||||
Insert(token string, createdAt int64, userId string) error
|
||||
Delete(token string) error
|
||||
DeleteByUserId(userId string) error
|
||||
DeleteOlderThan(minCreatedAt int64) error
|
||||
}
|
||||
|
||||
type EmojiStore interface {
|
||||
Save(emoji *model.Emoji) (*model.Emoji, error)
|
||||
Get(ctx context.Context, id string, allowFromCache bool) (*model.Emoji, error)
|
||||
|
||||
110
server/channels/store/storetest/desktop_tokens_store.go
Normal file
110
server/channels/store/storetest/desktop_tokens_store.go
Normal file
@@ -0,0 +1,110 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package storetest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDesktopTokensStore(t *testing.T, ss store.Store, s SqlStore) {
|
||||
t.Run("GetUserId", func(t *testing.T) { testGetUserId(t, ss) })
|
||||
t.Run("Insert", func(t *testing.T) { testInsert(t, ss) })
|
||||
t.Run("Delete", func(t *testing.T) { testDeleteToken(t, ss) })
|
||||
t.Run("DeleteByUserId", func(t *testing.T) { testDeleteByUserId(t, ss) })
|
||||
t.Run("DeleteOlderThan", func(t *testing.T) { testDeleteOlderThan(t, ss) })
|
||||
}
|
||||
|
||||
func testGetUserId(t *testing.T, ss store.Store) {
|
||||
err := ss.DesktopTokens().Insert("token_with_id", 1000, "user_id")
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("get user id", func(t *testing.T) {
|
||||
userId, err := ss.DesktopTokens().GetUserId("token_with_id", 1000)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "user_id", *userId)
|
||||
})
|
||||
|
||||
t.Run("get user id - expired", func(t *testing.T) {
|
||||
userId, err := ss.DesktopTokens().GetUserId("token_with_id", 10000)
|
||||
assert.Error(t, err)
|
||||
assert.IsType(t, &store.ErrNotFound{}, err)
|
||||
assert.Nil(t, userId)
|
||||
})
|
||||
}
|
||||
|
||||
func testInsert(t *testing.T, ss store.Store) {
|
||||
t.Run("insert", func(t *testing.T) {
|
||||
err := ss.DesktopTokens().Insert("token", 1000, "user_id")
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("insert - token too long", func(t *testing.T) {
|
||||
err := ss.DesktopTokens().Insert(
|
||||
"this token is way way way WAAAAAAAAAAAAAY WAAAAAAAAAAAAAY WAAAAAAAAAAAAAY TOO LONG",
|
||||
1000,
|
||||
"user_id",
|
||||
)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func testDeleteToken(t *testing.T, ss store.Store) {
|
||||
err := ss.DesktopTokens().Insert("deleteable_token", 3000, "user_id")
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("delete", func(t *testing.T) {
|
||||
userId, err := ss.DesktopTokens().GetUserId("deleteable_token", 3000)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "user_id", *userId)
|
||||
|
||||
err = ss.DesktopTokens().Delete("deleteable_token")
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = ss.DesktopTokens().GetUserId("deleteable_token", 3000)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func testDeleteByUserId(t *testing.T, ss store.Store) {
|
||||
err := ss.DesktopTokens().Insert("deleteable_token_2", 4000, "deleteable_user_id")
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("delete by user id", func(t *testing.T) {
|
||||
userId, err := ss.DesktopTokens().GetUserId("deleteable_token_2", 3000)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "deleteable_user_id", *userId)
|
||||
|
||||
err = ss.DesktopTokens().DeleteByUserId("deleteable_user_id")
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = ss.DesktopTokens().GetUserId("deleteable_token_2", 3000)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func testDeleteOlderThan(t *testing.T, ss store.Store) {
|
||||
err := ss.DesktopTokens().Insert("deleteable_token_old", 1000, "deleteable_user_id")
|
||||
require.NoError(t, err)
|
||||
err = ss.DesktopTokens().Insert("deleteable_token_new", 5000, "deleteable_user_id")
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("delete older than", func(t *testing.T) {
|
||||
_, err := ss.DesktopTokens().GetUserId("deleteable_token_old", 1000)
|
||||
assert.NoError(t, err)
|
||||
_, err = ss.DesktopTokens().GetUserId("deleteable_token_new", 5000)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = ss.DesktopTokens().DeleteOlderThan(2000)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = ss.DesktopTokens().GetUserId("deleteable_token_old", 1000)
|
||||
assert.Error(t, err)
|
||||
_, err = ss.DesktopTokens().GetUserId("deleteable_token_new", 5000)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
109
server/channels/store/storetest/mocks/DesktopTokensStore.go
Normal file
109
server/channels/store/storetest/mocks/DesktopTokensStore.go
Normal file
@@ -0,0 +1,109 @@
|
||||
// Code generated by mockery v2.23.2. DO NOT EDIT.
|
||||
|
||||
// Regenerate this file using `make store-mocks`.
|
||||
|
||||
package mocks
|
||||
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
|
||||
// DesktopTokensStore is an autogenerated mock type for the DesktopTokensStore type
|
||||
type DesktopTokensStore struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: token
|
||||
func (_m *DesktopTokensStore) Delete(token string) error {
|
||||
ret := _m.Called(token)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(token)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteByUserId provides a mock function with given fields: userId
|
||||
func (_m *DesktopTokensStore) DeleteByUserId(userId string) error {
|
||||
ret := _m.Called(userId)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(userId)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteOlderThan provides a mock function with given fields: minCreatedAt
|
||||
func (_m *DesktopTokensStore) DeleteOlderThan(minCreatedAt int64) error {
|
||||
ret := _m.Called(minCreatedAt)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(int64) error); ok {
|
||||
r0 = rf(minCreatedAt)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetUserId provides a mock function with given fields: token, minCreatedAt
|
||||
func (_m *DesktopTokensStore) GetUserId(token string, minCreatedAt int64) (*string, error) {
|
||||
ret := _m.Called(token, minCreatedAt)
|
||||
|
||||
var r0 *string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string, int64) (*string, error)); ok {
|
||||
return rf(token, minCreatedAt)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, int64) *string); ok {
|
||||
r0 = rf(token, minCreatedAt)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*string)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string, int64) error); ok {
|
||||
r1 = rf(token, minCreatedAt)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Insert provides a mock function with given fields: token, createdAt, userId
|
||||
func (_m *DesktopTokensStore) Insert(token string, createdAt int64, userId string) error {
|
||||
ret := _m.Called(token, createdAt, userId)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, int64, string) error); ok {
|
||||
r0 = rf(token, createdAt, userId)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
type mockConstructorTestingTNewDesktopTokensStore interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}
|
||||
|
||||
// NewDesktopTokensStore creates a new instance of DesktopTokensStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewDesktopTokensStore(t mockConstructorTestingTNewDesktopTokensStore) *DesktopTokensStore {
|
||||
mock := &DesktopTokensStore{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -187,6 +187,22 @@ func (_m *Store) Context() context.Context {
|
||||
return r0
|
||||
}
|
||||
|
||||
// DesktopTokens provides a mock function with given fields:
|
||||
func (_m *Store) DesktopTokens() store.DesktopTokensStore {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 store.DesktopTokensStore
|
||||
if rf, ok := ret.Get(0).(func() store.DesktopTokensStore); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(store.DesktopTokensStore)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Draft provides a mock function with given fields:
|
||||
func (_m *Store) Draft() store.DraftStore {
|
||||
ret := _m.Called()
|
||||
|
||||
@@ -61,6 +61,7 @@ type Store struct {
|
||||
PostAcknowledgementStore mocks.PostAcknowledgementStore
|
||||
PostPersistentNotificationStore mocks.PostPersistentNotificationStore
|
||||
TrueUpReviewStore mocks.TrueUpReviewStore
|
||||
DesktopTokensStore mocks.DesktopTokensStore
|
||||
}
|
||||
|
||||
func (s *Store) SetContext(context context.Context) { s.context = context }
|
||||
@@ -103,6 +104,7 @@ func (s *Store) ChannelMemberHistory() store.ChannelMemberHistoryStore {
|
||||
return &s.ChannelMemberHistoryStore
|
||||
}
|
||||
func (s *Store) TrueUpReview() store.TrueUpReviewStore { return &s.TrueUpReviewStore }
|
||||
func (s *Store) DesktopTokens() store.DesktopTokensStore { return &s.DesktopTokensStore }
|
||||
func (s *Store) NotifyAdmin() store.NotifyAdminStore { return &s.NotifyAdminStore }
|
||||
func (s *Store) Group() store.GroupStore { return &s.GroupStore }
|
||||
func (s *Store) LinkMetadata() store.LinkMetadataStore { return &s.LinkMetadataStore }
|
||||
@@ -177,5 +179,6 @@ func (s *Store) AssertExpectations(t mock.TestingT) bool {
|
||||
&s.PostPriorityStore,
|
||||
&s.PostAcknowledgementStore,
|
||||
&s.PostPersistentNotificationStore,
|
||||
&s.DesktopTokensStore,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ type TimerLayer struct {
|
||||
CommandStore store.CommandStore
|
||||
CommandWebhookStore store.CommandWebhookStore
|
||||
ComplianceStore store.ComplianceStore
|
||||
DesktopTokensStore store.DesktopTokensStore
|
||||
DraftStore store.DraftStore
|
||||
EmojiStore store.EmojiStore
|
||||
FileInfoStore store.FileInfoStore
|
||||
@@ -95,6 +96,10 @@ func (s *TimerLayer) Compliance() store.ComplianceStore {
|
||||
return s.ComplianceStore
|
||||
}
|
||||
|
||||
func (s *TimerLayer) DesktopTokens() store.DesktopTokensStore {
|
||||
return s.DesktopTokensStore
|
||||
}
|
||||
|
||||
func (s *TimerLayer) Draft() store.DraftStore {
|
||||
return s.DraftStore
|
||||
}
|
||||
@@ -275,6 +280,11 @@ type TimerLayerComplianceStore struct {
|
||||
Root *TimerLayer
|
||||
}
|
||||
|
||||
type TimerLayerDesktopTokensStore struct {
|
||||
store.DesktopTokensStore
|
||||
Root *TimerLayer
|
||||
}
|
||||
|
||||
type TimerLayerDraftStore struct {
|
||||
store.DraftStore
|
||||
Root *TimerLayer
|
||||
@@ -2968,6 +2978,86 @@ func (s *TimerLayerComplianceStore) Update(compliance *model.Compliance) (*model
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *TimerLayerDesktopTokensStore) Delete(token string) error {
|
||||
start := time.Now()
|
||||
|
||||
err := s.DesktopTokensStore.Delete(token)
|
||||
|
||||
elapsed := float64(time.Since(start)) / float64(time.Second)
|
||||
if s.Root.Metrics != nil {
|
||||
success := "false"
|
||||
if err == nil {
|
||||
success = "true"
|
||||
}
|
||||
s.Root.Metrics.ObserveStoreMethodDuration("DesktopTokensStore.Delete", success, elapsed)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TimerLayerDesktopTokensStore) DeleteByUserId(userId string) error {
|
||||
start := time.Now()
|
||||
|
||||
err := s.DesktopTokensStore.DeleteByUserId(userId)
|
||||
|
||||
elapsed := float64(time.Since(start)) / float64(time.Second)
|
||||
if s.Root.Metrics != nil {
|
||||
success := "false"
|
||||
if err == nil {
|
||||
success = "true"
|
||||
}
|
||||
s.Root.Metrics.ObserveStoreMethodDuration("DesktopTokensStore.DeleteByUserId", success, elapsed)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TimerLayerDesktopTokensStore) DeleteOlderThan(minCreatedAt int64) error {
|
||||
start := time.Now()
|
||||
|
||||
err := s.DesktopTokensStore.DeleteOlderThan(minCreatedAt)
|
||||
|
||||
elapsed := float64(time.Since(start)) / float64(time.Second)
|
||||
if s.Root.Metrics != nil {
|
||||
success := "false"
|
||||
if err == nil {
|
||||
success = "true"
|
||||
}
|
||||
s.Root.Metrics.ObserveStoreMethodDuration("DesktopTokensStore.DeleteOlderThan", success, elapsed)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TimerLayerDesktopTokensStore) GetUserId(token string, minCreatedAt int64) (*string, error) {
|
||||
start := time.Now()
|
||||
|
||||
result, err := s.DesktopTokensStore.GetUserId(token, minCreatedAt)
|
||||
|
||||
elapsed := float64(time.Since(start)) / float64(time.Second)
|
||||
if s.Root.Metrics != nil {
|
||||
success := "false"
|
||||
if err == nil {
|
||||
success = "true"
|
||||
}
|
||||
s.Root.Metrics.ObserveStoreMethodDuration("DesktopTokensStore.GetUserId", success, elapsed)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *TimerLayerDesktopTokensStore) Insert(token string, createdAt int64, userId string) error {
|
||||
start := time.Now()
|
||||
|
||||
err := s.DesktopTokensStore.Insert(token, createdAt, userId)
|
||||
|
||||
elapsed := float64(time.Since(start)) / float64(time.Second)
|
||||
if s.Root.Metrics != nil {
|
||||
success := "false"
|
||||
if err == nil {
|
||||
success = "true"
|
||||
}
|
||||
s.Root.Metrics.ObserveStoreMethodDuration("DesktopTokensStore.Insert", success, elapsed)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TimerLayerDraftStore) Delete(userID string, channelID string, rootID string) error {
|
||||
start := time.Now()
|
||||
|
||||
@@ -11613,6 +11703,7 @@ func New(childStore store.Store, metrics einterfaces.MetricsInterface) *TimerLay
|
||||
newStore.CommandStore = &TimerLayerCommandStore{CommandStore: childStore.Command(), Root: &newStore}
|
||||
newStore.CommandWebhookStore = &TimerLayerCommandWebhookStore{CommandWebhookStore: childStore.CommandWebhook(), Root: &newStore}
|
||||
newStore.ComplianceStore = &TimerLayerComplianceStore{ComplianceStore: childStore.Compliance(), Root: &newStore}
|
||||
newStore.DesktopTokensStore = &TimerLayerDesktopTokensStore{DesktopTokensStore: childStore.DesktopTokens(), Root: &newStore}
|
||||
newStore.DraftStore = &TimerLayerDraftStore{DraftStore: childStore.Draft(), Root: &newStore}
|
||||
newStore.EmojiStore = &TimerLayerEmojiStore{EmojiStore: childStore.Emoji(), Root: &newStore}
|
||||
newStore.FileInfoStore = &TimerLayerFileInfoStore{FileInfoStore: childStore.FileInfo(), Root: &newStore}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/i18n"
|
||||
@@ -344,6 +345,34 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
} else { // For web
|
||||
c.App.AttachSessionCookies(c.AppContext, w, r)
|
||||
}
|
||||
|
||||
desktopToken := ""
|
||||
if val, ok := props["desktop_token"]; ok {
|
||||
desktopToken = val
|
||||
}
|
||||
|
||||
if desktopToken != "" {
|
||||
serverToken, serverTokenErr := c.App.GenerateAndSaveDesktopToken(time.Now().Unix(), user)
|
||||
if serverTokenErr != nil {
|
||||
serverTokenErr.Translate(c.AppContext.T)
|
||||
c.LogErrorByCode(serverTokenErr)
|
||||
renderError(serverTokenErr)
|
||||
return
|
||||
}
|
||||
|
||||
queryString := map[string]string{
|
||||
"client_token": desktopToken,
|
||||
"server_token": *serverToken,
|
||||
}
|
||||
if val, ok := props["redirect_to"]; ok {
|
||||
queryString["redirect_to"] = val
|
||||
}
|
||||
if strings.HasPrefix(desktopToken, "dev-") {
|
||||
queryString["isDesktopDev"] = "true"
|
||||
}
|
||||
|
||||
redirectURL = utils.AppendQueryParamsToURL(c.GetSiteURLHeader()+"/login/desktop", queryString)
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
@@ -358,6 +387,7 @@ func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
loginHint := r.URL.Query().Get("login_hint")
|
||||
redirectURL := r.URL.Query().Get("redirect_to")
|
||||
desktopToken := r.URL.Query().Get("desktop_token")
|
||||
|
||||
if redirectURL != "" && !utils.IsValidWebAuthRedirectURL(c.App.Config(), redirectURL) {
|
||||
c.Err = model.NewAppError("loginWithOAuth", "api.invalid_redirect_url", nil, "", http.StatusBadRequest)
|
||||
@@ -370,7 +400,7 @@ func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
authURL, err := c.App.GetOAuthLoginEndpoint(w, r, c.Params.Service, teamId, model.OAuthActionLogin, redirectURL, loginHint, false)
|
||||
authURL, err := c.App.GetOAuthLoginEndpoint(w, r, c.Params.Service, teamId, model.OAuthActionLogin, redirectURL, loginHint, false, desktopToken)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
@@ -399,7 +429,7 @@ func mobileLoginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
authURL, err := c.App.GetOAuthLoginEndpoint(w, r, c.Params.Service, teamId, model.OAuthActionMobile, redirectURL, "", true)
|
||||
authURL, err := c.App.GetOAuthLoginEndpoint(w, r, c.Params.Service, teamId, model.OAuthActionMobile, redirectURL, "", true, "")
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
@@ -427,7 +457,9 @@ func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
authURL, err := c.App.GetOAuthSignupEndpoint(w, r, c.Params.Service, teamId)
|
||||
desktopToken := r.URL.Query().Get("desktop_token")
|
||||
|
||||
authURL, err := c.App.GetOAuthSignupEndpoint(w, r, c.Params.Service, teamId, desktopToken)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
@@ -59,6 +60,11 @@ func loginWithSaml(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
relayProps["redirect_to"] = redirectURL
|
||||
}
|
||||
|
||||
desktopToken := r.URL.Query().Get("desktop_token")
|
||||
if desktopToken != "" {
|
||||
relayProps["desktop_token"] = desktopToken
|
||||
}
|
||||
|
||||
relayProps[model.UserAuthServiceIsMobile] = strconv.FormatBool(isMobile)
|
||||
|
||||
if len(relayProps) > 0 {
|
||||
@@ -185,6 +191,30 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
c.App.AttachSessionCookies(c.AppContext, w, r)
|
||||
|
||||
desktopToken := relayProps["desktop_token"]
|
||||
if desktopToken != "" {
|
||||
serverToken, serverTokenErr := c.App.GenerateAndSaveDesktopToken(time.Now().Unix(), user)
|
||||
if serverTokenErr != nil {
|
||||
handleError(serverTokenErr)
|
||||
return
|
||||
}
|
||||
|
||||
queryString := map[string]string{
|
||||
"client_token": desktopToken,
|
||||
"server_token": *serverToken,
|
||||
}
|
||||
if val, ok := relayProps["redirect_to"]; ok {
|
||||
queryString["redirect_to"] = val
|
||||
}
|
||||
if strings.HasPrefix(desktopToken, "dev-") {
|
||||
queryString["isDesktopDev"] = "true"
|
||||
}
|
||||
|
||||
redirectURL = utils.AppendQueryParamsToURL(c.GetSiteURLHeader()+"/login/desktop", queryString)
|
||||
http.Redirect(w, r, redirectURL, http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
if hasRedirectURL {
|
||||
if isMobile {
|
||||
// Mobile clients with redirect url support
|
||||
|
||||
@@ -5079,6 +5079,18 @@
|
||||
"id": "app.custom_group.unique_name",
|
||||
"translation": "group name is not unique"
|
||||
},
|
||||
{
|
||||
"id": "app.desktop_token.generateServerToken.invalid_or_expired",
|
||||
"translation": "Token does not exist or is expired"
|
||||
},
|
||||
{
|
||||
"id": "app.desktop_token.validate.invalid",
|
||||
"translation": "Token is not valid or is expired"
|
||||
},
|
||||
{
|
||||
"id": "app.desktop_token.validate.no_user",
|
||||
"translation": "Cannot find a user for this token"
|
||||
},
|
||||
{
|
||||
"id": "app.draft.delete.app_error",
|
||||
"translation": "Unable to delete the Draft."
|
||||
|
||||
@@ -36,6 +36,7 @@ const (
|
||||
JobTypeInstallPluginNotifyAdmin = "install_plugin_notify_admin"
|
||||
JobTypeHostedPurchaseScreening = "hosted_purchase_screening"
|
||||
JobTypeS3PathMigration = "s3_path_migration"
|
||||
JobTypeCleanupDesktopTokens = "cleanup_desktop_tokens"
|
||||
|
||||
JobStatusPending = "pending"
|
||||
JobStatusInProgress = "in_progress"
|
||||
@@ -66,6 +67,7 @@ var AllJobTypes = [...]string{
|
||||
JobTypeExtractContent,
|
||||
JobTypeLastAccessiblePost,
|
||||
JobTypeLastAccessibleFile,
|
||||
JobTypeCleanupDesktopTokens,
|
||||
}
|
||||
|
||||
type Job struct {
|
||||
|
||||
@@ -62,6 +62,8 @@ const (
|
||||
UserLocaleMaxLength = 5
|
||||
UserTimezoneMaxRunes = 256
|
||||
UserRolesMaxLength = 256
|
||||
|
||||
DesktopTokenTTL = time.Minute * 3
|
||||
)
|
||||
|
||||
//msgp:tuple User
|
||||
|
||||
Reference in New Issue
Block a user