From 42a6c63d24e6ce461d3cbd3381872448823c8466 Mon Sep 17 00:00:00 2001 From: Tyler Creller Date: Tue, 9 Apr 2024 10:08:02 -0400 Subject: [PATCH] OCM-4965: Keyring configuration storage (#600) --- .github/workflows/check-pull-request.yaml | 6 + README.md | 21 +- cmd/ocm/config/cmd.go | 12 +- cmd/ocm/login/cmd.go | 33 +++- cmd/ocm/logout/cmd.go | 10 + go.mod | 31 +-- go.sum | 76 +++++--- pkg/config/config.go | 70 ++++++- pkg/properties/properties.go | 19 ++ testdata/test-gpg.key | 82 ++++++++ testdata/test-ownertrust-gpg.txt | 3 + tests/keychain_test.go | 177 +++++++++++++++++ tests/login_test.go | 70 +++++++ tests/mocks.go | 12 ++ tests/pass_test.go | 227 ++++++++++++++++++++++ tests/wincred_test.go | 168 ++++++++++++++++ 16 files changed, 958 insertions(+), 59 deletions(-) create mode 100644 pkg/properties/properties.go create mode 100644 testdata/test-gpg.key create mode 100644 testdata/test-ownertrust-gpg.txt create mode 100644 tests/keychain_test.go create mode 100644 tests/mocks.go create mode 100644 tests/pass_test.go create mode 100644 tests/wincred_test.go diff --git a/.github/workflows/check-pull-request.yaml b/.github/workflows/check-pull-request.yaml index 9a466a98..33efedc4 100644 --- a/.github/workflows/check-pull-request.yaml +++ b/.github/workflows/check-pull-request.yaml @@ -39,6 +39,12 @@ jobs: steps: - name: Checkout the source uses: actions/checkout@v3 + - name: Install Keyrings (macOS-only) + if: ${{ contains(fromJSON('["macos-latest"]'), matrix.platform) }} + run: brew install pass gnupg + - name: Install Keyrings (linux) + if: ${{ contains(fromJSON('["ubuntu-latest"]'), matrix.platform) }} + run: sudo apt-get install pass - name: Setup Go uses: actions/setup-go@v4 with: diff --git a/README.md b/README.md index 2fe1bed9..af73f35c 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ An `~/config/ocm/ocm.json` file stores login credentials for a single API server. Using multiple servers therefore requires having to log in and out a lot or the ability to utilize multiple config files. The latter functionality is provided with the `OCM_CONFIG` environment variable. If running `ocm login` was -successfull in both cases, the `ocm whoami` commands will return different +successful in both cases, the `ocm whoami` commands will return different results: ``` @@ -129,6 +129,25 @@ $ OCM_CONFIG=$HOME/ocm.json.stg ocm whoami NOTE: Tokens for production and staging will differ. +## Storing Configuration & Tokens in OS Keyring +The `OCM_KEYRING` environment variable provides the ability to store the OCM +configuration containing your tokens in your OS keyring. This is provided +as an alternative to storing the configuration in plain-text on your system. +`OCM_KEYRING` will override `OCM_CONFIG` if both are set. + +`OCM_KEYRING` supports the following keyrings: + +* [Windows Credential Manager](https://support.microsoft.com/en-us/windows/accessing-credential-manager-1b5c916a-6a16-889f-8581-fc16e8165ac0) - `wincred` +* [macOS Keychain](https://support.apple.com/en-us/guide/keychain-access/welcome/mac) - `keychain` +* Secret Service ([Gnome Keyring](https://wiki.gnome.org/Projects/GnomeKeyring), [KWallet](https://apps.kde.org/kwalletmanager5/), etc.) - `secret-service` +* [Pass](https://www.passwordstore.org/) - `pass` + +| | wincred | keychain | secret-service | pass | +| ------------- | ------------- | ------------- | ------------- | ------------- | +| Windows | :heavy_check_mark: | :x: | :x: | :x: | +| macOS | :x: | :heavy_check_mark:* | :x: | :heavy_check_mark: | +| Linux | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: | + ## Obtaining Tokens If you need the _OpenID_ access token to use it with some other tool, you can diff --git a/cmd/ocm/config/cmd.go b/cmd/ocm/config/cmd.go index 2db698da..a03df389 100644 --- a/cmd/ocm/config/cmd.go +++ b/cmd/ocm/config/cmd.go @@ -26,6 +26,7 @@ import ( "github.com/openshift-online/ocm-cli/cmd/ocm/config/get" "github.com/openshift-online/ocm-cli/cmd/ocm/config/set" "github.com/openshift-online/ocm-cli/pkg/config" + "github.com/openshift-online/ocm-cli/pkg/properties" ) func configVarDocs() (ret string) { @@ -60,7 +61,16 @@ The following variables are supported: Note that "ocm config get access_token" gives whatever the file contains - may be missing or expired; you probably want "ocm token" command instead which will obtain a fresh token if needed. -`, loc, configVarDocs()) + +If '%s' is set, the configuration file is ignored and the keyring is used instead. The +following backends are supported for the keyring: + +- macOS: keychain, pass +- Linux: secret-service, pass +- Windows: wincred + +Available Keyrings on your OS: %s +`, loc, configVarDocs(), properties.KeyringEnvKey, strings.Join(config.GetKeyrings(), ", ")) return } diff --git a/cmd/ocm/login/cmd.go b/cmd/ocm/login/cmd.go index 128c72ef..f574cc7d 100644 --- a/cmd/ocm/login/cmd.go +++ b/cmd/ocm/login/cmd.go @@ -28,6 +28,7 @@ import ( "github.com/openshift-online/ocm-cli/pkg/urls" sdk "github.com/openshift-online/ocm-sdk-go" "github.com/openshift-online/ocm-sdk-go/authentication" + "github.com/openshift-online/ocm-sdk-go/authentication/securestore" "github.com/spf13/cobra" ) @@ -150,7 +151,7 @@ func init() { "use-auth-code", false, "Login using OAuth Authorization Code. This should be used for most cases where a "+ - "browser is available.", + "browser is available. See --use-device-code for remote hosts and containers.", ) flags.BoolVar( &args.useDeviceCode, @@ -158,20 +159,32 @@ func init() { false, "Login using OAuth Device Code. "+ "This should only be used for remote hosts and containers where browsers are "+ - "not available. Use auth code for all other scenarios.", + "not available. See --use-auth-code for all other scenarios.", ) } +var ( + InitiateAuthCode = authentication.InitiateAuthCode +) + func run(cmd *cobra.Command, argv []string) error { ctx := context.Background() var err error + // Fail fast if OCM_KEYRING is provided and invalid + if keyring, ok := config.IsKeyringManaged(); ok { + err := securestore.ValidateBackend(keyring) + if err != nil { + return err + } + } + if args.useAuthCode { fmt.Println("You will now be redirected to Red Hat SSO login") // Short wait for a less jarring experience time.Sleep(2 * time.Second) - token, err := authentication.InitiateAuthCode(oauthClientID) + token, err := InitiateAuthCode(oauthClientID) if err != nil { return fmt.Errorf("an error occurred while retrieving the token : %v", err) } @@ -184,7 +197,7 @@ func run(cmd *cobra.Command, argv []string) error { ClientID: oauthClientID, } _, err = deviceAuthConfig.InitiateDeviceAuth(ctx) - if err != nil || deviceAuthConfig == nil { + if err != nil { return fmt.Errorf("an error occurred while initiating device auth: %v", err) } deviceAuthResp := deviceAuthConfig.DeviceAuthResponse @@ -201,9 +214,9 @@ func run(cmd *cobra.Command, argv []string) error { // Check that we have some kind of credentials: havePassword := args.user != "" && args.password != "" - haveSecret := args.clientID != "" && args.clientSecret != "" + haveClientCreds := args.clientID != "" && args.clientSecret != "" haveToken := args.token != "" - if !havePassword && !haveSecret && !haveToken { + if !havePassword && !haveClientCreds && !haveToken { // Allow bare `ocm login` to suggest the token page without noise of full help. fmt.Fprintf( os.Stderr, @@ -227,10 +240,10 @@ func run(cmd *cobra.Command, argv []string) error { ) } - // Load the configuration file: + // Load the configuration: cfg, err := config.Load() if err != nil { - return fmt.Errorf("Can't load config file: %v", err) + return fmt.Errorf("Can't load config: %v", err) } if cfg == nil { cfg = new(config.Config) @@ -331,7 +344,7 @@ func run(cmd *cobra.Command, argv []string) error { err = config.Save(cfg) if err != nil { - return fmt.Errorf("Can't save config file: %v", err) + return fmt.Errorf("can't save config: %v", err) } if args.useAuthCode || args.useDeviceCode { @@ -343,7 +356,7 @@ func run(cmd *cobra.Command, argv []string) error { fmt.Println("Login successful") fmt.Printf("To switch accounts, logout from %s and run `ocm logout` "+ - "before attempting to login again", ssoHost) + "before attempting to login again\n", ssoHost) } return nil diff --git a/cmd/ocm/logout/cmd.go b/cmd/ocm/logout/cmd.go index db2f5bc0..1d89db16 100644 --- a/cmd/ocm/logout/cmd.go +++ b/cmd/ocm/logout/cmd.go @@ -22,6 +22,7 @@ import ( "github.com/spf13/cobra" "github.com/openshift-online/ocm-cli/pkg/config" + "github.com/openshift-online/ocm-sdk-go/authentication/securestore" ) var Cmd = &cobra.Command{ @@ -33,6 +34,15 @@ var Cmd = &cobra.Command{ } func run(cmd *cobra.Command, argv []string) error { + + if keyring, ok := config.IsKeyringManaged(); ok { + err := securestore.RemoveConfigFromKeyring(keyring) + if err != nil { + return fmt.Errorf("can't remove configuration from keyring: %w", err) + } + return nil + } + // Load the configuration file: cfg, err := config.Load() if err != nil { diff --git a/go.mod b/go.mod index 63c60517..6ca96483 100644 --- a/go.mod +++ b/go.mod @@ -12,46 +12,54 @@ require ( github.com/nwidger/jsoncolor v0.3.2 github.com/onsi/ginkgo/v2 v2.11.0 github.com/onsi/gomega v1.27.8 - github.com/openshift-online/ocm-sdk-go v0.1.407 + github.com/openshift-online/ocm-sdk-go v0.1.413 github.com/openshift/rosa v1.2.24 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a - golang.org/x/term v0.15.0 + golang.org/x/term v0.17.0 golang.org/x/text v0.14.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/apimachinery v0.27.3 ) require ( + github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect + github.com/99designs/keyring v1.2.2 // indirect + github.com/alessio/shellescape v1.4.1 // indirect github.com/aws/aws-sdk-go v1.44.110 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/briandowns/spinner v1.19.0 // indirect github.com/cenkalti/backoff/v4 v4.1.3 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/danieljoos/wincred v1.2.0 // indirect + github.com/dvsekhvalnov/jose2go v1.6.0 // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fatih/color v1.13.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect github.com/google/uuid v1.3.0 // indirect github.com/gorilla/css v1.0.0 // indirect + github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/itchyny/gojq v0.12.9 // indirect github.com/itchyny/timefmt-go v0.1.4 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect - github.com/jackc/pgconn v1.13.0 // indirect + github.com/jackc/pgconn v1.14.3 // indirect github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgproto3/v2 v2.3.1 // indirect - github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect - github.com/jackc/pgtype v1.12.0 // indirect - github.com/jackc/pgx/v4 v4.17.2 // indirect + github.com/jackc/pgproto3/v2 v2.3.3 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgtype v1.14.0 // indirect + github.com/jackc/pgx/v4 v4.18.3 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect @@ -62,20 +70,21 @@ require ( github.com/microcosm-cc/bluemonday v1.0.23 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mtibben/percent v0.2.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.13.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect - github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/sirupsen/logrus v1.9.0 // indirect github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect + github.com/zalando/go-keyring v0.2.3 // indirect github.com/zgalor/weberr v0.7.0 // indirect - golang.org/x/crypto v0.17.0 // indirect - golang.org/x/net v0.19.0 // indirect + golang.org/x/crypto v0.20.0 // indirect + golang.org/x/net v0.21.0 // indirect golang.org/x/oauth2 v0.15.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/sys v0.17.0 // indirect golang.org/x/tools v0.9.3 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.31.0 // indirect diff --git a/go.sum b/go.sum index 388d3a06..9292808c 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,10 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= +github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -44,6 +48,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= +github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/aws/aws-sdk-go v1.44.110 h1:unno3l2FYQo6p0wYCp9gUk8YNzhOxqSktM0Y1vukl9k= github.com/aws/aws-sdk-go v1.44.110/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= @@ -73,9 +79,13 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= +github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY= +github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -104,6 +114,10 @@ github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -174,6 +188,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -198,8 +214,8 @@ github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsU github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= -github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys= -github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI= +github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= +github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= @@ -215,26 +231,26 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvW github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y= -github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= +github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= +github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= -github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w= -github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= +github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E= -github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw= +github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= +github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= @@ -258,8 +274,6 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxv github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= @@ -304,16 +318,20 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nwidger/jsoncolor v0.3.2 h1:rVJJlwAWDJShnbTYOQ5RM7yTA20INyKXlJ/fg4JMhHQ= github.com/nwidger/jsoncolor v0.3.2/go.mod h1:Cs34umxLbJvgBMnVNVqhji9BhoT/N/KinHqZptQ7cf4= 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/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc= github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ= -github.com/openshift-online/ocm-sdk-go v0.1.407 h1:KAeTfpG4I3QWiDThJD3VBAi4APuVwlnouBFAeWJ7tHI= -github.com/openshift-online/ocm-sdk-go v0.1.407/go.mod h1:8ECJertR5BiblaX5f2siXHXpi+ydYZjoROlVMppmmV4= +github.com/openshift-online/ocm-sdk-go v0.1.413 h1:+zIRjcmYFgx4InSptZZqYENxCf9yFVZXE8CfjtWuxOM= +github.com/openshift-online/ocm-sdk-go v0.1.413/go.mod h1:CiAu2jwl3ITKOxkeV0Qnhzv4gs35AmpIzVABQLtcI2Y= github.com/openshift/rosa v1.2.24 h1:vv0yYnWHx6CCPEAau/0rS54P2ksaf+uWXb1TQPWxiYE= github.com/openshift/rosa v1.2.24/go.mod h1:MVXB27O3PF8WoOic23I03mmq6/9kVxpFx6FKyLMCyrQ= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= @@ -352,8 +370,6 @@ github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0ua github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= @@ -377,21 +393,22 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= +github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zgalor/weberr v0.7.0 h1:tFBgr7szrPHyr3uFjrxXIgi3XC5dqtddI+FjbSIXYL8= github.com/zgalor/weberr v0.7.0/go.mod h1:cqK89mj84q3PRgqQXQFWJDzCorOd8xOtov/ulOnqDwc= @@ -425,9 +442,8 @@ golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWP golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= +golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -492,12 +508,11 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -566,13 +581,13 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -723,8 +738,9 @@ google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/config/config.go b/pkg/config/config.go index 8284af97..8561641a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -29,9 +29,11 @@ import ( "github.com/golang/glog" homedir "github.com/mitchellh/go-homedir" sdk "github.com/openshift-online/ocm-sdk-go" + "github.com/openshift-online/ocm-sdk-go/authentication/securestore" "github.com/openshift-online/ocm-cli/pkg/debug" "github.com/openshift-online/ocm-cli/pkg/info" + "github.com/openshift-online/ocm-cli/pkg/properties" ) // Config is the type used to store the configuration of the client. @@ -53,9 +55,39 @@ type Config struct { Pager string `json:"pager,omitempty" doc:"Pager command, for example 'less'. If empty no pager will be used."` } -// Load loads the configuration from the configuration file. If the configuration file doesn't exist -// it will return an empty configuration object. +// Load loads the configuration from the OS keyring first if available, load from the configuration file if not func Load() (cfg *Config, err error) { + if keyring, ok := IsKeyringManaged(); ok { + return loadFromOS(keyring) + } + + return loadFromFile() +} + +// loadFromOS loads the configuration from the OS keyring. If the configuration doesn't exist +// it will return an empty configuration object. +func loadFromOS(keyring string) (cfg *Config, err error) { + cfg = &Config{} + + data, err := securestore.GetConfigFromKeyring(keyring) + if err != nil { + return nil, fmt.Errorf("can't load config from OS keyring [%s]: %v", keyring, err) + } + // No config found, return + if len(data) == 0 { + return nil, nil + } + err = json.Unmarshal(data, cfg) + if err != nil { + // Treat the config as empty if it can't be unmarshaled, it is invalid + return nil, nil + } + return cfg, nil +} + +// loadFromFile loads the configuration from the configuration file. If the configuration file doesn't exist +// it will return an empty configuration object. +func loadFromFile() (cfg *Config, err error) { file, err := Location() if err != nil { return @@ -94,15 +126,26 @@ func Save(cfg *Config) error { if err != nil { return err } + + data, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return fmt.Errorf("can't marshal config: %v", err) + } + + if keyring, ok := IsKeyringManaged(); ok { + // Use the OS keyring if the OCM_CONFIG env var is set to a valid keyring backend + err := securestore.UpsertConfigToKeyring(keyring, data) + if err != nil { + return fmt.Errorf("can't save config to OS keyring [%s]: %v", keyring, err) + } + return nil + } + dir := filepath.Dir(file) err = os.MkdirAll(dir, os.FileMode(0755)) if err != nil { return fmt.Errorf("can't create directory %s: %v", dir, err) } - data, err := json.MarshalIndent(cfg, "", " ") - if err != nil { - return fmt.Errorf("can't marshal config: %v", err) - } err = os.WriteFile(file, data, 0600) if err != nil { return fmt.Errorf("can't write file '%s': %v", file, err) @@ -278,3 +321,18 @@ func (c *Config) Connection() (connection *sdk.Connection, err error) { return } + +// IsKeyringManaged returns the keyring name and a boolean indicating if the config is managed by the keyring. +func IsKeyringManaged() (keyring string, ok bool) { + keyring = os.Getenv(properties.KeyringEnvKey) + return keyring, keyring != "" +} + +// GetKeyrings returns the available keyrings on the current host +func GetKeyrings() []string { + backends := securestore.AvailableBackends() + if len(backends) == 0 { + return []string{"no available backends"} + } + return backends +} diff --git a/pkg/properties/properties.go b/pkg/properties/properties.go new file mode 100644 index 00000000..643510a2 --- /dev/null +++ b/pkg/properties/properties.go @@ -0,0 +1,19 @@ +/* +Copyright (c) 2024 Red Hat, 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 properties + +const KeyringEnvKey = "OCM_KEYRING" diff --git a/testdata/test-gpg.key b/testdata/test-gpg.key new file mode 100644 index 00000000..087cf635 --- /dev/null +++ b/testdata/test-gpg.key @@ -0,0 +1,82 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +mTmgGQ== +=I8cN +-----END PGP PRIVATE KEY BLOCK----- \ No newline at end of file diff --git a/testdata/test-ownertrust-gpg.txt b/testdata/test-ownertrust-gpg.txt new file mode 100644 index 00000000..1a26a32d --- /dev/null +++ b/testdata/test-ownertrust-gpg.txt @@ -0,0 +1,3 @@ +# List of assigned trustvalues, created Tue Feb 20 11:49:38 2024 EST +# (Use "gpg --import-ownertrust" to restore them) +D0F5D0EC5CF01F1EEE2389619BA9864A24FB1FA0:6: diff --git a/tests/keychain_test.go b/tests/keychain_test.go new file mode 100644 index 00000000..0b9fdde2 --- /dev/null +++ b/tests/keychain_test.go @@ -0,0 +1,177 @@ +//go:build darwin +// +build darwin + +/* +Copyright (c) 2024 Red Hat, 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 tests + +import ( + "context" + "os" + "time" + + . "github.com/onsi/ginkgo/v2" // nolint + . "github.com/onsi/gomega" // nolint + . "github.com/onsi/gomega/ghttp" // nolint + + "github.com/openshift-online/ocm-cli/cmd/ocm/login" + "github.com/openshift-online/ocm-cli/pkg/properties" + . "github.com/openshift-online/ocm-sdk-go/testing" // nolint +) + +var _ = Describe("Keychain Keyring", func() { + var ctx context.Context + var ssoServer *Server + + BeforeEach(func() { + // Create the context + ctx = context.Background() + + // Create the server + ssoServer = MakeTCPServer() + + // Remove the configuration from the keyring + result := NewCommand(). + Args( + "logout", + ). + Run(ctx) + Expect(result.ExitCode()).To(BeZero()) + Expect(result.ErrString()).To(BeEmpty()) + }) + + AfterEach(func() { + // Close the server + ssoServer.Close() + }) + + When("Using OCM_KEYRING", func() { + AfterEach(func() { + // reset keyring + os.Setenv(properties.KeyringEnvKey, "") + }) + + It("Stores/Removes configuration in Keychain", func() { + // Create the token + accessToken := MakeTokenString("Bearer", 15*time.Minute) + + // Prepare the server + ssoServer.AppendHandlers( + RespondWithAccessToken(accessToken), + ) + + os.Setenv(properties.KeyringEnvKey, "keychain") + + // Run login + result := NewCommand(). + Args( + "login", + "--client-id", "my-client", + "--client-secret", "my-secret", + "--token-url", ssoServer.URL(), + ). + Run(ctx) + + Expect(result.ExitCode()).To(BeZero()) + Expect(result.ErrString()).To(BeEmpty()) + // Verify no config file data exists + Expect(result.ConfigFile()).To(BeEmpty()) + Expect(result.ConfigString()).To(BeEmpty()) + + // Check the content of the keyring + result = NewCommand(). + Args( + "config", + "get", + "access_token", + ). + Run(ctx) + Expect(result.ExitCode()).To(BeZero()) + Expect(result.ErrString()).To(BeEmpty()) + Expect(result.OutLines()[0]).To(ContainSubstring(accessToken)) + + // Remove the configuration from the keyring + result = NewCommand(). + Args( + "logout", + ). + Run(ctx) + Expect(result.ExitCode()).To(BeZero()) + Expect(result.ErrString()).To(BeEmpty()) + Expect(result.OutLines()).To(BeEmpty()) + + // Ensure the keyring is empty + result = NewCommand(). + Args( + "config", + "get", + "access_token", + ). + Run(ctx) + Expect(result.ErrString()).To(BeEmpty()) + Expect(result.ExitCode()).To(BeZero()) + Expect(result.OutLines()[0]).To(BeEmpty()) + }) + + Context("Using auth code", func() { + It("Stores/Removes configuration in Keychain", func() { + os.Setenv(properties.KeyringEnvKey, "keychain") + + login.InitiateAuthCode = InitiateAuthCodeMock + + cmd := login.Cmd + cmd.SetArgs([]string{"--use-auth-code"}) + err := cmd.Execute() + Expect(err).NotTo(HaveOccurred()) + + // Check the content of the keyring + result := NewCommand(). + Args( + "config", + "get", + "access_token", + ). + Run(ctx) + Expect(result.ExitCode()).To(BeZero()) + Expect(result.ErrString()).To(BeEmpty()) + Expect(result.OutLines()[0]).NotTo(BeEmpty()) + + // Remove the configuration from the keyring + result = NewCommand(). + Args( + "logout", + ). + Run(ctx) + Expect(result.ExitCode()).To(BeZero()) + Expect(result.ErrString()).To(BeEmpty()) + Expect(result.OutLines()).To(BeEmpty()) + + // Ensure the keyring is empty + result = NewCommand(). + Args( + "config", + "get", + "access_token", + ). + Run(ctx) + Expect(result.ErrString()).To(BeEmpty()) + Expect(result.ExitCode()).To(BeZero()) + Expect(result.OutLines()[0]).To(BeEmpty()) + }) + }) + }) +}) diff --git a/tests/login_test.go b/tests/login_test.go index 9b8f099e..09ce471d 100644 --- a/tests/login_test.go +++ b/tests/login_test.go @@ -18,8 +18,10 @@ package tests import ( "context" + "os" "time" + "github.com/openshift-online/ocm-cli/pkg/properties" sdk "github.com/openshift-online/ocm-sdk-go" . "github.com/onsi/ginkgo/v2" // nolint @@ -32,6 +34,7 @@ import ( var _ = Describe("Login", func() { var ctx context.Context var ssoServer *Server + invalidKeyring := "not-a-keyring" BeforeEach(func() { // Create the context: @@ -47,6 +50,11 @@ var _ = Describe("Login", func() { }) When("Using offline token", func() { + AfterEach(func() { + // reset keyring + os.Setenv(properties.KeyringEnvKey, "") + }) + It("Creates the configuration file", func() { // Create the tokens: accessToken := MakeTokenString("Bearer", 15*time.Minute) @@ -84,6 +92,24 @@ var _ = Describe("Login", func() { "accessToken", accessToken, )) }) + + It("Fails for an invalid keyring", func() { + os.Setenv(properties.KeyringEnvKey, invalidKeyring) + // Create the tokens: + accessToken := MakeTokenString("Bearer", 15*time.Minute) + + // Run the command: + result := NewCommand(). + Args( + "login", + "--token", accessToken, + "--token-url", ssoServer.URL(), + ). + Run(ctx) + + Expect(result.ExitCode()).ToNot(BeZero()) + Expect(result.ErrString()).To(ContainSubstring("keyring is invalid")) + }) }) When("Using client credentials grant", func() { @@ -182,4 +208,48 @@ var _ = Describe("Login", func() { )) }) }) + + When("Using auth code flow", func() { + AfterEach(func() { + // reset keyring + os.Setenv(properties.KeyringEnvKey, "") + }) + + It("Fails for an invalid keyring", func() { + os.Setenv(properties.KeyringEnvKey, invalidKeyring) + + // Run the command: + result := NewCommand(). + Args( + "login", + "--use-auth-code", + ). + Run(ctx) + + Expect(result.ExitCode()).ToNot(BeZero()) + Expect(result.ErrString()).To(ContainSubstring("keyring is invalid")) + }) + }) + + When("Using device code flow", func() { + AfterEach(func() { + // reset keyring + os.Setenv(properties.KeyringEnvKey, "") + }) + + It("Fails for an invalid keyring", func() { + os.Setenv(properties.KeyringEnvKey, invalidKeyring) + + // Run the command: + result := NewCommand(). + Args( + "login", + "--use-device-code", + ). + Run(ctx) + + Expect(result.ExitCode()).ToNot(BeZero()) + Expect(result.ErrString()).To(ContainSubstring("keyring is invalid")) + }) + }) }) diff --git a/tests/mocks.go b/tests/mocks.go new file mode 100644 index 00000000..0a5c797c --- /dev/null +++ b/tests/mocks.go @@ -0,0 +1,12 @@ +package tests + +import ( + "time" + + . "github.com/openshift-online/ocm-sdk-go/testing" // nolint +) + +func InitiateAuthCodeMock(clientID string) (string, error) { + accessToken := MakeTokenString("Bearer", 15*time.Minute) + return accessToken, nil +} diff --git a/tests/pass_test.go b/tests/pass_test.go new file mode 100644 index 00000000..ee2c4775 --- /dev/null +++ b/tests/pass_test.go @@ -0,0 +1,227 @@ +//go:build !windows +// +build !windows + +/* +Copyright (c) 2024 Red Hat, 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 tests + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "time" + + . "github.com/onsi/ginkgo/v2" // nolint + . "github.com/onsi/gomega" // nolint + . "github.com/onsi/gomega/ghttp" // nolint + + "github.com/openshift-online/ocm-cli/cmd/ocm/login" + "github.com/openshift-online/ocm-cli/pkg/properties" + . "github.com/openshift-online/ocm-sdk-go/testing" // nolint +) + +// This test requires `pass` to be installed. +// macOS: `brew install pass` +// linux: `sudo apt-get install pass` or `sudo yum install pass` + +const keyring_dir = "keyring-pass-test-*" + +func runCmd(cmds ...string) { + cmd := exec.Command(cmds[0], cmds[1:]...) //nolint:gosec + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(cmd) + fmt.Println(string(out)) + Fail(err.Error()) + } +} + +var _ = Describe("Pass Keyring", Ordered, func() { + BeforeAll(func() { + pwd, err := os.Getwd() + if err != nil { + Fail(err.Error()) + } + pwdParent := filepath.Dir(pwd) + + // the default temp directory can't be used because gpg-agent complains with "socket name too long" + tmpdir, err := os.MkdirTemp("/tmp", keyring_dir) + if err != nil { + Fail(err.Error()) + + } + tmpdirPass, err := os.MkdirTemp("/tmp", ".password-store-*") + if err != nil { + Fail(err.Error()) + } + + // Initialise a blank GPG homedir; import & trust the test key + gnupghome := filepath.Join(tmpdir, ".gnupg") + err = os.Mkdir(gnupghome, os.FileMode(int(0700))) + if err != nil { + Fail(err.Error()) + } + os.Setenv("GNUPGHOME", gnupghome) + os.Setenv("PASSWORD_STORE_DIR", tmpdirPass) + os.Unsetenv("GPG_AGENT_INFO") + os.Unsetenv("GPG_TTY") + + runCmd("gpg", "--batch", "--import", filepath.Join(pwdParent, "testdata", "test-gpg.key")) + runCmd("gpg", "--batch", "--import-ownertrust", filepath.Join(pwdParent, "testdata", "test-ownertrust-gpg.txt")) + runCmd("pass", "init", "ocm-devel@redhat.com") + + DeferCleanup(func() { + os.Unsetenv("GNUPGHOME") + os.Unsetenv("PASSWORD_STORE_DIR") + os.RemoveAll(filepath.Join("/tmp", keyring_dir)) + }) + }) + + var ctx context.Context + var ssoServer *Server + + BeforeEach(func() { + // Create the context + ctx = context.Background() + + // Create the server + ssoServer = MakeTCPServer() + }) + + AfterEach(func() { + // Close the server + ssoServer.Close() + }) + + When("Using OCM_KEYRING", func() { + AfterEach(func() { + // reset keyring + os.Setenv(properties.KeyringEnvKey, "") + }) + + It("Stores/Removes configuration in Pass", func() { + // Create the token + accessToken := MakeTokenString("Bearer", 15*time.Minute) + + // Prepare the server + ssoServer.AppendHandlers( + RespondWithAccessToken(accessToken), + ) + + os.Setenv(properties.KeyringEnvKey, "pass") + + // Run login + result := NewCommand(). + Args( + "login", + "--client-id", "my-client", + "--client-secret", "my-secret", + "--token-url", ssoServer.URL(), + ). + Run(ctx) + + Expect(result.ErrString()).To(BeEmpty()) + Expect(result.ExitCode()).To(BeZero()) + // Verify no config file data exists + Expect(result.ConfigFile()).To(BeEmpty()) + Expect(result.ConfigString()).To(BeEmpty()) + + // Check the content of the keyring + result = NewCommand(). + Args( + "config", + "get", + "access_token", + ). + Run(ctx) + Expect(result.ErrString()).To(BeEmpty()) + Expect(result.ExitCode()).To(BeZero()) + Expect(result.OutLines()[0]).To(ContainSubstring(accessToken)) + + // Remove the configuration from the keyring + result = NewCommand(). + Args( + "logout", + ). + Run(ctx) + Expect(result.ErrString()).To(BeEmpty()) + Expect(result.ExitCode()).To(BeZero()) + Expect(result.OutLines()).To(BeEmpty()) + + // Ensure the keyring is empty + result = NewCommand(). + Args( + "config", + "get", + "access_token", + ). + Run(ctx) + Expect(result.ErrString()).To(BeEmpty()) + Expect(result.ExitCode()).To(BeZero()) + Expect(result.OutLines()[0]).To(BeEmpty()) + }) + + Context("Using auth code", func() { + It("Stores/Removes configuration in Keychain", func() { + os.Setenv(properties.KeyringEnvKey, "pass") + + login.InitiateAuthCode = InitiateAuthCodeMock + + cmd := login.Cmd + cmd.SetArgs([]string{"--use-auth-code"}) + err := cmd.Execute() + Expect(err).NotTo(HaveOccurred()) + + // Check the content of the keyring + result := NewCommand(). + Args( + "config", + "get", + "access_token", + ). + Run(ctx) + Expect(result.ExitCode()).To(BeZero()) + Expect(result.ErrString()).To(BeEmpty()) + Expect(result.OutLines()[0]).NotTo(BeEmpty()) + + // Remove the configuration from the keyring + result = NewCommand(). + Args( + "logout", + ). + Run(ctx) + Expect(result.ExitCode()).To(BeZero()) + Expect(result.ErrString()).To(BeEmpty()) + Expect(result.OutLines()).To(BeEmpty()) + + // Ensure the keyring is empty + result = NewCommand(). + Args( + "config", + "get", + "access_token", + ). + Run(ctx) + Expect(result.ErrString()).To(BeEmpty()) + Expect(result.ExitCode()).To(BeZero()) + Expect(result.OutLines()[0]).To(BeEmpty()) + }) + }) + }) +}) diff --git a/tests/wincred_test.go b/tests/wincred_test.go new file mode 100644 index 00000000..3956413f --- /dev/null +++ b/tests/wincred_test.go @@ -0,0 +1,168 @@ +//go:build windows +// +build windows + +/* +Copyright (c) 2024 Red Hat, 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 tests + +import ( + "context" + "os" + "time" + + . "github.com/onsi/ginkgo/v2" // nolint + . "github.com/onsi/gomega" // nolint + . "github.com/onsi/gomega/ghttp" // nolint + + "github.com/openshift-online/ocm-cli/cmd/ocm/login" + "github.com/openshift-online/ocm-cli/pkg/properties" + . "github.com/openshift-online/ocm-sdk-go/testing" // nolint +) + +var _ = Describe("Wincred Keyring", func() { + var ctx context.Context + var ssoServer *Server + + BeforeEach(func() { + // Create the context + ctx = context.Background() + + // Create the server + ssoServer = MakeTCPServer() + }) + + AfterEach(func() { + // Close the server + ssoServer.Close() + }) + + When("Using OCM_KEYRING", func() { + AfterEach(func() { + // reset keyring + os.Setenv(properties.KeyringEnvKey, "") + }) + + It("Stores/Removes configuration in Keychain", func() { + // Create the token + accessToken := MakeTokenString("Bearer", 15*time.Minute) + + // Prepare the server + ssoServer.AppendHandlers( + RespondWithAccessToken(accessToken), + ) + + os.Setenv(properties.KeyringEnvKey, "wincred") + + // Run login + result := NewCommand(). + Args( + "login", + "--client-id", "my-client", + "--client-secret", "my-secret", + "--token-url", ssoServer.URL(), + ). + Run(ctx) + + Expect(result.ExitCode()).To(BeZero()) + Expect(result.ErrString()).To(BeEmpty()) + // Verify no config file data exists + Expect(result.ConfigFile()).To(BeEmpty()) + Expect(result.ConfigString()).To(BeEmpty()) + + // Check the content of the keyring + result = NewCommand(). + Args( + "config", + "get", + "access_token", + ). + Run(ctx) + Expect(result.ExitCode()).To(BeZero()) + Expect(result.ErrString()).To(BeEmpty()) + Expect(result.OutLines()[0]).To(ContainSubstring(accessToken)) + + // Remove the configuration from the keyring + result = NewCommand(). + Args( + "logout", + ). + Run(ctx) + Expect(result.ExitCode()).To(BeZero()) + Expect(result.ErrString()).To(BeEmpty()) + Expect(result.OutLines()).To(BeEmpty()) + + // Ensure the keyring is empty + result = NewCommand(). + Args( + "config", + "get", + "access_token", + ). + Run(ctx) + Expect(result.ErrString()).To(BeEmpty()) + Expect(result.ExitCode()).To(BeZero()) + Expect(result.OutLines()[0]).To(BeEmpty()) + }) + + Context("Using auth code", func() { + It("Stores/Removes configuration in Keychain", func() { + os.Setenv(properties.KeyringEnvKey, "wincred") + + login.InitiateAuthCode = InitiateAuthCodeMock + + cmd := login.Cmd + cmd.SetArgs([]string{"--use-auth-code"}) + err := cmd.Execute() + Expect(err).NotTo(HaveOccurred()) + + // Check the content of the keyring + result := NewCommand(). + Args( + "config", + "get", + "access_token", + ). + Run(ctx) + Expect(result.ExitCode()).To(BeZero()) + Expect(result.ErrString()).To(BeEmpty()) + Expect(result.OutLines()[0]).NotTo(BeEmpty()) + + // Remove the configuration from the keyring + result = NewCommand(). + Args( + "logout", + ). + Run(ctx) + Expect(result.ExitCode()).To(BeZero()) + Expect(result.ErrString()).To(BeEmpty()) + Expect(result.OutLines()).To(BeEmpty()) + + // Ensure the keyring is empty + result = NewCommand(). + Args( + "config", + "get", + "access_token", + ). + Run(ctx) + Expect(result.ErrString()).To(BeEmpty()) + Expect(result.ExitCode()).To(BeZero()) + Expect(result.OutLines()[0]).To(BeEmpty()) + }) + }) + }) +})