Add read:self and delete:self to auth tokens and add logout command (#1162)

pull/1155/head^2
Jeff Mitchell 5 years ago committed by GitHub
parent 3fe94335d4
commit e6af51943d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -4,6 +4,16 @@ Canonical reference for changes, improvements, and bugfixes for Boundary.
## Next
### Deprecations/Changes
* Permissions in new scope default roles have been updated to include support
for `list`, `read:self`, and `delete:self` on `auth-token` resources. This
allows a user to list and manage their own authentication tokens. (As is the
case with other resources, `list` will still be limited to returning tokens on
which the user has authorization to perform actions, so granting this
capability does not automatically give user the ability to list other users'
authentication tokens.)
### New and Improved
* actions: The new `no-op` action allows a grant to be given to a principals
@ -13,17 +23,23 @@ Canonical reference for changes, improvements, and bugfixes for Boundary.
`read` or other capabilities on the resources. The default scope permissions
have been updated to convey `no-op,list` instead of `read,list`.
[PR](https://github.com/hashicorp/boundary/pull/1138)
* cli/api/sdk: User resources have new attributes for:
* cli/api/sdk: User resources have new attributes for:
* Primary Account ID
* Login Name
* Full Name
* Email
* Full Name
* Email
These new user attributes correspond to attributes from the user's primary
auth method account. These attributes will be empty when the user has no
auth method account. These attributes will be empty when the user has no
account in the primary auth method for their scope, or there is no designated
primary auth method for their scope.
primary auth method for their scope.
* cli: Support for reading and deleting the user's own token via the new
`read:self` and `delete:self` actions on auth tokens. If no token ID is
provided, the stored token's ID will be used (after prompting), or `"self"`
can be set to the ID to trigger this behavior without prompting.
([PR](https://github.com/hashicorp/boundary/pull/1162))
* cli: New `logout` command deletes the current token in Boundary and forgets it
from the local system credential store
([PR](https://github.com/hashicorp/boundary/pull/1134))
### Bug Fixes
@ -253,10 +269,11 @@ database migrate` command.
* controller/worker: Require names to be all lowercase. This removes ambiguity
or accidental mismatching when using upcoming filtering features.
* api/cli: Due to visibility changes on collection listing, a list
will not include any resources if the user only has `list` as an authorized action.
As a result `scope list`, which is used by the UI to populate the login scope dropdown,
will be empty if the role granting the `u_anon` user `list` privileges is not updated to also contain a `read` action
* api/cli: Due to visibility changes on collection listing, a list will not
include any resources if the user only has `list` as an authorized action. As
a result `scope list`, which is used by the UI to populate the login scope
dropdown, will be empty if the role granting the `u_anon` user `list`
privileges is not updated to also contain a `read` action
### New and Improved
@ -269,13 +286,14 @@ database migrate` command.
* api/cli: Most resource types now support recursive listing, allowing listing
to occur down a scope tree
([PR](https://github.com/hashicorp/boundary/pull/885))
* cli: Add a `database migrate` command which updates a database's schema to
the version supported by the boundary binary ([PR](https://github.com/hashicorp/boundary/pull/872)).
* cli: Add a `database migrate` command which updates a database's schema to the
version supported by the boundary binary
([PR](https://github.com/hashicorp/boundary/pull/872)).
### Bug Fixes
* controller/db: Correctly check if db init previously completed successfully
when starting a controller or when running `database init`
* controller/db: Correctly check if db init previously completed successfully
when starting a controller or when running `database init`
([Issue](https://github.com/hashicorp/boundary/issues/805))
([PR](https://github.com/hashicorp/boundary/pull/842))
* cli: When `output-curl-string` is used with `update` or `add-/remove-/set-`
@ -284,8 +302,8 @@ database migrate` command.
fetches the current version
([Issue](https://github.com/hashicorp/boundary/issues/856))
([PR](https://github.com/hashicorp/boundary/pull/858))
* db: Fix panic in `database init` when controller config block is missing
([Issue](https://github.com/hashicorp/boundary/issues/819))
* db: Fix panic in `database init` when controller config block is missing
([Issue](https://github.com/hashicorp/boundary/issues/819))
([PR](https://github.com/hashicorp/boundary/pull/851))
## 0.1.4 (2021/01/05)
@ -325,8 +343,8 @@ database migrate` command.
([PR](https://github.com/hashicorp/boundary/pull/831))
* controller: Improved error handling in hosts, host catalog and host set
([PR](https://github.com/hashicorp/boundary/pull/786))
* controller: Relax account login name constraints to allow dash as valid character
([Issue](https://github.com/hashicorp/boundary/issues/759))
* controller: Relax account login name constraints to allow dash as valid
character ([Issue](https://github.com/hashicorp/boundary/issues/759))
([PR](https://github.com/hashicorp/boundary/pull/806))
* cli/connect/http: Pass endpoint address through to allow setting TLS server
name directly in most cases

@ -3,8 +3,6 @@ package base
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"io"
@ -12,19 +10,15 @@ import (
"os"
"os/signal"
"regexp"
"runtime"
"strings"
"sync"
"syscall"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/authtokens"
"github.com/hashicorp/boundary/sdk/wrapper"
nkeyring "github.com/jefferai/keyring"
"github.com/mitchellh/cli"
"github.com/pkg/errors"
"github.com/posener/complete"
zkeyring "github.com/zalando/go-keyring"
)
const (
@ -126,7 +120,7 @@ func MakeShutdownCh() chan struct{} {
// Client returns the HTTP API client. The client is cached on the command to
// save performance on future calls.
func (c *Command) Client(opt ...Option) (*api.Client, error) {
// Read the test client if present
// Read the cached client if present
if c.client != nil {
return c.client, nil
}
@ -242,143 +236,6 @@ func (c *Command) Client(opt ...Option) (*api.Client, error) {
return c.client, nil
}
func (c *Command) DiscoverKeyringTokenInfo() (string, string, error) {
tokenName := "default"
if c.FlagTokenName != "" {
tokenName = c.FlagTokenName
}
if tokenName == "none" {
c.UI.Warn(`"-token-name=none" is deprecated, please use "-keyring-type=none"`)
c.FlagKeyringType = "none"
}
if c.FlagKeyringType == "none" {
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 "auto", "wincred", "pass":
foundKeyringType = true
if keyringType == "auto" {
keyringType = "wincred"
}
}
case "darwin":
switch keyringType {
case "auto", "keychain", "pass":
foundKeyringType = true
if keyringType == "auto" {
keyringType = "keychain"
}
}
default:
switch keyringType {
case "auto", "secret-service", "pass":
foundKeyringType = true
if keyringType == "auto" {
keyringType = "pass"
}
}
}
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 "wincred", "keychain":
available = true
case "pass", "secret-service":
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 "none":
return nil
case "wincred", "keychain":
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: "login",
PassPrefix: "HashiCorp_Boundary",
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
}
type FlagSetBit uint
const (

@ -62,6 +62,7 @@ func (b *Server) CreateInitialLoginRole(ctx context.Context) (*iam.Role, error)
"id=*;type=scope;actions=list,no-op",
"id=*;type=auth-method;actions=authenticate,list",
"id={{account.id}};actions=read,change-password",
"id=*;type=auth-token;actions=list,read:self,delete:self",
}); err != nil {
return nil, fmt.Errorf("error creating grant for default generated grants: %w", err)
}
@ -517,7 +518,8 @@ func (b *Server) CreateInitialTarget(ctx context.Context) (target.Target, error)
b.Info["generated target id"] = b.DevTargetId
// If we have an unprivileged dev user, add user to the role that grants
// list/read:self/cancel:self, and an authorize-session role
// list/read:self/cancel:self on sessions, read:self/delete:self/list on
// tokens, and an authorize-session role
if b.DevUnprivilegedUserId != "" {
iamRepo, err := iam.NewRepository(rw, rw, kmsCache, iam.WithRandomReader(b.SecureRandomReader))
if err != nil {

@ -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
}

@ -14,6 +14,7 @@ import (
"github.com/hashicorp/boundary/internal/cmd/commands/hostcatalogscmd"
"github.com/hashicorp/boundary/internal/cmd/commands/hostscmd"
"github.com/hashicorp/boundary/internal/cmd/commands/hostsetscmd"
"github.com/hashicorp/boundary/internal/cmd/commands/logout"
"github.com/hashicorp/boundary/internal/cmd/commands/rolescmd"
"github.com/hashicorp/boundary/internal/cmd/commands/scopescmd"
"github.com/hashicorp/boundary/internal/cmd/commands/server"
@ -539,6 +540,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
}, nil
},
"logout": func() (cli.Command, error) {
return &logout.LogoutCommand{
Command: base.NewCommand(ui),
}, nil
},
"roles": func() (cli.Command, error) {
return &rolescmd.Command{
Command: base.NewCommand(ui),

@ -130,11 +130,6 @@ func (c *Command) Run(args []string) int {
return base.CommandUserError
}
if strutil.StrListContains(flagsMap[c.Func], "id") && c.FlagId == "" {
c.PrintCliError(errors.New("ID is required but not passed in via -id"))
return base.CommandUserError
}
var opts []authtokens.Option
if strutil.StrListContains(flagsMap[c.Func], "scope-id") {

@ -1,13 +1,60 @@
package authtokenscmd
import (
"errors"
"fmt"
"time"
"github.com/hashicorp/boundary/api/authtokens"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/sdk/strutil"
)
const selfFlag = "self"
func init() {
extraFlagsHandlingFunc = extraFlagsHandlingFuncImpl
}
func extraFlagsHandlingFuncImpl(c *Command, _ *base.FlagSets, _ *[]authtokens.Option) bool {
if c.Func != "delete" && c.Func != "read" {
if strutil.StrListContains(flagsMap[c.Func], "id") && c.FlagId == "" {
c.PrintCliError(errors.New("ID is required but not passed in via -id"))
return false
}
return true
}
if c.FlagId == "" {
fmt.Printf("No ID provided; %s the stored token? (Pass an ID of %q to suppress this question.) y/n: ", c.Func, selfFlag)
var yesNo string
fmt.Scanf("%s", &yesNo)
switch yesNo {
case "y", "Y":
c.FlagId = selfFlag
default:
c.PrintCliError(errors.New(`"Y" or "y" not provided, refusing to continue`))
return false
}
}
if c.FlagId == selfFlag {
// We should have already read this and have it cached
client, err := c.Client()
if err != nil {
c.PrintCliError(fmt.Errorf("Error reading cached API client: %w", err))
return false
}
c.FlagId, err = base.TokenIdFromToken(client.Token())
if err != nil {
c.PrintCliError(err)
return false
}
}
return true
}
func (c *Command) printListTable(items []*authtokens.AuthToken) string {
if len(items) == 0 {
return "No auth tokens found"

@ -1,8 +1,6 @@
package config
import (
"fmt"
"github.com/hashicorp/boundary/api/authtokens"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/mitchellh/cli"
@ -25,7 +23,7 @@ type TokenCommand struct {
}
func (c *TokenCommand) Synopsis() string {
return fmt.Sprintf("Get the stored token, or its properties")
return "Get the stored token, or its properties"
}
func (c *TokenCommand) Help() string {
@ -125,27 +123,29 @@ func (c *TokenCommand) Run(args []string) (ret int) {
// Read from client first as that will override keyring anyways
var authToken *authtokens.AuthToken
// Fallback to env/CLI
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return base.CommandCliError
}
if client.Token() != "" {
authToken = &authtokens.AuthToken{Token: client.Token()}
}
if authToken == nil {
if optCount == 1 {
// In this case we need to read the full auth token stored, not just the
// actual token value for the client.
keyringType, tokenName, err := c.DiscoverKeyringTokenInfo()
if err != nil {
c.UI.Error(err.Error())
return base.CommandCliError
}
authToken = c.ReadTokenFromKeyring(keyringType, tokenName)
} else {
// Fallback to env/CLI but we can only get just the token value this way, at
// least for now
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return base.CommandCliError
}
if client.Token() != "" {
authToken = &authtokens.AuthToken{Token: client.Token()}
}
}
if authToken == nil {
if authToken == nil || authToken.Token == "" {
c.UI.Error("No token could be discovered")
return base.CommandCliError
}
@ -170,9 +170,6 @@ func (c *TokenCommand) Run(args []string) (ret int) {
c.UI.Output(authToken.AuthMethodId)
default:
if authToken.Token == "" {
return base.CommandUserError
}
c.UI.Output(authToken.Token)
}

@ -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
}

@ -155,7 +155,6 @@ var inputStructs = map[string][]*cmdInfo{
Pkg: "authtokens",
StdActions: []string{"read", "delete", "list"},
Container: "Scope",
HasId: true,
},
},
"groups": {

@ -313,6 +313,12 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o
return errors.Wrap(err, op, errors.WithMsg("unable to create in memory role grant"))
}
grants = append(grants, roleGrant)
roleGrant, err = NewRoleGrant(defaultRolePublicId, "id=*;type=auth-token;actions=list,read:self,delete:self")
if err != nil {
return errors.Wrap(err, op, errors.WithMsg("unable to create in memory role grant"))
}
grants = append(grants, roleGrant)
}
roleGrantOplogMsgs := make([]*oplog.Message, 0, 3)

@ -385,6 +385,7 @@ func (g Grant) validateType() error {
resource.Role,
resource.AuthMethod,
resource.Account,
resource.AuthToken,
resource.HostCatalog,
resource.HostSet,
resource.Host,

@ -24,7 +24,9 @@ var (
IdActions = action.ActionSet{
action.NoOp,
action.Read,
action.ReadSelf,
action.Delete,
action.DeleteSelf,
}
// CollectionActions contains the set of actions that can be performed on
@ -99,10 +101,17 @@ func (s Service) ListAuthTokens(ctx context.Context, req *pbs.ListAuthTokensRequ
for _, item := range ul {
item.Scope = scopeInfoMap[item.GetScopeId()]
res.ScopeId = item.Scope.Id
item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)).Strings()
if len(item.AuthorizedActions) == 0 {
authorizedActions := authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res))
if len(authorizedActions) == 0 {
continue
}
if authorizedActions.OnlySelf() && item.GetUserId() != authResults.UserId {
continue
}
item.AuthorizedActions = authorizedActions.Strings()
if filter.Match(item) {
finalItems = append(finalItems, item)
}
@ -115,7 +124,7 @@ func (s Service) GetAuthToken(ctx context.Context, req *pbs.GetAuthTokenRequest)
if err := validateGetRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Read)
authResults := s.authResult(ctx, req.GetId(), action.ReadSelf)
if authResults.Error != nil {
return nil, authResults.Error
}
@ -123,6 +132,15 @@ func (s Service) GetAuthToken(ctx context.Context, req *pbs.GetAuthTokenRequest)
if err != nil {
return nil, err
}
authzdActions := authResults.FetchActionSetForId(ctx, u.Id, IdActions)
// Check to see if we need to verify Read vs. just ReadSelf
if u.GetUserId() != authResults.UserId {
if !authzdActions.HasAction(action.Read) {
return nil, handlers.ForbiddenError()
}
}
u.Scope = authResults.Scope
u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings()
return &pbs.GetAuthTokenResponse{Item: u}, nil
@ -133,11 +151,24 @@ func (s Service) DeleteAuthToken(ctx context.Context, req *pbs.DeleteAuthTokenRe
if err := validateDeleteRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Delete)
authResults := s.authResult(ctx, req.GetId(), action.DeleteSelf)
if authResults.Error != nil {
return nil, authResults.Error
}
_, err := s.deleteFromRepo(ctx, req.GetId())
at, err := s.getFromRepo(ctx, req.GetId())
if err != nil {
return nil, err
}
authzdActions := authResults.FetchActionSetForId(ctx, at.Id, IdActions)
// Check to see if we need to verify Delete vs. just DeleteSelf
if at.GetUserId() != authResults.UserId {
if !authzdActions.HasAction(action.Delete) {
return nil, handlers.ForbiddenError()
}
}
_, err = s.deleteFromRepo(ctx, req.GetId())
if err != nil {
return nil, err
}

@ -1,8 +1,10 @@
package authtokens_test
import (
"context"
"errors"
"fmt"
"net/http/httptest"
"testing"
"github.com/google/go-cmp/cmp"
@ -14,9 +16,11 @@ import (
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/servers"
"github.com/hashicorp/boundary/internal/servers/controller/handlers"
"github.com/hashicorp/boundary/internal/servers/controller/handlers/authtokens"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/hashicorp/go-hclog"
"google.golang.org/grpc/codes"
"google.golang.org/protobuf/testing/protocmp"
@ -24,7 +28,89 @@ import (
"github.com/stretchr/testify/require"
)
var testAuthorizedActions = []string{"no-op", "read", "delete"}
var testAuthorizedActions = []string{"no-op", "read", "read:self", "delete", "delete:self"}
func TestGetSelf(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrap := db.TestWrapper(t)
logger := hclog.New(nil)
kms := kms.TestKms(t, conn, wrap)
iamRepoFn := func() (*iam.Repository, error) {
return iam.TestRepo(t, conn, wrap), nil
}
tokenRepoFn := func() (*authtoken.Repository, error) {
return authtoken.NewRepository(rw, rw, kms)
}
serversRepoFn := func() (*servers.Repository, error) {
return servers.NewRepository(rw, rw, kms)
}
a, err := authtokens.NewService(tokenRepoFn, iamRepoFn)
require.NoError(t, err, "Couldn't create new auth token service.")
o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap))
at1 := authtoken.TestAuthToken(t, conn, kms, o.GetPublicId())
at2 := authtoken.TestAuthToken(t, conn, kms, o.GetPublicId())
cases := []struct {
name string
token *authtoken.AuthToken
readId string
err error
}{
{
name: "at1 read self",
token: at1,
readId: at1.GetPublicId(),
},
{
name: "at1 read at2",
token: at1,
readId: at2.GetPublicId(),
err: handlers.ApiErrorWithCodeAndMessage(codes.PermissionDenied, "Forbidden."),
},
{
name: "at2 read self",
token: at2,
readId: at2.GetPublicId(),
},
{
name: "at2 read at1",
token: at2,
readId: at1.GetPublicId(),
err: handlers.ApiErrorWithCodeAndMessage(codes.PermissionDenied, "Forbidden."),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
require, assert := require.New(t), assert.New(t)
// Setup the auth request information
req := httptest.NewRequest("GET", fmt.Sprintf("http://127.0.0.1/v1/auth-tokens/%s", tc.readId), nil)
requestInfo := auth.RequestInfo{
Path: req.URL.Path,
Method: req.Method,
TokenFormat: auth.AuthTokenTypeBearer,
PublicId: tc.token.GetPublicId(),
Token: tc.token.GetToken(),
}
ctx := auth.NewVerifierContext(context.Background(), logger, iamRepoFn, tokenRepoFn, serversRepoFn, kms, requestInfo)
got, err := a.GetAuthToken(ctx, &pbs.GetAuthTokenRequest{Id: tc.readId})
if tc.err != nil {
require.EqualError(err, tc.err.Error())
require.Nil(got)
return
}
require.NoError(err)
require.NotNil(got)
assert.Equal(got.GetItem().GetId(), tc.token.GetPublicId())
// Ensure we didn't simply have e.g. read on all tokens
assert.Equal(got.Item.GetAuthorizedActions(), []string{"read:self", "delete:self"})
})
}
}
func TestGet(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
@ -101,6 +187,75 @@ func TestGet(t *testing.T) {
}
}
func TestList_Self(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrap := db.TestWrapper(t)
logger := hclog.New(nil)
kms := kms.TestKms(t, conn, wrap)
iamRepo := iam.TestRepo(t, conn, wrap)
iamRepoFn := func() (*iam.Repository, error) {
return iamRepo, nil
}
tokenRepoFn := func() (*authtoken.Repository, error) {
return authtoken.NewRepository(rw, rw, kms)
}
serversRepoFn := func() (*servers.Repository, error) {
return servers.NewRepository(rw, rw, kms)
}
// This will result in the scope having default permissions, which now
// includes list on auth tokens
o, _ := iam.TestScopes(t, iamRepo)
// Each of these should only end up being able to list themselves
at := authtoken.TestAuthToken(t, conn, kms, o.GetPublicId())
otherAt := authtoken.TestAuthToken(t, conn, kms, o.GetPublicId())
cases := []struct {
name string
requester *authtoken.AuthToken
count int
}{
{
name: "First token sees only self",
requester: at,
},
{
name: "Second token sees only self",
requester: otherAt,
},
}
a, err := authtokens.NewService(tokenRepoFn, iamRepoFn)
require.NoError(t, err)
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
require, assert := require.New(t), assert.New(t)
// Setup the auth request information
req := httptest.NewRequest("GET", fmt.Sprintf("http://127.0.0.1/v1/auth-tokens?scope_id=%s", o.GetPublicId()), nil)
requestInfo := auth.RequestInfo{
Path: req.URL.Path,
Method: req.Method,
TokenFormat: auth.AuthTokenTypeBearer,
PublicId: tc.requester.GetPublicId(),
Token: tc.requester.GetToken(),
}
ctx := auth.NewVerifierContext(context.Background(), logger, iamRepoFn, tokenRepoFn, serversRepoFn, kms, requestInfo)
got, err := a.ListAuthTokens(ctx, &pbs.ListAuthTokensRequest{ScopeId: o.GetPublicId()})
require.NoError(err)
require.Len(got.Items, 1)
assert.Equal(got.Items[0].GetId(), tc.requester.GetPublicId())
// Ensure we didn't simply have e.g. read on all tokens
assert.Equal(got.Items[0].GetAuthorizedActions(), []string{"read:self", "delete:self"})
})
}
}
func TestList(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
@ -243,6 +398,98 @@ func TestList(t *testing.T) {
}
}
func TestDeleteSelf(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrap := db.TestWrapper(t)
logger := hclog.New(nil)
kms := kms.TestKms(t, conn, wrap)
iamRepo := iam.TestRepo(t, conn, wrap)
iamRepoFn := func() (*iam.Repository, error) {
return iamRepo, nil
}
tokenRepoFn := func() (*authtoken.Repository, error) {
return authtoken.NewRepository(rw, rw, kms)
}
serversRepoFn := func() (*servers.Repository, error) {
return servers.NewRepository(rw, rw, kms)
}
a, err := authtokens.NewService(tokenRepoFn, iamRepoFn)
require.NoError(t, err, "Couldn't create new auth token service.")
o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap))
at1 := authtoken.TestAuthToken(t, conn, kms, o.GetPublicId())
at2 := authtoken.TestAuthToken(t, conn, kms, o.GetPublicId())
cases := []struct {
name string
token *authtoken.AuthToken
deleteId string
err error
}{
{
name: "at1 delete at2",
token: at1,
deleteId: at2.GetPublicId(),
err: handlers.ApiErrorWithCodeAndMessage(codes.PermissionDenied, "Forbidden."),
},
{
name: "at2 delete at1",
token: at2,
deleteId: at1.GetPublicId(),
err: handlers.ApiErrorWithCodeAndMessage(codes.PermissionDenied, "Forbidden."),
},
{
name: "at1 delete self",
token: at1,
deleteId: at1.GetPublicId(),
},
{
name: "at2 delete self",
token: at2,
deleteId: at2.GetPublicId(),
},
{
name: "at1 not found",
token: at1,
deleteId: at1.GetPublicId(),
err: handlers.ApiErrorWithCodeAndMessage(codes.NotFound, "Resource not found."),
},
{
name: "at2 not found",
token: at2,
deleteId: at2.GetPublicId(),
err: handlers.ApiErrorWithCodeAndMessage(codes.NotFound, "Resource not found."),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
// Setup the auth request information
req := httptest.NewRequest("DELETE", fmt.Sprintf("http://127.0.0.1/v1/auth-tokens/%s", tc.deleteId), nil)
requestInfo := auth.RequestInfo{
Path: req.URL.Path,
Method: req.Method,
TokenFormat: auth.AuthTokenTypeBearer,
PublicId: tc.token.GetPublicId(),
Token: tc.token.GetToken(),
}
ctx := auth.NewVerifierContext(context.Background(), logger, iamRepoFn, tokenRepoFn, serversRepoFn, kms, requestInfo)
got, err := a.DeleteAuthToken(ctx, &pbs.DeleteAuthTokenRequest{Id: tc.deleteId})
if tc.err != nil {
require.EqualError(err, tc.err.Error())
require.Nil(got)
return
}
require.NoError(err)
})
}
}
func TestDelete(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)

@ -77,14 +77,7 @@ func (s Service) GetSession(ctx context.Context, req *pbs.GetSessionRequest) (*p
authzdActions := authResults.FetchActionSetForId(ctx, ses.Id, IdActions)
// Check to see if we need to verify Read vs. just ReadSelf
if ses.GetUserId() != authResults.UserId {
var found bool
for _, v := range authzdActions {
if v == action.Read {
found = true
break
}
}
if !found {
if !authzdActions.HasAction(action.Read) {
return nil, handlers.ForbiddenError()
}
}
@ -143,14 +136,8 @@ func (s Service) ListSessions(ctx context.Context, req *pbs.ListSessionsRequest)
if len(authorizedActions) == 0 {
continue
}
onlySelf := true
for _, v := range authorizedActions {
if v != action.ReadSelf && v != action.CancelSelf {
onlySelf = false
break
}
}
if onlySelf && item.GetUserId() != authResults.UserId {
if authorizedActions.OnlySelf() && item.GetUserId() != authResults.UserId {
continue
}
@ -179,16 +166,9 @@ func (s Service) CancelSession(ctx context.Context, req *pbs.CancelSessionReques
return nil, err
}
authzdActions := authResults.FetchActionSetForId(ctx, ses.Id, IdActions)
// Check to see if we need to verify Read vs. just ReadSelf
// Check to see if we need to verify Cancel vs. just CancelSelf
if ses.GetUserId() != authResults.UserId {
var found bool
for _, v := range authzdActions {
if v == action.Cancel {
found = true
break
}
}
if !found {
if !authzdActions.HasAction(action.Cancel) {
return nil, handlers.ForbiddenError()
}
}

@ -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 ]
}

@ -49,8 +49,6 @@ export NEW_GRANT='id=*;type=*;actions=create,read,update,delete,list'
[ "$status" -eq 0 ]
}
@test "boundary/role/add-principals: $NEW_ROLE role contains default principal" {
local rid=$(role_id $NEW_ROLE $DEFAULT_GLOBAL)
run role_has_principal_id $rid $DEFAULT_USER

@ -1,5 +1,7 @@
package action
import "strings"
// Type defines a type for the Actions of Resources
// actions are also stored as a lookup db table named iam_action
type Type uint
@ -40,7 +42,8 @@ const (
ReadSelf Type = 31
CancelSelf Type = 32
ChangeState Type = 33
NoOp Type = 34
DeleteSelf Type = 34
NoOp Type = 35
)
var Map = map[string]Type{
@ -77,6 +80,7 @@ var Map = map[string]Type{
ReadSelf.String(): ReadSelf,
CancelSelf.String(): CancelSelf,
ChangeState.String(): ChangeState,
DeleteSelf.String(): DeleteSelf,
NoOp.String(): NoOp,
}
@ -116,6 +120,7 @@ func (a Type) String() string {
"read:self",
"cancel:self",
"change-state",
"delete:self",
"no-op",
}[a]
}
@ -134,3 +139,28 @@ func (a ActionSet) Strings() []string {
}
return ret
}
// HasAction returns whether the action set contains the given action.
func (a ActionSet) HasAction(act Type) bool {
for _, v := range a {
if v == act {
return true
}
}
return false
}
// OnlySelf returns true if all actions in the action set are self types. An
// empty set returns false. This may not be what you want so the caller should
// validate length and act appropriately as well.
func (a ActionSet) OnlySelf() bool {
if len(a) == 0 {
return false
}
for _, v := range a {
if !strings.HasSuffix(v.String(), ":self") {
return false
}
}
return true
}

@ -87,6 +87,10 @@ func TestAction(t *testing.T) {
action: ChangeState,
want: "change-state",
},
{
action: DeleteSelf,
want: "delete:self",
},
{
action: NoOp,
want: "no-op",
@ -128,3 +132,87 @@ func TestActionStrings(t *testing.T) {
})
}
}
func TestHasAction(t *testing.T) {
tests := []struct {
name string
actions ActionSet
action Type
want bool
}{
{
name: "has 1",
actions: ActionSet{Read, AuthorizeSession},
action: Read,
want: true,
},
{
name: "has 2",
actions: ActionSet{Read, AuthorizeSession},
action: AuthorizeSession,
want: true,
},
{
name: "empty",
actions: ActionSet{},
action: AuthorizeSession,
want: false,
},
{
name: "does not have 1",
actions: ActionSet{Read, AuthorizeSession},
action: ReadSelf,
want: false,
},
{
name: "does not have 2",
actions: ActionSet{Read, AuthorizeSession},
action: Delete,
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, tt.actions.HasAction(tt.action))
})
}
}
func TestOnlySelf(t *testing.T) {
tests := []struct {
name string
actions ActionSet
want bool
}{
{
name: "has only self 1",
actions: ActionSet{ReadSelf, CancelSelf},
want: true,
},
{
name: "has only self 2",
actions: ActionSet{ReadSelf},
want: true,
},
{
name: "empty is false",
actions: ActionSet{},
want: false,
},
{
name: "does not have only self 1",
actions: ActionSet{Read, AuthorizeSession},
want: false,
},
{
name: "does not have only self 2",
actions: ActionSet{ReadSelf, AuthorizeSession},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, tt.actions.OnlySelf())
})
}
}

Loading…
Cancel
Save