[MM-37984] Allow Desktop App to authenticate via external providers outside of the app on supported servers (#23795)

* WIP

* Add rate limiting for desktop token API

* Missing mocks

* Style fixes

* Update snapshots

* Maybe use an actual redirect link :P

* Refactoring for tests

* Add tests for server

* Fix lint issue

* Fix tests

* Fix lint

* Add front-end screen component

* Component logic

* Style changes

* Quick style fix

* Lint fixes

* Initial PR feedback

* Enable logging into the browser as well when completing the login process

* Refactor to push more logic to the other component

* Remove unnecessary helper code

* Fix i18n

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Devin Binnie
2023-07-12 09:25:05 -04:00
коммит произвёл GitHub
родитель 5e3c03a0a8
Коммит abdf4e58c3
41 изменённых файлов: 1847 добавлений и 24 удалений

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

@@ -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")
@@ -1966,6 +1967,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",

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

@@ -433,6 +433,7 @@ type AppIface interface {
AttachCloudSessionCookie(c *request.Context, w http.ResponseWriter, r *http.Request)
AttachDeviceId(sessionID string, deviceID string, expiresAt int64) *model.AppError
AttachSessionCookies(c *request.Context, w http.ResponseWriter, r *http.Request)
AuthenticateDesktopToken(token string, expiryTime int64, user *model.User) *model.AppError
AuthenticateUserForLogin(c *request.Context, id, loginId, password, mfaToken, cwsToken string, ldapOnly bool) (user *model.User, err *model.AppError)
AuthorizeOAuthUser(w http.ResponseWriter, r *http.Request, service, code, state, redirectURI string) (io.ReadCloser, string, map[string]string, *model.User, *model.AppError)
AutocompleteChannels(c request.CTX, userID, term string) (model.ChannelListWithTeamData, *model.AppError)
@@ -485,6 +486,7 @@ type AppIface interface {
CreateChannelWithUser(c request.CTX, channel *model.Channel, userID string) (*model.Channel, *model.AppError)
CreateCommand(cmd *model.Command) (*model.Command, *model.AppError)
CreateCommandWebhook(commandID string, args *model.CommandArgs) (*model.CommandWebhook, *model.AppError)
CreateDesktopToken(token string, createdAt int64) *model.AppError
CreateEmoji(c request.CTX, sessionUserId string, emoji *model.Emoji, multiPartImageData *multipart.Form) (*model.Emoji, *model.AppError)
CreateGroup(group *model.Group) (*model.Group, *model.AppError)
CreateGroupChannel(c request.CTX, userIDs []string, creatorId string) (*model.Channel, *model.AppError)
@@ -701,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)
@@ -1176,6 +1178,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)

78
server/channels/app/desktop_login.go Обычный файл
Просмотреть файл

@@ -0,0 +1,78 @@
// 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) CreateDesktopToken(token string, createdAt int64) *model.AppError {
// Check if the token already exists in the database somehow
// If so return an error
_, getErr := a.Srv().Store().DesktopTokens().GetUserId(token, 0)
if getErr == nil {
return model.NewAppError("CreateDesktopToken", "app.desktop_token.create.collision", nil, "", http.StatusBadRequest)
}
// Create token in the database
err := a.Srv().Store().DesktopTokens().Insert(token, createdAt, nil)
if err != nil {
return model.NewAppError("CreateDesktopToken", "app.desktop_token.create.error", nil, err.Error(), http.StatusInternalServerError)
}
return nil
}
func (a *App) AuthenticateDesktopToken(token string, expiryTime int64, user *model.User) *model.AppError {
// Throw an error if the token is expired
err := a.Srv().Store().DesktopTokens().SetUserId(token, expiryTime, user.Id)
if err != nil {
// Delete the token if it is expired
a.Srv().Go(func() {
a.Srv().Store().DesktopTokens().Delete(token)
})
return model.NewAppError("AuthenticateDesktopToken", "app.desktop_token.authenticate.invalid_or_expired", nil, err.Error(), http.StatusBadRequest)
}
return nil
}
func (a *App) ValidateDesktopToken(token string, expiryTime int64) (*model.User, *model.AppError) {
// Check if token is expired
userId, err := a.Srv().Store().DesktopTokens().GetUserId(token, expiryTime)
if err != nil {
// Delete the token if it is expired
a.Srv().Go(func() {
a.Srv().Store().DesktopTokens().Delete(token)
})
return nil, model.NewAppError("ValidateDesktopToken", "app.desktop_token.validate.expired", nil, err.Error(), http.StatusUnauthorized)
}
// If there's no user id, it's not authenticated yet
if userId == "" {
return nil, model.NewAppError("ValidateDesktopToken", "app.desktop_token.validate.invalid", nil, "", 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().Go(func() {
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().Go(func() {
a.Srv().Store().DesktopTokens().DeleteByUserId(userId)
})
return user, nil
}

124
server/channels/app/desktop_login_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,124 @@
// 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 TestCreateDesktopToken(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
existingErr := th.App.CreateDesktopToken("existing_token", time.Now().Unix())
require.Nil(t, existingErr)
t.Run("create token", func(t *testing.T) {
err := th.App.CreateDesktopToken("new_token", time.Now().Unix())
assert.Nil(t, err)
user, err := th.App.ValidateDesktopToken("new_token", time.Now().Add(-TTL).Unix())
assert.Nil(t, user)
assert.NotNil(t, err)
assert.Equal(t, "app.desktop_token.validate.invalid", err.Id)
})
t.Run("create token - already exists", func(t *testing.T) {
err := th.App.CreateDesktopToken("existing_token", time.Now().Unix())
assert.NotNil(t, err)
assert.Equal(t, "app.desktop_token.create.collision", err.Id)
})
}
func TestAuthenticateDesktopToken(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
err := th.App.CreateDesktopToken("unauthenticated_token", time.Now().Unix())
require.Nil(t, err)
err = th.App.CreateDesktopToken("expired_token", time.Now().Add(-ExpiredLength).Unix())
require.Nil(t, err)
t.Run("authenticate token", func(t *testing.T) {
err := th.App.AuthenticateDesktopToken("unauthenticated_token", time.Now().Add(-TTL).Unix(), th.BasicUser)
assert.Nil(t, err)
user, err := th.App.ValidateDesktopToken("unauthenticated_token", time.Now().Add(-TTL).Unix())
assert.Nil(t, err)
assert.NotNil(t, user)
assert.Equal(t, th.BasicUser.Id, user.Id)
})
t.Run("authenticate token - expired", func(t *testing.T) {
err := th.App.AuthenticateDesktopToken("expired_token", time.Now().Add(-TTL).Unix(), th.BasicUser)
assert.NotNil(t, err)
assert.Equal(t, "app.desktop_token.authenticate.invalid_or_expired", err.Id)
_, err = th.App.ValidateDesktopToken("expired_token", time.Now().Add(-TTL).Unix())
assert.NotNil(t, err)
})
}
func TestValidateDesktopToken(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
err := th.App.CreateDesktopToken("authenticated_token", time.Now().Unix())
require.Nil(t, err)
err = th.App.AuthenticateDesktopToken("authenticated_token", time.Now().Add(-TTL).Unix(), th.BasicUser)
require.Nil(t, err)
err = th.App.CreateDesktopToken("expired_token_2", time.Now().Add(-ExpiredLength).Unix())
require.Nil(t, err)
err = th.App.AuthenticateDesktopToken("expired_token_2", time.Now().Add(-ExpiredLength).Unix(), th.BasicUser)
require.Nil(t, err)
err = th.App.CreateDesktopToken("unauthenticated_token_2", time.Now().Unix())
require.Nil(t, err)
badUser := model.User{Id: "some_garbage_user_id"}
err = th.App.CreateDesktopToken("authenticated_token_bad_user", time.Now().Unix())
require.Nil(t, err)
err = th.App.AuthenticateDesktopToken("authenticated_token_bad_user", time.Now().Add(-TTL).Unix(), &badUser)
require.Nil(t, err)
t.Run("validate token", func(t *testing.T) {
user, err := th.App.ValidateDesktopToken("authenticated_token", 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("expired_token_2", time.Now().Add(-TTL).Unix())
assert.NotNil(t, err)
assert.Nil(t, user)
assert.Equal(t, "app.desktop_token.validate.expired", err.Id)
})
t.Run("validate token - not authenticated", func(t *testing.T) {
user, err := th.App.ValidateDesktopToken("unauthenticated_token_2", 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("authenticated_token_bad_user", time.Now().Add(-TTL).Unix())
assert.NotNil(t, err)
assert.Nil(t, user)
assert.Equal(t, "app.desktop_token.validate.no_user", err.Id)
})
}

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

@@ -431,7 +431,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 != "" {
@@ -442,6 +442,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)
@@ -452,13 +456,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

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

@@ -771,6 +771,28 @@ func (a *OpenTracingAppLayer) AttachSessionCookies(c *request.Context, w http.Re
a.app.AttachSessionCookies(c, w, r)
}
func (a *OpenTracingAppLayer) AuthenticateDesktopToken(token string, expiryTime int64, user *model.User) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AuthenticateDesktopToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.AuthenticateDesktopToken(token, expiryTime, user)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) AuthenticateUserForLogin(c *request.Context, id string, loginId string, password string, mfaToken string, cwsToken string, ldapOnly bool) (user *model.User, err *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AuthenticateUserForLogin")
@@ -2042,6 +2064,28 @@ func (a *OpenTracingAppLayer) CreateDefaultMemberships(c *request.Context, param
return resultVar0
}
func (a *OpenTracingAppLayer) CreateDesktopToken(token string, createdAt int64) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateDesktopToken")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.CreateDesktopToken(token, createdAt)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) CreateEmoji(c request.CTX, sessionUserId string, emoji *model.Emoji, multiPartImageData *multipart.Form) (*model.Emoji, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateEmoji")
@@ -7508,7 +7552,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")
@@ -7520,7 +7564,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))
@@ -7530,7 +7574,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")
@@ -7542,7 +7586,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))
@@ -18676,6 +18720,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"
@@ -1635,6 +1636,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
}

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

@@ -218,6 +218,8 @@ channels/db/migrations/mysql/000108_remove_orphaned_oauth_preferences.down.sql
channels/db/migrations/mysql/000108_remove_orphaned_oauth_preferences.up.sql
channels/db/migrations/mysql/000109_create_persistent_notifications.down.sql
channels/db/migrations/mysql/000109_create_persistent_notifications.up.sql
channels/db/migrations/mysql/000110_create_desktop_tokens.down.sql
channels/db/migrations/mysql/000110_create_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
@@ -436,3 +438,5 @@ channels/db/migrations/postgres/000108_remove_orphaned_oauth_preferences.down.sq
channels/db/migrations/postgres/000108_remove_orphaned_oauth_preferences.up.sql
channels/db/migrations/postgres/000109_create_persistent_notifications.down.sql
channels/db/migrations/postgres/000109_create_persistent_notifications.up.sql
channels/db/migrations/postgres/000110_create_desktop_tokens.down.sql
channels/db/migrations/postgres/000110_create_desktop_tokens.up.sql

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

@@ -0,0 +1 @@
DROP TABLE IF EXISTS DesktopTokens;

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

@@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS DesktopTokens (
DesktopToken varchar(64) NOT NULL,
UserId varchar(26) NULL,
CreateAt bigint NOT NULL,
PRIMARY KEY (DesktopToken)
);
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,
'SELECT 1',
'CREATE INDEX idx_desktoptokens_createat ON DesktopTokens(CreateAt);'
));
PREPARE createIndexIfNotExists FROM @preparedStatement;
EXECUTE createIndexIfNotExists;
DEALLOCATE PREPARE createIndexIfNotExists;

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

@@ -0,0 +1 @@
DROP TABLE IF EXISTS desktoptokens;

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

@@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS desktoptokens (
desktoptoken VARCHAR(64) NOT NULL,
userid VARCHAR(26),
createat BIGINT NOT NULL,
PRIMARY KEY (desktoptoken)
);
CREATE INDEX IF NOT EXISTS idx_desktoptokens_createat ON desktoptokens(createat);

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

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

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

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

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

@@ -27,6 +27,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
@@ -96,6 +97,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
}
@@ -276,6 +281,11 @@ type OpenTracingLayerComplianceStore struct {
Root *OpenTracingLayer
}
type OpenTracingLayerDesktopTokensStore struct {
store.DesktopTokensStore
Root *OpenTracingLayer
}
type OpenTracingLayerDraftStore struct {
store.DraftStore
Root *OpenTracingLayer
@@ -3271,6 +3281,114 @@ func (s *OpenTracingLayerComplianceStore) Update(compliance *model.Compliance) (
return result, err
}
func (s *OpenTracingLayerDesktopTokensStore) Delete(desktopToken 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(desktopToken)
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(desktopToken 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(desktopToken, minCreatedAt)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerDesktopTokensStore) Insert(desktopToken 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(desktopToken, createdAt, userId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerDesktopTokensStore) SetUserId(desktopToken string, minCreatedAt int64, userId string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "DesktopTokensStore.SetUserId")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.DesktopTokensStore.SetUserId(desktopToken, minCreatedAt, 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")
@@ -13030,6 +13148,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}

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

@@ -30,6 +30,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
@@ -99,6 +100,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
}
@@ -279,6 +284,11 @@ type RetryLayerComplianceStore struct {
Root *RetryLayer
}
type RetryLayerDesktopTokensStore struct {
store.DesktopTokensStore
Root *RetryLayer
}
type RetryLayerDraftStore struct {
store.DraftStore
Root *RetryLayer
@@ -3650,6 +3660,132 @@ func (s *RetryLayerComplianceStore) Update(compliance *model.Compliance) (*model
}
func (s *RetryLayerDesktopTokensStore) Delete(desktopToken string) error {
tries := 0
for {
err := s.DesktopTokensStore.Delete(desktopToken)
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(desktopToken string, minCreatedAt int64) (string, error) {
tries := 0
for {
result, err := s.DesktopTokensStore.GetUserId(desktopToken, 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(desktopToken string, createdAt int64, userId *string) error {
tries := 0
for {
err := s.DesktopTokensStore.Insert(desktopToken, 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 *RetryLayerDesktopTokensStore) SetUserId(desktopToken string, minCreatedAt int64, userId string) error {
tries := 0
for {
err := s.DesktopTokensStore.SetUserId(desktopToken, minCreatedAt, 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
@@ -14853,6 +14989,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
}

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

@@ -0,0 +1,163 @@
// 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(desktopToken string, minCreateAt int64) (string, error) {
query := s.getQueryBuilder().
Select("UserId").
From("DesktopTokens").
Where(sq.Eq{
"DesktopToken": desktopToken,
}).
Where(sq.GtOrEq{
"CreateAt": minCreateAt,
})
dt := struct{ UserId sql.NullString }{}
err := s.GetReplicaX().GetBuilder(&dt, query)
if err != nil {
if err == sql.ErrNoRows {
return "", store.NewErrNotFound("DesktopTokens", desktopToken)
}
return "", errors.Wrapf(err, "No token for %s", desktopToken)
}
// Check if the string is NULL, if so just return a blank string
if dt.UserId.Valid {
return dt.UserId.String, nil
}
return "", nil
}
func (s *SqlDesktopTokensStore) Insert(desktopToken string, createAt int64, userId *string) error {
builder := s.getQueryBuilder().
Insert("DesktopTokens").
Columns("DesktopToken", "CreateAt", "UserId").
Values(desktopToken, 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) SetUserId(desktopToken string, minCreateAt int64, userId string) error {
builder := s.getQueryBuilder().
Update("DesktopTokens").
Set("UserId", userId).
Where(sq.Eq{
"DesktopToken": desktopToken,
}).
Where(sq.GtOrEq{
"CreateAt": minCreateAt,
})
query, args, err := builder.ToSql()
if err != nil {
return errors.Wrap(err, "set_userid_desktoptokens_tosql")
}
result, err := s.GetMasterX().Exec(query, args...)
if err != nil {
return errors.Wrap(err, "failed to update token row")
}
num, err := result.RowsAffected()
if err != nil {
return errors.Wrap(err, "nothing updated")
}
if num == 0 {
return errors.New("no rows updated")
}
return nil
}
func (s *SqlDesktopTokensStore) Delete(desktopToken string) error {
builder := s.getQueryBuilder().
Delete("DesktopTokens").
Where(sq.Eq{
"DesktopToken": desktopToken,
})
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
}

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

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

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

@@ -105,6 +105,7 @@ type SqlStoreStores struct {
postAcknowledgement store.PostAcknowledgementStore
postPersistentNotification store.PostPersistentNotificationStore
trueUpReview store.TrueUpReviewStore
desktopTokens store.DesktopTokensStore
}
type SqlStore struct {
@@ -226,6 +227,7 @@ func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) *SqlS
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()
@@ -1077,6 +1079,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() {
var tableSchemaFn string
if ss.DriverName() == model.DatabaseDriverPostgres {

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

@@ -86,6 +86,7 @@ type Store interface {
PostAcknowledgement() PostAcknowledgementStore
PostPersistentNotification() PostPersistentNotificationStore
TrueUpReview() TrueUpReviewStore
DesktopTokens() DesktopTokensStore
}
type RetentionPolicyStore interface {
@@ -665,6 +666,15 @@ type TokenStore interface {
RemoveAllTokensByType(tokenType string) error
}
type DesktopTokensStore interface {
GetUserId(desktopToken string, minCreatedAt int64) (string, error)
Insert(desktopToken string, createdAt int64, userId *string) error
SetUserId(desktopToken string, minCreatedAt int64, userId string) error
Delete(desktopToken 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)

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

@@ -0,0 +1,173 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"github.com/mattermost/mattermost/server/public/model"
"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("SetUserId", func(t *testing.T) { testSetUserId(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, nil)
require.NoError(t, err)
err = ss.DesktopTokens().SetUserId("token_with_id", 1000, "user_id")
require.NoError(t, err)
err = ss.DesktopTokens().Insert("token_without_id", 2000, nil)
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.Equal(t, "", userId)
})
t.Run("get user id - no id set", func(t *testing.T) {
userId, err := ss.DesktopTokens().GetUserId("token_without_id", 2000)
assert.NoError(t, err)
assert.Equal(t, "", userId)
})
}
func testInsert(t *testing.T, ss store.Store) {
t.Run("insert", func(t *testing.T) {
err := ss.DesktopTokens().Insert("token", 1000, nil)
assert.NoError(t, err)
})
t.Run("insert with user id", func(t *testing.T) {
err := ss.DesktopTokens().Insert("token_2", 1000, model.NewString("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,
nil,
)
assert.Error(t, err)
})
}
func testSetUserId(t *testing.T, ss store.Store) {
err := ss.DesktopTokens().Insert("new_token", 1000, nil)
require.NoError(t, err)
err = ss.DesktopTokens().Insert("new_token_2", 1000, nil)
require.NoError(t, err)
t.Run("set user id", func(t *testing.T) {
err := ss.DesktopTokens().SetUserId("new_token", 1000, "user_id")
assert.NoError(t, err)
userId, err := ss.DesktopTokens().GetUserId("new_token", 1000)
assert.NoError(t, err)
assert.Equal(t, "user_id", userId)
})
t.Run("set user id - doesn't exist", func(t *testing.T) {
err := ss.DesktopTokens().SetUserId("different_token", 1000, "user_id")
assert.Error(t, err)
_, err = ss.DesktopTokens().GetUserId("different_token", 1000)
assert.Error(t, err)
})
t.Run("set user id - expired", func(t *testing.T) {
err := ss.DesktopTokens().SetUserId("new_token", 2000, "user_id")
assert.Error(t, err)
_, err = ss.DesktopTokens().GetUserId("new_token", 2000)
assert.Error(t, err)
})
t.Run("set user id - user id too long", func(t *testing.T) {
err := ss.DesktopTokens().SetUserId(
"new_token_2",
1000,
"user_id that is WAAAAAAAAAAAAAY WAAAAAAAAAAAAAY WAAAAAAAAAAAAAY TOO LONG",
)
assert.Error(t, err)
userId, err := ss.DesktopTokens().GetUserId("new_token_2", 1000)
assert.NoError(t, err)
assert.Equal(t, "", userId)
})
}
func testDeleteToken(t *testing.T, ss store.Store) {
err := ss.DesktopTokens().Insert("deleteable_token", 3000, nil)
require.NoError(t, err)
err = ss.DesktopTokens().SetUserId("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, nil)
require.NoError(t, err)
err = ss.DesktopTokens().SetUserId("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, nil)
require.NoError(t, err)
err = ss.DesktopTokens().Insert("deleteable_token_new", 5000, nil)
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)
})
}

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

@@ -0,0 +1,121 @@
// 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: desktopToken
func (_m *DesktopTokensStore) Delete(desktopToken string) error {
ret := _m.Called(desktopToken)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(desktopToken)
} 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: desktopToken, minCreatedAt
func (_m *DesktopTokensStore) GetUserId(desktopToken string, minCreatedAt int64) (string, error) {
ret := _m.Called(desktopToken, minCreatedAt)
var r0 string
var r1 error
if rf, ok := ret.Get(0).(func(string, int64) (string, error)); ok {
return rf(desktopToken, minCreatedAt)
}
if rf, ok := ret.Get(0).(func(string, int64) string); ok {
r0 = rf(desktopToken, minCreatedAt)
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func(string, int64) error); ok {
r1 = rf(desktopToken, minCreatedAt)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Insert provides a mock function with given fields: desktopToken, createdAt, userId
func (_m *DesktopTokensStore) Insert(desktopToken string, createdAt int64, userId *string) error {
ret := _m.Called(desktopToken, createdAt, userId)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64, *string) error); ok {
r0 = rf(desktopToken, createdAt, userId)
} else {
r0 = ret.Error(0)
}
return r0
}
// SetUserId provides a mock function with given fields: desktopToken, minCreatedAt, userId
func (_m *DesktopTokensStore) SetUserId(desktopToken string, minCreatedAt int64, userId string) error {
ret := _m.Called(desktopToken, minCreatedAt, userId)
var r0 error
if rf, ok := ret.Get(0).(func(string, int64, string) error); ok {
r0 = rf(desktopToken, minCreatedAt, 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 }
@@ -176,5 +178,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
@@ -3000,6 +3010,102 @@ func (s *TimerLayerComplianceStore) Update(compliance *model.Compliance) (*model
return result, err
}
func (s *TimerLayerDesktopTokensStore) Delete(desktopToken string) error {
start := time.Now()
err := s.DesktopTokensStore.Delete(desktopToken)
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(desktopToken string, minCreatedAt int64) (string, error) {
start := time.Now()
result, err := s.DesktopTokensStore.GetUserId(desktopToken, 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(desktopToken string, createdAt int64, userId *string) error {
start := time.Now()
err := s.DesktopTokensStore.Insert(desktopToken, 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 *TimerLayerDesktopTokensStore) SetUserId(desktopToken string, minCreatedAt int64, userId string) error {
start := time.Now()
err := s.DesktopTokensStore.SetUserId(desktopToken, minCreatedAt, 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.SetUserId", success, elapsed)
}
return err
}
func (s *TimerLayerDraftStore) Delete(userID string, channelID string, rootID string) error {
start := time.Now()
@@ -11741,6 +11847,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}

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

@@ -10,6 +10,7 @@ import (
"net/url"
"path/filepath"
"strings"
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/i18n"
@@ -343,6 +344,29 @@ 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 != "" {
desktopTokenErr := c.App.AuthenticateDesktopToken(desktopToken, time.Now().Add(-model.DesktopTokenTTL).Unix(), user)
if desktopTokenErr != nil {
desktopTokenErr.Translate(c.AppContext.T)
c.LogErrorByCode(desktopTokenErr)
renderError(desktopTokenErr)
return
}
queryString := map[string]string{
"desktopAuthComplete": "true",
}
if val, ok := props["redirect_to"]; ok {
queryString["redirect_to"] = val
}
redirectURL = utils.AppendQueryParamsToURL(c.GetSiteURLHeader()+"/login/desktop", queryString)
}
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
@@ -357,6 +381,15 @@ 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 desktopToken != "" {
desktopTokenErr := c.App.CreateDesktopToken(desktopToken, time.Now().Unix())
if desktopTokenErr != nil {
c.Err = desktopTokenErr
return
}
}
if redirectURL != "" && !utils.IsValidWebAuthRedirectURL(c.App.Config(), redirectURL) {
c.Err = model.NewAppError("loginWithOAuth", "api.invalid_redirect_url", nil, "", http.StatusBadRequest)
@@ -369,7 +402,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
@@ -398,7 +431,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
@@ -426,7 +459,17 @@ 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")
if desktopToken != "" {
desktopTokenErr := c.App.CreateDesktopToken(desktopToken, time.Now().Unix())
if desktopTokenErr != nil {
c.Err = desktopTokenErr
return
}
}
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,16 @@ func loginWithSaml(c *Context, w http.ResponseWriter, r *http.Request) {
relayProps["redirect_to"] = redirectURL
}
desktopToken := r.URL.Query().Get("desktop_token")
if desktopToken != "" {
desktopTokenErr := c.App.CreateDesktopToken(desktopToken, time.Now().Unix())
if desktopTokenErr != nil {
c.Err = err
return
}
relayProps["desktop_token"] = desktopToken
}
relayProps[model.UserAuthServiceIsMobile] = strconv.FormatBool(isMobile)
if len(relayProps) > 0 {
@@ -185,6 +196,26 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) {
c.App.AttachSessionCookies(c.AppContext, w, r)
desktopToken := relayProps["desktop_token"]
if desktopToken != "" {
desktopTokenErr := c.App.AuthenticateDesktopToken(desktopToken, time.Now().Add(-model.DesktopTokenTTL).Unix(), user)
if desktopTokenErr != nil {
handleError(desktopTokenErr)
return
}
queryString := map[string]string{
"desktopAuthComplete": "true",
}
if val, ok := relayProps["redirect_to"]; ok {
queryString["redirect_to"] = val
}
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

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

@@ -5023,6 +5023,30 @@
"id": "app.custom_group.unique_name",
"translation": "group name is not unique"
},
{
"id": "app.desktop_token.authenticate.invalid_or_expired",
"translation": "Token does not exist or is expired"
},
{
"id": "app.desktop_token.create.collision",
"translation": "This token already exists in the system"
},
{
"id": "app.desktop_token.create.error",
"translation": "An error occured attempted to create the token"
},
{
"id": "app.desktop_token.validate.expired",
"translation": "Token is expired"
},
{
"id": "app.desktop_token.validate.invalid",
"translation": "Token is not valid"
},
{
"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."

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

@@ -35,6 +35,7 @@ const (
JobTypePostPersistentNotifications = "post_persistent_notifications"
JobTypeInstallPluginNotifyAdmin = "install_plugin_notify_admin"
JobTypeHostedPurchaseScreening = "hosted_purchase_screening"
JobTypeCleanupDesktopTokens = "cleanup_desktop_tokens"
JobStatusPending = "pending"
JobStatusInProgress = "in_progress"
@@ -65,6 +66,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