Files
threshold/internal/cmd/base/keyring.go
T
Johan Brandhorst-Satzkorn 1a6320794e cmd: propagate keyring reading errors (#5360)
Reading a token from the keyring can sometimes fail for reasons
other than the token not being present in the keyring. This PR
introduces a new error used to indicate that the token requested
could not be found in the keyring and propagates errors from the
keyring up to the caller, so that the caller can act on them.

Additionally, this fixes a bug in the client cache where it would
incorrectly assume that a token had been removed from the
keyring, when in fact there was an error reading from the keyring.
2024-12-23 09:21:18 -08:00

191 lines
5.6 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package base
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"os"
"runtime"
"strings"
"github.com/hashicorp/boundary/api/authtokens"
nkeyring "github.com/jefferai/keyring"
"github.com/mitchellh/cli"
zkeyring "github.com/zalando/go-keyring"
)
const (
NoneKeyring = "none"
AutoKeyring = "auto"
WincredKeyring = "wincred"
PassKeyring = "pass"
KeychainKeyring = "keychain"
SecretServiceKeyring = "secret-service"
DefaultTokenName = "default"
LoginCollection = "login"
PassPrefix = "HashiCorp_Boundary"
)
// ErrNoToken is returned when no matching token could be found in the keyring
var ErrNoToken = errors.New("no token found")
func (c *Command) DiscoverKeyringTokenInfo() (string, string, error) {
// Stops the underlying library from invoking a dbus call that ends up
// freezing some systems
os.Setenv("DISABLE_KWALLET", "1")
tokenName := DefaultTokenName
if c.FlagTokenName != "" {
tokenName = c.FlagTokenName
}
if tokenName == NoneKeyring {
c.UI.Warn(`"-token-name=none" is deprecated, please use "-keyring-type=none"`)
c.FlagKeyringType = NoneKeyring
}
if c.FlagKeyringType == NoneKeyring {
return "", "", nil
}
// Set so we can look it up later when printing out curl strings
os.Setenv(EnvTokenName, tokenName)
var foundKeyringType bool
keyringType := c.FlagKeyringType
switch runtime.GOOS {
case "windows":
switch keyringType {
case AutoKeyring, WincredKeyring, PassKeyring:
foundKeyringType = true
if keyringType == AutoKeyring {
keyringType = WincredKeyring
}
}
case "darwin":
switch keyringType {
case AutoKeyring, KeychainKeyring, PassKeyring:
foundKeyringType = true
if keyringType == AutoKeyring {
keyringType = KeychainKeyring
}
}
default:
switch keyringType {
case AutoKeyring, SecretServiceKeyring, PassKeyring:
foundKeyringType = true
if keyringType == AutoKeyring {
keyringType = PassKeyring
}
}
}
if !foundKeyringType {
return "", "", fmt.Errorf("Given keyring type %q is not valid, or not valid for this platform", c.FlagKeyringType)
}
var available bool
switch keyringType {
case WincredKeyring, KeychainKeyring:
available = true
case PassKeyring, SecretServiceKeyring:
avail := nkeyring.AvailableBackends()
for _, a := range avail {
if keyringType == string(a) {
available = true
}
}
}
if !available {
return "", "", fmt.Errorf("Keyring type %q is not available on this machine. For help with setting up keyrings, see https://www.boundaryproject.io/docs/api-clients/cli.", keyringType)
}
os.Setenv(EnvKeyringType, keyringType)
return keyringType, tokenName, nil
}
// ReadTokenFromKeyring will attempt to read the token with the name tokenName from the keyring
// described by keyringType. It will return ErrNoToken if no token is found in the provided keyring.
func ReadTokenFromKeyring(ui cli.Ui, keyringType, tokenName string) (*authtokens.AuthToken, error) {
const op = "base.ReadTokenFromKeyring"
var token string
var err error
switch keyringType {
case NoneKeyring:
return nil, ErrNoToken
case WincredKeyring, KeychainKeyring:
token, err = zkeyring.Get(StoredTokenName, tokenName)
if err != nil {
if err == zkeyring.ErrNotFound {
ui.Error("No saved credential found, continuing without")
} else {
ui.Warn("Token must be provided via BOUNDARY_TOKEN env var or -token flag. Reading the token can also be disabled via -keyring-type=none.")
return nil, fmt.Errorf("%s: error reading auth token from keyring: %w", op, err)
}
}
default:
krConfig := nkeyring.Config{
LibSecretCollectionName: LoginCollection,
PassPrefix: PassPrefix,
AllowedBackends: []nkeyring.BackendType{nkeyring.BackendType(keyringType)},
}
kr, err := nkeyring.Open(krConfig)
if err != nil {
ui.Warn("Token must be provided via BOUNDARY_TOKEN env var or -token flag. Reading the token can also be disabled via -keyring-type=none.")
return nil, fmt.Errorf("%s: failed to open keyring: %w", op, err)
}
item, err := kr.Get(tokenName)
if err != nil {
ui.Warn("Token must be provided via BOUNDARY_TOKEN env var or -token flag. Reading the token can also be disabled via -keyring-type=none.")
return nil, fmt.Errorf("%s: failed to get token from keyring: %w", op, err)
}
token = string(item.Data)
}
if token != "" {
tokenBytes, err := base64.RawStdEncoding.DecodeString(token)
switch {
case err != nil:
return nil, fmt.Errorf("error base64-unmarshaling stored token from system credential store: %w", err)
case len(tokenBytes) == 0:
return nil, errors.New("zero length token after decoding stored token from system credential store")
default:
var authToken authtokens.AuthToken
if err := json.Unmarshal(tokenBytes, &authToken); err != nil {
return nil, fmt.Errorf("error unmarshaling stored token information after reading from system credential store: %s", err)
} else {
return &authToken, nil
}
}
}
return nil, ErrNoToken
}
// ReadTokenFromKeyring will attempt to read the token with the name tokenName from the keyring
// described by keyringType. It will return ErrNoToken if no token is found in the provided keyring.
func (c *Command) ReadTokenFromKeyring(keyringType, tokenName string) (*authtokens.AuthToken, error) {
return ReadTokenFromKeyring(c.UI, keyringType, tokenName)
}
func TokenIdFromToken(token string) (string, error) {
split := strings.Split(token, "_")
if len(split) < 3 {
return "", errors.New("Unexpected stored token format")
}
return strings.Join(split[0:2], "_"), nil
}