Skip to content

Commit

Permalink
Create ferry add-token and status commands
Browse files Browse the repository at this point in the history
  • Loading branch information
talanknight committed Feb 28, 2024
1 parent 2c24d09 commit e527962
Show file tree
Hide file tree
Showing 15 changed files with 654 additions and 91 deletions.
4 changes: 2 additions & 2 deletions internal/clientcache/cmd/daemon/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ import (
var errDaemonNotRunning = stderr.New("The daemon process is not running.")

var (
_ cli.Command = (*AddTokenCommand)(nil)
_ cli.CommandAutocomplete = (*AddTokenCommand)(nil)
_ cli.Command = (*StatusCommand)(nil)
_ cli.CommandAutocomplete = (*StatusCommand)(nil)
)

type StatusCommand struct {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,57 +16,29 @@ import (

"github.com/hashicorp/boundary/internal/clientcache/internal/daemon"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/internal/cmd/wrapper"
"github.com/mitchellh/cli"
)

// Keep this interface aligned with the interface at internal/cmd/commands.go
type cacheEnabledCommand interface {
cli.Command
BaseCommand() *base.Command
func init() {
wrapper.RegisterSuccessfulCommandCallback("clientcache", hook)
}

// CommandWrapper starts the boundary daemon after the command was Run and attempts
// to send the current persona to any running daemon.
type CommandWrapper struct {
cacheEnabledCommand
}

// Wrap returns a cli.CommandFactory that returns a command wrapped in the CommandWrapper.
func Wrap(c cacheEnabledCommand) cli.CommandFactory {
return func() (cli.Command, error) {
return &CommandWrapper{
cacheEnabledCommand: c,
}, nil
}
}

// Run runs the wrapped command and then attempts to start the boundary daemon and send
// the current persona
func (w *CommandWrapper) Run(args []string) int {
// potentially intercept the token in case it isn't stored in the keyring
var token string
w.cacheEnabledCommand.BaseCommand().Opts = append(w.cacheEnabledCommand.BaseCommand().Opts, base.WithInterceptedToken(&token))
r := w.cacheEnabledCommand.Run(args)
if w.BaseCommand().FlagSkipCacheDaemon {
return r
}

if r != base.CommandSuccess {
// if we were not successful in running our command, do not continue to
// start the daemon and add the token.
return r
// hook is the callback that is registered with the wrapper package to be called.
// The daemon is not started and the token is not added to the cache if the flag
// SkipCacheDaemon is set.
func hook(ctx context.Context, baseCmd *base.Command, token string) {
if baseCmd.FlagSkipCacheDaemon {
return
}

ctx := context.Background()
if w.startDaemon(ctx) {
w.addTokenToCache(ctx, token)
if startDaemon(ctx, baseCmd) {
addTokenToCache(ctx, baseCmd, token)
}
return r
}

// startDaemon attempts to start a daemon and returns true if we have attempted to start
// the daemon and either it was successful or it was already running.
func (w *CommandWrapper) startDaemon(ctx context.Context) bool {
func startDaemon(ctx context.Context, baseCmd *base.Command) bool {
// Ignore errors related to checking if the process is already running since
// this can fall back to running the process.
if dotPath, err := DefaultDotDirectory(ctx); err == nil {
Expand All @@ -79,7 +51,7 @@ func (w *CommandWrapper) startDaemon(ctx context.Context) bool {

cmdName, err := os.Executable()
if err != nil {
w.BaseCommand().UI.Error(fmt.Sprintf("unable to find boundary binary for daemon startup: %s", err.Error()))
baseCmd.UI.Error(fmt.Sprintf("unable to find boundary binary for daemon startup: %s", err.Error()))
return false
}

Expand All @@ -105,13 +77,13 @@ func silentUi() *cli.BasicUi {

// addTokenToCache runs AddTokenCommand with the token used in, or retrieved by
// the wrapped command.
func (w *CommandWrapper) addTokenToCache(ctx context.Context, token string) bool {
com := AddTokenCommand{Command: base.NewCommand(w.BaseCommand().UI)}
client, err := w.BaseCommand().Client()
func addTokenToCache(ctx context.Context, baseCmd *base.Command, token string) bool {
com := AddTokenCommand{Command: base.NewCommand(baseCmd.UI)}
client, err := baseCmd.Client()
if err != nil {
return false
}
keyringType, tokName, err := w.BaseCommand().DiscoverKeyringTokenInfo()
keyringType, tokName, err := baseCmd.DiscoverKeyringTokenInfo()
if err != nil && token == "" {
return false
}
Expand All @@ -138,7 +110,7 @@ func (w *CommandWrapper) addTokenToCache(ctx context.Context, token string) bool
// provided context is done. It returns an error if the unix socket is not found
// before the context is done.
func waitForDaemon(ctx context.Context) error {
const op = "daemon.waitForDaemon"
const op = "wrapper.waitForDaemon"
dotPath, err := DefaultDotDirectory(ctx)
if err != nil {
return err
Expand Down
13 changes: 13 additions & 0 deletions internal/cmd/base/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ const (
EnvKeyringType = "BOUNDARY_KEYRING_TYPE"
envRecoveryConfig = "BOUNDARY_RECOVERY_CONFIG"
envSkipCacheDaemon = "BOUNDARY_SKIP_CACHE_DAEMON"
envSkipFerry = "BOUNDARY_SKIP_FERRY"
EnvFerryDaemonPort = "BOUNDARY_FERRY_LISTENING_PORT"

StoredTokenName = "HashiCorp Boundary Auth Token"
)
Expand Down Expand Up @@ -106,6 +108,9 @@ type Command struct {
FlagRecoveryConfig string
FlagOutputCurlString bool
FlagSkipCacheDaemon bool
FlagSkipFerry bool

FlagFerryDaemonPort uint

FlagScopeId string
FlagScopeName string
Expand Down Expand Up @@ -479,6 +484,14 @@ func (c *Command) FlagSet(bit FlagSetBit) *FlagSets {
EnvVar: envSkipCacheDaemon,
Usage: "Skips starting the caching daemon or sending the current used/retrieved token to the caching daemon.",
})

f.UintVar(&UintVar{
Name: "ferry-port",
Target: &c.FlagFerryDaemonPort,
Default: 9300,
EnvVar: EnvFerryDaemonPort,
Usage: "The port on which the ferry daemon is listening.",
})
}

if bit&FlagSetOutputFormat != 0 {
Expand Down
21 changes: 10 additions & 11 deletions internal/cmd/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"github.com/hashicorp/boundary/internal/cmd/commands/userscmd"
"github.com/hashicorp/boundary/internal/cmd/commands/version"
"github.com/hashicorp/boundary/internal/cmd/commands/workerscmd"
"github.com/hashicorp/boundary/internal/cmd/wrapper"

"github.com/mitchellh/cli"
)
Expand Down Expand Up @@ -573,6 +574,13 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
}, nil
},

"ferry": func() (cli.Command, error) {
return &unsupported.UnsupportedCommand{
Command: base.NewCommand(ui, opts...),
CommandName: "ferry",
}, nil
},

"groups": func() (cli.Command, error) {
return &groupscmd.Command{
Command: base.NewCommand(ui, opts...),
Expand Down Expand Up @@ -1329,16 +1337,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {

var extraCommandsFuncs []func(ui, serverCmdUi cli.Ui, runOpts *RunOptions)

// Keep this interface aligned with the interface at internal/clientcache/cmd/daemon/command_wrapper.go
type cacheEnabledCommand interface {
cli.Command
BaseCommand() *base.Command
}

// clientCacheWrapper wraps all short lived, non server, command factories.
// The default func is a noop.
var clientCacheWrapper = func(c cacheEnabledCommand) cli.CommandFactory {
return func() (cli.Command, error) {
return c, nil
}
func clientCacheWrapper(c wrapper.WrappableCommand) cli.CommandFactory {
return wrapper.Wrap(c)
}
187 changes: 187 additions & 0 deletions internal/cmd/commands/ferry/addtoken.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package ferry

import (
"context"
"errors"
"fmt"
"strings"

"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)

var (
_ cli.Command = (*AddTokenCommand)(nil)
_ cli.CommandAutocomplete = (*AddTokenCommand)(nil)
)

type AddTokenCommand struct {
*base.Command
}

func (c *AddTokenCommand) Synopsis() string {
return "Add an auth token to a running Boundary ferry daemon"
}

func (c *AddTokenCommand) Help() string {
helpText := `
Usage: boundary ferry add-token [options]
Add an auth token to the ferry daemon:
$ boundary ferry add-token
For a full list of examples, please see the documentation.
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}

func (c *AddTokenCommand) Flags() *base.FlagSets {
set := c.FlagSet(base.FlagSetOutputFormat)
f := set.NewFlagSet("Connection Options")

f.StringVar(&base.StringVar{
Name: base.FlagNameAddr,
Target: &c.FlagAddr,
EnvVar: api.EnvBoundaryAddr,
Completion: complete.PredictAnything,
Usage: "Addr of the Boundary controller, as a complete URL (e.g. https://boundary.example.com:9200).",
})

f = set.NewFlagSet("Client 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 storing the token in the system credential store. This can allow switching user identities for different commands.`,
})

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".`,
})

f.StringVar(&base.StringVar{
Name: "token",
Target: &c.FlagToken,
Usage: `A URL pointing to a file on disk (file://) from which a token will be read or an env var (env://) from which the token will be read. Overrides the "token-name" parameter.`,
})

f.BoolVar(&base.BoolVar{
Name: "output-curl-string",
Target: &c.FlagOutputCurlString,
Usage: "Instead of executing the request, print an equivalent cURL command string and exit.",
})

f.UintVar(&base.UintVar{
Name: "ferry-port",
Target: &c.FlagFerryDaemonPort,
Default: 9300,
EnvVar: base.EnvFerryDaemonPort,
Usage: "The port on which the ferry daemon is listening.",
})

return set
}

func (c *AddTokenCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictNothing
}

func (c *AddTokenCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}

func (c *AddTokenCommand) Run(args []string) int {
ctx := c.Context
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(err)
return base.CommandCliError
}

resp, apiErr, err := c.Add(ctx, c.UI, client)
if err != nil {
c.PrintCliError(err)
return base.CommandCliError
}
if apiErr != nil {
c.PrintApiError(apiErr, "Error from daemon when adding a token")
return base.CommandApiError
}
switch base.Format(c.UI) {
case "json":
if ok := c.PrintJsonItem(resp); !ok {
return base.CommandCliError
}
case "table":
c.UI.Output("The ferry add-token operation completed successfully.")
}
return base.CommandSuccess
}

// userTokenToAdd is the request body to this handler.
type UpsertTokenRequest struct {
// BoundaryAddr is a required field for all requests
BoundaryUrl string `json:"boundary_url,omitempty"`
// The raw auth token for this user.
Token string `json:"token,omitempty"`
}

// Add builds the UpsertTokenRequest using the client's address and token.
// It then sends the request to the ferry daemon.
// The passed in cli.Ui is used to print out any errors when looking up the
// auth token from the keyring. This allows background operations calling this
// method to pass in a silent UI to suppress any output.
func (c *AddTokenCommand) Add(ctx context.Context, ui cli.Ui, apiClient *api.Client) (*api.Response, *api.Error, error) {
pa := UpsertTokenRequest{
BoundaryUrl: apiClient.Addr(),
}
token := apiClient.Token()
if token == "" {
return nil, nil, errors.New("The client auth token is empty.")
}
if parts := strings.SplitN(token, "_", 4); len(parts) != 3 {
return nil, nil, errors.New("The client provided auth token is not in the proper format.")
}

if c.FlagOutputCurlString {
pa.Token = "/*token*/"
} else {
pa.Token = token
}

ferryClient, err := api.NewClient(&api.Config{
Addr: ferryAddress(c.FlagFerryDaemonPort),
OutputCurlString: c.FlagOutputCurlString,
})
if err != nil {
return nil, nil, err
}

req, err := ferryClient.NewRequest(ctx, "POST", "/token", pa)
if err != nil {
return nil, nil, err
}
resp, err := ferryClient.Do(req)
if err != nil {
return nil, nil, fmt.Errorf("Error when sending request to the ferry daemon: %w.", err)
}
apiErr, err := resp.Decode(nil)
return resp, apiErr, err
}
Loading

0 comments on commit e527962

Please sign in to comment.