From f34e6a888fd3995ac11e84d1a0f4e9d9472192e1 Mon Sep 17 00:00:00 2001 From: Jeffrey Smith II Date: Thu, 28 Jul 2022 10:53:19 -0400 Subject: [PATCH] feat: add username and password login (#418) * feat: add username and password login * fix: make sure cookie is not empty * chore: go mod tidy * fix: prevent local config from influencing tests * fix: small cleanup on error handling * fix: remove unnecessary trim --- api/api_signin.gen.go | 203 +++++++++++++++++++++++++++++++++++ api/client.gen.go | 3 + api/configuration_default.go | 5 +- api/contract/cli.yml | 2 + clients/config/config.go | 13 +++ clients/signin/signin.go | 56 ++++++++++ cmd/influx/config.go | 19 +++- cmd/influx/global.go | 9 +- cmd/influx/main_test.go | 4 + config/config.go | 1 + go.mod | 2 +- 11 files changed, 313 insertions(+), 4 deletions(-) create mode 100644 api/api_signin.gen.go create mode 100644 clients/signin/signin.go diff --git a/api/api_signin.gen.go b/api/api_signin.gen.go new file mode 100644 index 0000000..3091579 --- /dev/null +++ b/api/api_signin.gen.go @@ -0,0 +1,203 @@ +/* + * Subset of Influx API covered by Influx CLI + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * API version: 2.0.0 + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package api + +import ( + _context "context" + _fmt "fmt" + _io "io" + _nethttp "net/http" + _neturl "net/url" +) + +// Linger please +var ( + _ _context.Context +) + +type SigninApi interface { + + /* + * PostSignin Create a user session. + * Authenticates ***Basic Auth*** credentials for a user. If successful, creates a new UI session for the user. + * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return ApiPostSigninRequest + */ + PostSignin(ctx _context.Context) ApiPostSigninRequest + + /* + * PostSigninExecute executes the request + */ + PostSigninExecute(r ApiPostSigninRequest) error + + /* + * PostSigninExecuteWithHttpInfo executes the request with HTTP response info returned. The response body is not + * available on the returned HTTP response as it will have already been read and closed; access to the response body + * content should be achieved through the returned response model if applicable. + */ + PostSigninExecuteWithHttpInfo(r ApiPostSigninRequest) (*_nethttp.Response, error) +} + +// SigninApiService SigninApi service +type SigninApiService service + +type ApiPostSigninRequest struct { + ctx _context.Context + ApiService SigninApi + zapTraceSpan *string +} + +func (r ApiPostSigninRequest) ZapTraceSpan(zapTraceSpan string) ApiPostSigninRequest { + r.zapTraceSpan = &zapTraceSpan + return r +} +func (r ApiPostSigninRequest) GetZapTraceSpan() *string { + return r.zapTraceSpan +} + +func (r ApiPostSigninRequest) Execute() error { + return r.ApiService.PostSigninExecute(r) +} + +func (r ApiPostSigninRequest) ExecuteWithHttpInfo() (*_nethttp.Response, error) { + return r.ApiService.PostSigninExecuteWithHttpInfo(r) +} + +/* + * PostSignin Create a user session. + * Authenticates ***Basic Auth*** credentials for a user. If successful, creates a new UI session for the user. + * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return ApiPostSigninRequest + */ +func (a *SigninApiService) PostSignin(ctx _context.Context) ApiPostSigninRequest { + return ApiPostSigninRequest{ + ApiService: a, + ctx: ctx, + } +} + +/* + * Execute executes the request + */ +func (a *SigninApiService) PostSigninExecute(r ApiPostSigninRequest) error { + _, err := a.PostSigninExecuteWithHttpInfo(r) + return err +} + +/* + * ExecuteWithHttpInfo executes the request with HTTP response info returned. The response body is not available on the + * returned HTTP response as it will have already been read and closed; access to the response body content should be + * achieved through the returned response model if applicable. + */ +func (a *SigninApiService) PostSigninExecuteWithHttpInfo(r ApiPostSigninRequest) (*_nethttp.Response, error) { + var ( + localVarHTTPMethod = _nethttp.MethodPost + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "SigninApiService.PostSignin") + if err != nil { + return nil, GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/api/v2/signin" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := _neturl.Values{} + localVarFormParams := _neturl.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + if r.zapTraceSpan != nil { + localVarHeaderParams["Zap-Trace-Span"] = parameterToString(*r.zapTraceSpan, "") + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarHTTPResponse, err + } + + newErr := GenericOpenAPIError{ + buildHeader: localVarHTTPResponse.Header.Get("X-Influxdb-Build"), + } + + if localVarHTTPResponse.StatusCode >= 300 { + body, err := GunzipIfNeeded(localVarHTTPResponse) + if err != nil { + body.Close() + newErr.error = err.Error() + return localVarHTTPResponse, newErr + } + localVarBody, err := _io.ReadAll(body) + body.Close() + if err != nil { + newErr.error = err.Error() + return localVarHTTPResponse, newErr + } + newErr.body = localVarBody + newErr.error = localVarHTTPResponse.Status + if localVarHTTPResponse.StatusCode == 401 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = _fmt.Sprintf("%s: %s", newErr.Error(), err.Error()) + return localVarHTTPResponse, newErr + } + v.SetMessage(_fmt.Sprintf("%s: %s", newErr.Error(), v.GetMessage())) + newErr.model = &v + return localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 403 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = _fmt.Sprintf("%s: %s", newErr.Error(), err.Error()) + return localVarHTTPResponse, newErr + } + v.SetMessage(_fmt.Sprintf("%s: %s", newErr.Error(), v.GetMessage())) + newErr.model = &v + return localVarHTTPResponse, newErr + } + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = _fmt.Sprintf("%s: %s", newErr.Error(), err.Error()) + return localVarHTTPResponse, newErr + } + v.SetMessage(_fmt.Sprintf("%s: %s", newErr.Error(), v.GetMessage())) + newErr.model = &v + return localVarHTTPResponse, newErr + } + + return localVarHTTPResponse, nil +} diff --git a/api/client.gen.go b/api/client.gen.go index 964d554..3663439 100644 --- a/api/client.gen.go +++ b/api/client.gen.go @@ -90,6 +90,8 @@ type APIClient struct { SetupApi SetupApi + SigninApi SigninApi + StacksApi StacksApi TasksApi TasksApi @@ -143,6 +145,7 @@ func NewAPIClient(cfg *Configuration) *APIClient { c.RestoreApi = (*RestoreApiService)(&c.common) c.SecretsApi = (*SecretsApiService)(&c.common) c.SetupApi = (*SetupApiService)(&c.common) + c.SigninApi = (*SigninApiService)(&c.common) c.StacksApi = (*StacksApiService)(&c.common) c.TasksApi = (*TasksApiService)(&c.common) c.TelegrafsApi = (*TelegrafsApiService)(&c.common) diff --git a/api/configuration_default.go b/api/configuration_default.go index 9c0beb1..2217257 100644 --- a/api/configuration_default.go +++ b/api/configuration_default.go @@ -11,6 +11,7 @@ type ConfigParams struct { Host *url.URL UserAgent string Token *string + Cookie *string TraceId *string AllowInsecureTLS bool Debug bool @@ -26,8 +27,10 @@ func NewAPIConfig(params ConfigParams) *Configuration { apiConfig.Scheme = params.Host.Scheme apiConfig.UserAgent = params.UserAgent apiConfig.HTTPClient = &http.Client{Transport: clientTransport} - if params.Token != nil { + if params.Token != nil && *params.Token != "" { apiConfig.DefaultHeader["Authorization"] = fmt.Sprintf("Token %s", *params.Token) + } else if params.Cookie != nil && *params.Cookie != "" { + apiConfig.DefaultHeader["Cookie"] = fmt.Sprintf("influxdb-oss-session=%s", *params.Cookie) } if params.TraceId != nil { // NOTE: This is circumventing our codegen. If the header we use for tracing ever changes, diff --git a/api/contract/cli.yml b/api/contract/cli.yml index 7fc70b5..7e1019b 100644 --- a/api/contract/cli.yml +++ b/api/contract/cli.yml @@ -129,6 +129,8 @@ paths: $ref: "./openapi/src/svc/invocable-scripts/paths/scripts_scriptID.yml" /api/v2/scripts/{scriptID}/invoke: $ref: "./overrides/paths/scripts_scriptID_invoke.yml" + /api/v2/signin: + $ref: "./openapi/src/common/paths/signin.yml" components: parameters: TraceSpan: diff --git a/clients/config/config.go b/clients/config/config.go index 1f7837a..2644aa4 100644 --- a/clients/config/config.go +++ b/clients/config/config.go @@ -1,6 +1,7 @@ package config import ( + "encoding/base64" "errors" "fmt" "net/url" @@ -47,6 +48,18 @@ func (c Client) Create(cfg config.Config) error { return c.printConfigs(configPrintOpts{config: &cfg}) } +func (c Client) CreateWithUserPass(cfg config.Config, userPass string) error { + if userPass != "" && cfg.Token != "" { + return fmt.Errorf("token and username-password cannot be specified together, please choose just one") + } + + if cfg.Token == "" && userPass != "" { + cfg.Cookie = base64.StdEncoding.EncodeToString([]byte(userPass)) + } + + return c.Create(cfg) +} + func (c Client) Delete(names []string) error { deleted := make(config.Configs) for _, name := range names { diff --git a/clients/signin/signin.go b/clients/signin/signin.go new file mode 100644 index 0000000..9df98de --- /dev/null +++ b/clients/signin/signin.go @@ -0,0 +1,56 @@ +package signin + +import ( + "context" + "encoding/base64" + "fmt" + "strings" + "syscall" + + "github.com/influxdata/influx-cli/v2/api" + "golang.org/x/term" +) + +func GetCookie(ctx context.Context, params api.ConfigParams, userPass string) (string, error) { + bufUserPass, err := base64.StdEncoding.DecodeString(userPass) + if err != nil { + return "", err + } + + splitUserPass := strings.Split(string(bufUserPass), ":") + if len(splitUserPass) < 1 { + return "", fmt.Errorf("bad config") + } + username := splitUserPass[0] + var password string + if len(splitUserPass) != 2 { + fmt.Print("Please provide your password: ") + bytePassword, err := term.ReadPassword(int(syscall.Stdin)) + if err != nil { + return "", err + } + password = string(bytePassword) + fmt.Println() + } else { + password = splitUserPass[1] + } + + cfg := api.NewAPIConfig(params) + client := api.NewAPIClient(cfg) + ctx = context.WithValue(ctx, api.ContextBasicAuth, api.BasicAuth{ + UserName: username, + Password: password, + }) + res, err := client.SigninApi.PostSignin(ctx).ExecuteWithHttpInfo() + if err != nil { + emsg := fmt.Errorf("error signing in, verify signin was not called against cloud influxdb: %w", err) + return "", emsg + } + + cookies := res.Cookies() + if len(cookies) != 1 { + return "", fmt.Errorf("failure getting session cookie, multiple cookies") + } + + return cookies[0].Value, nil +} diff --git a/cmd/influx/config.go b/cmd/influx/config.go index c7ffcbc..27a4505 100644 --- a/cmd/influx/config.go +++ b/cmd/influx/config.go @@ -66,6 +66,7 @@ https://docs.influxdata.com/influxdb/latest/reference/cli/influx/config/ func newConfigCreateCmd() cli.Command { var cfg config.Config + var userpass string return cli.Command{ Name: "create", Usage: "Create config", @@ -73,6 +74,12 @@ func newConfigCreateCmd() cli.Command { The influx config create command creates a new InfluxDB connection configuration and stores it in the configs file (by default, stored at ~/.influxdbv2/configs). +Authentication: + Authentication can be provided by either an api token or username/password, but not both. + When setting the username and password, the password is saved unencrypted in your local config file. + Optionally, you can omit the password and only provide the username. + You will then be prompted for the password each time. + Examples: # create a config and set it active influx config create -a -n $CFG_NAME -u $HOST_URL -t $TOKEN -o $ORG_NAME @@ -103,9 +110,13 @@ https://docs.influxdata.com/influxdb/latest/reference/cli/influx/config/create/ &cli.StringFlag{ Name: "token, t", Usage: "Auth token to use when communicating with the InfluxDB server", - Required: true, Destination: &cfg.Token, }, + &cli.StringFlag{ + Name: "username-password, p", + Usage: "Username (and optionally password) to use for authentication. Only supported in OSS", + Destination: &userpass, + }, &cli.StringFlag{ Name: "org, o", Usage: "Default organization name to use in the new config", @@ -118,7 +129,13 @@ https://docs.influxdata.com/influxdb/latest/reference/cli/influx/config/create/ }, ), Action: func(ctx *cli.Context) error { + if cfg.Token != "" && userpass != "" { + return fmt.Errorf("cannot specify `--token` and `--username-password` together, please choose one") + } client := cmd.Client{CLI: getCLI(ctx)} + if userpass != "" { + return client.CreateWithUserPass(cfg, userpass) + } return client.Create(cfg) }, } diff --git a/cmd/influx/global.go b/cmd/influx/global.go index 680a69f..d6245ba 100644 --- a/cmd/influx/global.go +++ b/cmd/influx/global.go @@ -10,6 +10,7 @@ import ( "github.com/influxdata/influx-cli/v2/api" "github.com/influxdata/influx-cli/v2/clients" + "github.com/influxdata/influx-cli/v2/clients/signin" "github.com/influxdata/influx-cli/v2/config" "github.com/influxdata/influx-cli/v2/pkg/cli/middleware" "github.com/influxdata/influx-cli/v2/pkg/signals" @@ -86,8 +87,14 @@ func newApiClient(ctx *cli.Context, configSvc config.Service, injectToken bool) } configParams.Host = parsedHost - if injectToken { + if injectToken && cfg.Token != "" { configParams.Token = &cfg.Token + } else if cfg.Cookie != "" { + cookie, err := signin.GetCookie(getContext(ctx), configParams, cfg.Cookie) + if err != nil { + return nil, fmt.Errorf("error creating session: %w", err) + } + configParams.Cookie = &cookie } if ctx.IsSet(traceIdFlagName) { configParams.TraceId = api.PtrString(ctx.String(traceIdFlagName)) diff --git a/cmd/influx/main_test.go b/cmd/influx/main_test.go index f049c8c..b5ba64a 100644 --- a/cmd/influx/main_test.go +++ b/cmd/influx/main_test.go @@ -5,6 +5,8 @@ import ( "fmt" "net/http" "net/http/httptest" + "os" + "path" "testing" "github.com/influxdata/influx-cli/v2/pkg/cli/middleware" @@ -135,6 +137,8 @@ func TestApp_HostSpecificErrors(t *testing.T) { args := []string{ "influx", "ping", + "--configs-path", + path.Join(os.TempDir(), "configs"), "--host", svr.URL, } diff --git a/config/config.go b/config/config.go index 8139fff..fe1fdc0 100644 --- a/config/config.go +++ b/config/config.go @@ -18,6 +18,7 @@ type Config struct { Org string `toml:"org" json:"org"` Active bool `toml:"active,omitempty" json:"active,omitempty"` PreviousActive bool `toml:"previous,omitempty" json:"previous,omitempty"` + Cookie string `toml:"cookie,omitempty" json:"cookie,omitempty"` } // DefaultConfig is default config without token diff --git a/go.mod b/go.mod index 93373d2..9353610 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/stretchr/testify v1.7.0 github.com/urfave/cli v1.22.5 go.etcd.io/bbolt v1.3.6 + golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 golang.org/x/text v0.3.7 golang.org/x/tools v0.1.11-0.20220316014157-77aa08bb151a google.golang.org/protobuf v1.27.1 @@ -54,7 +55,6 @@ require ( golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e // indirect golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect - golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect )