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
This commit is contained in:
Jeffrey Smith II 2022-07-28 10:53:19 -04:00 committed by GitHub
parent 182303e31d
commit f34e6a888f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 313 additions and 4 deletions

203
api/api_signin.gen.go Normal file
View File

@ -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
}

View File

@ -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)

View File

@ -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,

View File

@ -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:

View File

@ -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 {

56
clients/signin/signin.go Normal file
View File

@ -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
}

View File

@ -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)
},
}

View File

@ -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))

View File

@ -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,
}

View File

@ -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

2
go.mod
View File

@ -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
)