diff options
author | Bjørn Christian Seime <bjorncs@verizonmedia.com> | 2021-11-11 17:32:30 +0100 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@verizonmedia.com> | 2021-11-12 15:39:59 +0100 |
commit | 3cfbef21b4aa9ba1caba4e24529f27bb6cf2c0ab (patch) | |
tree | f7304458215f80fa85029e2e564bbaeae77065fe /client | |
parent | 144ab4c24a6d94fd284f92f61b92268fa3189b21 (diff) |
Wiring of Auth0 access token to cloud specific HTTP requests
Diffstat (limited to 'client')
-rw-r--r-- | client/go/cli/cli.go | 49 | ||||
-rw-r--r-- | client/go/cmd/helpers.go | 37 | ||||
-rw-r--r-- | client/go/cmd/login.go | 38 | ||||
-rw-r--r-- | client/go/cmd/root.go | 120 | ||||
-rw-r--r-- | client/go/vespa/deploy.go | 20 | ||||
-rw-r--r-- | client/go/vespa/target.go | 79 | ||||
-rw-r--r-- | client/go/vespa/target_test.go | 7 |
7 files changed, 188 insertions, 162 deletions
diff --git a/client/go/cli/cli.go b/client/go/cli/cli.go index 7ec554fb92d..e1dde387b89 100644 --- a/client/go/cli/cli.go +++ b/client/go/cli/cli.go @@ -7,11 +7,13 @@ import ( "encoding/json" "errors" "fmt" + "github.com/joeshaw/envdecode" "github.com/pkg/browser" "github.com/vespa-engine/vespa/client/go/util" "io/ioutil" "net/http" "os" + "os/signal" "path/filepath" "sort" "sync" @@ -75,26 +77,53 @@ func (c *Cli) IsLoggedIn() bool { return true } +// default to vespa-cd.auth0.com +var ( + authCfg struct { + Audience string `env:"AUTH0_AUDIENCE,default=https://vespa-cd.auth0.com/api/v2/"` + ClientID string `env:"AUTH0_CLIENT_ID,default=4wYWA496zBP28SLiz0PuvCt8ltL11DZX"` + DeviceCodeEndpoint string `env:"AUTH0_DEVICE_CODE_ENDPOINT,default=https://vespa-cd.auth0.com/oauth/device/code"` + OauthTokenEndpoint string `env:"AUTH0_OAUTH_TOKEN_ENDPOINT,default=https://vespa-cd.auth0.com/oauth/token"` + } +) + +func ContextWithCancel() context.Context { + ctx, cancel := context.WithCancel(context.Background()) + ch := make(chan os.Signal, 1) + signal.Notify(ch, os.Interrupt) + go func() { + <-ch + defer cancel() + os.Exit(0) + }() + return ctx +} + // Setup will try to initialize the config context, as well as figure out if // there's a readily available tenant. -func (c *Cli) Setup(ctx context.Context) error { - if err := c.init(); err != nil { - return err +func GetCli(configPath string) (*Cli, error) { + c := Cli{} + c.Path = configPath + if err := envdecode.StrictDecode(&authCfg); err != nil { + return nil, fmt.Errorf("could not decode env: %w", err) } - - _, err := c.prepareTenant(ctx) - if err != nil { - return err + c.Authenticator = &auth.Authenticator{ + Audience: authCfg.Audience, + ClientID: authCfg.ClientID, + DeviceCodeEndpoint: authCfg.DeviceCodeEndpoint, + OauthTokenEndpoint: authCfg.OauthTokenEndpoint, } - - return nil + return &c, nil } // prepareTenant loads the Tenant, refreshing its token if necessary. // The Tenant access token needs a refresh if: // 1. the Tenant scopes are different from the currently required scopes. // 2. the access token is expired. -func (c *Cli) prepareTenant(ctx context.Context) (Tenant, error) { +func (c *Cli) PrepareTenant(ctx context.Context) (Tenant, error) { + if err := c.init(); err != nil { + return Tenant{}, err + } t, err := c.getTenant() if err != nil { return Tenant{}, err diff --git a/client/go/cmd/helpers.go b/client/go/cmd/helpers.go index 4768290e33e..54d8798b71d 100644 --- a/client/go/cmd/helpers.go +++ b/client/go/cmd/helpers.go @@ -153,10 +153,17 @@ func getConsoleURL() string { } func getApiURL() string { - if getSystem() == "publiccd" { - return "https://api.vespa-external-cd.aws.oath.cloud:4443" + if vespa.Auth0AccessTokenEnabled() { + if getSystem() == "publiccd" { + return "https://api.vespa-external-cd.aws.oath.cloud:443" + } + return "https://api.vespa-external.aws.oath.cloud:443" + } else { + if getSystem() == "publiccd" { + return "https://api.vespa-external-cd.aws.oath.cloud:4443" + } + return "https://api.vespa-external.aws.oath.cloud:4443" } - return "https://api.vespa-external.aws.oath.cloud:4443" } func getTarget() vespa.Target { @@ -174,9 +181,12 @@ func getTarget() vespa.Target { fatalErr(err, "Could not load config") return nil } - apiKey, err := ioutil.ReadFile(cfg.APIKeyPath(deployment.Application.Tenant)) - if err != nil { - fatalErrHint(err, "Deployment to cloud requires an API key. Try 'vespa api-key'") + var apiKey []byte = nil + if !vespa.Auth0AccessTokenEnabled() { + apiKey, err = ioutil.ReadFile(cfg.APIKeyPath(deployment.Application.Tenant)) + if err != nil { + fatalErrHint(err, "Deployment to cloud requires an API key. Try 'vespa api-key'") + } } privateKeyFile, err := cfg.PrivateKeyPath(deployment.Application) if err != nil { @@ -201,7 +211,8 @@ func getTarget() vespa.Target { vespa.LogOptions{ Writer: stdout, Level: vespa.LogLevel(logLevelArg), - }) + }, + cfg.AuthConfigPath()) } fatalErrHint(fmt.Errorf("Invalid target: %s", targetType), "Valid targets are 'local', 'cloud' or an URL") return nil @@ -232,11 +243,13 @@ func getDeploymentOpts(cfg *Config, pkg vespa.ApplicationPackage, target vespa.T fatalErrHint(fmt.Errorf("Missing certificate in application package"), "Applications in Vespa Cloud require a certificate", "Try 'vespa cert'") return opts } - var err error - opts.APIKey, err = cfg.ReadAPIKey(deployment.Application.Tenant) - if err != nil { - fatalErrHint(err, "Deployment to cloud requires an API key. Try 'vespa api-key'") - return opts + if !vespa.Auth0AccessTokenEnabled() { + var err error + opts.APIKey, err = cfg.ReadAPIKey(deployment.Application.Tenant) + if err != nil { + fatalErrHint(err, "Deployment to cloud requires an API key. Try 'vespa api-key'") + return opts + } } opts.Deployment = deployment } diff --git a/client/go/cmd/login.go b/client/go/cmd/login.go index e1352681905..767d462b0be 100644 --- a/client/go/cmd/login.go +++ b/client/go/cmd/login.go @@ -3,20 +3,32 @@ package cmd import ( "github.com/spf13/cobra" "github.com/vespa-engine/vespa/client/go/cli" + "github.com/vespa-engine/vespa/client/go/vespa" ) -func loginCmd(c *cli.Cli) *cobra.Command { - cmd := &cobra.Command{ - Use: "login", - Args: cobra.NoArgs, - Short: "Authenticate the Vespa CLI", - Example: "$ vespa login", - DisableAutoGenTag: true, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - _, err := cli.RunLogin(ctx, c, false) - return err - }, +func init() { + if vespa.Auth0AccessTokenEnabled() { + rootCmd.AddCommand(loginCmd) } - return cmd +} + +var loginCmd = &cobra.Command{ + Use: "login", + Args: cobra.NoArgs, + Short: "Authenticate the Vespa CLI", + Example: "$ vespa login", + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + cfg, err := LoadConfig() + if err != nil { + return err + } + c, err := cli.GetCli(cfg.AuthConfigPath()) + if err != nil { + return err + } + _, err = cli.RunLogin(ctx, c, false) + return err + }, } diff --git a/client/go/cmd/root.go b/client/go/cmd/root.go index 33837965a51..5aae55ab6e4 100644 --- a/client/go/cmd/root.go +++ b/client/go/cmd/root.go @@ -5,34 +5,33 @@ package cmd import ( - "context" "fmt" "io" "io/ioutil" "log" "os" - "os/signal" - "github.com/joeshaw/envdecode" "github.com/logrusorgru/aurora/v3" "github.com/mattn/go-colorable" "github.com/mattn/go-isatty" "github.com/spf13/cobra" - "github.com/vespa-engine/vespa/client/go/auth" - "github.com/vespa-engine/vespa/client/go/cli" ) var ( - // default to vespa-cd.auth0.com - authCfg struct { - Audience string `env:"AUTH0_AUDIENCE,default=https://vespa-cd.auth0.com/api/v2/"` - ClientID string `env:"AUTH0_CLIENT_ID,default=4wYWA496zBP28SLiz0PuvCt8ltL11DZX"` - DeviceCodeEndpoint string `env:"AUTH0_DEVICE_CODE_ENDPOINT,default=https://vespa-cd.auth0.com/oauth/device/code"` - OauthTokenEndpoint string `env:"AUTH0_OAUTH_TOKEN_ENDPOINT,default=https://vespa-cd.auth0.com/oauth/token"` - } + rootCmd = &cobra.Command{ + Use: "vespa command-name", + Short: "The command-line tool for Vespa.ai", + Long: `The command-line tool for Vespa.ai. - c = &cli.Cli{} - rootCmd = buildRootCmd(c) +Use it on Vespa instances running locally, remotely or in the cloud. +Prefer web service API's to this in production. + +Vespa documentation: https://docs.vespa.ai`, + DisableAutoGenTag: true, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + configureOutput() + }, + } targetArg string applicationArg string @@ -54,61 +53,6 @@ const ( quietFlag = "quiet" ) -func buildRootCmd(cli *cli.Cli) *cobra.Command { - rootCmd := &cobra.Command{ - Use: "vespa command-name", - Short: "The command-line tool for Vespa.ai", - Long: `The command-line tool for Vespa.ai. - -Use it on Vespa instances running locally, remotely or in the cloud. -Prefer web service API's to this in production. - -Vespa documentation: https://docs.vespa.ai`, - DisableAutoGenTag: true, - SilenceUsage: true, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - configureOutput() - - // auth - if err := envdecode.StrictDecode(&authCfg); err != nil { - return fmt.Errorf("could not decode env: %w", err) - } - cli.Authenticator = &auth.Authenticator{ - Audience: authCfg.Audience, - ClientID: authCfg.ClientID, - DeviceCodeEndpoint: authCfg.DeviceCodeEndpoint, - OauthTokenEndpoint: authCfg.OauthTokenEndpoint, - } - - // TODO: whitelist other commands - // If the user is trying to log in, no need to go through setup - if cmd.Use == "login" && cmd.Parent().Use == "vespa command-name" { - return nil - } - - return cli.Setup(cmd.Context()) - }, - } - return rootCmd -} - -func addPersistentFlags(rootCmd *cobra.Command) { - rootCmd.PersistentFlags().StringVarP(&targetArg, targetFlag, "t", "local", "The name or URL of the recipient of this command") - rootCmd.PersistentFlags().StringVarP(&applicationArg, applicationFlag, "a", "", "The application to manage") - rootCmd.PersistentFlags().IntVarP(&waitSecsArg, waitFlag, "w", 0, "Number of seconds to wait for a service to become ready") - rootCmd.PersistentFlags().StringVarP(&colorArg, colorFlag, "c", "auto", "Whether to use colors in output. Can be \"auto\", \"never\" or \"always\"") - rootCmd.PersistentFlags().BoolVarP(&quietArg, quietFlag, "q", false, "Quiet mode. Only errors are printed.") - bindFlagToConfig(targetFlag, rootCmd) - bindFlagToConfig(applicationFlag, rootCmd) - bindFlagToConfig(waitFlag, rootCmd) - bindFlagToConfig(colorFlag, rootCmd) - bindFlagToConfig(quietFlag, rootCmd) -} - -func addSubcommands(rootCmd *cobra.Command, cli *cli.Cli) { - rootCmd.AddCommand(loginCmd(cli)) -} - func isTerminal() bool { if f, ok := stdout.(*os.File); ok { return isatty.IsTerminal(f.Fd()) @@ -130,12 +74,6 @@ func configureOutput() { if err != nil { fatalErr(err, "Could not load config") } - - // path to auth config - if c.Path == "" { - c.Path = config.AuthConfigPath() - } - colorValue, err := config.Get(colorFlag) if err != nil { fatalErr(err) @@ -149,29 +87,23 @@ func configureOutput() { colorize = true case "never": default: - fatalErrHint(fmt.Errorf("invalid value for %s option", colorFlag), "Must be \"auto\", \"never\" or \"always\"") + fatalErrHint(fmt.Errorf("Invalid value for %s option", colorFlag), "Must be \"auto\", \"never\" or \"always\"") } color = aurora.NewAurora(colorize) } -func contextWithCancel() context.Context { - ctx, cancel := context.WithCancel(context.Background()) - - ch := make(chan os.Signal, 1) - signal.Notify(ch, os.Interrupt) - - go func() { - <-ch - defer cancel() - os.Exit(0) - }() - - return ctx +func init() { + rootCmd.PersistentFlags().StringVarP(&targetArg, targetFlag, "t", "local", "The name or URL of the recipient of this command") + rootCmd.PersistentFlags().StringVarP(&applicationArg, applicationFlag, "a", "", "The application to manage") + rootCmd.PersistentFlags().IntVarP(&waitSecsArg, waitFlag, "w", 0, "Number of seconds to wait for a service to become ready") + rootCmd.PersistentFlags().StringVarP(&colorArg, colorFlag, "c", "auto", "Whether to use colors in output. Can be \"auto\", \"never\" or \"always\"") + rootCmd.PersistentFlags().BoolVarP(&quietArg, quietFlag, "q", false, "Quiet mode. Only errors are printed.") + bindFlagToConfig(targetFlag, rootCmd) + bindFlagToConfig(applicationFlag, rootCmd) + bindFlagToConfig(waitFlag, rootCmd) + bindFlagToConfig(colorFlag, rootCmd) + bindFlagToConfig(quietFlag, rootCmd) } // Execute executes the root command. -func Execute() error { - addPersistentFlags(rootCmd) - addSubcommands(rootCmd, c) - return rootCmd.ExecuteContext(contextWithCancel()) -} +func Execute() error { return rootCmd.Execute() } diff --git a/client/go/vespa/deploy.go b/client/go/vespa/deploy.go index 3718c7d813a..9c5fb3a12ac 100644 --- a/client/go/vespa/deploy.go +++ b/client/go/vespa/deploy.go @@ -327,12 +327,12 @@ func Submit(opts DeploymentOpts) error { Header: make(http.Header), } request.Header.Set("Content-Type", writer.FormDataContentType()) - signer := NewRequestSigner(opts.Deployment.Application.SerializedForm(), opts.APIKey) - if err := signer.SignRequest(request); err != nil { + serviceDescription := "Submit service" + sigKeyId := opts.Deployment.Application.SerializedForm() + if err := opts.Target.PrepareApiRequest(request, sigKeyId); err != nil { return err } - serviceDescription := "Submit service" - response, err := util.HttpDo(request, time.Minute*10, serviceDescription) + response, err := util.HttpDo(request, time.Minute*10, sigKeyId) if err != nil { return err } @@ -344,7 +344,7 @@ func checkDeploymentOpts(opts DeploymentOpts) error { if !opts.ApplicationPackage.HasCertificate() { return fmt.Errorf("%s: missing certificate in package", opts) } - if opts.APIKey == nil { + if !Auth0AccessTokenEnabled() && opts.APIKey == nil { return fmt.Errorf("%s: missing api key", opts.String()) } return nil @@ -363,13 +363,11 @@ func uploadApplicationPackage(url *url.URL, opts DeploymentOpts) (int64, error) Header: header, Body: ioutil.NopCloser(zipReader), } - if opts.APIKey != nil { - signer := NewRequestSigner(opts.Deployment.Application.SerializedForm(), opts.APIKey) - if err := signer.SignRequest(request); err != nil { - return 0, err - } - } serviceDescription := "Deploy service" + sigKeyId := opts.Deployment.Application.SerializedForm() + if err := opts.Target.PrepareApiRequest(request, sigKeyId); err != nil { + return 0, err + } response, err := util.HttpDo(request, time.Minute*10, serviceDescription) if err != nil { return 0, err diff --git a/client/go/vespa/target.go b/client/go/vespa/target.go index 8a09440f5cc..f497bf5b3cd 100644 --- a/client/go/vespa/target.go +++ b/client/go/vespa/target.go @@ -6,13 +6,16 @@ import ( "crypto/tls" "encoding/json" "fmt" + "github.com/vespa-engine/vespa/client/go/cli" "io" "io/ioutil" "math" "net/http" "net/url" + "os" "sort" "strconv" + "strings" "time" "github.com/vespa-engine/vespa/client/go/util" @@ -35,6 +38,7 @@ type Service struct { BaseURL string Name string TLSOptions TLSOptions + Target *Target } // Target represents a Vespa platform, running named Vespa services. @@ -47,6 +51,8 @@ type Target interface { // PrintLog writes the logs of this deployment using given options to control output. PrintLog(options LogOptions) error + + PrepareApiRequest(req *http.Request, sigKeyId string) error } // TLSOptions configures the certificate to use for service requests. @@ -66,11 +72,21 @@ type LogOptions struct { Level int } +func Auth0AccessTokenEnabled() bool { + v, present := os.LookupEnv("VESPA_CLI_OAUTH2_DEVICE_FLOW") + if !present { + return false + } + return strings.ToLower(v) == "true" || v == "1" || v == "" +} + type customTarget struct { targetType string baseURL string } +func (t *customTarget) PrepareApiRequest(req *http.Request, sigKeyId string) error { return nil } + // Do sends request to this service. Any required authentication happens automatically. func (s *Service) Do(request *http.Request, timeout time.Duration) (*http.Response, error) { if s.TLSOptions.KeyPair.Certificate != nil { @@ -192,8 +208,9 @@ type cloudTarget struct { tlsOptions TLSOptions logOptions LogOptions - queryURL string - documentURL string + queryURL string + documentURL string + authConfigPath string } func (t *cloudTarget) Type() string { return t.targetType } @@ -221,6 +238,30 @@ func (t *cloudTarget) Service(name string, timeout time.Duration, runID int64) ( return nil, fmt.Errorf("unknown service: %s", name) } +func (t *cloudTarget) PrepareApiRequest(req *http.Request, sigKeyId string) error { + if Auth0AccessTokenEnabled() { + if err := t.addAuth0AccessToken(req); err != nil { + return err + } + } else if t.apiKey != nil { + signer := NewRequestSigner(sigKeyId, t.apiKey) + if err := signer.SignRequest(req); err != nil { + return err + } + } + return nil +} + +func (t *cloudTarget) addAuth0AccessToken(request *http.Request) error { + c, err := cli.GetCli(t.authConfigPath) + tenant, err := c.PrepareTenant(cli.ContextWithCancel()) + if err != nil { + return err + } + request.Header.Set("Authorization", "Bearer "+tenant.AccessToken) + return nil +} + func (t *cloudTarget) logsURL() string { return fmt.Sprintf("%s/application/v4/tenant/%s/application/%s/instance/%s/environment/%s/region/%s/logs", t.apiURL, @@ -233,7 +274,6 @@ func (t *cloudTarget) PrintLog(options LogOptions) error { if err != nil { return err } - signer := NewRequestSigner(t.deployment.Application.SerializedForm(), t.apiKey) lastFrom := options.From requestFunc := func() *http.Request { fromMillis := lastFrom.Unix() * 1000 @@ -244,9 +284,7 @@ func (t *cloudTarget) PrintLog(options LogOptions) error { q.Set("to", strconv.FormatInt(toMillis, 10)) } req.URL.RawQuery = q.Encode() - if err := signer.SignRequest(req); err != nil { - panic(err) - } + t.PrepareApiRequest(req, t.deployment.Application.SerializedForm()) return req } logFunc := func(status int, response []byte) (bool, error) { @@ -280,16 +318,15 @@ func (t *cloudTarget) PrintLog(options LogOptions) error { } func (t *cloudTarget) waitForEndpoints(timeout time.Duration, runID int64) error { - signer := NewRequestSigner(t.deployment.Application.SerializedForm(), t.apiKey) if runID > 0 { - if err := t.waitForRun(signer, runID, timeout); err != nil { + if err := t.waitForRun(runID, timeout); err != nil { return err } } - return t.discoverEndpoints(signer, timeout) + return t.discoverEndpoints(timeout) } -func (t *cloudTarget) waitForRun(signer *RequestSigner, runID int64, timeout time.Duration) error { +func (t *cloudTarget) waitForRun(runID int64, timeout time.Duration) error { runURL := fmt.Sprintf("%s/application/v4/tenant/%s/application/%s/instance/%s/job/%s-%s/run/%d", t.apiURL, t.deployment.Application.Tenant, t.deployment.Application.Application, t.deployment.Application.Instance, @@ -303,7 +340,7 @@ func (t *cloudTarget) waitForRun(signer *RequestSigner, runID int64, timeout tim q := req.URL.Query() q.Set("after", strconv.FormatInt(lastID, 10)) req.URL.RawQuery = q.Encode() - if err := signer.SignRequest(req); err != nil { + if err := t.PrepareApiRequest(req, t.deployment.Application.SerializedForm()); err != nil { panic(err) } return req @@ -353,7 +390,7 @@ func (t *cloudTarget) printLog(response jobResponse, last int64) int64 { return response.LastID } -func (t *cloudTarget) discoverEndpoints(signer *RequestSigner, timeout time.Duration) error { +func (t *cloudTarget) discoverEndpoints(timeout time.Duration) error { deploymentURL := fmt.Sprintf("%s/application/v4/tenant/%s/application/%s/instance/%s/environment/%s/region/%s", t.apiURL, t.deployment.Application.Tenant, t.deployment.Application.Application, t.deployment.Application.Instance, @@ -362,7 +399,7 @@ func (t *cloudTarget) discoverEndpoints(signer *RequestSigner, timeout time.Dura if err != nil { return err } - if err := signer.SignRequest(req); err != nil { + if err := t.PrepareApiRequest(req, t.deployment.Application.SerializedForm()); err != nil { return err } var endpointURL string @@ -409,14 +446,16 @@ func CustomTarget(baseURL string) Target { } // CloudTarget creates a Target for the Vespa Cloud platform. -func CloudTarget(apiURL string, deployment Deployment, apiKey []byte, tlsOptions TLSOptions, logOptions LogOptions) Target { +func CloudTarget(apiURL string, deployment Deployment, apiKey []byte, tlsOptions TLSOptions, logOptions LogOptions, + authConfigPath string) Target { return &cloudTarget{ - apiURL: apiURL, - targetType: cloudTargetType, - deployment: deployment, - apiKey: apiKey, - tlsOptions: tlsOptions, - logOptions: logOptions, + apiURL: apiURL, + targetType: cloudTargetType, + deployment: deployment, + apiKey: apiKey, + tlsOptions: tlsOptions, + logOptions: logOptions, + authConfigPath: authConfigPath, } } diff --git a/client/go/vespa/target_test.go b/client/go/vespa/target_test.go index ed924059297..d4d23901513 100644 --- a/client/go/vespa/target_test.go +++ b/client/go/vespa/target_test.go @@ -143,7 +143,10 @@ func createCloudTarget(t *testing.T, url string, logWriter io.Writer) Target { x509KeyPair, err := tls.X509KeyPair(kp.Certificate, kp.PrivateKey) assert.Nil(t, err) - apiKey, err := CreateAPIKey() + var apiKey []byte = nil + if !Auth0AccessTokenEnabled() { + apiKey, err = CreateAPIKey() + } assert.Nil(t, err) target := CloudTarget( @@ -154,7 +157,7 @@ func createCloudTarget(t *testing.T, url string, logWriter io.Writer) Target { }, apiKey, TLSOptions{KeyPair: x509KeyPair}, - LogOptions{Writer: logWriter}) + LogOptions{Writer: logWriter}, "") if ct, ok := target.(*cloudTarget); ok { ct.apiURL = url } else { |