diff --git a/server/cmd/mmctl/client/client.go b/server/cmd/mmctl/client/client.go index db21e3c9f5..f6d476d789 100644 --- a/server/cmd/mmctl/client/client.go +++ b/server/cmd/mmctl/client/client.go @@ -150,4 +150,9 @@ type Client interface { ResetSamlAuthDataToEmail(ctx context.Context, includeDeleted bool, dryRun bool, userIDs []string) (int64, *model.Response, error) GenerateSupportPacket(ctx context.Context) ([]byte, *model.Response, error) GetOAuthApps(ctx context.Context, page, perPage int) ([]*model.OAuthApp, *model.Response, error) + GetPreferences(ctx context.Context, userId string) (model.Preferences, *model.Response, error) + GetPreferencesByCategory(ctx context.Context, userId, category string) (model.Preferences, *model.Response, error) + GetPreferenceByCategoryAndName(ctx context.Context, userId, category, preferenceName string) (*model.Preference, *model.Response, error) + UpdatePreferences(ctx context.Context, userId string, preferences model.Preferences) (*model.Response, error) + DeletePreferences(ctx context.Context, userId string, preferences model.Preferences) (*model.Response, error) } diff --git a/server/cmd/mmctl/commands/user.go b/server/cmd/mmctl/commands/user.go index 2784b6047a..2fe310e9db 100644 --- a/server/cmd/mmctl/commands/user.go +++ b/server/cmd/mmctl/commands/user.go @@ -9,6 +9,7 @@ import ( "fmt" "net/http" "os" + "sort" "github.com/mattermost/mattermost/server/public/model" @@ -251,6 +252,45 @@ var MigrateAuthCmd = &cobra.Command{ RunE: withClient(migrateAuthCmdF), } +var PreferenceCmd = &cobra.Command{ + Use: "preference", + Aliases: []string{"pref"}, + Short: "Manage user preferences", +} + +var PreferenceListCmd = &cobra.Command{ + Use: "list [--category category] [users]", + Short: "List user preferences", + Example: "preference list user@example.com", + Args: cobra.MinimumNArgs(1), + RunE: withClient(preferencesListCmdF), +} + +var PreferenceGetCmd = &cobra.Command{ + Use: "get --category [category] --name [name] [users]", + Short: "Get a specific user preference", + Example: "preference get --category display_settings --name use_military_time user@example.com", + Args: cobra.MinimumNArgs(1), + RunE: withClient(preferencesGetCmdF), +} + +var PreferenceUpdateCmd = &cobra.Command{ + Use: "set --category [category] --name [name] --value [value] [users]", + Aliases: []string{"update"}, + Short: "Set a specific user preference", + Example: "preference set --category display_settings --name use_military_time --value true user@example.com", + Args: cobra.MinimumNArgs(1), + RunE: withClient(preferencesUpdateCmdF), +} + +var PreferenceDeleteCmd = &cobra.Command{ + Use: "delete --category [category] --name [name] [users]", + Short: "Delete a specific user preference", + Example: "preference delete --category display_settings --name use_military_time user@example.com", + Args: cobra.MinimumNArgs(1), + RunE: withClient(preferencesDeleteCmdF), +} + func init() { UserCreateCmd.Flags().String("username", "", "Required. Username for the new user account") _ = UserCreateCmd.MarkFlagRequired("username") @@ -336,6 +376,22 @@ Global Flags: {{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}} `) + PreferenceListCmd.Flags().StringP("category", "c", "", "The optional category by which to filter") + PreferenceGetCmd.Flags().StringP("category", "c", "", "The category of the preference") + PreferenceGetCmd.Flags().StringP("name", "n", "", "The name of the preference") + _ = PreferenceGetCmd.MarkFlagRequired("category") + _ = PreferenceGetCmd.MarkFlagRequired("name") + PreferenceUpdateCmd.Flags().StringP("category", "c", "", "The category of the preference") + PreferenceUpdateCmd.Flags().StringP("name", "n", "", "The name of the preference") + PreferenceUpdateCmd.Flags().StringP("value", "v", "", "The value of the preference") + _ = PreferenceUpdateCmd.MarkFlagRequired("category") + _ = PreferenceUpdateCmd.MarkFlagRequired("name") + _ = PreferenceUpdateCmd.MarkFlagRequired("value") + PreferenceDeleteCmd.Flags().StringP("category", "c", "", "The category of the preference") + PreferenceDeleteCmd.Flags().StringP("name", "n", "", "The name of the preference") + _ = PreferenceDeleteCmd.MarkFlagRequired("category") + _ = PreferenceDeleteCmd.MarkFlagRequired("name") + UserCmd.AddCommand( UserActivateCmd, UserDeactivateCmd, @@ -355,6 +411,13 @@ Global Flags: MigrateAuthCmd, PromoteGuestToUserCmd, DemoteUserToGuestCmd, + PreferenceCmd, + ) + PreferenceCmd.AddCommand( + PreferenceListCmd, + PreferenceGetCmd, + PreferenceUpdateCmd, + PreferenceDeleteCmd, ) RootCmd.AddCommand(UserCmd) @@ -1000,3 +1063,179 @@ func demoteUserToGuestCmdF(c client.Client, _ *cobra.Command, userArgs []string) return errs.ErrorOrNil() } + +type ByPreference model.Preferences + +func (p ByPreference) Len() int { return len(p) } +func (p ByPreference) Swap(i, j int) { p[i], p[j] = p[j], p[i] } +func (p ByPreference) Less(i, j int) bool { + if p[i].UserId < p[j].UserId { + return true + } + + if p[i].Category < p[j].Category { + return true + } + + if p[i].Name < p[j].Name { + return true + } + + return p[i].Value < p[j].Value +} + +func preferencesListCmdF(c client.Client, cmd *cobra.Command, userArgs []string) error { + category, _ := cmd.Flags().GetString("category") + + var errs *multierror.Error + for i, user := range getUsersFromUserArgs(c, userArgs) { + if user == nil { + err := fmt.Errorf("can't find user '%s'", userArgs[i]) + errs = multierror.Append(errs, err) + printer.PrintError(err.Error()) + continue + } + + var preferences model.Preferences + var err error + if category == "" { + preferences, _, err = c.GetPreferences(context.TODO(), user.Id) + + if err != nil { + err = fmt.Errorf("unable to list user preferences %s: %w", userArgs[i], err) + errs = multierror.Append(errs, err) + printer.PrintError(err.Error()) + continue + } + } else { + preferences, _, err = c.GetPreferencesByCategory(context.TODO(), user.Id, category) + + if err != nil { + err = fmt.Errorf("unable to list user preferences by category %s for %s: %w", category, userArgs[i], err) + errs = multierror.Append(errs, err) + printer.PrintError(err.Error()) + continue + } + } + + sort.Sort(ByPreference(preferences)) + + for j, preference := range preferences { + tpl := `user_id: {{.UserId}} +category: {{.Category}} +name: {{.Name}} +value: {{.Value}}` + if j > 0 { + tpl = "------------------------------\n" + tpl + } + + printer.PrintT(tpl, preference) + } + } + + return errs.ErrorOrNil() +} + +func preferencesGetCmdF(c client.Client, cmd *cobra.Command, userArgs []string) error { + category, _ := cmd.Flags().GetString("category") + preferenceName, _ := cmd.Flags().GetString("name") + + var errs *multierror.Error + for i, user := range getUsersFromUserArgs(c, userArgs) { + if user == nil { + err := fmt.Errorf("can't find user '%s'", userArgs[i]) + errs = multierror.Append(errs, err) + printer.PrintError(err.Error()) + continue + } + + preference, _, err := c.GetPreferenceByCategoryAndName(context.TODO(), user.Id, category, preferenceName) + if err != nil { + err = fmt.Errorf("unable to get user preference %s %s for %s: %w", category, preferenceName, userArgs[i], err) + errs = multierror.Append(errs, err) + printer.PrintError(err.Error()) + continue + } + + tpl := `user_id: {{.UserId}} +category: {{.Category}} +name: {{.Name}} +value: {{.Value}}` + + printer.PrintT(tpl, preference) + } + + return errs.ErrorOrNil() +} + +func preferencesUpdateCmdF(c client.Client, cmd *cobra.Command, userArgs []string) error { + category, _ := cmd.Flags().GetString("category") + preferenceName, _ := cmd.Flags().GetString("name") + value, _ := cmd.Flags().GetString("value") + + var errs *multierror.Error + for i, user := range getUsersFromUserArgs(c, userArgs) { + if user == nil { + err := fmt.Errorf("can't find user '%s'", userArgs[i]) + errs = multierror.Append(errs, err) + printer.PrintError(err.Error()) + continue + } + + preferences := model.Preferences{ + model.Preference{ + UserId: user.Id, + Category: category, + Name: preferenceName, + Value: value, + }, + } + + _, err := c.UpdatePreferences(context.TODO(), user.Id, preferences) + if err != nil { + err = fmt.Errorf("unable to update user preference %s %s for %s: %w", category, preferenceName, userArgs[i], err) + errs = multierror.Append(errs, err) + printer.PrintError(err.Error()) + continue + } + + printer.Print(fmt.Sprintf("Preference %s %s for %s updated successfully", category, preferenceName, userArgs[i])) + } + + return errs.ErrorOrNil() +} + +func preferencesDeleteCmdF(c client.Client, cmd *cobra.Command, userArgs []string) error { + category, _ := cmd.Flags().GetString("category") + preferenceName, _ := cmd.Flags().GetString("name") + + var errs *multierror.Error + for i, user := range getUsersFromUserArgs(c, userArgs) { + if user == nil { + err := fmt.Errorf("can't find user '%s'", userArgs[i]) + errs = multierror.Append(errs, err) + printer.PrintError(err.Error()) + continue + } + + preferences := model.Preferences{ + model.Preference{ + UserId: user.Id, + Category: category, + Name: preferenceName, + }, + } + + _, err := c.DeletePreferences(context.TODO(), user.Id, preferences) + if err != nil { + err = fmt.Errorf("unable to delete user preference %s %s for %s: %w", category, preferenceName, userArgs[i], err) + errs = multierror.Append(errs, err) + printer.PrintError(err.Error()) + continue + } + + printer.Print(fmt.Sprintf("Preference %s %s for %s deleted successfully", category, preferenceName, userArgs[i])) + } + + return errs.ErrorOrNil() +} diff --git a/server/cmd/mmctl/commands/user_e2e_test.go b/server/cmd/mmctl/commands/user_e2e_test.go index e66b5946c2..861238a010 100644 --- a/server/cmd/mmctl/commands/user_e2e_test.go +++ b/server/cmd/mmctl/commands/user_e2e_test.go @@ -4,6 +4,7 @@ package commands import ( + "context" "fmt" "net/http" @@ -1067,3 +1068,449 @@ func (s *MmctlE2ETestSuite) TestMigrateAuthCmd() { s.Require().Equal(model.UserAuthServiceLdap, updatedUser.AuthService) }) } + +func (s *MmctlE2ETestSuite) cleanUpPreferences(userID string) { + s.T().Helper() + + // Delete any existing preferences + preferences, _, err := s.th.SystemAdminClient.GetPreferences(context.Background(), userID) + s.NoError(err) + + _, err = s.th.SystemAdminClient.DeletePreferences(context.Background(), userID, preferences) + s.NoError(err) +} + +func (s *MmctlE2ETestSuite) TestPreferenceListCmd() { + s.SetupTestHelper().InitBasic() + + s.cleanUpPreferences(s.th.BasicUser.Id) + s.cleanUpPreferences(s.th.BasicUser2.Id) + + preference1 := model.Preference{UserId: s.th.BasicUser.Id, Category: "display_settings", Name: "collapsed_reply_threads", Value: "threads_view"} + _, err := s.th.SystemAdminClient.UpdatePreferences(context.Background(), s.th.BasicUser.Id, model.Preferences{preference1}) + s.NoError(err) + preference2 := model.Preference{UserId: s.th.BasicUser.Id, Category: "display_settings", Name: "colorize_usernames", Value: "threads_view"} + _, err = s.th.SystemAdminClient.UpdatePreferences(context.Background(), s.th.BasicUser.Id, model.Preferences{preference2}) + s.NoError(err) + preference3 := model.Preference{UserId: s.th.BasicUser.Id, Category: "drafts", Name: "drafts_tour_tip_showed", Value: `{"drafts_tour_tip_showed":true}`} + _, err = s.th.SystemAdminClient.UpdatePreferences(context.Background(), s.th.BasicUser.Id, model.Preferences{preference3}) + s.NoError(err) + + preference4 := model.Preference{UserId: s.th.BasicUser2.Id, Category: "display_settings", Name: "collapsed_reply_threads", Value: "threads_view"} + _, err = s.th.SystemAdminClient.UpdatePreferences(context.Background(), s.th.BasicUser2.Id, model.Preferences{preference4}) + s.NoError(err) + + s.Run("list all preferences for single user", func() { + printer.Clean() + + cmd := &cobra.Command{} + cmd.Flags().StringP("category", "c", "", "") + + err = preferencesListCmdF(s.th.Client, cmd, []string{s.th.BasicUser.Email}) + s.Require().NoError(err) + s.Require().Len(printer.GetLines(), 3) + s.Require().Equal(preference1, printer.GetLines()[0]) + s.Require().Equal(preference2, printer.GetLines()[1]) + s.Require().Equal(preference3, printer.GetLines()[2]) + s.Require().Len(printer.GetErrorLines(), 0) + }) + + s.Run("list filtered preferences for single user", func() { + printer.Clean() + + cmd := &cobra.Command{} + cmd.Flags().StringP("category", "c", preference1.Category, "") + + err = preferencesListCmdF(s.th.Client, cmd, []string{s.th.BasicUser.Email}) + s.Require().NoError(err) + s.Require().Len(printer.GetLines(), 2) + s.Require().Equal(preference1, printer.GetLines()[0]) + s.Require().Equal(preference2, printer.GetLines()[1]) + s.Require().Len(printer.GetErrorLines(), 0) + }) + + s.Run("list all preferences for multiple users as admin", func() { + printer.Clean() + + cmd := &cobra.Command{} + cmd.Flags().StringP("category", "c", "", "") + + err = preferencesListCmdF(s.th.SystemAdminClient, cmd, []string{s.th.BasicUser.Email, s.th.BasicUser2.Email}) + s.Require().NoError(err) + s.Require().Len(printer.GetLines(), 4) + s.Require().Equal(preference1, printer.GetLines()[0]) + s.Require().Equal(preference2, printer.GetLines()[1]) + s.Require().Equal(preference3, printer.GetLines()[2]) + s.Require().Equal(preference4, printer.GetLines()[3]) + s.Require().Len(printer.GetErrorLines(), 0) + }) + + s.Run("list filtered preferences for multiple users as admin", func() { + printer.Clean() + + cmd := &cobra.Command{} + cmd.Flags().StringP("category", "c", preference1.Category, "") + + err = preferencesListCmdF(s.th.SystemAdminClient, cmd, []string{s.th.BasicUser.Email, s.th.BasicUser2.Email}) + s.Require().NoError(err) + s.Require().Len(printer.GetLines(), 3) + s.Require().Equal(preference1, printer.GetLines()[0]) + s.Require().Equal(preference2, printer.GetLines()[1]) + s.Require().Equal(preference4, printer.GetLines()[2]) + s.Require().Len(printer.GetErrorLines(), 0) + }) + + s.Run("list preferences for multiple users as non-admin", func() { + printer.Clean() + + cmd := &cobra.Command{} + cmd.Flags().StringP("category", "c", preference1.Category, "") + + err = preferencesListCmdF(s.th.Client, cmd, []string{s.th.BasicUser.Email, s.th.BasicUser2.Email}) + s.Require().Error(err) + }) +} + +func (s *MmctlE2ETestSuite) TestPreferenceGetCmd() { + s.SetupTestHelper().InitBasic() + + s.cleanUpPreferences(s.th.BasicUser.Id) + s.cleanUpPreferences(s.th.BasicUser2.Id) + + preference1 := model.Preference{UserId: s.th.BasicUser.Id, Category: "display_settings", Name: "collapsed_reply_threads", Value: "threads_view"} + _, err := s.th.SystemAdminClient.UpdatePreferences(context.Background(), s.th.BasicUser.Id, model.Preferences{preference1}) + s.NoError(err) + preference2 := model.Preference{UserId: s.th.BasicUser.Id, Category: "display_settings", Name: "colorize_usernames", Value: "threads_view"} + _, err = s.th.SystemAdminClient.UpdatePreferences(context.Background(), s.th.BasicUser.Id, model.Preferences{preference2}) + s.NoError(err) + preference3 := model.Preference{UserId: s.th.BasicUser.Id, Category: "drafts", Name: "drafts_tour_tip_showed", Value: `{"drafts_tour_tip_showed":true}`} + _, err = s.th.SystemAdminClient.UpdatePreferences(context.Background(), s.th.BasicUser.Id, model.Preferences{preference3}) + s.NoError(err) + + preference4 := model.Preference{UserId: s.th.BasicUser2.Id, Category: "display_settings", Name: "collapsed_reply_threads", Value: "threads_view"} + _, err = s.th.SystemAdminClient.UpdatePreferences(context.Background(), s.th.BasicUser2.Id, model.Preferences{preference4}) + s.NoError(err) + + s.Run("get preference for single user", func() { + printer.Clean() + + cmd := &cobra.Command{} + cmd.Flags().StringP("category", "c", preference1.Category, "") + cmd.Flags().StringP("name", "n", preference1.Name, "") + + err = preferencesGetCmdF(s.th.Client, cmd, []string{s.th.BasicUser.Email}) + s.Require().NoError(err) + s.Require().Len(printer.GetLines(), 1) + s.Require().Equal(&preference1, printer.GetLines()[0]) + s.Require().Len(printer.GetErrorLines(), 0) + }) + + s.Run("get preferences for multiple users as admin", func() { + printer.Clean() + + cmd := &cobra.Command{} + cmd.Flags().StringP("category", "c", preference1.Category, "") + cmd.Flags().StringP("name", "n", preference1.Name, "") + + err = preferencesGetCmdF(s.th.SystemAdminClient, cmd, []string{s.th.BasicUser.Email, s.th.BasicUser2.Email}) + s.Require().NoError(err) + s.Require().Len(printer.GetLines(), 2) + s.Require().Equal(&preference1, printer.GetLines()[0]) + s.Require().Equal(&preference4, printer.GetLines()[1]) + s.Require().Len(printer.GetErrorLines(), 0) + }) + + s.Run("get preferences for multiple users as non-admin", func() { + printer.Clean() + + cmd := &cobra.Command{} + cmd.Flags().StringP("category", "c", preference1.Category, "") + cmd.Flags().StringP("name", "n", preference1.Name, "") + + err = preferencesGetCmdF(s.th.Client, cmd, []string{s.th.BasicUser.Email, s.th.BasicUser2.Email}) + s.Require().Error(err) + }) +} + +func (s *MmctlE2ETestSuite) TestPreferenceUpdateCmd() { + s.SetupTestHelper().InitBasic() + + preference1 := model.Preference{UserId: s.th.BasicUser.Id, Category: "display_settings", Name: "collapsed_reply_threads", Value: "threads_view"} + preference2 := model.Preference{UserId: s.th.BasicUser.Id, Category: "display_settings", Name: "colorize_usernames", Value: "threads_view"} + preference3 := model.Preference{UserId: s.th.BasicUser.Id, Category: "drafts", Name: "drafts_tour_tip_showed", Value: `{"drafts_tour_tip_showed":true}`} + + preference4 := model.Preference{UserId: s.th.BasicUser2.Id, Category: "display_settings", Name: "collapsed_reply_threads", Value: "threads_view"} + + setup := func() { + s.T().Helper() + + s.cleanUpPreferences(s.th.BasicUser.Id) + s.cleanUpPreferences(s.th.BasicUser2.Id) + + _, err := s.th.SystemAdminClient.UpdatePreferences(context.Background(), s.th.BasicUser.Id, model.Preferences{preference1}) + s.NoError(err) + _, err = s.th.SystemAdminClient.UpdatePreferences(context.Background(), s.th.BasicUser.Id, model.Preferences{preference2}) + s.NoError(err) + _, err = s.th.SystemAdminClient.UpdatePreferences(context.Background(), s.th.BasicUser.Id, model.Preferences{preference3}) + s.NoError(err) + + _, err = s.th.SystemAdminClient.UpdatePreferences(context.Background(), s.th.BasicUser2.Id, model.Preferences{preference4}) + s.NoError(err) + } + + s.Run("add new preference for single user", func() { + setup() + printer.Clean() + + preferenceNew := model.Preference{UserId: s.th.BasicUser.Id, Category: "zzz_custom", Name: "new", Value: "value"} + + cmd := &cobra.Command{} + cmd.Flags().StringP("category", "c", preferenceNew.Category, "") + cmd.Flags().StringP("name", "n", preferenceNew.Name, "") + cmd.Flags().StringP("value", "v", preferenceNew.Value, "") + + err := preferencesUpdateCmdF(s.th.Client, cmd, []string{s.th.BasicUser.Email}) + s.Require().NoError(err) + s.Require().Len(printer.GetErrorLines(), 0) + + actualPreferencesUser1, _, err := s.th.SystemAdminClient.GetPreferences(context.Background(), s.th.BasicUser.Id) + s.NoError(err) + s.Require().Len(actualPreferencesUser1, 4) + s.Require().Equal(preference1, actualPreferencesUser1[0]) + s.Require().Equal(preference2, actualPreferencesUser1[1]) + s.Require().Equal(preference3, actualPreferencesUser1[2]) + s.Require().Equal(preferenceNew, actualPreferencesUser1[3]) + + // Second user unaffected + actualPreferencesUser2, _, err := s.th.SystemAdminClient.GetPreferences(context.Background(), s.th.BasicUser2.Id) + s.NoError(err) + s.Require().Len(actualPreferencesUser2, 1) + s.Require().Equal(preference4, actualPreferencesUser2[0]) + }) + + s.Run("add new preference for multiple users as admin", func() { + setup() + printer.Clean() + + preferenceNew := model.Preference{UserId: s.th.BasicUser.Id, Category: "zzz_custom", Name: "new", Value: "value"} + preferenceNew2 := model.Preference{UserId: s.th.BasicUser2.Id, Category: "zzz_custom", Name: "new", Value: "value"} + + cmd := &cobra.Command{} + cmd.Flags().StringP("category", "c", preferenceNew.Category, "") + cmd.Flags().StringP("name", "n", preferenceNew.Name, "") + cmd.Flags().StringP("value", "v", preferenceNew.Value, "") + + err := preferencesUpdateCmdF(s.th.SystemAdminClient, cmd, []string{s.th.BasicUser.Email, s.th.BasicUser2.Email}) + s.Require().NoError(err) + s.Require().Len(printer.GetErrorLines(), 0) + + actualPreferencesUser1, _, err := s.th.SystemAdminClient.GetPreferences(context.Background(), s.th.BasicUser.Id) + s.NoError(err) + s.Require().Len(actualPreferencesUser1, 4) + s.Require().Equal(preference1, actualPreferencesUser1[0]) + s.Require().Equal(preference2, actualPreferencesUser1[1]) + s.Require().Equal(preference3, actualPreferencesUser1[2]) + s.Require().Equal(preferenceNew, actualPreferencesUser1[3]) + + // Second user unaffected + actualPreferencesUser2, _, err := s.th.SystemAdminClient.GetPreferences(context.Background(), s.th.BasicUser2.Id) + s.NoError(err) + s.Require().Len(actualPreferencesUser2, 2) + s.Require().Equal(preference4, actualPreferencesUser2[0]) + s.Require().Equal(preferenceNew2, actualPreferencesUser2[1]) + }) + + s.Run("add new preference for multiple users as non-admin", func() { + setup() + printer.Clean() + + preference := model.Preference{Category: "display_settings", Name: "collapsed_reply_threads", Value: "threads_view"} + + cmd := &cobra.Command{} + cmd.Flags().StringP("category", "c", preference.Category, "") + cmd.Flags().StringP("name", "n", preference.Name, "") + cmd.Flags().StringP("value", "v", preference.Value, "") + + err := preferencesUpdateCmdF(s.th.Client, cmd, []string{s.th.BasicUser.Email, s.th.BasicUser2.Email}) + s.Require().Error(err) + }) + + s.Run("update existing preference for single user", func() { + setup() + printer.Clean() + + preferenceUpdated := model.Preference{UserId: s.th.BasicUser.Id, Category: preference1.Category, Name: preference1.Name, Value: "new_value"} + + cmd := &cobra.Command{} + cmd.Flags().StringP("category", "c", preferenceUpdated.Category, "") + cmd.Flags().StringP("name", "n", preferenceUpdated.Name, "") + cmd.Flags().StringP("value", "v", preferenceUpdated.Value, "") + + err := preferencesUpdateCmdF(s.th.Client, cmd, []string{s.th.BasicUser.Email}) + s.Require().NoError(err) + s.Require().Len(printer.GetErrorLines(), 0) + + actualPreferencesUser1, _, err := s.th.SystemAdminClient.GetPreferences(context.Background(), s.th.BasicUser.Id) + s.NoError(err) + s.Require().Len(actualPreferencesUser1, 3) + s.Require().Equal(preferenceUpdated, actualPreferencesUser1[0]) + s.Require().Equal(preference2, actualPreferencesUser1[1]) + s.Require().Equal(preference3, actualPreferencesUser1[2]) + + // Second user unaffected + actualPreferencesUser2, _, err := s.th.SystemAdminClient.GetPreferences(context.Background(), s.th.BasicUser2.Id) + s.NoError(err) + s.Require().Len(actualPreferencesUser2, 1) + s.Require().Equal(preference4, actualPreferencesUser2[0]) + }) + + s.Run("update existing preference for multiple users as admin", func() { + setup() + printer.Clean() + + preferenceUpdated := model.Preference{UserId: s.th.BasicUser.Id, Category: preference1.Category, Name: preference1.Name, Value: "new_value"} + preferenceUpdated2 := model.Preference{UserId: s.th.BasicUser2.Id, Category: preference1.Category, Name: preference1.Name, Value: "new_value"} + + cmd := &cobra.Command{} + cmd.Flags().StringP("category", "c", preferenceUpdated.Category, "") + cmd.Flags().StringP("name", "n", preferenceUpdated.Name, "") + cmd.Flags().StringP("value", "v", preferenceUpdated.Value, "") + + err := preferencesUpdateCmdF(s.th.SystemAdminClient, cmd, []string{s.th.BasicUser.Email, s.th.BasicUser2.Email}) + s.Require().NoError(err) + s.Require().Len(printer.GetErrorLines(), 0) + + actualPreferencesUser1, _, err := s.th.SystemAdminClient.GetPreferences(context.Background(), s.th.BasicUser.Id) + s.NoError(err) + s.Require().Len(actualPreferencesUser1, 3) + s.Require().Equal(preferenceUpdated, actualPreferencesUser1[0]) + s.Require().Equal(preference2, actualPreferencesUser1[1]) + s.Require().Equal(preference3, actualPreferencesUser1[2]) + + actualPreferencesUser2, _, err := s.th.SystemAdminClient.GetPreferences(context.Background(), s.th.BasicUser2.Id) + s.NoError(err) + s.Require().Len(actualPreferencesUser2, 1) + s.Require().Equal(preferenceUpdated2, actualPreferencesUser2[0]) + }) + + s.Run("update existing preference for multiple users as non-admin", func() { + setup() + printer.Clean() + + preferenceUpdated := model.Preference{Category: preference1.Category, Name: preference1.Name, Value: "new_value"} + + cmd := &cobra.Command{} + cmd.Flags().StringP("category", "c", preferenceUpdated.Category, "") + cmd.Flags().StringP("name", "n", preferenceUpdated.Name, "") + cmd.Flags().StringP("value", "v", preferenceUpdated.Value, "") + + err := preferencesUpdateCmdF(s.th.Client, cmd, []string{s.th.BasicUser.Email, s.th.BasicUser2.Email}) + s.Require().Error(err) + }) +} + +func (s *MmctlE2ETestSuite) TestPreferenceDeleteCmd() { + s.SetupTestHelper().InitBasic() + + s.cleanUpPreferences(s.th.BasicUser.Id) + s.cleanUpPreferences(s.th.BasicUser2.Id) + + preference1 := model.Preference{UserId: s.th.BasicUser.Id, Category: "display_settings", Name: "collapsed_reply_threads", Value: "threads_view"} + _, err := s.th.SystemAdminClient.UpdatePreferences(context.Background(), s.th.BasicUser.Id, model.Preferences{preference1}) + s.NoError(err) + preference2 := model.Preference{UserId: s.th.BasicUser.Id, Category: "display_settings", Name: "colorize_usernames", Value: "threads_view"} + _, err = s.th.SystemAdminClient.UpdatePreferences(context.Background(), s.th.BasicUser.Id, model.Preferences{preference2}) + s.NoError(err) + preference3 := model.Preference{UserId: s.th.BasicUser.Id, Category: "drafts", Name: "drafts_tour_tip_showed", Value: `{"drafts_tour_tip_showed":true}`} + _, err = s.th.SystemAdminClient.UpdatePreferences(context.Background(), s.th.BasicUser.Id, model.Preferences{preference3}) + s.NoError(err) + + preference4 := model.Preference{UserId: s.th.BasicUser2.Id, Category: "display_settings", Name: "collapsed_reply_threads", Value: "threads_view"} + _, err = s.th.SystemAdminClient.UpdatePreferences(context.Background(), s.th.BasicUser2.Id, model.Preferences{preference4}) + s.NoError(err) + + s.Run("delete non-existing preference for single user", func() { + printer.Clean() + + preference := model.Preference{Category: "does", Name: "not"} + + cmd := &cobra.Command{} + cmd.Flags().StringP("category", "c", preference.Category, "") + cmd.Flags().StringP("name", "n", preference.Name, "") + + err := preferencesDeleteCmdF(s.th.Client, cmd, []string{s.th.BasicUser.Email}) + s.Require().NoError(err) + s.Require().Len(printer.GetErrorLines(), 0) + + actualPreferencesUser1, _, err := s.th.SystemAdminClient.GetPreferences(context.Background(), s.th.BasicUser.Id) + s.NoError(err) + s.Require().Len(actualPreferencesUser1, 3) + s.Require().Equal(preference1, actualPreferencesUser1[0]) + s.Require().Equal(preference2, actualPreferencesUser1[1]) + s.Require().Equal(preference3, actualPreferencesUser1[2]) + + // Second user unaffected + actualPreferencesUser2, _, err := s.th.SystemAdminClient.GetPreferences(context.Background(), s.th.BasicUser2.Id) + s.NoError(err) + s.Require().Len(actualPreferencesUser2, 1) + s.Require().Equal(preference4, actualPreferencesUser2[0]) + }) + + s.Run("delete existing preference for single user", func() { + printer.Clean() + + cmd := &cobra.Command{} + cmd.Flags().StringP("category", "c", preference1.Category, "") + cmd.Flags().StringP("name", "n", preference1.Name, "") + + err := preferencesDeleteCmdF(s.th.Client, cmd, []string{s.th.BasicUser.Email}) + s.Require().NoError(err) + s.Require().Len(printer.GetErrorLines(), 0) + + actualPreferencesUser1, _, err := s.th.SystemAdminClient.GetPreferences(context.Background(), s.th.BasicUser.Id) + s.NoError(err) + s.Require().Len(actualPreferencesUser1, 2) + s.Require().Equal(preference2, actualPreferencesUser1[0]) + s.Require().Equal(preference3, actualPreferencesUser1[1]) + + // Second user unaffected + actualPreferencesUser2, _, err := s.th.SystemAdminClient.GetPreferences(context.Background(), s.th.BasicUser2.Id) + s.NoError(err) + s.Require().Len(actualPreferencesUser2, 1) + s.Require().Equal(preference4, actualPreferencesUser2[0]) + }) + + s.Run("delete existing preferences for multiple users as admin", func() { + printer.Clean() + + cmd := &cobra.Command{} + cmd.Flags().StringP("category", "c", preference1.Category, "") + cmd.Flags().StringP("name", "n", preference1.Name, "") + + err := preferencesDeleteCmdF(s.th.SystemAdminClient, cmd, []string{s.th.BasicUser.Email, s.th.BasicUser2.Email}) + s.Require().NoError(err) + s.Require().Len(printer.GetErrorLines(), 0) + + actualPreferencesUser1, _, err := s.th.SystemAdminClient.GetPreferences(context.Background(), s.th.BasicUser.Id) + s.NoError(err) + s.Require().Len(actualPreferencesUser1, 2) + s.Require().Equal(preference2, actualPreferencesUser1[0]) + s.Require().Equal(preference3, actualPreferencesUser1[1]) + + // Second user unaffected + actualPreferencesUser2, _, err := s.th.SystemAdminClient.GetPreferences(context.Background(), s.th.BasicUser2.Id) + s.NoError(err) + s.Require().Len(actualPreferencesUser2, 0) + }) + + s.Run("delete existing preferences for multiple users as non-admin", func() { + printer.Clean() + + cmd := &cobra.Command{} + cmd.Flags().StringP("category", "c", preference1.Category, "") + cmd.Flags().StringP("name", "n", preference1.Name, "") + + err := preferencesDeleteCmdF(s.th.Client, cmd, []string{s.th.BasicUser.Email, s.th.BasicUser2.Email}) + s.Require().Error(err) + }) +} diff --git a/server/cmd/mmctl/docs/mmctl_user.rst b/server/cmd/mmctl/docs/mmctl_user.rst index 383e9886f3..0ae5f153ef 100644 --- a/server/cmd/mmctl/docs/mmctl_user.rst +++ b/server/cmd/mmctl/docs/mmctl_user.rst @@ -49,6 +49,7 @@ SEE ALSO * `mmctl user invite `_ - Send user an email invite to a team. * `mmctl user list `_ - List users * `mmctl user migrate-auth `_ - Mass migrate user accounts authentication type +* `mmctl user preference `_ - Manage user preferences * `mmctl user promote `_ - Promote guests to users * `mmctl user reset-password `_ - Send users an email to reset their password * `mmctl user resetmfa `_ - Turn off MFA diff --git a/server/cmd/mmctl/docs/mmctl_user_preference.rst b/server/cmd/mmctl/docs/mmctl_user_preference.rst new file mode 100644 index 0000000000..939c6638ad --- /dev/null +++ b/server/cmd/mmctl/docs/mmctl_user_preference.rst @@ -0,0 +1,44 @@ +.. _mmctl_user_preference: + +mmctl user preference +--------------------- + +Manage user preferences + +Synopsis +~~~~~~~~ + + +Manage user preferences + +Options +~~~~~~~ + +:: + + -h, --help help for preference + +Options inherited from parent commands +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + --config string path to the configuration file (default "$XDG_CONFIG_HOME/mmctl/config") + --disable-pager disables paged output + --insecure-sha1-intermediate allows to use insecure TLS protocols, such as SHA-1 + --insecure-tls-version allows to use TLS versions 1.0 and 1.1 + --json the output format will be in json format + --local allows communicating with the server through a unix socket + --quiet prevent mmctl to generate output for the commands + --strict will only run commands if the mmctl version matches the server one + --suppress-warnings disables printing warning messages + +SEE ALSO +~~~~~~~~ + +* `mmctl user `_ - Management of users +* `mmctl user preference delete `_ - Delete a specific user preference +* `mmctl user preference get `_ - Get a specific user preference +* `mmctl user preference list `_ - List user preferences +* `mmctl user preference set `_ - Set a specific user preference + diff --git a/server/cmd/mmctl/docs/mmctl_user_preference_delete.rst b/server/cmd/mmctl/docs/mmctl_user_preference_delete.rst new file mode 100644 index 0000000000..620f1a49fe --- /dev/null +++ b/server/cmd/mmctl/docs/mmctl_user_preference_delete.rst @@ -0,0 +1,53 @@ +.. _mmctl_user_preference_delete: + +mmctl user preference delete +---------------------------- + +Delete a specific user preference + +Synopsis +~~~~~~~~ + + +Delete a specific user preference + +:: + + mmctl user preference delete --category [category] --name [name] [users] [flags] + +Examples +~~~~~~~~ + +:: + + preference delete --category display_settings --name use_military_time user@example.com + +Options +~~~~~~~ + +:: + + -c, --category string The category of the preference + -h, --help help for delete + -n, --name string The name of the preference + +Options inherited from parent commands +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + --config string path to the configuration file (default "$XDG_CONFIG_HOME/mmctl/config") + --disable-pager disables paged output + --insecure-sha1-intermediate allows to use insecure TLS protocols, such as SHA-1 + --insecure-tls-version allows to use TLS versions 1.0 and 1.1 + --json the output format will be in json format + --local allows communicating with the server through a unix socket + --quiet prevent mmctl to generate output for the commands + --strict will only run commands if the mmctl version matches the server one + --suppress-warnings disables printing warning messages + +SEE ALSO +~~~~~~~~ + +* `mmctl user preference `_ - Manage user preferences + diff --git a/server/cmd/mmctl/docs/mmctl_user_preference_get.rst b/server/cmd/mmctl/docs/mmctl_user_preference_get.rst new file mode 100644 index 0000000000..215235922c --- /dev/null +++ b/server/cmd/mmctl/docs/mmctl_user_preference_get.rst @@ -0,0 +1,53 @@ +.. _mmctl_user_preference_get: + +mmctl user preference get +------------------------- + +Get a specific user preference + +Synopsis +~~~~~~~~ + + +Get a specific user preference + +:: + + mmctl user preference get --category [category] --name [name] [users] [flags] + +Examples +~~~~~~~~ + +:: + + preference get --category display_settings --name use_military_time user@example.com + +Options +~~~~~~~ + +:: + + -c, --category string The category of the preference + -h, --help help for get + -n, --name string The name of the preference + +Options inherited from parent commands +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + --config string path to the configuration file (default "$XDG_CONFIG_HOME/mmctl/config") + --disable-pager disables paged output + --insecure-sha1-intermediate allows to use insecure TLS protocols, such as SHA-1 + --insecure-tls-version allows to use TLS versions 1.0 and 1.1 + --json the output format will be in json format + --local allows communicating with the server through a unix socket + --quiet prevent mmctl to generate output for the commands + --strict will only run commands if the mmctl version matches the server one + --suppress-warnings disables printing warning messages + +SEE ALSO +~~~~~~~~ + +* `mmctl user preference `_ - Manage user preferences + diff --git a/server/cmd/mmctl/docs/mmctl_user_preference_list.rst b/server/cmd/mmctl/docs/mmctl_user_preference_list.rst new file mode 100644 index 0000000000..1b6518b6a7 --- /dev/null +++ b/server/cmd/mmctl/docs/mmctl_user_preference_list.rst @@ -0,0 +1,52 @@ +.. _mmctl_user_preference_list: + +mmctl user preference list +-------------------------- + +List user preferences + +Synopsis +~~~~~~~~ + + +List user preferences + +:: + + mmctl user preference list [--category category] [users] [flags] + +Examples +~~~~~~~~ + +:: + + preference list user@example.com + +Options +~~~~~~~ + +:: + + -c, --category string The optional category by which to filter + -h, --help help for list + +Options inherited from parent commands +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + --config string path to the configuration file (default "$XDG_CONFIG_HOME/mmctl/config") + --disable-pager disables paged output + --insecure-sha1-intermediate allows to use insecure TLS protocols, such as SHA-1 + --insecure-tls-version allows to use TLS versions 1.0 and 1.1 + --json the output format will be in json format + --local allows communicating with the server through a unix socket + --quiet prevent mmctl to generate output for the commands + --strict will only run commands if the mmctl version matches the server one + --suppress-warnings disables printing warning messages + +SEE ALSO +~~~~~~~~ + +* `mmctl user preference `_ - Manage user preferences + diff --git a/server/cmd/mmctl/docs/mmctl_user_preference_set.rst b/server/cmd/mmctl/docs/mmctl_user_preference_set.rst new file mode 100644 index 0000000000..16db258e4d --- /dev/null +++ b/server/cmd/mmctl/docs/mmctl_user_preference_set.rst @@ -0,0 +1,54 @@ +.. _mmctl_user_preference_set: + +mmctl user preference set +------------------------- + +Set a specific user preference + +Synopsis +~~~~~~~~ + + +Set a specific user preference + +:: + + mmctl user preference set --category [category] --name [name] --value [value] [users] [flags] + +Examples +~~~~~~~~ + +:: + + preference set --category display_settings --name use_military_time --value true user@example.com + +Options +~~~~~~~ + +:: + + -c, --category string The category of the preference + -h, --help help for set + -n, --name string The name of the preference + -v, --value string The value of the preference + +Options inherited from parent commands +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + --config string path to the configuration file (default "$XDG_CONFIG_HOME/mmctl/config") + --disable-pager disables paged output + --insecure-sha1-intermediate allows to use insecure TLS protocols, such as SHA-1 + --insecure-tls-version allows to use TLS versions 1.0 and 1.1 + --json the output format will be in json format + --local allows communicating with the server through a unix socket + --quiet prevent mmctl to generate output for the commands + --strict will only run commands if the mmctl version matches the server one + --suppress-warnings disables printing warning messages + +SEE ALSO +~~~~~~~~ + +* `mmctl user preference `_ - Manage user preferences + diff --git a/server/cmd/mmctl/mocks/client_mock.go b/server/cmd/mmctl/mocks/client_mock.go index 4584494a4a..c67b5a9e09 100644 --- a/server/cmd/mmctl/mocks/client_mock.go +++ b/server/cmd/mmctl/mocks/client_mock.go @@ -417,6 +417,21 @@ func (mr *MockClientMockRecorder) DeleteOutgoingWebhook(arg0, arg1 interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOutgoingWebhook", reflect.TypeOf((*MockClient)(nil).DeleteOutgoingWebhook), arg0, arg1) } +// DeletePreferences mocks base method. +func (m *MockClient) DeletePreferences(arg0 context.Context, arg1 string, arg2 model.Preferences) (*model.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeletePreferences", arg0, arg1, arg2) + ret0, _ := ret[0].(*model.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeletePreferences indicates an expected call of DeletePreferences. +func (mr *MockClientMockRecorder) DeletePreferences(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePreferences", reflect.TypeOf((*MockClient)(nil).DeletePreferences), arg0, arg1, arg2) +} + // DemoteUserToGuest mocks base method. func (m *MockClient) DemoteUserToGuest(arg0 context.Context, arg1 string) (*model.Response, error) { m.ctrl.T.Helper() @@ -1103,6 +1118,54 @@ func (mr *MockClientMockRecorder) GetPostsSince(arg0, arg1, arg2, arg3 interface return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPostsSince", reflect.TypeOf((*MockClient)(nil).GetPostsSince), arg0, arg1, arg2, arg3) } +// GetPreferenceByCategoryAndName mocks base method. +func (m *MockClient) GetPreferenceByCategoryAndName(arg0 context.Context, arg1, arg2, arg3 string) (*model.Preference, *model.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPreferenceByCategoryAndName", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(*model.Preference) + ret1, _ := ret[1].(*model.Response) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetPreferenceByCategoryAndName indicates an expected call of GetPreferenceByCategoryAndName. +func (mr *MockClientMockRecorder) GetPreferenceByCategoryAndName(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPreferenceByCategoryAndName", reflect.TypeOf((*MockClient)(nil).GetPreferenceByCategoryAndName), arg0, arg1, arg2, arg3) +} + +// GetPreferences mocks base method. +func (m *MockClient) GetPreferences(arg0 context.Context, arg1 string) (model.Preferences, *model.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPreferences", arg0, arg1) + ret0, _ := ret[0].(model.Preferences) + ret1, _ := ret[1].(*model.Response) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetPreferences indicates an expected call of GetPreferences. +func (mr *MockClientMockRecorder) GetPreferences(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPreferences", reflect.TypeOf((*MockClient)(nil).GetPreferences), arg0, arg1) +} + +// GetPreferencesByCategory mocks base method. +func (m *MockClient) GetPreferencesByCategory(arg0 context.Context, arg1, arg2 string) (model.Preferences, *model.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPreferencesByCategory", arg0, arg1, arg2) + ret0, _ := ret[0].(model.Preferences) + ret1, _ := ret[1].(*model.Response) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetPreferencesByCategory indicates an expected call of GetPreferencesByCategory. +func (mr *MockClientMockRecorder) GetPreferencesByCategory(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPreferencesByCategory", reflect.TypeOf((*MockClient)(nil).GetPreferencesByCategory), arg0, arg1, arg2) +} + // GetPrivateChannelsForTeam mocks base method. func (m *MockClient) GetPrivateChannelsForTeam(arg0 context.Context, arg1 string, arg2, arg3 int, arg4 string) ([]*model.Channel, *model.Response, error) { m.ctrl.T.Helper() @@ -2010,6 +2073,21 @@ func (mr *MockClientMockRecorder) UpdateOutgoingWebhook(arg0, arg1 interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOutgoingWebhook", reflect.TypeOf((*MockClient)(nil).UpdateOutgoingWebhook), arg0, arg1) } +// UpdatePreferences mocks base method. +func (m *MockClient) UpdatePreferences(arg0 context.Context, arg1 string, arg2 model.Preferences) (*model.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdatePreferences", arg0, arg1, arg2) + ret0, _ := ret[0].(*model.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdatePreferences indicates an expected call of UpdatePreferences. +func (mr *MockClientMockRecorder) UpdatePreferences(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePreferences", reflect.TypeOf((*MockClient)(nil).UpdatePreferences), arg0, arg1, arg2) +} + // UpdateTeam mocks base method. func (m *MockClient) UpdateTeam(arg0 context.Context, arg1 *model.Team) (*model.Team, *model.Response, error) { m.ctrl.T.Helper()