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 <mattermod@users.noreply.github.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
d7cb890f34
Коммит
e8081b7a0f
@@ -267,6 +267,8 @@ type AppIface interface {
|
|||||||
// This function deviates from other authorization checks in returning an error instead of just
|
// 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.
|
// a boolean, allowing the permission failure to be exposed with more granularity.
|
||||||
SessionHasPermissionToManageBot(session model.Session, botUserId string) *model.AppError
|
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 sets LHS icon for a bot.
|
||||||
SetBotIconImage(botUserId string, file io.ReadSeeker) *model.AppError
|
SetBotIconImage(botUserId string, file io.ReadSeeker) *model.AppError
|
||||||
// SetBotIconImageFromMultiPartFile sets LHS icon for a bot.
|
// SetBotIconImageFromMultiPartFile sets LHS icon for a bot.
|
||||||
@@ -350,6 +352,7 @@ type AppIface interface {
|
|||||||
AddUserToTeamByInviteId(inviteId string, userId string) (*model.Team, *model.AppError)
|
AddUserToTeamByInviteId(inviteId string, userId string) (*model.Team, *model.AppError)
|
||||||
AddUserToTeamByTeamId(teamId string, user *model.User) *model.AppError
|
AddUserToTeamByTeamId(teamId string, user *model.User) *model.AppError
|
||||||
AddUserToTeamByToken(userId string, tokenId string) (*model.Team, *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)
|
AllowOAuthAppAccessToUser(userId string, authRequest *model.AuthorizeRequest) (string, *model.AppError)
|
||||||
AsymmetricSigningKey() *ecdsa.PrivateKey
|
AsymmetricSigningKey() *ecdsa.PrivateKey
|
||||||
AttachDeviceId(sessionId string, deviceId string, expiresAt int64) *model.AppError
|
AttachDeviceId(sessionId string, deviceId string, expiresAt int64) *model.AppError
|
||||||
|
|||||||
@@ -165,6 +165,12 @@ func (a *App) DoLogin(w http.ResponseWriter, r *http.Request, user *model.User,
|
|||||||
|
|
||||||
a.SetSession(session)
|
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 {
|
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
||||||
a.Srv().Go(func() {
|
a.Srv().Go(func() {
|
||||||
pluginContext := a.PluginContext()
|
pluginContext := a.PluginContext()
|
||||||
|
|||||||
@@ -541,6 +541,28 @@ func (a *OpenTracingAppLayer) AddUserToTeamByToken(userId string, tokenId string
|
|||||||
return resultVar0, resultVar1
|
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) {
|
func (a *OpenTracingAppLayer) AllowOAuthAppAccessToUser(userId string, authRequest *model.AuthorizeRequest) (string, *model.AppError) {
|
||||||
origCtx := a.ctx
|
origCtx := a.ctx
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AllowOAuthAppAccessToUser")
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AllowOAuthAppAccessToUser")
|
||||||
@@ -12768,6 +12790,23 @@ func (a *OpenTracingAppLayer) SessionHasPermissionToUserOrBot(session model.Sess
|
|||||||
return resultVar0
|
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 {
|
func (a *OpenTracingAppLayer) SetActiveChannel(userId string, channelId string) *model.AppError {
|
||||||
origCtx := a.ctx
|
origCtx := a.ctx
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetActiveChannel")
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SetActiveChannel")
|
||||||
|
|||||||
15
app/user.go
15
app/user.go
@@ -855,12 +855,11 @@ func (a *App) SetProfileImageFromMultiPartFile(userId string, file multipart.Fil
|
|||||||
return a.SetProfileImageFromFile(userId, file)
|
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
|
// Decode image into Image object
|
||||||
img, _, err := image.Decode(file)
|
img, _, err := image.Decode(file)
|
||||||
if err != nil {
|
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)
|
orientation, _ := getImageOrientation(file)
|
||||||
@@ -873,9 +872,17 @@ func (a *App) SetProfileImageFromFile(userId string, file io.Reader) *model.AppE
|
|||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
err = png.Encode(buf, img)
|
err = png.Encode(buf, img)
|
||||||
if err != nil {
|
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"
|
path := "users/" + userId + "/profile.png"
|
||||||
|
|
||||||
if _, err := a.WriteFile(buf, path); err != nil {
|
if _, err := a.WriteFile(buf, path); err != nil {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/mattermost/mattermost-server/v5/einterfaces"
|
"github.com/mattermost/mattermost-server/v5/einterfaces"
|
||||||
"github.com/mattermost/mattermost-server/v5/model"
|
"github.com/mattermost/mattermost-server/v5/model"
|
||||||
oauthgitlab "github.com/mattermost/mattermost-server/v5/model/gitlab"
|
oauthgitlab "github.com/mattermost/mattermost-server/v5/model/gitlab"
|
||||||
|
"github.com/mattermost/mattermost-server/v5/utils/testutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIsUsernameTaken(t *testing.T) {
|
func TestIsUsernameTaken(t *testing.T) {
|
||||||
@@ -121,6 +122,31 @@ func TestSetDefaultProfileImage(t *testing.T) {
|
|||||||
assert.Equal(t, int64(0), user.LastPictureUpdate)
|
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) {
|
func TestUpdateUserToRestrictedDomain(t *testing.T) {
|
||||||
th := Setup(t)
|
th := Setup(t)
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ type webConnDirectMessage struct {
|
|||||||
msg model.WebSocketMessage
|
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.
|
// Hub is the central place to manage all websocket connections in the server.
|
||||||
// It handles different websocket events and sending messages to individual
|
// It handles different websocket events and sending messages to individual
|
||||||
// user connections.
|
// user connections.
|
||||||
@@ -47,6 +53,7 @@ type Hub struct {
|
|||||||
activity chan *webConnActivityMessage
|
activity chan *webConnActivityMessage
|
||||||
directMsg chan *webConnDirectMessage
|
directMsg chan *webConnDirectMessage
|
||||||
explicitStop bool
|
explicitStop bool
|
||||||
|
checkRegistered chan *webConnSessionMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWebHub creates a new Hub.
|
// NewWebHub creates a new Hub.
|
||||||
@@ -61,6 +68,7 @@ func (a *App) NewWebHub() *Hub {
|
|||||||
invalidateUser: make(chan string),
|
invalidateUser: make(chan string),
|
||||||
activity: make(chan *webConnActivityMessage),
|
activity: make(chan *webConnActivityMessage),
|
||||||
directMsg: make(chan *webConnDirectMessage),
|
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.
|
// Register registers a connection to the hub.
|
||||||
func (h *Hub) Register(webConn *WebConn) {
|
func (h *Hub) Register(webConn *WebConn) {
|
||||||
select {
|
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.
|
// Broadcast broadcasts the message to all connections in the hub.
|
||||||
func (h *Hub) Broadcast(message *model.WebSocketEvent) {
|
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()`
|
// 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 {
|
for {
|
||||||
select {
|
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:
|
case webConn := <-h.register:
|
||||||
connIndex.Add(webConn)
|
connIndex.Add(webConn)
|
||||||
atomic.StoreInt64(&h.connectionCount, int64(len(connIndex.All())))
|
atomic.StoreInt64(&h.connectionCount, int64(len(connIndex.All())))
|
||||||
|
|||||||
@@ -12,13 +12,16 @@ import (
|
|||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
goi18n "github.com/mattermost/go-i18n/i18n"
|
goi18n "github.com/mattermost/go-i18n/i18n"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-server/v5/mlog"
|
||||||
"github.com/mattermost/mattermost-server/v5/model"
|
"github.com/mattermost/mattermost-server/v5/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func dummyWebsocketHandler(t *testing.T) http.HandlerFunc {
|
func dummyWebsocketHandler(t *testing.T) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, req *http.Request) {
|
return func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
mlog.Debug("dummyWebsocketHandler")
|
||||||
upgrader := &websocket.Upgrader{
|
upgrader := &websocket.Upgrader{
|
||||||
ReadBufferSize: 1024,
|
ReadBufferSize: 1024,
|
||||||
WriteBufferSize: 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")
|
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))
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,4 +21,5 @@ type LdapInterface interface {
|
|||||||
GetGroup(groupUID string) (*model.Group, *model.AppError)
|
GetGroup(groupUID string) (*model.Group, *model.AppError)
|
||||||
GetAllGroupsPage(page int, perPage int, opts model.LdapGroupSearchOpts) ([]*model.Group, int, *model.AppError)
|
GetAllGroupsPage(page int, perPage int, opts model.LdapGroupSearchOpts) ([]*model.Group, int, *model.AppError)
|
||||||
FirstLoginSync(userID, userAuthService, userAuthData, email string) *model.AppError
|
FirstLoginSync(userID, userAuthService, userAuthData, email string) *model.AppError
|
||||||
|
UpdateProfilePictureIfNecessary(*model.User, *model.Session)
|
||||||
}
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user