From cfc1503d622dfd387c5ecaff3c91723c50f9ab1f Mon Sep 17 00:00:00 2001 From: Ben Schumacher Date: Thu, 19 Jun 2025 11:52:16 +0200 Subject: [PATCH] [MM-63355] Add AuthData to mmctl user search output (#30478) --- server/channels/api4/user_test.go | 53 ++++++++++++++++++ server/channels/app/users/utils.go | 1 + server/channels/store/storetest/user_store.go | 56 ------------------- server/cmd/mmctl/commands/user.go | 8 ++- server/cmd/mmctl/commands/user_e2e_test.go | 40 +++++++++++++ server/cmd/mmctl/commands/user_test.go | 18 ++++++ server/public/model/user.go | 34 ++++++----- 7 files changed, 138 insertions(+), 72 deletions(-) diff --git a/server/channels/api4/user_test.go b/server/channels/api4/user_test.go index 0b866b2972f..7d4375facf8 100644 --- a/server/channels/api4/user_test.go +++ b/server/channels/api4/user_test.go @@ -1638,6 +1638,38 @@ func TestSearchUsers(t *testing.T) { require.NoError(t, err) require.Equal(t, users[0].Id, th.BasicUser.Id) }) + + // Create LDAP user + authData := "some auth data" + ldapUser := &model.User{ + Email: th.GenerateTestEmail(), + Username: GenerateTestUsername(), + EmailVerified: true, + AuthService: model.UserAuthServiceLdap, + AuthData: &authData, + } + ldapUser, appErr = th.App.CreateUser(th.Context, ldapUser) + require.Nil(t, appErr) + + t.Run("LDAP authdata field is returned appropriately", func(t *testing.T) { + // Search as regular user + search := &model.UserSearch{Term: ldapUser.Username} + users, resp, err := th.Client.SearchUsers(context.Background(), search) + require.NoError(t, err) + CheckOKStatus(t, resp) + require.Len(t, users, 1, "should find the ldap user") + require.Equal(t, ldapUser.Id, users[0].Id) + require.Empty(t, users[0].AuthData, "regular user should not see AuthData") + + // Search as system admin + users, resp, err = th.SystemAdminClient.SearchUsers(context.Background(), search) + require.NoError(t, err) + CheckOKStatus(t, resp) + require.Len(t, users, 1, "should find the ldap user") + require.Equal(t, ldapUser.Id, users[0].Id) + require.NotNil(t, users[0].AuthData, "admin should see AuthData") + require.Equal(t, *ldapUser.AuthData, *users[0].AuthData) + }) } func findUserInList(id string, users []*model.User) bool { //nolint:unused @@ -2993,6 +3025,27 @@ func TestGetUsers(t *testing.T) { require.Equal(t, err.Error(), "Invalid or missing role in request body.") }) + th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) { + user := &model.User{ + Email: th.GenerateTestEmail(), + Username: GenerateTestUsername(), + AuthService: model.UserAuthServiceLdap, + AuthData: model.NewPointer(model.NewId()), + } + u, resp, err := c.CreateUser(context.Background(), user) + require.NoError(t, err) + CheckCreatedStatus(t, resp) + require.NotNil(t, u) + + u, resp, err = c.GetUser(context.Background(), u.Id, "") + require.NoError(t, err) + CheckOKStatus(t, resp) + require.NotNil(t, u) + + assert.Equal(t, user.AuthService, u.AuthService) + assert.Equal(t, user.AuthData, u.AuthData) + }, "AuthData is returned for admins") + _, err := th.Client.Logout(context.Background()) require.NoError(t, err) _, resp, err := th.Client.GetUsers(context.Background(), 0, 60, "") diff --git a/server/channels/app/users/utils.go b/server/channels/app/users/utils.go index 0c8368974c1..4a711e4f636 100644 --- a/server/channels/app/users/utils.go +++ b/server/channels/app/users/utils.go @@ -51,6 +51,7 @@ func (us *UserService) GetSanitizeOptions(asAdmin bool) map[string]bool { options["email"] = true options["fullname"] = true options["authservice"] = true + options["authdata"] = true } return options } diff --git a/server/channels/store/storetest/user_store.go b/server/channels/store/storetest/user_store.go index 524694492c2..a7a23870fb7 100644 --- a/server/channels/store/storetest/user_store.go +++ b/server/channels/store/storetest/user_store.go @@ -2795,13 +2795,6 @@ func testUserStoreSearch(t *testing.T, rctx request.CTX, ss store.Store) { require.NoError(t, err) defer func() { require.NoError(t, ss.User().PermanentDelete(rctx, u3.Id)) }() - // The users returned from the database will have AuthData as an empty string. - nilAuthData := new(string) - *nilAuthData = "" - u1.AuthData = nilAuthData - u2.AuthData = nilAuthData - u3.AuthData = nilAuthData - t1id := model.NewId() _, nErr := ss.Team().SaveMember(rctx, &model.TeamMember{TeamId: t1id, UserId: u1.Id, SchemeAdmin: true, SchemeUser: true}, -1) require.NoError(t, nErr) @@ -2981,14 +2974,6 @@ func testUserStoreSearchNotInChannel(t *testing.T, rctx request.CTX, ss store.St _, nErr = ss.Team().SaveMember(rctx, &model.TeamMember{TeamId: tid, UserId: u3.Id}, -1) require.NoError(t, nErr) - // The users returned from the database will have AuthData as an empty string. - nilAuthData := new(string) - *nilAuthData = "" - - u1.AuthData = nilAuthData - u2.AuthData = nilAuthData - u3.AuthData = nilAuthData - ch1 := model.Channel{ TeamId: tid, DisplayName: "NameName", @@ -3210,14 +3195,6 @@ func testUserStoreSearchInChannel(t *testing.T, rctx request.CTX, ss store.Store _, nErr = ss.Team().SaveMember(rctx, &model.TeamMember{TeamId: tid, UserId: u3.Id}, -1) require.NoError(t, nErr) - // The users returned from the database will have AuthData as an empty string. - nilAuthData := new(string) - *nilAuthData = "" - - u1.AuthData = nilAuthData - u2.AuthData = nilAuthData - u3.AuthData = nilAuthData - ch1 := model.Channel{ TeamId: tid, DisplayName: "NameName", @@ -3481,17 +3458,6 @@ func testUserStoreSearchNotInTeam(t *testing.T, rctx request.CTX, ss store.Store _, nErr = ss.Team().SaveMember(rctx, &model.TeamMember{TeamId: teamID2, UserId: u4.Id}, -1) require.NoError(t, nErr) - // The users returned from the database will have AuthData as an empty string. - nilAuthData := new(string) - *nilAuthData = "" - - u1.AuthData = nilAuthData - u2.AuthData = nilAuthData - u3.AuthData = nilAuthData - u4.AuthData = nilAuthData - u5.AuthData = nilAuthData - u6.AuthData = nilAuthData - testCases := []struct { Description string TeamID string @@ -3629,14 +3595,6 @@ func testUserStoreSearchWithoutTeam(t *testing.T, rctx request.CTX, ss store.Sto _, nErr = ss.Team().SaveMember(rctx, &model.TeamMember{TeamId: tid, UserId: u3.Id}, -1) require.NoError(t, nErr) - // The users returned from the database will have AuthData as an empty string. - nilAuthData := new(string) - *nilAuthData = "" - - u1.AuthData = nilAuthData - u2.AuthData = nilAuthData - u3.AuthData = nilAuthData - testCases := []struct { Description string Term string @@ -3721,13 +3679,6 @@ func testUserStoreSearchInGroup(t *testing.T, rctx request.CTX, ss store.Store) require.NoError(t, err) defer func() { require.NoError(t, ss.User().PermanentDelete(rctx, u3.Id)) }() - // The users returned from the database will have AuthData as an empty string. - nilAuthData := model.NewPointer("") - - u1.AuthData = nilAuthData - u2.AuthData = nilAuthData - u3.AuthData = nilAuthData - g1 := &model.Group{ Name: model.NewPointer(NewTestID()), DisplayName: NewTestID(), @@ -3864,13 +3815,6 @@ func testUserStoreSearchNotInGroup(t *testing.T, rctx request.CTX, ss store.Stor require.NoError(t, err) defer func() { require.NoError(t, ss.User().PermanentDelete(rctx, u3.Id)) }() - // The users returned from the database will have AuthData as an empty string. - nilAuthData := model.NewPointer("") - - u1.AuthData = nilAuthData - u2.AuthData = nilAuthData - u3.AuthData = nilAuthData - g1 := &model.Group{ Name: model.NewPointer(NewTestID()), DisplayName: NewTestID(), diff --git a/server/cmd/mmctl/commands/user.go b/server/cmd/mmctl/commands/user.go index 48ee13ce8f0..c076eb8da89 100644 --- a/server/cmd/mmctl/commands/user.go +++ b/server/cmd/mmctl/commands/user.go @@ -779,6 +779,7 @@ func deleteAllUsersCmdF(c client.Client, cmd *cobra.Command, args []string) erro type userOut struct { *model.User Deactivated bool + AuthData string } func searchUserCmdF(c client.Client, cmd *cobra.Command, args []string) error { @@ -799,6 +800,10 @@ func searchUserCmdF(c client.Client, cmd *cobra.Command, args []string) error { User: user, Deactivated: !(user.DeleteAt == 0), } + if user.AuthData != nil { + uout.AuthData = *user.AuthData + } + tpl := `id: {{.Id}} deactivated: {{.Deactivated}} username: {{.Username}} @@ -807,7 +812,8 @@ position: {{.Position}} first_name: {{.FirstName}} last_name: {{.LastName}} email: {{.Email}} -auth_service: {{.AuthService}}` +auth_service: {{.AuthService}} +auth_data: {{.AuthData}}` if i > 0 { tpl = "------------------------------\n" + tpl } diff --git a/server/cmd/mmctl/commands/user_e2e_test.go b/server/cmd/mmctl/commands/user_e2e_test.go index 9593d8228d1..d062c991988 100644 --- a/server/cmd/mmctl/commands/user_e2e_test.go +++ b/server/cmd/mmctl/commands/user_e2e_test.go @@ -128,6 +128,8 @@ func (s *MmctlE2ETestSuite) TestSearchUserCmd() { user := printer.GetLines()[0].(userOut) s.Equal(s.th.BasicUser.Username, user.Username) s.False(user.Deactivated) + s.Empty(user.AuthData) + s.Empty(user.AuthService) s.Len(printer.GetErrorLines(), 0) }) @@ -149,6 +151,44 @@ func (s *MmctlE2ETestSuite) TestSearchUserCmd() { user := printer.GetLines()[0].(userOut) s.Equal(disabledUser.Username, user.Username) s.True(user.Deactivated) // Verify user shows as deactivated + s.Empty(user.AuthData) + s.Empty(user.AuthService) + s.Len(printer.GetErrorLines(), 0) + }) + + // Create a LDAP user + ldapUser, appErr := s.th.App.CreateUser(s.th.Context, &model.User{ + Email: s.th.GenerateTestEmail(), + Username: model.NewUsername(), + AuthData: model.NewPointer("1234"), + AuthService: model.UserAuthServiceLdap, + }) + s.Require().Nil(appErr) + + s.RunForSystemAdminAndLocal("Search for a user with authData", func(c client.Client) { + printer.Clean() + err := searchUserCmdF(c, &cobra.Command{}, []string{ldapUser.Email}) + s.Require().Nil(err) + s.Len(printer.GetLines(), 1) + user := printer.GetLines()[0].(userOut) + s.Equal(ldapUser.Username, user.Username) + s.False(user.Deactivated) + s.Equal(*ldapUser.AuthData, user.AuthData) + s.Equal(ldapUser.AuthService, user.AuthService) + s.Len(printer.GetErrorLines(), 0) + }) + + s.Run("Search for a user with authData/Client", func() { + printer.Clean() + // Non-admin should not be able to see AuthData or AuthService + err := searchUserCmdF(s.th.Client, &cobra.Command{}, []string{ldapUser.Email}) + s.Require().Nil(err) + s.Len(printer.GetLines(), 1) + user := printer.GetLines()[0].(userOut) + s.Equal(ldapUser.Username, user.Username) + s.False(user.Deactivated) + s.Equal("", user.AuthData) + s.Equal("", user.AuthService) s.Len(printer.GetErrorLines(), 0) }) diff --git a/server/cmd/mmctl/commands/user_test.go b/server/cmd/mmctl/commands/user_test.go index 0f16b1727df..3e229ed9f2a 100644 --- a/server/cmd/mmctl/commands/user_test.go +++ b/server/cmd/mmctl/commands/user_test.go @@ -628,6 +628,24 @@ func (s *MmctlUnitTestSuite) TestSearchUserCmd() { s.Require().Len(printer.GetErrorLines(), 0) }) + s.Run("Search for a user with authData", func() { + printer.Clean() + emailArg := "example@example.com" + mockUser := &model.User{Username: "ExampleUser", Email: emailArg, AuthData: model.NewPointer("1234"), AuthService: model.UserAuthServiceLdap} + + s.client. + EXPECT(). + GetUserByEmail(context.TODO(), emailArg, ""). + Return(mockUser, &model.Response{}, nil). + Times(1) + + err := searchUserCmdF(s.client, &cobra.Command{}, []string{emailArg}) + s.Require().Nil(err) + s.Require().Len(printer.GetLines(), 1) + s.Require().Equal(userOut{User: mockUser, Deactivated: false, AuthData: "1234"}, printer.GetLines()[0]) + s.Require().Len(printer.GetErrorLines(), 0) + }) + s.Run("Search for a nonexistent user", func() { printer.Clean() arg := "example@example.com" diff --git a/server/public/model/user.go b/server/public/model/user.go index 3805d30450e..f65927aa829 100644 --- a/server/public/model/user.go +++ b/server/public/model/user.go @@ -659,24 +659,28 @@ func (u *User) Etag(showFullName, showEmail bool) string { // Remove any private data from the user object func (u *User) Sanitize(options map[string]bool) { u.Password = "" - u.AuthData = NewPointer("") u.MfaSecret = "" u.MfaUsedTimestamps = nil u.LastLogin = 0 - if len(options) != 0 && !options["email"] { - u.Email = "" - delete(u.Props, UserPropsKeyRemoteEmail) - } - if len(options) != 0 && !options["fullname"] { - u.FirstName = "" - u.LastName = "" - } - if len(options) != 0 && !options["passwordupdate"] { - u.LastPasswordUpdate = 0 - } - if len(options) != 0 && !options["authservice"] { - u.AuthService = "" + if len(options) != 0 { + if !options["email"] { + u.Email = "" + delete(u.Props, UserPropsKeyRemoteEmail) + } + if !options["fullname"] { + u.FirstName = "" + u.LastName = "" + } + if !options["passwordupdate"] { + u.LastPasswordUpdate = 0 + } + if !options["authservice"] { + u.AuthService = "" + } + if !options["authdata"] { + u.AuthData = NewPointer("") + } } } @@ -703,7 +707,6 @@ func (u *User) SanitizeInput(isAdmin bool) { func (u *User) ClearNonProfileFields(asAdmin bool) { u.Password = "" - u.AuthData = NewPointer("") u.MfaSecret = "" u.MfaUsedTimestamps = nil u.EmailVerified = false @@ -711,6 +714,7 @@ func (u *User) ClearNonProfileFields(asAdmin bool) { u.LastPasswordUpdate = 0 if !asAdmin { + u.AuthData = NewPointer("") u.NotifyProps = StringMap{} u.FailedAttempts = 0 }