From e8081b7a0f3edb3020be803cbfdd9aec4c1b09b7 Mon Sep 17 00:00:00 2001 From: Scott Bishel Date: Tue, 19 May 2020 08:25:52 -0600 Subject: [PATCH] MM24459: Implement ldap picture sync (#14540) * implement ldap picture sync * add testing timeout, and no change * Revert "add testing timeout, and no change" This reverts commit 765621a7290074e5664c4ca2a2843c84e01f4cf1. * update app-layer * updates from code review * update app-layers * remove debug statements Co-authored-by: mattermod --- app/app_iface.go | 3 ++ app/login.go | 6 ++++ app/opentracing_layer.go | 39 ++++++++++++++++++++++++++ app/user.go | 15 +++++++--- app/user_test.go | 26 ++++++++++++++++++ app/web_hub.go | 59 ++++++++++++++++++++++++++++++++++------ app/web_hub_test.go | 31 +++++++++++++++++++++ einterfaces/ldap.go | 1 + 8 files changed, 167 insertions(+), 13 deletions(-) diff --git a/app/app_iface.go b/app/app_iface.go index 44fccfe578..b3f4e3b034 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -267,6 +267,8 @@ type AppIface interface { // This function deviates from other authorization checks in returning an error instead of just // a boolean, allowing the permission failure to be exposed with more granularity. SessionHasPermissionToManageBot(session model.Session, botUserId string) *model.AppError + // SessionIsRegistered determines if a specific session has been registered + SessionIsRegistered(session model.Session) bool // SetBotIconImage sets LHS icon for a bot. SetBotIconImage(botUserId string, file io.ReadSeeker) *model.AppError // SetBotIconImageFromMultiPartFile sets LHS icon for a bot. @@ -350,6 +352,7 @@ type AppIface interface { AddUserToTeamByInviteId(inviteId string, userId string) (*model.Team, *model.AppError) AddUserToTeamByTeamId(teamId string, user *model.User) *model.AppError AddUserToTeamByToken(userId string, tokenId string) (*model.Team, *model.AppError) + AdjustImage(file io.Reader) (*bytes.Buffer, *model.AppError) AllowOAuthAppAccessToUser(userId string, authRequest *model.AuthorizeRequest) (string, *model.AppError) AsymmetricSigningKey() *ecdsa.PrivateKey AttachDeviceId(sessionId string, deviceId string, expiresAt int64) *model.AppError diff --git a/app/login.go b/app/login.go index 94c6d60986..0a4b9dc032 100644 --- a/app/login.go +++ b/app/login.go @@ -165,6 +165,12 @@ func (a *App) DoLogin(w http.ResponseWriter, r *http.Request, user *model.User, a.SetSession(session) + if user.AuthService == model.USER_AUTH_SERVICE_LDAP && a.Ldap() != nil { + a.Srv().Go(func() { + a.Ldap().UpdateProfilePictureIfNecessary(user, session) + }) + } + if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil { a.Srv().Go(func() { pluginContext := a.PluginContext() diff --git a/app/opentracing_layer.go b/app/opentracing_layer.go index 9868b7fbf5..f2fe1fa8bd 100644 --- a/app/opentracing_layer.go +++ b/app/opentracing_layer.go @@ -541,6 +541,28 @@ func (a *OpenTracingAppLayer) AddUserToTeamByToken(userId string, tokenId string return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) AdjustImage(file io.Reader) (*bytes.Buffer, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AdjustImage") + + 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.AdjustImage(file) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) AllowOAuthAppAccessToUser(userId string, authRequest *model.AuthorizeRequest) (string, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AllowOAuthAppAccessToUser") @@ -12768,6 +12790,23 @@ func (a *OpenTracingAppLayer) SessionHasPermissionToUserOrBot(session model.Sess return resultVar0 } +func (a *OpenTracingAppLayer) SessionIsRegistered(session model.Session) bool { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SessionIsRegistered") + + 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.SessionIsRegistered(session) + + return resultVar0 +} + func (a *OpenTracingAppLayer) SetActiveChannel(userId string, channelId string) *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetActiveChannel") diff --git a/app/user.go b/app/user.go index 87c77306ea..62c057e948 100644 --- a/app/user.go +++ b/app/user.go @@ -855,12 +855,11 @@ func (a *App) SetProfileImageFromMultiPartFile(userId string, file multipart.Fil return a.SetProfileImageFromFile(userId, file) } -func (a *App) SetProfileImageFromFile(userId string, file io.Reader) *model.AppError { - +func (a *App) AdjustImage(file io.Reader) (*bytes.Buffer, *model.AppError) { // Decode image into Image object img, _, err := image.Decode(file) if err != nil { - return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.decode.app_error", nil, err.Error(), http.StatusBadRequest) + return nil, model.NewAppError("SetProfileImage", "api.user.upload_profile_user.decode.app_error", nil, err.Error(), http.StatusBadRequest) } orientation, _ := getImageOrientation(file) @@ -873,9 +872,17 @@ func (a *App) SetProfileImageFromFile(userId string, file io.Reader) *model.AppE buf := new(bytes.Buffer) err = png.Encode(buf, img) if err != nil { - return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.encode.app_error", nil, err.Error(), http.StatusInternalServerError) + return nil, model.NewAppError("SetProfileImage", "api.user.upload_profile_user.encode.app_error", nil, err.Error(), http.StatusInternalServerError) } + return buf, nil +} +func (a *App) SetProfileImageFromFile(userId string, file io.Reader) *model.AppError { + + buf, err := a.AdjustImage(file) + if err != nil { + return err + } path := "users/" + userId + "/profile.png" if _, err := a.WriteFile(buf, path); err != nil { diff --git a/app/user_test.go b/app/user_test.go index 0d3d9db44a..6e232f5e3b 100644 --- a/app/user_test.go +++ b/app/user_test.go @@ -18,6 +18,7 @@ import ( "github.com/mattermost/mattermost-server/v5/einterfaces" "github.com/mattermost/mattermost-server/v5/model" oauthgitlab "github.com/mattermost/mattermost-server/v5/model/gitlab" + "github.com/mattermost/mattermost-server/v5/utils/testutils" ) func TestIsUsernameTaken(t *testing.T) { @@ -121,6 +122,31 @@ func TestSetDefaultProfileImage(t *testing.T) { assert.Equal(t, int64(0), user.LastPictureUpdate) } +func TestAdjustProfileImage(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + _, err := th.App.AdjustImage(bytes.NewReader([]byte{})) + require.Error(t, err) + + // test image isn't the correct dimensions + // it should be adjusted + testjpg, error := testutils.ReadTestFile("testjpg.jpg") + require.Nil(t, error) + adjusted, err := th.App.AdjustImage(bytes.NewReader(testjpg)) + require.Nil(t, err) + assert.True(t, adjusted.Len() > 0) + assert.NotEqual(t, testjpg, adjusted) + + // default image should require adjustement + user := th.BasicUser + image, err := th.App.GetDefaultProfileImage(user) + require.Nil(t, err) + image2, err := th.App.AdjustImage(bytes.NewReader(image)) + require.Nil(t, err) + assert.Equal(t, image, image2.Bytes()) +} + func TestUpdateUserToRestrictedDomain(t *testing.T) { th := Setup(t) defer th.TearDown() diff --git a/app/web_hub.go b/app/web_hub.go index fd10126db2..d19813c45a 100644 --- a/app/web_hub.go +++ b/app/web_hub.go @@ -29,6 +29,12 @@ type webConnDirectMessage struct { msg model.WebSocketMessage } +type webConnSessionMessage struct { + userId string + sessionToken string + isRegistered chan bool +} + // Hub is the central place to manage all websocket connections in the server. // It handles different websocket events and sending messages to individual // user connections. @@ -47,20 +53,22 @@ type Hub struct { activity chan *webConnActivityMessage directMsg chan *webConnDirectMessage explicitStop bool + checkRegistered chan *webConnSessionMessage } // NewWebHub creates a new Hub. func (a *App) NewWebHub() *Hub { return &Hub{ - app: a, - register: make(chan *WebConn), - unregister: make(chan *WebConn), - broadcast: make(chan *model.WebSocketEvent, broadcastQueueSize), - stop: make(chan struct{}), - didStop: make(chan struct{}), - invalidateUser: make(chan string), - activity: make(chan *webConnActivityMessage), - directMsg: make(chan *webConnDirectMessage), + app: a, + register: make(chan *WebConn), + unregister: make(chan *WebConn), + broadcast: make(chan *model.WebSocketEvent, broadcastQueueSize), + stop: make(chan struct{}), + didStop: make(chan struct{}), + invalidateUser: make(chan string), + activity: make(chan *webConnActivityMessage), + directMsg: make(chan *webConnDirectMessage), + checkRegistered: make(chan *webConnSessionMessage), } } @@ -289,6 +297,15 @@ func (a *App) UpdateWebConnUserActivity(session model.Session, activityAt int64) } } +// SessionIsRegistered determines if a specific session has been registered +func (a *App) SessionIsRegistered(session model.Session) bool { + hub := a.GetHubForUserId(session.UserId) + if hub != nil { + return hub.IsRegistered(session.UserId, session.Token) + } + return false +} + // Register registers a connection to the hub. func (h *Hub) Register(webConn *WebConn) { select { @@ -305,6 +322,21 @@ func (h *Hub) Unregister(webConn *WebConn) { } } +// Determines if a user's session is registered a connection from the hub. +func (h *Hub) IsRegistered(userId, sessionToken string) bool { + ws := &webConnSessionMessage{ + userId: userId, + sessionToken: sessionToken, + isRegistered: make(chan bool), + } + select { + case h.checkRegistered <- ws: + return <-ws.isRegistered + case <-h.stop: + } + return false +} + // Broadcast broadcasts the message to all connections in the hub. func (h *Hub) Broadcast(message *model.WebSocketEvent) { // XXX: The hub nil check is because of the way we setup our tests. We call `app.NewServer()` @@ -375,6 +407,15 @@ func (h *Hub) Start() { for { select { + case webSessionMessage := <-h.checkRegistered: + conns := connIndex.ForUser(webSessionMessage.userId) + var isRegistered bool + for _, item := range conns { + if item.sessionToken.Load().(string) == webSessionMessage.sessionToken { + isRegistered = true + } + } + webSessionMessage.isRegistered <- isRegistered case webConn := <-h.register: connIndex.Add(webConn) atomic.StoreInt64(&h.connectionCount, int64(len(connIndex.All()))) diff --git a/app/web_hub_test.go b/app/web_hub_test.go index d4a27f8d9d..a1f586f048 100644 --- a/app/web_hub_test.go +++ b/app/web_hub_test.go @@ -12,13 +12,16 @@ import ( "github.com/gorilla/websocket" goi18n "github.com/mattermost/go-i18n/i18n" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/mattermost/mattermost-server/v5/mlog" "github.com/mattermost/mattermost-server/v5/model" ) func dummyWebsocketHandler(t *testing.T) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { + mlog.Debug("dummyWebsocketHandler") upgrader := &websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, @@ -106,3 +109,31 @@ func TestHubStopRaceCondition(t *testing.T) { require.FailNow(t, "hub call did not return within 15 seconds after stop") } } + +func TestHubIsRegistered(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + s := httptest.NewServer(dummyWebsocketHandler(t)) + defer s.Close() + + th.App.HubStart() + wc1 := registerDummyWebConn(t, th.App, s.Listener.Addr(), th.BasicUser.Id) + wc2 := registerDummyWebConn(t, th.App, s.Listener.Addr(), th.BasicUser.Id) + wc3 := registerDummyWebConn(t, th.App, s.Listener.Addr(), th.BasicUser.Id) + defer wc1.Close() + defer wc2.Close() + defer wc3.Close() + + session1 := wc1.session.Load().(*model.Session) + + assert.True(t, th.App.SessionIsRegistered(*session1)) + assert.True(t, th.App.SessionIsRegistered(*wc2.session.Load().(*model.Session))) + assert.True(t, th.App.SessionIsRegistered(*wc3.session.Load().(*model.Session))) + + session4, appErr := th.App.CreateSession(&model.Session{ + UserId: th.BasicUser2.Id, + }) + require.Nil(t, appErr) + assert.False(t, th.App.SessionIsRegistered(*session4)) +} diff --git a/einterfaces/ldap.go b/einterfaces/ldap.go index 3572f68dc8..53bbadfeca 100644 --- a/einterfaces/ldap.go +++ b/einterfaces/ldap.go @@ -21,4 +21,5 @@ type LdapInterface interface { GetGroup(groupUID string) (*model.Group, *model.AppError) GetAllGroupsPage(page int, perPage int, opts model.LdapGroupSearchOpts) ([]*model.Group, int, *model.AppError) FirstLoginSync(userID, userAuthService, userAuthData, email string) *model.AppError + UpdateProfilePictureIfNecessary(*model.User, *model.Session) }