diff --git a/.github/workflows/check-pull-request.yaml b/.github/workflows/check-pull-request.yaml index 9a466a98..301953d7 100644 --- a/.github/workflows/check-pull-request.yaml +++ b/.github/workflows/check-pull-request.yaml @@ -56,8 +56,12 @@ jobs: run: go mod download - name: Setup Ginkgo run: go install github.com/onsi/ginkgo/v2/ginkgo@v2.9 - - name: Run the tests + - name: Run the tests (linux, windows) + if: ${{ contains(fromJSON('["ubuntu-latest", "windows-latest"]'), matrix.platform) }} run: make tests + - name: Run the tests (macOS-only) + if: ${{ contains(fromJSON('["macos-latest"]'), matrix.platform) }} + run: make tests-cgo golangci: name: Lint diff --git a/.github/workflows/publish-release.yaml b/.github/workflows/publish-release.yaml index 6c29b281..d59afc6a 100644 --- a/.github/workflows/publish-release.yaml +++ b/.github/workflows/publish-release.yaml @@ -22,9 +22,8 @@ on: - '*' jobs: - - release: - name: Publish release + release-linux-windows: + name: Publish release (Linux and Windows) runs-on: ubuntu-latest steps: - name: Checkout the source @@ -85,8 +84,6 @@ jobs: os.rename(binary, asset) # Build for the supported operating systems and architectures: - build("darwin", "amd64") - build("darwin", "arm64") build("linux", "amd64") build("linux", "arm64") build("linux", "ppc64le") @@ -152,3 +149,110 @@ jobs: ), ) response.raise_for_status() + + release-macos: + needs: release-linux-windows + name: Publish release (macOS) + runs-on: macos-latest + steps: + - name: Checkout the source + uses: actions/checkout@v3 + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: '3.10' + cache: 'pip' + + - name: Install Python modules + run: pip install -r .github/workflows/requirements.txt + + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version: 1.18 + + - name: Create release + shell: python + run: | + import os + import re + import requests + import shutil + import subprocess + + # Get the context and secret data that we will need: + repository = "${{ github.repository }}" + reference = "${{ github.ref }}" + token = "${{ secrets.GITHUB_TOKEN }}" + + # Calculate the version number: + version = re.sub(r"^refs/tags/v(.*)$", r"\1", reference) + + # Make sure that the assets directory exists and is empty: + assets = "assets" + shutil.rmtree(assets, ignore_errors=True) + os.mkdir(assets) + + def build(goos: str, goarch: str): + # Set the environment variables that tell the Go compiler which + # operating system and architecture to build for: + env = dict(os.environ) + env["GOOS"] = goos + env["GOARCH"] = goarch + + # Build the binary: + args = ["make", "cmds-cgo"] + + subprocess.run(check=True, env=env, args=args) + + # Copy the generated binary to the assets directory: + binary = "ocm" + asset = os.path.join(assets, f"ocm-{goos}-{goarch}") + os.rename(binary, asset) + + # Build for the supported operating systems and architectures: + build("darwin", "amd64") + build("darwin", "arm64") + + # Calculate the SHA256 digests: + for asset in os.listdir(assets): + digest = os.path.join(assets, f"{asset}.sha256") + with open(digest, "wb") as stream: + args = ["shasum", "-a", "256"] + subprocess.run(check=True, cwd=assets, stdout=stream, args=args) + + # Get the release: + response = requests.get( + headers={ + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + }, + url=( + "https://api.github.com" + f"/repos/{repository}/releases/tags/v{version}" + ), + ) + response.raise_for_status() + + # Get the release identifier: + release = response.json()["id"] + + # Upload the assets: + for asset in os.listdir(assets): + file = os.path.join(assets, asset) + with open(file, "rb") as stream: + response = requests.post( + headers={ + "Authorization": f"Bearer {token}", + "Content-Type": "application/octet-stream", + "Accept": "application/json", + }, + data=stream, + url=( + "https://uploads.github.com" + f"/repos/{repository}/releases/{release}/assets?name={asset}" + ), + ) + response.raise_for_status() diff --git a/Makefile b/Makefile index 658c37c0..7e631009 100644 --- a/Makefile +++ b/Makefile @@ -33,14 +33,31 @@ cmds: go build "./cmd/$${cmd}" || exit 1; \ done +# Used for compiling with CGO_ENABLED=1 (macOS keychain support) +.PHONY: cmds-cgo +cmds-cgo: + for cmd in $$(ls cmd); do \ + CGO_ENABLED=1 \ + go build "./cmd/$${cmd}" || exit 1; \ + done + .PHONY: install install: go install ./cmd/ocm +# CGO_ENABLED=1 is required for keychain support on macOS +.PHONY: install-cgo +install-cgo: + CGO_ENABLED=1 go install ./cmd/ocm + .PHONY: test tests test tests: cmds ginkgo run -r +.PHONY: test-cgo tests-cgo +test-cgo tests-cgo: cmds-cgo + ginkgo run -r + .PHONY: fmt fmt: gofmt -s -l -w cmd pkg tests diff --git a/README.md b/README.md index 2fe1bed9..6d2cf113 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,38 @@ $ OCM_CONFIG=$HOME/ocm.json.stg ocm whoami NOTE: Tokens for production and staging will differ. +## Storing Configuration & Tokens in OS Keyring +The `RH_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. + +`RH_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: | + +* if building from source CGO_ENABLED=1 is required for macOS keychain support. + +#### See Available Keyrings +The following will list available keyrings in your current context +``` +$ ocm config get keyrings +``` + +#### Remove Keyring Configuration +The following will remove the OCM configuration from your `RH_KEYRING` +``` +$ ocm config reset keyring +``` + ## 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..38ad67db 100644 --- a/cmd/ocm/config/cmd.go +++ b/cmd/ocm/config/cmd.go @@ -24,6 +24,7 @@ import ( "github.com/spf13/cobra" "github.com/openshift-online/ocm-cli/cmd/ocm/config/get" + "github.com/openshift-online/ocm-cli/cmd/ocm/config/reset" "github.com/openshift-online/ocm-cli/cmd/ocm/config/set" "github.com/openshift-online/ocm-cli/pkg/config" ) @@ -73,4 +74,5 @@ var Cmd = &cobra.Command{ func init() { Cmd.AddCommand(get.Cmd) Cmd.AddCommand(set.Cmd) + Cmd.AddCommand(reset.Cmd) } diff --git a/cmd/ocm/config/get/get.go b/cmd/ocm/config/get/get.go index a9dc002f..91f716ff 100644 --- a/cmd/ocm/config/get/get.go +++ b/cmd/ocm/config/get/get.go @@ -23,6 +23,7 @@ import ( "github.com/spf13/cobra" "github.com/openshift-online/ocm-cli/pkg/config" + "github.com/openshift-online/ocm-sdk-go/authentication/securestore" ) var args struct { @@ -48,17 +49,25 @@ func init() { } func run(cmd *cobra.Command, argv []string) error { - // Load the configuration file: - cfg, err := config.Load() - if err != nil { - return fmt.Errorf("Can't load config file: %v", err) + // The following variables are not stored in the configuration file + // and can skip loading configuration: + skipConfigLoadMap := map[string]bool{ + "keyrings": true, } - // If the configuration file doesn't exist yet assume that all the configuration settings - // are empty: - if cfg == nil { - fmt.Printf("\n") - return nil + cfg := config.Config{} + if !skipConfigLoadMap[argv[0]] { + // Load the configuration file: + cfg, err := config.Load() + if err != nil { + return fmt.Errorf("Can't load config file: %v", err) + } + // If the configuration file doesn't exist yet assume that all the configuration settings + // are empty: + if cfg == nil { + fmt.Printf("\n") + return nil + } } // Print the value of the requested configuration setting: @@ -83,9 +92,22 @@ func run(cmd *cobra.Command, argv []string) error { fmt.Fprintf(os.Stdout, "%s\n", cfg.URL) case "pager": fmt.Fprintf(os.Stdout, "%s\n", cfg.Pager) + case "keyrings": + keyrings := getKeyrings() + for _, keyring := range keyrings { + fmt.Println(keyring) + } default: return fmt.Errorf("Unknown setting") } return nil } + +func getKeyrings() []string { + backends := securestore.AvailableBackends() + if len(backends) == 0 { + fmt.Printf("Error: No keyrings available\n") + } + return backends +} diff --git a/cmd/ocm/config/reset/reset.go b/cmd/ocm/config/reset/reset.go new file mode 100644 index 00000000..7e4d1127 --- /dev/null +++ b/cmd/ocm/config/reset/reset.go @@ -0,0 +1,66 @@ +/* +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 reset + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/openshift-online/ocm-sdk-go/authentication/securestore" +) + +var args struct { + debug bool +} + +var Cmd = &cobra.Command{ + Use: "reset [flags] VARIABLE", + Short: "Resets/removes the requested option from configuration", + Long: "Resets/removes requested option from configuration", + Args: cobra.ExactArgs(1), + RunE: run, +} + +func init() { + flags := Cmd.Flags() + flags.BoolVar( + &args.debug, + "debug", + false, + "Enable debug mode.", + ) +} + +func run(cmd *cobra.Command, argv []string) error { + switch argv[0] { + case "keyring": + keyring := os.Getenv("RH_KEYRING") + if keyring == "" { + return fmt.Errorf("RH_KEYRING is required to reset config") + } + err := securestore.RemoveConfigFromKeyring(keyring) + if err != nil { + return fmt.Errorf("can't reset keyring: %v", err) + } + default: + return fmt.Errorf("unknown setting") + } + + return nil +} diff --git a/cmd/ocm/login/cmd.go b/cmd/ocm/login/cmd.go index b5798fd4..66b25a2f 100644 --- a/cmd/ocm/login/cmd.go +++ b/cmd/ocm/login/cmd.go @@ -27,6 +27,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" ) @@ -162,7 +163,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.MarkHidden("use-auth-code") flags.BoolVar( @@ -171,7 +172,7 @@ 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.", ) flags.MarkHidden("use-device-code") } @@ -186,6 +187,15 @@ func run(cmd *cobra.Command, argv []string) error { return fmt.Errorf("Option '--url' is mandatory") } + // Fail fast if RH_KEYRING is provided and invalid + keyring := os.Getenv("RH_KEYRING") + if keyring != "" { + 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 @@ -220,9 +230,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, @@ -246,10 +256,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) @@ -342,7 +352,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 { @@ -353,8 +363,8 @@ func run(cmd *cobra.Command, argv []string) error { ssoHost := ssoURL.Scheme + "://" + ssoURL.Hostname() fmt.Println("Login successful") - fmt.Printf("To switch accounts, logout from %s and run `ocm logout` "+ - "before attempting to login again", ssoHost) + fmt.Println(fmt.Sprintf("To switch accounts, logout from %s and run `ocm logout` "+ + "before attempting to login again", ssoHost)) } return nil diff --git a/go.mod b/go.mod index 9e0b67b7..c56d9636 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ 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.398 + github.com/openshift-online/ocm-sdk-go v0.1.399 github.com/openshift/rosa v1.2.24 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 github.com/spf13/cobra v1.7.0 @@ -25,22 +25,28 @@ require ( ) require ( + github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect + github.com/99designs/keyring v1.2.2 // 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.5.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/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 @@ -62,13 +68,13 @@ 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/zgalor/weberr v0.7.0 // indirect @@ -81,3 +87,5 @@ require ( google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) + +replace github.com/openshift-online/ocm-sdk-go => ../ocm-sdk-go diff --git a/go.sum b/go.sum index 7fdfe2e4..2dc7fccd 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= @@ -73,9 +77,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.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM= +github.com/dvsekhvalnov/jose2go v1.5.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 +112,8 @@ 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/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 +184,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= @@ -258,8 +270,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 +314,18 @@ 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.398 h1:6C1mDcPxzG4jSduOaWixTTI5gSEO+Jm7OW/00jVoWGI= -github.com/openshift-online/ocm-sdk-go v0.1.398/go.mod h1:tke8vKcE7eHKyRbkJv6qo4ljo919zhx04uyQTcgF5cQ= 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 +364,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= @@ -378,6 +388,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ 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= @@ -723,8 +735,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..8a3f7485 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -29,6 +29,7 @@ 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" @@ -53,9 +54,41 @@ 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. +// Loads the configuration from the OS keyring first if available, load from the configuration file if not func Load() (cfg *Config, err error) { + keyring := os.Getenv("RH_KEYRING") + + if keyring != "" { + return loadFromOS(keyring) + } + + return loadFromFile() +} + +// Loads the configuration from the OS keyring. If the configuration file 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 +} + +// 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 +127,28 @@ 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) + } + + keyring := os.Getenv("RH_KEYRING") + + if keyring != "" { + // 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) diff --git a/tests/login_test.go b/tests/login_test.go index 9b8f099e..9ad9de5f 100644 --- a/tests/login_test.go +++ b/tests/login_test.go @@ -18,6 +18,7 @@ package tests import ( "context" + "os" "time" sdk "github.com/openshift-online/ocm-sdk-go" @@ -47,6 +48,11 @@ var _ = Describe("Login", func() { }) When("Using offline token", func() { + AfterEach(func() { + // reset keyring + os.Setenv("RH_KEYRING", "") + }) + It("Creates the configuration file", func() { // Create the tokens: accessToken := MakeTokenString("Bearer", 15*time.Minute) @@ -84,6 +90,25 @@ var _ = Describe("Login", func() { "accessToken", accessToken, )) }) + + It("Fails for an invalid keyring", func() { + keyring := "not-a-keyring" + os.Setenv("RH_KEYRING", keyring) + // 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 +207,52 @@ var _ = Describe("Login", func() { )) }) }) + + When("Using auth code flow", func() { + AfterEach(func() { + // reset keyring + os.Setenv("RH_KEYRING", "") + }) + + It("Fails for an invalid keyring", func() { + keyring := "not-a-keyring" + + os.Setenv("RH_KEYRING", keyring) + + // 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("RH_KEYRING", "") + }) + + It("Fails for an invalid keyring", func() { + keyring := "not-a-keyring" + + os.Setenv("RH_KEYRING", keyring) + + // 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")) + }) + }) })