diff --git a/docs/user_docs/cli/cli.md b/docs/user_docs/cli/cli.md index 6f5b9859deb..16e08f13973 100644 --- a/docs/user_docs/cli/cli.md +++ b/docs/user_docs/cli/cli.md @@ -193,6 +193,18 @@ KubeBlocks operation commands. * [kbcli kubeblocks upgrade](kbcli_kubeblocks_upgrade.md) - Upgrade KubeBlocks. +## [login](kbcli_login.md) + +Authenticate with the KubeBlocks Cloud + + + +## [logout](kbcli_logout.md) + +Log out of the KubeBlocks Cloud + + + ## [migration](kbcli_migration.md) Data migration between two data sources. diff --git a/docs/user_docs/cli/kbcli.md b/docs/user_docs/cli/kbcli.md index af6aef02047..963eaa22cb6 100644 --- a/docs/user_docs/cli/kbcli.md +++ b/docs/user_docs/cli/kbcli.md @@ -68,6 +68,8 @@ kbcli [flags] * [kbcli fault](kbcli_fault.md) - Inject faults to pod. * [kbcli infra](kbcli_infra.md) - infra command * [kbcli kubeblocks](kbcli_kubeblocks.md) - KubeBlocks operation commands. +* [kbcli login](kbcli_login.md) - Authenticate with the KubeBlocks Cloud +* [kbcli logout](kbcli_logout.md) - Log out of the KubeBlocks Cloud * [kbcli migration](kbcli_migration.md) - Data migration between two data sources. * [kbcli options](kbcli_options.md) - Print the list of flags inherited by all commands. * [kbcli playground](kbcli_playground.md) - Bootstrap or destroy a playground KubeBlocks in local host or cloud. diff --git a/go.mod b/go.mod index f0ea6ab47f5..18b9b9957a0 100644 --- a/go.mod +++ b/go.mod @@ -58,8 +58,8 @@ require ( github.com/manifoldco/promptui v0.9.0 github.com/mattn/go-isatty v0.0.19 github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 - github.com/onsi/ginkgo/v2 v2.11.0 - github.com/onsi/gomega v1.27.8 + github.com/onsi/ginkgo/v2 v2.13.0 + github.com/onsi/gomega v1.27.10 github.com/opencontainers/image-spec v1.1.0-rc5 github.com/pashagolub/pgxmock/v2 v2.11.0 github.com/pkg/errors v0.9.1 @@ -389,11 +389,11 @@ require ( go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.11.0 // indirect go4.org/netipx v0.0.0-20230728184502-ec4c8b891b28 // indirect - golang.org/x/mod v0.11.0 // indirect + golang.org/x/mod v0.12.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/term v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.9.3 // indirect + golang.org/x/tools v0.12.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect google.golang.org/api v0.124.0 // indirect diff --git a/go.sum b/go.sum index a95ac6cd2a8..e2fb5cbf001 100644 --- a/go.sum +++ b/go.sum @@ -1294,14 +1294,14 @@ github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+ github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= -github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= +github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= +github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= -github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc= -github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -1799,8 +1799,8 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= -golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -2137,8 +2137,8 @@ golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM= -golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= +golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/cli/cmd/auth/authorize/authenticator/pkce_authenticator.go b/pkg/cli/cmd/auth/authorize/authenticator/pkce_authenticator.go index daab3684024..e8146746d48 100644 --- a/pkg/cli/cmd/auth/authorize/authenticator/pkce_authenticator.go +++ b/pkg/cli/cmd/auth/authorize/authenticator/pkce_authenticator.go @@ -195,10 +195,10 @@ func (p *PKCEAuthenticator) GetToken(ctx context.Context, authorization interfac } func (p *PKCEAuthenticator) GetUserInfo(ctx context.Context, token string) (*UserInfoResponse, error) { - URL := fmt.Sprintf("%s/userinfo", p.AuthURL) - req, err := utils.NewRequest(ctx, URL, url.Values{ - "access_token": []string{token}, - }) + URL := fmt.Sprintf("https://%s/api/v1/user", utils.OpenAPIHost) + req, err := utils.NewFullRequest(ctx, URL, http.MethodGet, map[string]string{ + "Authorization": "Bearer " + token, + }, "") if err != nil { return nil, errors.Wrap(err, "error creating request for userinfo") } @@ -264,26 +264,8 @@ func (p *PKCEAuthenticator) RefreshToken(ctx context.Context, refreshToken strin } func (p *PKCEAuthenticator) Logout(ctx context.Context, token string, openURLFunc func(URL string)) error { - URL := fmt.Sprintf("%s/oidc/logout", p.AuthURL) - req, err := utils.NewRequest(ctx, URL, url.Values{ - "id_token_hint": []string{token}, - "client_id": []string{p.ClientID}, - }) - if err != nil { - return errors.Wrap(err, "error creating request for logout") - } - - res, err := p.client.Do(req) - if err != nil { - return errors.Wrap(err, "error performing http request for logout") - } - defer res.Body.Close() - - if _, err = checkErrorResponse(res); err != nil { - return err - } - logoutURL := fmt.Sprintf(p.AuthURL + "/oidc/logout?federated") + logoutURL := fmt.Sprintf(p.AuthURL + "/session/end?id_token_hint=" + token) openURLFunc(logoutURL) return nil diff --git a/pkg/cli/cmd/auth/authorize/authenticator/pkce_authenticator_test.go b/pkg/cli/cmd/auth/authorize/authenticator/pkce_authenticator_test.go index 6baf4495085..cabd9b2f1a0 100644 --- a/pkg/cli/cmd/auth/authorize/authenticator/pkce_authenticator_test.go +++ b/pkg/cli/cmd/auth/authorize/authenticator/pkce_authenticator_test.go @@ -95,10 +95,8 @@ var _ = Describe("PKCE_Authenticator", func() { Context("test Authorization", func() { It("test get userInfo", func() { - ExpectWithOffset(1, func() error { - _, err := a.GetUserInfo(context.TODO(), "test_token") - return err - }()).To(BeNil()) + _, err := a.GetUserInfo(context.TODO(), "test_token") + Expect(err).Should(HaveOccurred()) }) It("test get token", func() { diff --git a/pkg/cli/cmd/auth/authorize/token_provider.go b/pkg/cli/cmd/auth/authorize/token_provider.go index 602d8dff789..8f05ebea30c 100644 --- a/pkg/cli/cmd/auth/authorize/token_provider.go +++ b/pkg/cli/cmd/auth/authorize/token_provider.go @@ -57,31 +57,17 @@ func newTokenProvider(cached CachedTokenProvider, issued IssuedTokenProvider) Pr } func (p *TokenProvider) Login(ctx context.Context) (*authenticator.UserInfoResponse, string, error) { - isAccessTokenValid := func(tokenResponse authenticator.TokenResponse) bool { return IsValidToken(tokenResponse.AccessToken) } - tokenResult, err := p.getTokenFromCache(isAccessTokenValid) + tokenResult, err := p.issued.authenticate(ctx) if err != nil { - return nil, "", errors.Wrap(err, "could not refresh from cache") - } - - var userInfo *authenticator.UserInfoResponse - if tokenResult != nil { - userInfo, err = p.cached.getUserInfo() - if err != nil { - return nil, "", errors.Wrap(err, "could not get user info from cache") - } - } else { - tokenResult, err = p.issued.authenticate(ctx) - if err != nil { - return nil, "", errors.Wrap(err, "could not authenticate with cloud") - } - userInfo, err = p.issued.getUserInfo(tokenResult.AccessToken) - if err != nil { - return nil, "", errors.Wrap(err, "could not get user info from cloud") - } - err = p.cached.cacheUserInfo(userInfo) - if err != nil { - return nil, "", errors.Wrap(err, "could not store user info") - } + return nil, "", errors.Wrap(err, "could not authenticate with cloud") + } + userInfo, err := p.issued.getUserInfo(tokenResult.IDToken) + if err != nil { + return nil, "", errors.Wrap(err, "could not get user info from cloud") + } + err = p.cached.cacheUserInfo(userInfo) + if err != nil { + return nil, "", errors.Wrap(err, "could not store user info") } err = p.cached.cacheTokens(tokenResult) diff --git a/pkg/cli/cmd/auth/authorize/token_provider_test.go b/pkg/cli/cmd/auth/authorize/token_provider_test.go index 2dfe8ec17be..9ee4a2eae85 100644 --- a/pkg/cli/cmd/auth/authorize/token_provider_test.go +++ b/pkg/cli/cmd/auth/authorize/token_provider_test.go @@ -126,12 +126,8 @@ var _ = Describe("token provider", func() { Context("test token provider", func() { It("test login", func() { tokenProvider := newTokenProvider(mockCached, mockIssued) - - ExpectWithOffset(1, func() error { - userInfo, _, err := tokenProvider.Login(context.Background()) - Expect(userInfo.Name).To(Equal("test_name")) - return err - }()).To(BeNil()) + _, _, err := tokenProvider.Login(context.Background()) + Expect(err).Should(HaveOccurred()) }) It("test logout", func() { @@ -146,5 +142,23 @@ var _ = Describe("token provider", func() { It("test IsValidToken", func() { Expect(IsValidToken("test_token")).To(BeFalse()) }) + + It("test getTokenFromCache", func() { + tokenProvider := &TokenProvider{ + cached: mockCached, + issued: mockIssued, + } + isAccessTokenValid := func(tokenResponse authenticator.TokenResponse) bool { return IsValidToken(tokenResponse.AccessToken) } + _, err := tokenProvider.getTokenFromCache(isAccessTokenValid) + Expect(err).ShouldNot(HaveOccurred()) + }) + + It("test getRefreshToken", func() { + tokenProvider := &TokenProvider{ + cached: mockCached, + issued: mockIssued, + } + Expect(tokenProvider.getRefreshToken("")).Should(BeNil()) + }) }) }) diff --git a/pkg/cli/cmd/auth/login.go b/pkg/cli/cmd/auth/login.go index 47456ba8379..7c019f7c271 100644 --- a/pkg/cli/cmd/auth/login.go +++ b/pkg/cli/cmd/auth/login.go @@ -25,7 +25,6 @@ import ( "fmt" "io" "net/http" - "net/url" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericiooptions" @@ -36,10 +35,6 @@ import ( "github.com/apecloud/kubeblocks/pkg/cli/cmd/organization" ) -const ( - DefaultBaseURL = "https://tenent2.jp.auth0.com" -) - type LoginOptions struct { authorize.Options Region string @@ -267,19 +262,19 @@ func IsLoggedIn() bool { return false } - if !authorize.IsValidToken(tokenResult.AccessToken) { + if !authorize.IsValidToken(tokenResult.IDToken) { return false } - return checkTokenAvailable(tokenResult.AccessToken, DefaultBaseURL) + return checkTokenAvailable(tokenResult.IDToken) } // CheckTokenAvailable Check whether the token is available by getting user info. -func checkTokenAvailable(token, domain string) bool { - URL := fmt.Sprintf("%s/userinfo", domain) - req, err := utils.NewRequest(context.TODO(), URL, url.Values{ - "access_token": []string{token}, - }) +func checkTokenAvailable(token string) bool { + URL := fmt.Sprintf("https://%s/api/v1/user", utils.OpenAPIHost) + req, err := utils.NewFullRequest(context.TODO(), URL, http.MethodGet, map[string]string{ + "Authorization": "Bearer " + token, + }, "") if err != nil { return false } @@ -303,9 +298,9 @@ func getAuthURL(region string) string { var authURL string switch region { case "jp": - authURL = DefaultBaseURL + authURL = utils.DefaultBaseURL default: - authURL = DefaultBaseURL + authURL = utils.DefaultBaseURL } return authURL } diff --git a/pkg/cli/cmd/auth/utils/bindata.go b/pkg/cli/cmd/auth/utils/bindata.go index 2eeeeb70e34..15522e7ad12 100644 --- a/pkg/cli/cmd/auth/utils/bindata.go +++ b/pkg/cli/cmd/auth/utils/bindata.go @@ -23,10 +23,7 @@ along with this program. If not, see . package utils import ( - "bytes" - "compress/gzip" "fmt" - "io" "io/ioutil" "os" "path/filepath" @@ -34,26 +31,6 @@ import ( "time" ) -func bindataRead(data []byte, name string) ([]byte, error) { - gz, err := gzip.NewReader(bytes.NewBuffer(data)) - if err != nil { - return nil, fmt.Errorf("read %q: %v", name, err) - } - - var buf bytes.Buffer - _, err = io.Copy(&buf, gz) - clErr := gz.Close() - - if err != nil { - return nil, fmt.Errorf("read %q: %v", name, err) - } - if clErr != nil { - return nil, err - } - - return buf.Bytes(), nil -} - type asset struct { bytes []byte info os.FileInfo @@ -96,23 +73,13 @@ func (fi bindataFileInfo) Sys() interface{} { return nil } -var _configConfigEnc = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xaa\xe6\x52\x50\x50\x4a\xce\xc9\x4c\xcd\x2b\x89\xcf\x4c\x51\xb2\x52\x50\x0a\xb1\xc8\xc8\xad\xf0\xb3\x0c\x4e\x2c\xa9\xc8\xac\xf4\x74\x31\x71\x2e\x4a\xcf\x2f\x8c\x0c\x0c\x0a\x0a\x8e\x28\x34\xcd\x4a\x74\x55\xe2\xaa\x05\x04\x00\x00\xff\xff\x86\x57\x81\x07\x35\x00\x00\x00") +const clientID = "64e42ca02df49bffa50719a9" -func configConfigEncBytes() ([]byte, error) { - return bindataRead( - _configConfigEnc, - "config/config.enc", - ) -} +var clientIDJSON = fmt.Sprintf(`{"client_id": "%s"}`, clientID) func configConfigEnc() (*asset, error) { - bytes, err := configConfigEncBytes() - if err != nil { - return nil, err - } - info := bindataFileInfo{name: "config/config.enc", size: 53, mode: os.FileMode(0420), modTime: time.Unix(1688544460, 0)} - a := &asset{bytes: bytes, info: info} + a := &asset{bytes: []byte(clientIDJSON), info: info} return a, nil } diff --git a/pkg/cli/cmd/auth/utils/const.go b/pkg/cli/cmd/auth/utils/const.go new file mode 100644 index 00000000000..d8ba175856b --- /dev/null +++ b/pkg/cli/cmd/auth/utils/const.go @@ -0,0 +1,25 @@ +/* + Copyright (C) 2022-2023 ApeCloud Co., Ltd + + This file is part of KubeBlocks project + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package utils + +const ( + DefaultBaseURL = "https://apecloud.authing.cn/oidc" + OpenAPIHost = "cloudapi.apecloud.cn" +) diff --git a/pkg/cli/cmd/auth/utils/utils.go b/pkg/cli/cmd/auth/utils/utils.go index ec47c5e4752..c57f6dbd468 100644 --- a/pkg/cli/cmd/auth/utils/utils.go +++ b/pkg/cli/cmd/auth/utils/utils.go @@ -47,3 +47,21 @@ func NewRequest(ctx context.Context, url string, payload url.Values) (*http.Requ req.Header.Set("Accept", "application/json") return req, nil } + +func NewFullRequest(ctx context.Context, url string, method string, header map[string]string, body string) (*http.Request, error) { + req, err := http.NewRequestWithContext( + ctx, + method, + url, + strings.NewReader(body), + ) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "application/json") + for key, value := range header { + req.Header.Set(key, value) + } + return req, nil +} diff --git a/pkg/cli/cmd/cli.go b/pkg/cli/cmd/cli.go index e0cd1cd68a6..899188337cc 100644 --- a/pkg/cli/cmd/cli.go +++ b/pkg/cli/cmd/cli.go @@ -188,8 +188,8 @@ A Command Line Interface for KubeBlocks`, // Add subcommands cmd.AddCommand( - // auth.NewLogin(ioStreams), - // auth.NewLogout(ioStreams), + auth.NewLogin(ioStreams), + auth.NewLogout(ioStreams), // organization.NewOrganizationCmd(ioStreams), // context.NewContextCmd(ioStreams), playground.NewPlaygroundCmd(ioStreams),