From b173f81af88bcec87ed1581252e431f2ab5eab85 Mon Sep 17 00:00:00 2001 From: Gustavo Bazan Date: Wed, 2 Feb 2022 16:29:57 +0000 Subject: [PATCH 1/4] CLOUDP-106123: [mongocli ] Implement mongocli auth logout --- go.mod | 2 +- go.sum | 4 +- internal/cli/auth/login.go | 1 + internal/cli/auth/login_test.go | 2 +- internal/cli/auth/logout.go | 93 ++++++++++++++++++++++++++++++++ internal/cli/auth/logout_test.go | 64 ++++++++++++++++++++++ internal/cli/delete_opts.go | 8 ++- internal/mocks/mock_logout.go | 88 ++++++++++++++++++++++++++++++ 8 files changed, 257 insertions(+), 5 deletions(-) create mode 100644 internal/cli/auth/logout.go create mode 100644 internal/cli/auth/logout_test.go create mode 100644 internal/mocks/mock_logout.go diff --git a/go.mod b/go.mod index d295ed6747..34218173eb 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/spf13/viper v1.10.1 github.com/stretchr/testify v1.7.0 github.com/tangzero/inflector v1.0.0 - go.mongodb.org/atlas v0.14.1-0.20220126105350-5816bca2f88c + go.mongodb.org/atlas v0.14.1-0.20220202080947-4a7d97e77246 go.mongodb.org/ops-manager v0.34.0 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 12a7e90c87..2f5fcb8a65 100644 --- a/go.sum +++ b/go.sum @@ -401,8 +401,8 @@ go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQc go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= go.mongodb.org/atlas v0.14.0/go.mod h1:lQhRHIxc6jQHEK3/q9WLu/SdBkPj2fQYhjLGUF6Z3U8= -go.mongodb.org/atlas v0.14.1-0.20220126105350-5816bca2f88c h1:K3Q2ggsg1XtJuurBbJspNRzmkPtIZl5lD1oICR6lW28= -go.mongodb.org/atlas v0.14.1-0.20220126105350-5816bca2f88c/go.mod h1:lQhRHIxc6jQHEK3/q9WLu/SdBkPj2fQYhjLGUF6Z3U8= +go.mongodb.org/atlas v0.14.1-0.20220202080947-4a7d97e77246 h1:BkSHgSH3o6KawlMdsnR+XpZ/pCDbWK5secDmcUmytL0= +go.mongodb.org/atlas v0.14.1-0.20220202080947-4a7d97e77246/go.mod h1:lQhRHIxc6jQHEK3/q9WLu/SdBkPj2fQYhjLGUF6Z3U8= go.mongodb.org/ops-manager v0.34.0 h1:d8TgpJpPFeVLr+6HrAbbbYnKkdk+2JW6E20OBdgqXg8= go.mongodb.org/ops-manager v0.34.0/go.mod h1:85LPPdME1TFJ/Eau/IgOmy37YWGw1p/S8PBSME8ukXs= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= diff --git a/internal/cli/auth/login.go b/internal/cli/auth/login.go index 67b1b1a7f7..013b3f99ed 100644 --- a/internal/cli/auth/login.go +++ b/internal/cli/auth/login.go @@ -228,6 +228,7 @@ func Builder() *cobra.Command { } cmd.AddCommand( LoginBuilder(), + LogoutBuilder(), ) return cmd diff --git a/internal/cli/auth/login_test.go b/internal/cli/auth/login_test.go index a0596df49f..2b584671fe 100644 --- a/internal/cli/auth/login_test.go +++ b/internal/cli/auth/login_test.go @@ -35,7 +35,7 @@ func TestBuilder(t *testing.T) { test.CmdValidator( t, Builder(), - 1, + 2, []string{}, ) } diff --git a/internal/cli/auth/logout.go b/internal/cli/auth/logout.go new file mode 100644 index 0000000000..2cff5e4c06 --- /dev/null +++ b/internal/cli/auth/logout.go @@ -0,0 +1,93 @@ +// Copyright 2022 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auth + +import ( + "context" + "fmt" + "io" + + "github.com/mongodb/mongocli/internal/cli" + "github.com/mongodb/mongocli/internal/cli/require" + "github.com/mongodb/mongocli/internal/config" + "github.com/mongodb/mongocli/internal/oauth" + "github.com/spf13/cobra" + atlas "go.mongodb.org/atlas/mongodbatlas" +) + +type logoutOpts struct { + *cli.DeleteOpts + OutWriter io.Writer + config ConfigDeleter + flow Revoker +} + +//go:generate mockgen -destination=../../mocks/mock_logout.go -package=mocks github.com/mongodb/mongocli/internal/cli/auth Revoker,ConfigDeleter + +type ConfigDeleter interface { + Delete() error +} + +type Revoker interface { + Revoke(context.Context, string, string) (*atlas.Response, error) +} + +func (opts *logoutOpts) initFlow() error { + var err error + opts.flow, err = oauth.FlowWithConfig(config.Default()) + return err +} + +func (opts *logoutOpts) Run(ctx context.Context) error { + _, err := opts.flow.Revoke(ctx, config.RefreshToken(), "refresh_token") + if err != nil { + return err + } + + if err := opts.Delete(opts.config.Delete); err != nil { + return err + } + + _, _ = fmt.Fprintln(opts.OutWriter, "Successfully logged out.") + return nil +} + +func LogoutBuilder() *cobra.Command { + opts := &logoutOpts{ + DeleteOpts: cli.NewDeleteOpts("Successfully logged out\n", "Project not deleted"), + } + + cmd := &cobra.Command{ + Use: "logout", + Short: "Log out the CLI.", + Example: ` To log out from the CLI: + $ mongocli auth logout +`, + PreRunE: func(cmd *cobra.Command, args []string) error { + opts.OutWriter = cmd.OutOrStdout() + opts.config = config.Default() + return opts.initFlow() + }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := opts.PromptWithMessage("Are you sure you want to log out"); err != nil { + return err + } + return opts.Run(cmd.Context()) + }, + Args: require.NoArgs, + } + + return cmd +} diff --git a/internal/cli/auth/logout_test.go b/internal/cli/auth/logout_test.go new file mode 100644 index 0000000000..3091f5aa7e --- /dev/null +++ b/internal/cli/auth/logout_test.go @@ -0,0 +1,64 @@ +// Copyright 2022 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build unit +// +build unit + +package auth + +import ( + "bytes" + "context" + "testing" + + "github.com/golang/mock/gomock" + "github.com/mongodb/mongocli/internal/mocks" + "github.com/mongodb/mongocli/internal/test" + "github.com/stretchr/testify/require" +) + +func TestLogoutBuilder(t *testing.T) { + test.CmdValidator( + t, + LogoutBuilder(), + 0, + []string{}, + ) +} + +func Test_logoutOpts_Run(t *testing.T) { + ctrl := gomock.NewController(t) + mockFlow := mocks.NewMockRevoker(ctrl) + mockConfig := mocks.NewMockConfigDeleter(ctrl) + defer ctrl.Finish() + buf := new(bytes.Buffer) + + opts := logoutOpts{ + OutWriter: buf, + config: mockConfig, + flow: mockFlow, + } + ctx := context.TODO() + mockFlow. + EXPECT(). + Revoke(ctx, gomock.Any(), gomock.Any()). + Return(nil, nil). + Times(1) + mockConfig. + EXPECT(). + Delete(). + Return(nil). + Times(1) + require.NoError(t, opts.Run(ctx)) +} diff --git a/internal/cli/delete_opts.go b/internal/cli/delete_opts.go index 3b0696bad1..cab4c567d2 100644 --- a/internal/cli/delete_opts.go +++ b/internal/cli/delete_opts.go @@ -54,6 +54,8 @@ func (opts *DeleteOpts) Delete(d interface{}, a ...string) error { var err error switch f := d.(type) { + case func() error: + err = f() case func(string) error: err = f(opts.Entry) case func(string, string) error: @@ -91,7 +93,11 @@ func (opts *DeleteOpts) PromptWithMessage(message string) error { return nil } - p := prompt.NewConfirm(fmt.Sprintf(message, opts.Entry)) + m := message + if opts.Entry != "" { + m = fmt.Sprintf(message, opts.Entry) + } + p := prompt.NewConfirm(m) return survey.AskOne(p, &opts.Confirm) } diff --git a/internal/mocks/mock_logout.go b/internal/mocks/mock_logout.go new file mode 100644 index 0000000000..241302bd49 --- /dev/null +++ b/internal/mocks/mock_logout.go @@ -0,0 +1,88 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/mongodb/mongocli/internal/cli/auth (interfaces: Revoker,ConfigDeleter) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + mongodbatlas "go.mongodb.org/atlas/mongodbatlas" +) + +// MockRevoker is a mock of Revoker interface. +type MockRevoker struct { + ctrl *gomock.Controller + recorder *MockRevokerMockRecorder +} + +// MockRevokerMockRecorder is the mock recorder for MockRevoker. +type MockRevokerMockRecorder struct { + mock *MockRevoker +} + +// NewMockRevoker creates a new mock instance. +func NewMockRevoker(ctrl *gomock.Controller) *MockRevoker { + mock := &MockRevoker{ctrl: ctrl} + mock.recorder = &MockRevokerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRevoker) EXPECT() *MockRevokerMockRecorder { + return m.recorder +} + +// Revoke mocks base method. +func (m *MockRevoker) Revoke(arg0 context.Context, arg1, arg2 string) (*mongodbatlas.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Revoke", arg0, arg1, arg2) + ret0, _ := ret[0].(*mongodbatlas.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Revoke indicates an expected call of Revoke. +func (mr *MockRevokerMockRecorder) Revoke(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Revoke", reflect.TypeOf((*MockRevoker)(nil).Revoke), arg0, arg1, arg2) +} + +// MockConfigDeleter is a mock of ConfigDeleter interface. +type MockConfigDeleter struct { + ctrl *gomock.Controller + recorder *MockConfigDeleterMockRecorder +} + +// MockConfigDeleterMockRecorder is the mock recorder for MockConfigDeleter. +type MockConfigDeleterMockRecorder struct { + mock *MockConfigDeleter +} + +// NewMockConfigDeleter creates a new mock instance. +func NewMockConfigDeleter(ctrl *gomock.Controller) *MockConfigDeleter { + mock := &MockConfigDeleter{ctrl: ctrl} + mock.recorder = &MockConfigDeleterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockConfigDeleter) EXPECT() *MockConfigDeleterMockRecorder { + return m.recorder +} + +// Delete mocks base method. +func (m *MockConfigDeleter) Delete() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete") + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockConfigDeleterMockRecorder) Delete() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockConfigDeleter)(nil).Delete)) +} From b3b398ae07a2879b6e5a21e8d6814568a212afbd Mon Sep 17 00:00:00 2001 From: Gustavo Bazan Date: Wed, 2 Feb 2022 17:09:48 +0000 Subject: [PATCH 2/4] fixes --- internal/cli/auth/logout.go | 22 ++++++++++++---------- internal/cli/auth/logout_test.go | 4 ++++ internal/cli/delete_opts.go | 7 +++++-- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/internal/cli/auth/logout.go b/internal/cli/auth/logout.go index 2cff5e4c06..e5c663a3cd 100644 --- a/internal/cli/auth/logout.go +++ b/internal/cli/auth/logout.go @@ -16,13 +16,15 @@ package auth import ( "context" - "fmt" + "errors" "io" "github.com/mongodb/mongocli/internal/cli" "github.com/mongodb/mongocli/internal/cli/require" "github.com/mongodb/mongocli/internal/config" + "github.com/mongodb/mongocli/internal/flag" "github.com/mongodb/mongocli/internal/oauth" + "github.com/mongodb/mongocli/internal/usage" "github.com/spf13/cobra" atlas "go.mongodb.org/atlas/mongodbatlas" ) @@ -51,22 +53,17 @@ func (opts *logoutOpts) initFlow() error { } func (opts *logoutOpts) Run(ctx context.Context) error { - _, err := opts.flow.Revoke(ctx, config.RefreshToken(), "refresh_token") - if err != nil { + // revoking a refresh token revokes the access token + if _, err := opts.flow.Revoke(ctx, config.RefreshToken(), "refresh_token"); err != nil { return err } - if err := opts.Delete(opts.config.Delete); err != nil { - return err - } - - _, _ = fmt.Fprintln(opts.OutWriter, "Successfully logged out.") - return nil + return opts.Delete(opts.config.Delete) } func LogoutBuilder() *cobra.Command { opts := &logoutOpts{ - DeleteOpts: cli.NewDeleteOpts("Successfully logged out\n", "Project not deleted"), + DeleteOpts: cli.NewDeleteOpts("Successfully logged out\n", " "), } cmd := &cobra.Command{ @@ -81,6 +78,9 @@ func LogoutBuilder() *cobra.Command { return opts.initFlow() }, RunE: func(cmd *cobra.Command, args []string) error { + if config.RefreshToken() == "" { + return errors.New("not logged in") + } if err := opts.PromptWithMessage("Are you sure you want to log out"); err != nil { return err } @@ -89,5 +89,7 @@ func LogoutBuilder() *cobra.Command { Args: require.NoArgs, } + cmd.Flags().BoolVar(&opts.Confirm, flag.Force, false, usage.Force) + return cmd } diff --git a/internal/cli/auth/logout_test.go b/internal/cli/auth/logout_test.go index 3091f5aa7e..3655ba7eab 100644 --- a/internal/cli/auth/logout_test.go +++ b/internal/cli/auth/logout_test.go @@ -23,6 +23,7 @@ import ( "testing" "github.com/golang/mock/gomock" + "github.com/mongodb/mongocli/internal/cli" "github.com/mongodb/mongocli/internal/mocks" "github.com/mongodb/mongocli/internal/test" "github.com/stretchr/testify/require" @@ -48,6 +49,9 @@ func Test_logoutOpts_Run(t *testing.T) { OutWriter: buf, config: mockConfig, flow: mockFlow, + DeleteOpts: &cli.DeleteOpts{ + Confirm: true, + }, } ctx := context.TODO() mockFlow. diff --git a/internal/cli/delete_opts.go b/internal/cli/delete_opts.go index cab4c567d2..b4ed230a91 100644 --- a/internal/cli/delete_opts.go +++ b/internal/cli/delete_opts.go @@ -71,8 +71,11 @@ func (opts *DeleteOpts) Delete(d interface{}, a ...string) error { if err != nil { return err } - - fmt.Printf(opts.SuccessMessage(), opts.Entry) + if opts.Entry == "" { + fmt.Print(opts.SuccessMessage()) + } else { + fmt.Printf(opts.SuccessMessage(), opts.Entry) + } return nil } From 81b0210fa3907e9c83140557c066b26b93ae6484 Mon Sep 17 00:00:00 2001 From: Gustavo Bazan Date: Wed, 2 Feb 2022 17:21:55 +0000 Subject: [PATCH 3/4] fix --- internal/cli/auth/logout_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/cli/auth/logout_test.go b/internal/cli/auth/logout_test.go index 3655ba7eab..27c10ff949 100644 --- a/internal/cli/auth/logout_test.go +++ b/internal/cli/auth/logout_test.go @@ -24,6 +24,7 @@ import ( "github.com/golang/mock/gomock" "github.com/mongodb/mongocli/internal/cli" + "github.com/mongodb/mongocli/internal/flag" "github.com/mongodb/mongocli/internal/mocks" "github.com/mongodb/mongocli/internal/test" "github.com/stretchr/testify/require" @@ -34,7 +35,7 @@ func TestLogoutBuilder(t *testing.T) { t, LogoutBuilder(), 0, - []string{}, + []string{flag.Force}, ) } From a727b60b7713bb50195698106871af627e58f79c Mon Sep 17 00:00:00 2001 From: Gustavo Bazan Date: Wed, 2 Feb 2022 18:08:50 +0000 Subject: [PATCH 4/4] PR comments --- internal/cli/auth/logout.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cli/auth/logout.go b/internal/cli/auth/logout.go index e5c663a3cd..743504d5c5 100644 --- a/internal/cli/auth/logout.go +++ b/internal/cli/auth/logout.go @@ -81,7 +81,7 @@ func LogoutBuilder() *cobra.Command { if config.RefreshToken() == "" { return errors.New("not logged in") } - if err := opts.PromptWithMessage("Are you sure you want to log out"); err != nil { + if err := opts.PromptWithMessage("Are you sure you want to log out?"); err != nil { return err } return opts.Run(cmd.Context())