Skip to content

Commit

Permalink
Migrates packages from ocm cli
Browse files Browse the repository at this point in the history
This commit migrates the config, connection-builder and properties
packages from the ocm CLI to be shared across ocm-using codebases for
consistency.
  • Loading branch information
iamkirkbater committed Nov 21, 2024
1 parent 7e27b75 commit 81c7a33
Show file tree
Hide file tree
Showing 8 changed files with 1,479 additions and 25 deletions.
59 changes: 50 additions & 9 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,64 @@ require (
github.com/aws/aws-sdk-go-v2/service/route53 v1.40.3
github.com/aws/aws-sdk-go-v2/service/sts v1.28.5
github.com/go-jose/go-jose/v4 v4.0.2
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/hashicorp/go-version v1.6.0
github.com/mitchellh/go-homedir v1.1.0
github.com/onsi/ginkgo/v2 v2.17.1
github.com/onsi/gomega v1.30.0
github.com/openshift-online/ocm-sdk-go v0.1.421
github.com/openshift-online/ocm-cli v1.0.2
github.com/openshift-online/ocm-sdk-go v0.1.445
github.com/sirupsen/logrus v1.9.3
go.uber.org/mock v0.3.0
golang.org/x/crypto v0.22.0
)

require github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 // indirect

require (
github.com/aws/smithy-go v1.20.3
github.com/kr/pretty v0.1.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
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-v2/aws/protocol/eventstream v1.6.2 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // 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/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // 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.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.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/matttproud/golang_protobuf_extensions v1.0.2 // indirect
github.com/microcosm-cc/bluemonday v1.0.23 // 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/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/zalando/go-keyring v0.2.3 // indirect
golang.org/x/oauth2 v0.19.0 // indirect
golang.org/x/term v0.19.0 // indirect
google.golang.org/protobuf v1.34.0 // indirect
)

require github.com/aws/smithy-go v1.20.3

require (
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
Expand All @@ -45,14 +86,14 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.3 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/glog v1.2.0
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.17.0 // indirect
Expand Down
647 changes: 633 additions & 14 deletions go.sum

Large diffs are not rendered by default.

277 changes: 277 additions & 0 deletions pkg/ocm/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
/*
Copyright (c) 2018 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.
*/

// This file contains the types and functions used to manage the configuration of the command line
// client.

package config

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"

homedir "github.com/mitchellh/go-homedir"
"github.com/openshift-online/ocm-sdk-go/authentication/securestore"

"github.com/openshift-online/ocm-cli/pkg/properties"
)

// Config is the type used to store the configuration of the client.
// There's no way to line-split or predefine tags, so...
// nolint:lll
type Config struct {
// TODO(efried): Better docs for things like AccessToken
// TODO(efried): Dedup with flag docs in cmd/ocm/login/cmd.go:init where possible
AccessToken string `json:"access_token,omitempty" doc:"Bearer access token."`
ClientID string `json:"client_id,omitempty" doc:"OpenID client identifier."`
ClientSecret string `json:"client_secret,omitempty" doc:"OpenID client secret."`
Insecure bool `json:"insecure,omitempty" doc:"Enables insecure communication with the server. This disables verification of TLS certificates and host names."`
Password string `json:"password,omitempty" doc:"User password."`
RefreshToken string `json:"refresh_token,omitempty" doc:"Offline or refresh token."`
Scopes []string `json:"scopes,omitempty" doc:"OpenID scope. If this option is used it will replace completely the default scopes. Can be repeated multiple times to specify multiple scopes."`
TokenURL string `json:"token_url,omitempty" doc:"OpenID token URL."`
URL string `json:"url,omitempty" doc:"URL of the API gateway. The value can be the complete URL or an alias. The valid aliases are 'production', 'staging' and 'integration'."`
User string `json:"user,omitempty" doc:"User name."`
Pager string `json:"pager,omitempty" doc:"Pager command, for example 'less'. If empty no pager will be used."`
}

// 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
}
_, err = os.Stat(file)
if os.IsNotExist(err) {
cfg = &Config{}
err = nil
return
}
if err != nil {
err = fmt.Errorf("can't check if config file '%s' exists: %v", file, err)
return
}
// #nosec G304
data, err := os.ReadFile(file)
if err != nil {
err = fmt.Errorf("can't read config file '%s': %v", file, err)
return
}
cfg = &Config{}
if len(data) == 0 {
return
}
err = json.Unmarshal(data, cfg)
if err != nil {
err = fmt.Errorf("can't parse config file '%s': %v", file, err)
return
}
return
}

// Save saves the given configuration to the configuration file.
func Save(cfg *Config) error {
file, err := Location()
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)
}
err = os.WriteFile(file, data, 0600)
if err != nil {
return fmt.Errorf("can't write file '%s': %v", file, err)
}
return nil
}

// Location returns the location of the configuration file. If a configuration file
// already exists in the HOME directory, it uses that, otherwise it prefers to
// use the XDG config directory.
func Location() (path string, err error) {
if ocmconfig := os.Getenv("OCM_CONFIG"); ocmconfig != "" {
return ocmconfig, nil
}

// Determine home directory to use for the legacy file path
home, err := homedir.Dir()
if err != nil {
return "", err
}

path = filepath.Join(home, ".ocm.json")

_, err = os.Stat(path)
if os.IsNotExist(err) {
// Determine standard config directory
configDir, err := os.UserConfigDir()
if err != nil {
return path, err
}

// Use standard config directory
path = filepath.Join(configDir, "/ocm/ocm.json")
}

return path, nil
}

// Armed checks if the configuration contains either credentials or tokens that haven't expired, so
// that it can be used to perform authenticated requests.
func (c *Config) Armed() (armed bool, reason string, err error) {
// Check URLs:
haveURL := c.URL != ""
haveTokenURL := c.TokenURL != ""
haveURLs := haveURL && haveTokenURL

// Check credentials:
havePassword := c.User != "" && c.Password != ""
haveSecret := c.ClientID != "" && c.ClientSecret != ""
haveCredentials := havePassword || haveSecret

// Check tokens:
haveAccess := c.AccessToken != ""
accessUsable := false
if haveAccess {
accessUsable, err = tokenUsable(c.AccessToken, 5*time.Second)
if err != nil {
return
}
}
haveRefresh := c.RefreshToken != ""
refreshUsable := false
if haveRefresh {
if IsEncryptedToken(c.RefreshToken) {
// We have no way of knowing an encrypted token expiration, so
// we assume it's valid and let the access token request fail.
refreshUsable = true
} else {
refreshUsable, err = tokenUsable(c.RefreshToken, 10*time.Second)
if err != nil {
return
}
}
}

// Calculate the result:
armed = haveURLs && (haveCredentials || accessUsable || refreshUsable)
if armed {
return
}

// If it isn't armed then we should return a human readable reason. We should try to
// generate a message that describes the more relevant reason. For example, missing
// credentials is more important than missing URLs, so that condition should be checked
// first.
switch {
case haveAccess && !haveRefresh && !accessUsable:
reason = "access token is expired"
case !haveAccess && haveRefresh && !refreshUsable:
reason = "refresh token is expired"
case haveAccess && !accessUsable && haveRefresh && !refreshUsable:
reason = "access and refresh tokens are expired"
case !haveCredentials:
reason = "credentials aren't set"
case !haveURL && haveTokenURL:
reason = "server URL isn't set"
case haveURL && !haveTokenURL:
reason = "token URL isn't set"
case !haveURL && !haveTokenURL:
reason = "server and token URLs aren't set"
}

return
}

// Disarm removes from the configuration all the settings that are needed for authentication.
func (c *Config) Disarm() {
c.AccessToken = ""
c.ClientID = ""
c.ClientSecret = ""
c.Insecure = false
c.Password = ""
c.RefreshToken = ""
c.Scopes = nil
c.TokenURL = ""
c.URL = ""
c.User = ""
}

// 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
}
Loading

0 comments on commit 81c7a33

Please sign in to comment.