From e0a36cf60a2d5d0c81fb6f9df13399498aeb5136 Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Tue, 5 Dec 2023 12:25:35 -0500 Subject: [PATCH] mmctl: add listing oauth apps (#25497) * mmctl: add listing oauth apps * Fix tests * Update mmctl docs * Fix tests * Update server/cmd/mmctl/commands/oauth.go Co-authored-by: Ibrahim Serdar Acikgoz * Update server/cmd/mmctl/commands/oauth.go Co-authored-by: Ibrahim Serdar Acikgoz * Add paging * Update docs --------- Co-authored-by: Ibrahim Serdar Acikgoz --- server/cmd/mmctl/client/client.go | 1 + server/cmd/mmctl/commands/oauth.go | 83 +++++++++++++ server/cmd/mmctl/commands/oauth_test.go | 138 +++++++++++++++++++++ server/cmd/mmctl/docs/mmctl.rst | 1 + server/cmd/mmctl/docs/mmctl_oauth.rst | 41 ++++++ server/cmd/mmctl/docs/mmctl_oauth_list.rst | 53 ++++++++ server/cmd/mmctl/mocks/client_mock.go | 16 +++ 7 files changed, 333 insertions(+) create mode 100644 server/cmd/mmctl/commands/oauth.go create mode 100644 server/cmd/mmctl/commands/oauth_test.go create mode 100644 server/cmd/mmctl/docs/mmctl_oauth.rst create mode 100644 server/cmd/mmctl/docs/mmctl_oauth_list.rst diff --git a/server/cmd/mmctl/client/client.go b/server/cmd/mmctl/client/client.go index 0c6b52a49d66..db21e3c9f54b 100644 --- a/server/cmd/mmctl/client/client.go +++ b/server/cmd/mmctl/client/client.go @@ -149,4 +149,5 @@ type Client interface { GeneratePresignedURL(ctx context.Context, name string) (*model.PresignURLResponse, *model.Response, error) 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) } diff --git a/server/cmd/mmctl/commands/oauth.go b/server/cmd/mmctl/commands/oauth.go new file mode 100644 index 000000000000..330a5af34e0b --- /dev/null +++ b/server/cmd/mmctl/commands/oauth.go @@ -0,0 +1,83 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package commands + +import ( + "context" + "fmt" + + "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" + + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var OAuthCmd = &cobra.Command{ + Use: "oauth", + Short: "Management of OAuth2 apps", +} + +var ListOAuthAppsCmd = &cobra.Command{ + Use: "list", + Short: "List OAuth2 apps", + Long: "list all OAuth2 apps", + Example: " oauth list", + RunE: withClient(listOAuthAppsCmdF), + Args: cobra.NoArgs, +} + +func listOAuthAppsCmdF(c client.Client, command *cobra.Command, args []string) error { + page, err := command.Flags().GetInt("page") + if err != nil { + return err + } + perPage, err := command.Flags().GetInt("per-page") + if err != nil { + return err + } + + apps, _, err := c.GetOAuthApps(context.Background(), page, perPage) + if err != nil { + return errors.Wrap(err, "Failed to fetch oauth2 apps") + } + + userIds := make([]string, len(apps)) + for i := range apps { + userIds[i] = apps[i].CreatorId + } + + users, _, err := c.GetUsersByIds(context.Background(), userIds) + if err != nil { + return errors.Wrap(err, "Failed to fetch users for oauth2 apps") + } + + usersByID := map[string]*model.User{} + for _, user := range users { + usersByID[user.Id] = user + } + + for _, app := range apps { + ownerName := app.CreatorId + if owner, ok := usersByID[app.CreatorId]; ok { + ownerName = owner.Username + } + printer.PrintT(fmt.Sprintf("{{.Id}}: {{.Name}} (Created by %s)", ownerName), app) + } + + return nil +} + +func init() { + ListOAuthAppsCmd.Flags().Int("page", 0, "Page number to fetch for the list of OAuth2 apps") + ListOAuthAppsCmd.Flags().Int("per-page", 200, "Number of OAuth2 apps to be fetched") + + OAuthCmd.AddCommand( + ListOAuthAppsCmd, + ) + + RootCmd.AddCommand(OAuthCmd) +} diff --git a/server/cmd/mmctl/commands/oauth_test.go b/server/cmd/mmctl/commands/oauth_test.go new file mode 100644 index 000000000000..5631c7cb0538 --- /dev/null +++ b/server/cmd/mmctl/commands/oauth_test.go @@ -0,0 +1,138 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package commands + +import ( + "context" + "strconv" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/pkg/errors" + + "github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer" + + "github.com/spf13/cobra" +) + +func (s *MmctlUnitTestSuite) TestListOAuthAppsCmd() { + oauthAppID := "oauthAppID" + oauthAppName := "oauthAppName" + userID := "userID" + + cmd := &cobra.Command{} + cmd.Flags().Int("page", 0, "") + cmd.Flags().Int("per-page", 200, "") + + s.Run("Listing oauth apps", func() { + printer.Clean() + + mockOAuthApp := model.OAuthApp{ + Id: oauthAppID, + Name: oauthAppName, + CreatorId: userID, + } + mockUser := model.User{Id: mockOAuthApp.CreatorId, Username: "mockuser"} + + s.client. + EXPECT(). + GetOAuthApps(context.Background(), 0, 200). + Return([]*model.OAuthApp{&mockOAuthApp}, &model.Response{}, nil). + Times(1) + + s.client. + EXPECT(). + GetUsersByIds(context.Background(), []string{mockOAuthApp.CreatorId}). + Return([]*model.User{&mockUser}, &model.Response{}, nil). + Times(1) + + err := listOAuthAppsCmdF(s.client, cmd, []string{}) + s.Require().Nil(err) + s.Len(printer.GetLines(), 1) + s.Len(printer.GetErrorLines(), 0) + s.Require().Equal(&mockOAuthApp, printer.GetLines()[0]) + }) + + s.Run("Listing oauth apps with paging", func() { + printer.Clean() + + mockOAuthApp := model.OAuthApp{ + Id: oauthAppID, + Name: oauthAppName, + CreatorId: userID, + } + mockUser := model.User{Id: mockOAuthApp.CreatorId, Username: "mockuser"} + + pageCmd := &cobra.Command{} + pageCmd.Flags().Int("page", 0, "") + pageCmd.Flags().Int("per-page", 200, "") + + page := 1 + perPage := 2 + _ = pageCmd.Flags().Set("page", strconv.Itoa(page)) + _ = pageCmd.Flags().Set("per-page", strconv.Itoa(perPage)) + + s.client. + EXPECT(). + GetOAuthApps(context.Background(), 1, 2). + Return([]*model.OAuthApp{&mockOAuthApp}, &model.Response{}, nil). + Times(1) + + s.client. + EXPECT(). + GetUsersByIds(context.Background(), []string{mockOAuthApp.CreatorId}). + Return([]*model.User{&mockUser}, &model.Response{}, nil). + Times(1) + + err := listOAuthAppsCmdF(s.client, pageCmd, []string{}) + s.Require().Nil(err) + s.Len(printer.GetLines(), 1) + s.Len(printer.GetErrorLines(), 0) + s.Require().Equal(&mockOAuthApp, printer.GetLines()[0]) + }) + + s.Run("Unable to list oauth apps", func() { + printer.Clean() + + mockError := errors.New("mock error") + + s.client. + EXPECT(). + GetOAuthApps(context.Background(), 0, 200). + Return(nil, &model.Response{}, mockError). + Times(1) + + err := listOAuthAppsCmdF(s.client, cmd, []string{}) + s.Require().NotNil(err) + s.Len(printer.GetLines(), 0) + s.EqualError(err, "Failed to fetch oauth2 apps: mock error") + }) + + s.Run("Unable to get users for oauth apps", func() { + printer.Clean() + + mockOAuthApp := model.OAuthApp{ + Id: oauthAppID, + Name: oauthAppName, + CreatorId: userID, + } + mockError := errors.New("mock error") + + s.client. + EXPECT(). + GetOAuthApps(context.Background(), 0, 200). + Return([]*model.OAuthApp{&mockOAuthApp}, &model.Response{}, nil). + Times(1) + + s.client. + EXPECT(). + GetUsersByIds(context.Background(), []string{mockOAuthApp.CreatorId}). + Return(nil, &model.Response{}, mockError). + Times(1) + + err := listOAuthAppsCmdF(s.client, cmd, []string{}) + s.Require().NotNil(err) + s.Len(printer.GetLines(), 0) + s.EqualError(err, "Failed to fetch users for oauth2 apps: mock error") + }) +} diff --git a/server/cmd/mmctl/docs/mmctl.rst b/server/cmd/mmctl/docs/mmctl.rst index 413680016ce9..c5532b335900 100644 --- a/server/cmd/mmctl/docs/mmctl.rst +++ b/server/cmd/mmctl/docs/mmctl.rst @@ -45,6 +45,7 @@ SEE ALSO * `mmctl ldap `_ - LDAP related utilities * `mmctl license `_ - Licensing commands * `mmctl logs `_ - Display logs in a human-readable format +* `mmctl oauth `_ - Management of OAuth2 apps * `mmctl permissions `_ - Management of permissions * `mmctl plugin `_ - Management of plugins * `mmctl post `_ - Management of posts diff --git a/server/cmd/mmctl/docs/mmctl_oauth.rst b/server/cmd/mmctl/docs/mmctl_oauth.rst new file mode 100644 index 000000000000..21fbd5a9895b --- /dev/null +++ b/server/cmd/mmctl/docs/mmctl_oauth.rst @@ -0,0 +1,41 @@ +.. _mmctl_oauth: + +mmctl oauth +----------- + +Management of OAuth2 apps + +Synopsis +~~~~~~~~ + + +Management of OAuth2 apps + +Options +~~~~~~~ + +:: + + -h, --help help for oauth + +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 `_ - Remote client for the Open Source, self-hosted Slack-alternative +* `mmctl oauth list `_ - List OAuth2 apps + diff --git a/server/cmd/mmctl/docs/mmctl_oauth_list.rst b/server/cmd/mmctl/docs/mmctl_oauth_list.rst new file mode 100644 index 000000000000..f428004c6e27 --- /dev/null +++ b/server/cmd/mmctl/docs/mmctl_oauth_list.rst @@ -0,0 +1,53 @@ +.. _mmctl_oauth_list: + +mmctl oauth list +---------------- + +List OAuth2 apps + +Synopsis +~~~~~~~~ + + +list all OAuth2 apps + +:: + + mmctl oauth list [flags] + +Examples +~~~~~~~~ + +:: + + oauth list + +Options +~~~~~~~ + +:: + + -h, --help help for list + --page int Page number to fetch for the list of OAuth2 apps + --per-page int Number of OAuth2 apps 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 oauth `_ - Management of OAuth2 apps + diff --git a/server/cmd/mmctl/mocks/client_mock.go b/server/cmd/mmctl/mocks/client_mock.go index 5870286e3db9..4584494a4af5 100644 --- a/server/cmd/mmctl/mocks/client_mock.go +++ b/server/cmd/mmctl/mocks/client_mock.go @@ -927,6 +927,22 @@ func (mr *MockClientMockRecorder) GetMarketplacePlugins(arg0, arg1 interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMarketplacePlugins", reflect.TypeOf((*MockClient)(nil).GetMarketplacePlugins), arg0, arg1) } +// GetOAuthApps mocks base method. +func (m *MockClient) GetOAuthApps(arg0 context.Context, arg1, arg2 int) ([]*model.OAuthApp, *model.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetOAuthApps", arg0, arg1, arg2) + ret0, _ := ret[0].([]*model.OAuthApp) + ret1, _ := ret[1].(*model.Response) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetOAuthApps indicates an expected call of GetOAuthApps. +func (mr *MockClientMockRecorder) GetOAuthApps(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOAuthApps", reflect.TypeOf((*MockClient)(nil).GetOAuthApps), arg0, arg1, arg2) +} + // GetOutgoingWebhook mocks base method. func (m *MockClient) GetOutgoingWebhook(arg0 context.Context, arg1 string) (*model.OutgoingWebhook, *model.Response, error) { m.ctrl.T.Helper()