From 3dae305dc73c0d1842c36d39a151257739964fee Mon Sep 17 00:00:00 2001 From: Ben Schumacher Date: Mon, 22 Apr 2024 12:19:53 +0200 Subject: [PATCH] [MM-56000] Add LDAP job command to mmctl (#25633) --- server/cmd/mmctl/commands/completion.go | 2 +- server/cmd/mmctl/commands/ldap.go | 62 +++++++ server/cmd/mmctl/commands/ldap_e2e_test.go | 109 +++++++++++- server/cmd/mmctl/commands/ldap_test.go | 162 +++++++++++++++++- server/cmd/mmctl/docs/mmctl_ldap.rst | 1 + server/cmd/mmctl/docs/mmctl_ldap_job.rst | 42 +++++ server/cmd/mmctl/docs/mmctl_ldap_job_list.rst | 54 ++++++ server/cmd/mmctl/docs/mmctl_ldap_job_show.rst | 51 ++++++ 8 files changed, 476 insertions(+), 7 deletions(-) create mode 100644 server/cmd/mmctl/docs/mmctl_ldap_job.rst create mode 100644 server/cmd/mmctl/docs/mmctl_ldap_job_list.rst create mode 100644 server/cmd/mmctl/docs/mmctl_ldap_job_show.rst diff --git a/server/cmd/mmctl/commands/completion.go b/server/cmd/mmctl/commands/completion.go index a5c263018a..201cc9a6e0 100644 --- a/server/cmd/mmctl/commands/completion.go +++ b/server/cmd/mmctl/commands/completion.go @@ -213,7 +213,7 @@ func noCompletion(_ *cobra.Command, _ []string, _ string) ([]string, cobra.Shell type validateArgsFn func(ctx context.Context, c client.Client, cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) -func validateArgsWithClient(fn validateArgsFn) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { //nolint:unused // Remove with https://github.com/mattermost/mattermost/pull/25633 +func validateArgsWithClient(fn validateArgsFn) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx, cancel := context.WithTimeout(context.Background(), shellCompleteTimeout) defer cancel() diff --git a/server/cmd/mmctl/commands/ldap.go b/server/cmd/mmctl/commands/ldap.go index f19eecdbf4..bd504f6ddb 100644 --- a/server/cmd/mmctl/commands/ldap.go +++ b/server/cmd/mmctl/commands/ldap.go @@ -5,10 +5,12 @@ package commands import ( "context" + "fmt" "net/http" "github.com/spf13/cobra" + "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/v8/cmd/mmctl/client" "github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer" ) @@ -40,11 +42,47 @@ var LdapIDMigrate = &cobra.Command{ RunE: withClient(ldapIDMigrateCmdF), } +var LdapJobCmd = &cobra.Command{ + Use: "job", + Short: "List and show LDAP sync jobs", +} + +var LdapJobListCmd = &cobra.Command{ + Use: "list", + Example: " ldap job list", + Short: "List LDAP sync jobs", + // Alisases cause error in zsh. Supposedly, completion V2 will fix that: https://github.com/spf13/cobra/pull/1146 + // https://mattermost.atlassian.net/browse/MM-57062 + // Aliases: []string{"ls"}, + Args: cobra.NoArgs, + ValidArgsFunction: noCompletion, + RunE: withClient(ldapJobListCmdF), +} + +var LdapJobShowCmd = &cobra.Command{ + Use: "show [ldapJobID]", + Example: " import ldap show f3d68qkkm7n8xgsfxwuo498rah", + Short: "Show LDAP sync job", + ValidArgsFunction: validateArgsWithClient(ldapJobShowCompletionF), + RunE: withClient(ldapJobShowCmdF), +} + func init() { LdapSyncCmd.Flags().Bool("include-removed-members", false, "Include members who left or were removed from a group-synced team/channel") + + LdapJobListCmd.Flags().Int("page", 0, "Page number to fetch for the list of import jobs") + LdapJobListCmd.Flags().Int("per-page", 200, "Number of import jobs to be fetched") + LdapJobListCmd.Flags().Bool("all", false, "Fetch all import jobs. --page flag will be ignore if provided") + + LdapJobCmd.AddCommand( + LdapJobListCmd, + LdapJobShowCmd, + ) + LdapCmd.AddCommand( LdapSyncCmd, LdapIDMigrate, + LdapJobCmd, ) RootCmd.AddCommand(LdapCmd) } @@ -81,3 +119,27 @@ func ldapIDMigrateCmdF(c client.Client, cmd *cobra.Command, args []string) error return nil } + +func ldapJobListCmdF(c client.Client, command *cobra.Command, args []string) error { + return jobListCmdF(c, command, model.JobTypeLdapSync) +} + +func ldapJobShowCmdF(c client.Client, command *cobra.Command, args []string) error { + job, _, err := c.GetJob(context.TODO(), args[0]) + if err != nil { + return fmt.Errorf("failed to get LDAP sync job: %w", err) + } + + printJob(job) + + return nil +} + +func ldapJobShowCompletionF(ctx context.Context, c client.Client, cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return fetchAndComplete( + func(ctx context.Context, c client.Client, page, perPage int) ([]*model.Job, *model.Response, error) { + return c.GetJobsByType(ctx, model.JobTypeLdapSync, page, perPage) + }, + func(t *model.Job) []string { return []string{t.Id} }, + )(ctx, c, cmd, args, toComplete) +} diff --git a/server/cmd/mmctl/commands/ldap_e2e_test.go b/server/cmd/mmctl/commands/ldap_e2e_test.go index be221199c4..77cfc4d4b6 100644 --- a/server/cmd/mmctl/commands/ldap_e2e_test.go +++ b/server/cmd/mmctl/commands/ldap_e2e_test.go @@ -52,7 +52,7 @@ func (s *MmctlE2ETestSuite) TestLdapSyncCmd() { s.SetupEnterpriseTestHelper().InitBasic() configForLdap(s.th) - s.Run("MM-T3971 Should not allow regular user to sync LDAP groups", func() { + s.Run("MM-T3971 Should not allow regular user to start LDAP sync job", func() { printer.Clean() err := ldapSyncCmdF(s.th.Client, &cobra.Command{}, nil) @@ -61,7 +61,7 @@ func (s *MmctlE2ETestSuite) TestLdapSyncCmd() { s.Require().Len(printer.GetErrorLines(), 0) }) - s.RunForSystemAdminAndLocal("MM-T2529 Should sync LDAP groups", func(c client.Client) { + s.RunForSystemAdminAndLocal("MM-T2529 Should start LDAP sync job", func(c client.Client) { printer.Clean() jobs, appErr := s.th.App.GetJobsByTypePage(s.th.Context, model.JobTypeLdapSync, 0, 100) @@ -127,3 +127,108 @@ func (s *MmctlE2ETestSuite) TestLdapIDMigrateCmd() { s.Require().Equal("Dev1", *updatedUser.AuthData) }) } + +func (s *MmctlE2ETestSuite) TestLdapJobListCmd() { + s.SetupEnterpriseTestHelper().InitBasic() + configForLdap(s.th) + + s.Run("Should not allow regular user to list LDAP groups", func() { + printer.Clean() + + err := ldapJobListCmdF(s.th.Client, &cobra.Command{}, nil) + s.Require().Error(err) + s.Require().Len(printer.GetLines(), 0) + s.Require().Len(printer.GetErrorLines(), 0) + }) + + s.RunForSystemAdminAndLocal("No LDAP jobs", func(c client.Client) { + printer.Clean() + + cmd := &cobra.Command{} + cmd.Flags().Int("page", 0, "") + cmd.Flags().Int("per-page", 200, "") + cmd.Flags().Bool("all", false, "") + + err := ldapJobListCmdF(c, cmd, nil) + s.Require().Nil(err) + s.Require().Len(printer.GetLines(), 1) + s.Require().Empty(printer.GetErrorLines()) + s.Equal("No jobs found", printer.GetLines()[0]) + }) + + s.RunForSystemAdminAndLocal("get some LDAP sync jobs", func(c client.Client) { + printer.Clean() + + cmd := &cobra.Command{} + perPage := 2 + cmd.Flags().Int("page", 0, "") + cmd.Flags().Int("per-page", perPage, "") + cmd.Flags().Bool("all", false, "") + + _, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{ + Type: model.JobTypeLdapSync, + }) + s.Require().Nil(appErr) + + time.Sleep(time.Millisecond) + + job2, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{ + Type: model.JobTypeLdapSync, + }) + s.Require().Nil(appErr) + + time.Sleep(time.Millisecond) + + job3, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{ + Type: model.JobTypeLdapSync, + }) + s.Require().Nil(appErr) + + err := ldapJobListCmdF(c, cmd, nil) + s.Require().Nil(err) + s.Require().Len(printer.GetLines(), perPage) + s.Require().Empty(printer.GetErrorLines()) + s.Require().Equal(job3, printer.GetLines()[0].(*model.Job)) + s.Require().Equal(job2, printer.GetLines()[1].(*model.Job)) + }) +} + +func (s *MmctlE2ETestSuite) TestLdapJobShowCmdF() { + s.SetupEnterpriseTestHelper().InitBasic() + configForLdap(s.th) + + job, appErr := s.th.App.CreateJob(s.th.Context, &model.Job{ + Type: model.JobTypeLdapSync, + }) + s.Require().Nil(appErr) + + time.Sleep(time.Millisecond) + + s.Run("no permissions", func() { + printer.Clean() + + err := ldapJobShowCmdF(s.th.Client, &cobra.Command{}, []string{job.Id}) + s.Require().EqualError(err, "failed to get LDAP sync job: You do not have the appropriate permissions.") + s.Require().Empty(printer.GetLines()) + s.Require().Empty(printer.GetErrorLines()) + }) + + s.RunForSystemAdminAndLocal("not found", func(c client.Client) { + printer.Clean() + + err := ldapJobShowCmdF(c, &cobra.Command{}, []string{model.NewId()}) + s.Require().ErrorContains(err, "failed to get LDAP sync job: Unable to get the job.") + s.Require().Empty(printer.GetLines()) + s.Require().Empty(printer.GetErrorLines()) + }) + + s.RunForSystemAdminAndLocal("found", func(c client.Client) { + printer.Clean() + + err := ldapJobShowCmdF(c, &cobra.Command{}, []string{job.Id}) + s.Require().Nil(err) + s.Require().Empty(printer.GetErrorLines()) + s.Require().Len(printer.GetLines(), 1) + s.Require().Equal(job, printer.GetLines()[0].(*model.Job)) + }) +} diff --git a/server/cmd/mmctl/commands/ldap_test.go b/server/cmd/mmctl/commands/ldap_test.go index dc8dbea13a..a853dd1149 100644 --- a/server/cmd/mmctl/commands/ldap_test.go +++ b/server/cmd/mmctl/commands/ldap_test.go @@ -5,14 +5,14 @@ package commands import ( "context" + "fmt" "net/http" - "github.com/mattermost/mattermost/server/public/model" "github.com/pkg/errors" - - "github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer" - "github.com/spf13/cobra" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer" ) func (s *MmctlUnitTestSuite) TestLdapSyncCmd() { @@ -114,3 +114,157 @@ func (s *MmctlUnitTestSuite) TestLdapMigrateID() { s.Require().Len(printer.GetLines(), 0) }) } + +func (s *MmctlUnitTestSuite) TestLdapJobListCmdF() { + s.Run("no LDAP jobs", func() { + printer.Clean() + var mockJobs []*model.Job + + cmd := &cobra.Command{} + perPage := 10 + cmd.Flags().Int("page", 0, "") + cmd.Flags().Int("per-page", perPage, "") + cmd.Flags().Bool("all", false, "") + + s.client. + EXPECT(). + GetJobsByType(context.TODO(), model.JobTypeLdapSync, 0, perPage). + Return(mockJobs, &model.Response{}, nil). + Times(1) + + err := ldapJobListCmdF(s.client, cmd, nil) + s.Require().Nil(err) + s.Len(printer.GetLines(), 1) + s.Empty(printer.GetErrorLines()) + s.Equal("No jobs found", printer.GetLines()[0]) + }) + + s.Run("some LDAP jobs", func() { + printer.Clean() + mockJobs := []*model.Job{ + { + Id: model.NewId(), + }, + { + Id: model.NewId(), + }, + { + Id: model.NewId(), + }, + } + + cmd := &cobra.Command{} + perPage := 3 + cmd.Flags().Int("page", 0, "") + cmd.Flags().Int("per-page", perPage, "") + cmd.Flags().Bool("all", false, "") + + s.client. + EXPECT(). + GetJobsByType(context.TODO(), model.JobTypeLdapSync, 0, perPage). + Return(mockJobs, &model.Response{}, nil). + Times(1) + + err := ldapJobListCmdF(s.client, cmd, nil) + s.Require().Nil(err) + s.Len(printer.GetLines(), len(mockJobs)) + s.Empty(printer.GetErrorLines()) + for i, line := range printer.GetLines() { + s.Equal(mockJobs[i], line.(*model.Job)) + } + }) +} + +func (s *MmctlUnitTestSuite) TestLdapJobShowCmdF() { + s.Run("not found", func() { + printer.Clean() + + jobID := model.NewId() + + s.client. + EXPECT(). + GetJob(context.TODO(), jobID). + Return(nil, &model.Response{StatusCode: http.StatusNotFound}, errors.New("not found")). + Times(1) + + err := ldapJobShowCmdF(s.client, &cobra.Command{}, []string{jobID}) + s.Require().NotNil(err) + s.Empty(printer.GetLines()) + s.Empty(printer.GetErrorLines()) + }) + + s.Run("found", func() { + printer.Clean() + mockJob := &model.Job{ + Id: model.NewId(), + } + + s.client. + EXPECT(). + GetJob(context.TODO(), mockJob.Id). + Return(mockJob, &model.Response{}, nil). + Times(1) + + err := ldapJobShowCmdF(s.client, &cobra.Command{}, []string{mockJob.Id}) + s.Require().Nil(err) + s.Len(printer.GetLines(), 1) + s.Empty(printer.GetErrorLines()) + s.Equal(mockJob, printer.GetLines()[0].(*model.Job)) + }) + + s.Run("shell completion", func() { + s.Run("no match for empty argument", func() { + r, dir := ldapJobShowCompletionF(context.Background(), s.client, nil, nil, "") + s.Equal(cobra.ShellCompDirectiveNoFileComp, dir) + s.Equal([]string{}, r) + }) + + s.Run("one element matches", func() { + mockJobs := []*model.Job{ + { + Id: "0_id", + }, + { + Id: "1_id", + }, + { + Id: "2_id", + }, + } + + s.client. + EXPECT(). + GetJobsByType(context.Background(), model.JobTypeLdapSync, 0, DefaultPageSize). + Return(mockJobs, &model.Response{}, nil). + Times(1) + + r, dir := ldapJobShowCompletionF(context.Background(), s.client, nil, nil, "1") + s.Equal(cobra.ShellCompDirectiveNoFileComp, dir) + s.Equal([]string{"1_id"}, r) + }) + + s.Run("more elements then the limit match", func() { + var mockJobs []*model.Job + for i := 0; i < 100; i++ { + mockJobs = append(mockJobs, &model.Job{ + Id: fmt.Sprintf("id_%d", i), + }) + } + + var expected []string + for i := 0; i < shellCompletionMaxItems; i++ { + expected = append(expected, fmt.Sprintf("id_%d", i)) + } + + s.client. + EXPECT(). + GetJobsByType(context.Background(), model.JobTypeLdapSync, 0, DefaultPageSize). + Return(mockJobs, &model.Response{}, nil). + Times(1) + + r, dir := ldapJobShowCompletionF(context.Background(), s.client, nil, nil, "id_") + s.Equal(cobra.ShellCompDirectiveNoFileComp, dir) + s.Equal(expected, r) + }) + }) +} diff --git a/server/cmd/mmctl/docs/mmctl_ldap.rst b/server/cmd/mmctl/docs/mmctl_ldap.rst index 255f5aa9d7..97eaa3c0cb 100644 --- a/server/cmd/mmctl/docs/mmctl_ldap.rst +++ b/server/cmd/mmctl/docs/mmctl_ldap.rst @@ -38,5 +38,6 @@ SEE ALSO * `mmctl `_ - Remote client for the Open Source, self-hosted Slack-alternative * `mmctl ldap idmigrate `_ - Migrate LDAP IdAttribute to new value +* `mmctl ldap job `_ - List and show LDAP sync jobs * `mmctl ldap sync `_ - Synchronize now diff --git a/server/cmd/mmctl/docs/mmctl_ldap_job.rst b/server/cmd/mmctl/docs/mmctl_ldap_job.rst new file mode 100644 index 0000000000..8e45b687ca --- /dev/null +++ b/server/cmd/mmctl/docs/mmctl_ldap_job.rst @@ -0,0 +1,42 @@ +.. _mmctl_ldap_job: + +mmctl ldap job +-------------- + +List and show LDAP sync jobs + +Synopsis +~~~~~~~~ + + +List and show LDAP sync jobs + +Options +~~~~~~~ + +:: + + -h, --help help for job + +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 ldap `_ - LDAP related utilities +* `mmctl ldap job list `_ - List LDAP sync jobs +* `mmctl ldap job show `_ - Show LDAP sync job + diff --git a/server/cmd/mmctl/docs/mmctl_ldap_job_list.rst b/server/cmd/mmctl/docs/mmctl_ldap_job_list.rst new file mode 100644 index 0000000000..8d0f5c5f54 --- /dev/null +++ b/server/cmd/mmctl/docs/mmctl_ldap_job_list.rst @@ -0,0 +1,54 @@ +.. _mmctl_ldap_job_list: + +mmctl ldap job list +------------------- + +List LDAP sync jobs + +Synopsis +~~~~~~~~ + + +List LDAP sync jobs + +:: + + mmctl ldap job list [flags] + +Examples +~~~~~~~~ + +:: + + ldap job list + +Options +~~~~~~~ + +:: + + --all Fetch all import jobs. --page flag will be ignore if provided + -h, --help help for list + --page int Page number to fetch for the list of import jobs + --per-page int Number of import jobs to be fetched (default 200) + +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 ldap job `_ - List and show LDAP sync jobs + diff --git a/server/cmd/mmctl/docs/mmctl_ldap_job_show.rst b/server/cmd/mmctl/docs/mmctl_ldap_job_show.rst new file mode 100644 index 0000000000..02cd62bb0c --- /dev/null +++ b/server/cmd/mmctl/docs/mmctl_ldap_job_show.rst @@ -0,0 +1,51 @@ +.. _mmctl_ldap_job_show: + +mmctl ldap job show +------------------- + +Show LDAP sync job + +Synopsis +~~~~~~~~ + + +Show LDAP sync job + +:: + + mmctl ldap job show [ldapJobID] [flags] + +Examples +~~~~~~~~ + +:: + + import ldap show f3d68qkkm7n8xgsfxwuo498rah + +Options +~~~~~~~ + +:: + + -h, --help help for show + +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 ldap job `_ - List and show LDAP sync jobs +