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:
parent
182303e31d
commit
f34e6a888f
203
api/api_signin.gen.go
Normal file
203
api/api_signin.gen.go
Normal 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
|
||||
}
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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:
|
||||
|
@ -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
56
clients/signin/signin.go
Normal 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
|
||||
}
|
@ -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)
|
||||
},
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
2
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
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user