Use primary auth method if no auth method ID is provided (#2725)

* feat: default to using primary auth method with authenticating

* Add ability to specify scope ID flag in authenticate method

---------

Co-authored-by: Jeff Mitchell <jeffrey.mitchell@gmail.com>
This commit is contained in:
Jeff MalnickandJeff Mitchell authored and GitHub committed 2023-01-31 10:07:10 -08:00
1 parent 2e3fd041ef
commit a3c4cef648
6 files changed
+268 -31

No files matched your search

@@ -1,7 +1,16 @@
package authenticate
import (
"errors"
"fmt"
"strings"
"github.com/hashicorp/boundary/api/authmethods"
"github.com/hashicorp/boundary/internal/auth/oidc"
"github.com/hashicorp/boundary/internal/auth/password"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/internal/cmd/common"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/mitchellh/cli"
"github.com/mitchellh/go-wordwrap"
)
@@ -22,18 +31,84 @@ func (c *Command) Help() string {
"",
" This command authenticates the Boundary commandline client using a specified auth method. Examples:",
"",
" Authenticate with a password auth method:",
" Authenticate with the primary auth method in the global scope:",
"",
" $ boundary authenticate password -auth-method-id ampw_1234567890 -login-name foo",
" $ boundary authenticate",
"",
" Authenticate with an OIDC auth method:",
" Authenticate using the primary auth method in a specific scope",
"",
" $ boundary authenticate password -scope-id o_1234567890",
"",
" Authenticate with a password auth method using a specific auth method ID:",
"",
" $ boundary authenticate password -auth-method-id ampw_1234567890",
"",
" Authenticate with an OIDC auth method using a specific auth method ID:",
"",
" $ boundary authenticate oidc -auth-method-id amoidc_1234567890",
"",
" Please see the auth method subcommand help for detailed usage information.",
}) + c.Flags().Help()
}
func (c *Command) Flags() *base.FlagSets {
set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat)
f := set.NewFlagSet("Command Options")
f.StringVar(&base.StringVar{
Name: "scope-id",
EnvVar: "BOUNDARY_SCOPE_ID",
Target: &c.FlagScopeId,
Usage: "The scope to use for the operation",
})
return set
}
func (c *Command) Run(args []string) int {
return cli.RunResultHelp
client, err := c.Client(base.WithNoTokenScope(), base.WithNoTokenValue())
if c.WrapperCleanupFunc != nil {
defer func() {
if err := c.WrapperCleanupFunc(); err != nil {
c.PrintCliError(fmt.Errorf("Error cleaning kms wrapper: %w", err))
}
}()
}
if err != nil {
c.PrintCliError(fmt.Errorf("Error creating API client: %w", err))
return base.CommandCliError
}
f := c.Flags()
if err := f.Parse(args); err != nil {
c.PrintCliError(err)
return base.CommandUserError
}
// Lookup the primary auth method ID in the global scope
aClient := authmethods.NewClient(client)
pri, err := getPrimaryAuthMethodId(c.Context, aClient, scope.Global.String(), "")
if err != nil {
c.PrintCliError(errors.New("Error looking up primary auth method ID for the global scope. Try setting a primary auth method for the global scope, or use an auth method subcommand (see 'boundary authenticate -h' for available sub command usage)."))
return base.CommandUserError
}
c.FlagAuthMethodId = pri
switch {
case strings.HasPrefix(c.FlagAuthMethodId, password.AuthMethodPrefix):
cmd := PasswordCommand{Command: c.Command, Opts: []common.Option{common.WithSkipScopeIdFlag(true)}}
cmd.Run([]string{})
case strings.HasPrefix(c.FlagAuthMethodId, oidc.AuthMethodPrefix):
cmd := OidcCommand{Command: c.Command, Opts: []common.Option{common.WithSkipScopeIdFlag(true)}}
cmd.Run([]string{})
default:
c.PrintCliError(fmt.Errorf("The primary auth method was of an unsupported type. The given ID was %s; only 'ampw' (password) and 'amoidc' (OIDC) auth method prefixes are supported.", c.FlagAuthMethodId))
return cli.RunResultHelp
}
return 0
}
@@ -1,9 +1,11 @@
package authenticate
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/hashicorp/boundary/api/authmethods"
@@ -100,3 +102,26 @@ func saveAndOrPrintToken(c *base.Command, result *authmethods.AuthenticateResult
return base.CommandSuccess
}
// getPrimaryAuthMethodId iterates over client.List() to find the primary auth method ID for the
// given scopeId. If scope ID is empty or no primary auth method is found, it returns an error.
func getPrimaryAuthMethodId(ctx context.Context, client *authmethods.Client, scopeId, amType string) (string, error) {
if scopeId == "" {
return "", fmt.Errorf("Must pass a non empty scope ID string to GetPrimaryAuthMethodId()")
}
authMethodListResult, err := client.List(ctx, scopeId)
if err != nil {
return "", err
}
for _, m := range authMethodListResult.GetItems() {
if m.IsPrimary {
if !strings.HasPrefix(m.Id, amType) {
return "", fmt.Errorf("Error looking up primary auth method in scope '%s': got '%s' but the command requires an auth method prefix of '%s'. Make sure the sub command you're using matches the primary auth method type in the scope being used. For example, if using the password sub command the primary auth method must have a prefix of 'ampw'.\n\nSee 'boundary authenticate -h' for available sub command usage.", scopeId, m.Id, amType)
}
return m.Id, nil
}
}
return "", fmt.Errorf("Primary auth method not found for scope ID: '%s'. Please set a primary auth method on this scope or pass one explicitly using an authenticate sub command (see 'boundary authenticate -h') along with the -auth-method-id flag.", scopeId)
}
+38 -6
View File
@@ -11,6 +11,8 @@ import (
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/authmethods"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/internal/cmd/common"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/hashicorp/cap/util"
"github.com/mitchellh/cli"
"github.com/mitchellh/go-wordwrap"
@@ -24,6 +26,9 @@ var (
type OidcCommand struct {
*base.Command
Opts []common.Option
parsedOpts *common.Options
}
func (c *OidcCommand) Synopsis() string {
@@ -53,6 +58,15 @@ func (c *OidcCommand) Flags() *base.FlagSets {
Usage: "The auth-method resource to use for the operation",
})
if c.parsedOpts == nil || !c.parsedOpts.WithSkipScopeIdFlag {
f.StringVar(&base.StringVar{
Name: "scope-id",
EnvVar: "BOUNDARY_SCOPE_ID",
Target: &c.FlagScopeId,
Usage: "The scope ID to use for the operation.",
})
}
return set
}
@@ -65,6 +79,13 @@ func (c *OidcCommand) AutocompleteFlags() complete.Flags {
}
func (c *OidcCommand) Run(args []string) int {
opts, err := common.GetOpts(c.Opts...)
if err != nil {
c.PrintCliError(err)
return base.CommandCliError
}
c.parsedOpts = opts
f := c.Flags()
if err := f.Parse(args); err != nil {
@@ -72,12 +93,6 @@ func (c *OidcCommand) Run(args []string) int {
return base.CommandUserError
}
switch {
case c.FlagAuthMethodId == "":
c.PrintCliError(errors.New("Auth method ID must be provided via -auth-method-id"))
return base.CommandUserError
}
client, err := c.Client(base.WithNoTokenScope(), base.WithNoTokenValue())
if c.WrapperCleanupFunc != nil {
defer func() {
@@ -92,6 +107,23 @@ func (c *OidcCommand) Run(args []string) int {
}
aClient := authmethods.NewClient(client)
// if auth method ID isn't passed on the CLI, try looking up the primary auth method ID
if c.FlagAuthMethodId == "" {
// if flag for scope is empty try looking up global
if c.FlagScopeId == "" {
c.FlagScopeId = scope.Global.String()
}
pri, err := getPrimaryAuthMethodId(c.Context, aClient, c.FlagScopeId, "amoidc")
if err != nil {
c.PrintCliError(err)
return base.CommandUserError
}
c.FlagAuthMethodId = pri
}
result, err := aClient.Authenticate(c.Context, c.FlagAuthMethodId, "start", nil)
if err != nil {
if apiErr := api.AsServerError(err); apiErr != nil {
+61 -21
View File
@@ -9,6 +9,7 @@ import (
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/authmethods"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/internal/cmd/common"
"github.com/hashicorp/go-secure-stdlib/parseutil"
"github.com/hashicorp/go-secure-stdlib/password"
"github.com/mitchellh/cli"
@@ -31,6 +32,9 @@ type PasswordCommand struct {
flagLoginName string
flagPassword string
Opts []common.Option
parsedOpts *common.Options
}
func (c *PasswordCommand) Synopsis() string {
@@ -74,6 +78,15 @@ func (c *PasswordCommand) Flags() *base.FlagSets {
Usage: "The auth-method resource to use for the operation.",
})
if c.parsedOpts == nil || !c.parsedOpts.WithSkipScopeIdFlag {
f.StringVar(&base.StringVar{
Name: "scope-id",
EnvVar: "BOUNDARY_SCOPE_ID",
Target: &c.FlagScopeId,
Usage: "The scope ID to use for the operation.",
})
}
return set
}
@@ -86,6 +99,13 @@ func (c *PasswordCommand) AutocompleteFlags() complete.Flags {
}
func (c *PasswordCommand) Run(args []string) int {
opts, err := common.GetOpts(c.Opts...)
if err != nil {
c.PrintCliError(err)
return base.CommandCliError
}
c.parsedOpts = opts
f := c.Flags()
if err := f.Parse(args); err != nil {
@@ -93,13 +113,47 @@ func (c *PasswordCommand) Run(args []string) int {
return base.CommandUserError
}
switch {
case c.flagLoginName == "":
c.PrintCliError(errors.New("Login name must be provided via -login-name"))
return base.CommandUserError
case c.FlagAuthMethodId == "":
c.PrintCliError(errors.New("Auth method ID must be provided via -auth-method-id"))
return base.CommandUserError
client, err := c.Client(base.WithNoTokenScope(), base.WithNoTokenValue())
if c.WrapperCleanupFunc != nil {
defer func() {
if err := c.WrapperCleanupFunc(); err != nil {
c.PrintCliError(fmt.Errorf("Error cleaning kms wrapper: %w", err))
}
}()
}
if err != nil {
c.PrintCliError(fmt.Errorf("Error creating API client: %w", err))
return base.CommandCliError
}
aClient := authmethods.NewClient(client)
// if auth method ID isn't passed on the CLI, try looking up the primary auth method ID
if c.FlagAuthMethodId == "" {
// if flag for scope is empty try looking up global
if c.FlagScopeId == "" {
c.FlagScopeId = "global"
}
pri, err := getPrimaryAuthMethodId(c.Context, aClient, c.FlagScopeId, "ampw")
if err != nil {
c.PrintCliError(err)
return base.CommandUserError
}
c.FlagAuthMethodId = pri
}
switch c.flagLoginName {
case "":
fmt.Print("Please enter the login name (it will be hidden): ")
value, err := password.Read(os.Stdin)
fmt.Print("\n")
if err != nil {
c.UI.Error(fmt.Sprintf("An error occurred attempting to read the login name. The raw error message is shown below but usually this is because you attempted to pipe a value into the command or you are executing outside of a terminal (TTY). The raw error was:\n\n%s", err.Error()))
return base.CommandUserError
}
c.flagLoginName = strings.TrimSpace(value)
}
switch c.flagPassword {
@@ -127,20 +181,6 @@ func (c *PasswordCommand) Run(args []string) int {
c.flagPassword = password
}
client, err := c.Client(base.WithNoTokenScope(), base.WithNoTokenValue())
if c.WrapperCleanupFunc != nil {
defer func() {
if err := c.WrapperCleanupFunc(); err != nil {
c.PrintCliError(fmt.Errorf("Error cleaning kms wrapper: %w", err))
}
}()
}
if err != nil {
c.PrintCliError(fmt.Errorf("Error creating API client: %w", err))
return base.CommandCliError
}
aClient := authmethods.NewClient(client)
result, err := aClient.Authenticate(c.Context, c.FlagAuthMethodId, "login",
map[string]any{
"login_name": c.flagLoginName,
+36
View File
@@ -0,0 +1,36 @@
package common
// GetOpts - iterate the inbound Options and return a struct.
func GetOpts(opt ...Option) (*Options, error) {
opts := getDefaultOptions()
for _, o := range opt {
if o == nil {
continue
}
if err := o(opts); err != nil {
return nil, err
}
}
return opts, nil
}
// Option - how Options are passed as arguments.
type Option func(*Options) error
// Options - how Options are represented.
type Options struct {
WithSkipScopeIdFlag bool
}
func getDefaultOptions() *Options {
return &Options{}
}
// WithSkipScopeIdFlag tells a command to not create a scope ID flag (usually
// because it's already been defined)
func WithSkipScopeIdFlag(with bool) Option {
return func(o *Options) error {
o.WithSkipScopeIdFlag = with
return nil
}
}
+29
View File
@@ -0,0 +1,29 @@
package common
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Test_GetOpts provides unit tests for GetOpts and all the options
func Test_GetOpts(t *testing.T) {
t.Parallel()
t.Run("nil-options", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
opts, err := GetOpts(nil, nil)
require.NoError(err)
testOpts := getDefaultOptions()
assert.Equal(opts, testOpts)
})
t.Run("WithSkipScopeIdFlag", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
opts, err := GetOpts(WithSkipScopeIdFlag(true))
require.NoError(err)
testOpts := getDefaultOptions()
testOpts.WithSkipScopeIdFlag = true
assert.Equal(opts, testOpts)
})
}