Add read:self and delete:self to auth tokens and add logout command (#1162)
This commit is contained in:
1 parent
3fe94335d4
commit
e6af51943d
20 files changed
+955
-212
No files matched your search
+28
-10
@@ -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
|
||||
@@ -18,12 +28,18 @@ Canonical reference for changes, improvements, and bugfixes for Boundary.
|
||||
* Login Name
|
||||
* 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
|
||||
account in the primary auth method for their scope, or there is no designated
|
||||
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,8 +286,9 @@ 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
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-144
@@ -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,7 +123,18 @@ 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
|
||||
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())
|
||||
@@ -134,18 +143,9 @@ func (c *TokenCommand) Run(args []string) (ret int) {
|
||||
if client.Token() != "" {
|
||||
authToken = &authtokens.AuthToken{Token: client.Token()}
|
||||
}
|
||||
|
||||
if authToken == nil {
|
||||
keyringType, tokenName, err := c.DiscoverKeyringTokenInfo()
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return base.CommandCliError
|
||||
}
|
||||
|
||||
authToken = c.ReadTokenFromKeyring(keyringType, tokenName)
|
||||
}
|
||||
|
||||
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())
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user