mirror of https://github.com/hashicorp/boundary
Add read:self and delete:self to auth tokens and add logout command (#1162)
parent
3fe94335d4
commit
e6af51943d
@ -0,0 +1,173 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary/api/authtokens"
|
||||
nkeyring "github.com/jefferai/keyring"
|
||||
"github.com/pkg/errors"
|
||||
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"
|
||||
)
|
||||
|
||||
func (c *Command) DiscoverKeyringTokenInfo() (string, string, error) {
|
||||
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
|
||||
}
|
||||
|
||||
func (c *Command) ReadTokenFromKeyring(keyringType, tokenName string) *authtokens.AuthToken {
|
||||
var token string
|
||||
var err error
|
||||
|
||||
switch keyringType {
|
||||
case NoneKeyring:
|
||||
return nil
|
||||
|
||||
case WincredKeyring, KeychainKeyring:
|
||||
token, err = zkeyring.Get(StoredTokenName, tokenName)
|
||||
if err != nil {
|
||||
if err == zkeyring.ErrNotFound {
|
||||
c.UI.Error("No saved credential found, continuing without")
|
||||
} else {
|
||||
c.UI.Error(fmt.Sprintf("Error reading auth token from keyring: %s", err))
|
||||
c.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.")
|
||||
}
|
||||
token = ""
|
||||
}
|
||||
|
||||
default:
|
||||
krConfig := nkeyring.Config{
|
||||
LibSecretCollectionName: LoginCollection,
|
||||
PassPrefix: PassPrefix,
|
||||
AllowedBackends: []nkeyring.BackendType{nkeyring.BackendType(keyringType)},
|
||||
}
|
||||
|
||||
kr, err := nkeyring.Open(krConfig)
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error opening keyring: %s", err))
|
||||
c.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.")
|
||||
break
|
||||
}
|
||||
|
||||
item, err := kr.Get(tokenName)
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error fetching token from keyring: %s", err))
|
||||
c.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.")
|
||||
break
|
||||
}
|
||||
|
||||
token = string(item.Data)
|
||||
}
|
||||
|
||||
if token != "" {
|
||||
tokenBytes, err := base64.RawStdEncoding.DecodeString(token)
|
||||
switch {
|
||||
case err != nil:
|
||||
c.UI.Error(fmt.Errorf("Error base64-unmarshaling stored token from system credential store: %w", err).Error())
|
||||
case len(tokenBytes) == 0:
|
||||
c.UI.Error("Zero length token after decoding stored token from system credential store")
|
||||
default:
|
||||
var authToken authtokens.AuthToken
|
||||
if err := json.Unmarshal(tokenBytes, &authToken); err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error unmarshaling stored token information after reading from system credential store: %s", err))
|
||||
} else {
|
||||
return &authToken
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@ -0,0 +1,161 @@
|
||||
package logout
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/hashicorp/boundary/api"
|
||||
"github.com/hashicorp/boundary/api/authtokens"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
nkeyring "github.com/jefferai/keyring"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
zkeyring "github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
var (
|
||||
_ cli.Command = (*LogoutCommand)(nil)
|
||||
_ cli.CommandAutocomplete = (*LogoutCommand)(nil)
|
||||
)
|
||||
|
||||
type LogoutCommand struct {
|
||||
*base.Command
|
||||
|
||||
Func string
|
||||
}
|
||||
|
||||
func (c *LogoutCommand) Synopsis() string {
|
||||
return "Delete the current token within Boundary and forget it locally"
|
||||
}
|
||||
|
||||
func (c *LogoutCommand) Help() string {
|
||||
var args []string
|
||||
args = append(args,
|
||||
"Usage: boundary logout [options]",
|
||||
"",
|
||||
" Delete the current token (as selected by -token-name) within Boundary and forget it from the local store. Example:",
|
||||
"",
|
||||
` $ boundary logout`,
|
||||
"",
|
||||
)
|
||||
|
||||
return base.WrapForHelpText(args) + c.Flags().Help()
|
||||
}
|
||||
|
||||
func (c *LogoutCommand) Flags() *base.FlagSets {
|
||||
set := c.FlagSet(base.FlagSetNone)
|
||||
|
||||
f := set.NewFlagSet("Command Options")
|
||||
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: "token-name",
|
||||
Target: &c.FlagTokenName,
|
||||
EnvVar: base.EnvTokenName,
|
||||
Usage: `If specified, the given value will be used as the name when loading the token from the system credential store. This must correspond to a name used when authenticating.`,
|
||||
})
|
||||
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: "keyring-type",
|
||||
Target: &c.FlagKeyringType,
|
||||
Default: "auto",
|
||||
EnvVar: base.EnvKeyringType,
|
||||
Usage: `The type of keyring to use. Defaults to "auto" which will use the Windows credential manager, OSX keychain, or cross-platform password store depending on platform. Set to "none" to disable keyring functionality. Available types, depending on platform, are: "wincred", "keychain", "pass", and "secret-service".`,
|
||||
})
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
func (c *LogoutCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictAnything
|
||||
}
|
||||
|
||||
func (c *LogoutCommand) AutocompleteFlags() complete.Flags {
|
||||
return c.Flags().Completions()
|
||||
}
|
||||
|
||||
func (c *LogoutCommand) Run(args []string) (ret int) {
|
||||
f := c.Flags()
|
||||
if err := f.Parse(args); err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
client, err := c.Client()
|
||||
if err != nil {
|
||||
c.PrintCliError(fmt.Errorf("Error reading API client: %w", err))
|
||||
return base.CommandCliError
|
||||
}
|
||||
|
||||
if client.Token() == "" {
|
||||
c.PrintCliError(errors.New("Empty or no token found in store. It might have already been deleted."))
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
id, err := base.TokenIdFromToken(client.Token())
|
||||
if err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
authtokensClient := authtokens.NewClient(client)
|
||||
_, err = authtokensClient.Delete(c.Context, id)
|
||||
if apiErr := api.AsServerError(err); apiErr != nil && apiErr.Response().StatusCode() == http.StatusNotFound {
|
||||
c.UI.Output("The token was not found on the Boundary controller; proceeding to delete from the local store.")
|
||||
goto DeleteLocal
|
||||
}
|
||||
if err != nil {
|
||||
if apiErr := api.AsServerError(err); apiErr != nil {
|
||||
c.PrintApiError(apiErr, "Error from controller when performing delete on token")
|
||||
return base.CommandApiError
|
||||
}
|
||||
c.PrintCliError(fmt.Errorf("Error trying to delete auth token: %w", err))
|
||||
return base.CommandCliError
|
||||
}
|
||||
|
||||
c.UI.Output("The token was successfully deleted within the Boundary controller.")
|
||||
|
||||
DeleteLocal:
|
||||
keyringType, tokenName, err := c.DiscoverKeyringTokenInfo()
|
||||
if err != nil {
|
||||
c.PrintCliError(fmt.Errorf("Error fetching keyring information to delete local stored token: %w", err))
|
||||
return base.CommandCliError
|
||||
}
|
||||
if keyringType == "none" ||
|
||||
tokenName == "none" ||
|
||||
keyringType == "" ||
|
||||
tokenName == "" {
|
||||
c.UI.Output("Keyring type set to none or empty; not deleting local stored token.")
|
||||
return base.CommandSuccess
|
||||
}
|
||||
|
||||
switch keyringType {
|
||||
case "wincred", "keychain":
|
||||
if err := zkeyring.Delete(base.StoredTokenName, tokenName); err != nil {
|
||||
c.PrintCliError(fmt.Errorf("Error deleting auth token from %q keyring: %w", keyringType, err))
|
||||
return base.CommandCliError
|
||||
}
|
||||
|
||||
default:
|
||||
krConfig := nkeyring.Config{
|
||||
LibSecretCollectionName: "login",
|
||||
PassPrefix: "HashiCorp_Boundary",
|
||||
AllowedBackends: []nkeyring.BackendType{nkeyring.BackendType(keyringType)},
|
||||
}
|
||||
|
||||
kr, err := nkeyring.Open(krConfig)
|
||||
if err != nil {
|
||||
c.PrintCliError(fmt.Errorf("Error opening %q keyring: %w", keyringType, err))
|
||||
return base.CommandCliError
|
||||
}
|
||||
|
||||
if err := kr.Remove(tokenName); err != nil {
|
||||
c.PrintCliError(fmt.Errorf("Error deleting token from %q keyring: %w", keyringType, err))
|
||||
return base.CommandCliError
|
||||
}
|
||||
}
|
||||
|
||||
c.UI.Output("The token was successfully removed from the local credential store.")
|
||||
|
||||
return base.CommandSuccess
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
function read_token() {
|
||||
if [[ "x$1" == "x" ]]
|
||||
then
|
||||
echo "y" | boundary auth-tokens read
|
||||
else
|
||||
boundary auth-tokens read -id $1
|
||||
fi
|
||||
}
|
||||
|
||||
function delete_token() {
|
||||
if [[ "x$1" == "x" ]]
|
||||
then
|
||||
echo "y" | boundary auth-tokens delete
|
||||
else
|
||||
boundary auth-tokens delete -id $1
|
||||
fi
|
||||
}
|
||||
|
||||
function token_id() {
|
||||
local tid=$1
|
||||
strip $(read_token $tid | jq '.item.id')
|
||||
}
|
||||
|
||||
function logout_cmd() {
|
||||
boundary logout
|
||||
}
|
||||
|
||||
function get_token() {
|
||||
boundary config get-token
|
||||
}
|
||||
|
||||
function read_token_no_keyring() {
|
||||
boundary auth-tokens read -keyring-type=none -id $1
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
load _auth
|
||||
load _auth_tokens
|
||||
load _helpers
|
||||
|
||||
export NEW_USER='test'
|
||||
|
||||
@test "boundary/token: can login as unpriv user" {
|
||||
run login $DEFAULT_UNPRIVILEGED_LOGIN
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "boundary/token: can read own token with no id given" {
|
||||
run read_token ""
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "boundary/token: can read own token with self given" {
|
||||
run read_token "self"
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "boundary/token: can read own token id given" {
|
||||
local tid=$(token_id "self")
|
||||
run read_token "$tid"
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "boundary/token: can delete own token with no id given" {
|
||||
run login $DEFAULT_UNPRIVILEGED_LOGIN
|
||||
[ "$status" -eq 0 ]
|
||||
run delete_token ""
|
||||
[ "$status" -eq 0 ]
|
||||
run read_token ""
|
||||
[ "$status" -eq 1 ]
|
||||
}
|
||||
|
||||
@test "boundary/token: can delete own token with self given" {
|
||||
run login $DEFAULT_UNPRIVILEGED_LOGIN
|
||||
[ "$status" -eq 0 ]
|
||||
run delete_token "self"
|
||||
[ "$status" -eq 0 ]
|
||||
run read_token ""
|
||||
[ "$status" -eq 1 ]
|
||||
}
|
||||
|
||||
@test "boundary/token: can delete own token with id given" {
|
||||
run login $DEFAULT_UNPRIVILEGED_LOGIN
|
||||
[ "$status" -eq 0 ]
|
||||
run token_id "self"
|
||||
[ "$status" -eq 0 ]
|
||||
tid=$(echo "$output")
|
||||
run delete_token $tid
|
||||
[ "$status" -eq 0 ]
|
||||
run read_token $tid
|
||||
[ "$status" -eq 1 ]
|
||||
}
|
||||
|
||||
@test "boundary/token/logout: can logout" {
|
||||
run login $DEFAULT_UNPRIVILEGED_LOGIN
|
||||
[ "$status" -eq 0 ]
|
||||
run get_token
|
||||
[ "$status" -eq 0 ]
|
||||
local tid=$(token_id "self")
|
||||
run logout_cmd
|
||||
[ "$status" -eq 0 ]
|
||||
run get_token
|
||||
[ "$status" -eq 2 ]
|
||||
run read_token_no_keyring $tid
|
||||
[ "$status" -eq 1 ]
|
||||
}
|
||||
Loading…
Reference in new issue