diff --git a/auth/auth.go b/auth/auth.go index 6ee8b8f..efbbcb7 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -1,7 +1,40 @@ package auth -import "net/http" +import ( + "net/http" + "strings" +) type Authenticator interface { Authenticate(*http.Request) error } + +type AuthType int + +const ( + AuthTypeUnknown AuthType = iota + AuthTypePat + AuthTypeOauthU2M + AuthTypeOauthM2M +) + +var authTypeNames []string = []string{"Unknown", "Pat", "OauthU2M", "OauthM2M"} + +func (at AuthType) String() string { + if at >= 0 && int(at) < len(authTypeNames) { + return authTypeNames[at] + } + + return authTypeNames[0] +} + +func ParseAuthType(typeString string) AuthType { + typeString = strings.ToLower(typeString) + for i, n := range authTypeNames { + if strings.ToLower(n) == typeString { + return AuthType(i) + } + } + + return AuthTypeUnknown +} diff --git a/auth/oauth/m2m/m2m.go b/auth/oauth/m2m/m2m.go new file mode 100644 index 0000000..aa72eb9 --- /dev/null +++ b/auth/oauth/m2m/m2m.go @@ -0,0 +1,91 @@ +package m2m + +// clientid e92aa085-4875-42fe-ad75-ba38fb3c9706 +// secretid vUdzecmn4aUi2jRDamaBOy3qThu9LSgeV_BW4UnQ + +import ( + "context" + "fmt" + "net/http" + "sync" + + "github.com/databricks/databricks-sql-go/auth" + "github.com/databricks/databricks-sql-go/auth/oauth" + "github.com/rs/zerolog/log" + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" +) + +func NewAuthenticator(clientID, clientSecret, hostName string) auth.Authenticator { + scopes := oauth.GetScopes(hostName, []string{}) + return &authClient{ + clientID: clientID, + clientSecret: clientSecret, + hostName: hostName, + scopes: scopes, + } +} + +type authClient struct { + clientID string + clientSecret string + hostName string + scopes []string + tokenSource oauth2.TokenSource + mx sync.Mutex +} + +// Auth will start the OAuth Authorization Flow to authenticate the cli client +// using the users credentials in the browser. Compatible with SSO. +func (c *authClient) Authenticate(r *http.Request) error { + c.mx.Lock() + defer c.mx.Unlock() + if c.tokenSource != nil { + token, err := c.tokenSource.Token() + if err != nil { + return err + } + token.SetAuthHeader(r) + return nil + } + + config, err := GetConfig(context.Background(), c.hostName, c.clientID, c.clientSecret, c.scopes) + if err != nil { + return fmt.Errorf("unable to generate clientCredentials.Config: %w", err) + } + + c.tokenSource = GetTokenSource(config) + token, err := c.tokenSource.Token() + log.Info().Msgf("token fetched successfully") + if err != nil { + log.Err(err).Msg("failed to get token") + + return err + } + token.SetAuthHeader(r) + + return nil + +} + +func GetTokenSource(config clientcredentials.Config) oauth2.TokenSource { + tokenSource := config.TokenSource(context.Background()) + return tokenSource +} + +func GetConfig(ctx context.Context, issuerURL, clientID, clientSecret string, scopes []string) (clientcredentials.Config, error) { + // Get the endpoint based on the host name + endpoint, err := oauth.GetEndpoint(ctx, issuerURL) + if err != nil { + return clientcredentials.Config{}, fmt.Errorf("could not lookup provider details: %w", err) + } + + config := clientcredentials.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + TokenURL: endpoint.TokenURL, + Scopes: scopes, + } + + return config, nil +} diff --git a/auth/oauth/oauth.go b/auth/oauth/oauth.go new file mode 100644 index 0000000..82c952d --- /dev/null +++ b/auth/oauth/oauth.go @@ -0,0 +1,122 @@ +package oauth + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/coreos/go-oidc/v3/oidc" + "golang.org/x/oauth2" +) + +const ( + azureTenantId = "4a67d088-db5c-48f1-9ff2-0aace800ae68" +) + +func GetEndpoint(ctx context.Context, hostName string) (oauth2.Endpoint, error) { + if ctx == nil { + ctx = context.Background() + } + + cloud := InferCloudFromHost(hostName) + + if cloud == Unknown { + return oauth2.Endpoint{}, errors.New("unsupported cloud type") + } + + if cloud == Azure { + authURL := fmt.Sprintf("https://%s/oidc/oauth2/v2.0/authorize", hostName) + tokenURL := fmt.Sprintf("https://%s/oidc/oauth2/v2.0/token", hostName) + return oauth2.Endpoint{AuthURL: authURL, TokenURL: tokenURL}, nil + } + + issuerURL := fmt.Sprintf("https://%s/oidc", hostName) + ctx = oidc.InsecureIssuerURLContext(ctx, issuerURL) + provider, err := oidc.NewProvider(ctx, issuerURL) + if err != nil { + return oauth2.Endpoint{}, err + } + + endpoint := provider.Endpoint() + + return endpoint, err +} + +func GetScopes(hostName string, scopes []string) []string { + for _, s := range []string{oidc.ScopeOfflineAccess} { + if !hasScope(scopes, s) { + scopes = append(scopes, s) + } + } + + cloudType := InferCloudFromHost(hostName) + if cloudType == Azure { + userImpersonationScope := fmt.Sprintf("%s/user_impersonation", azureTenantId) + if !hasScope(scopes, userImpersonationScope) { + scopes = append(scopes, userImpersonationScope) + } + } else { + if !hasScope(scopes, "sql") { + scopes = append(scopes, "sql") + } + } + + return scopes +} + +func hasScope(scopes []string, scope string) bool { + for _, s := range scopes { + if s == scope { + return true + } + } + return false +} + +var databricksAWSDomains []string = []string{ + ".cloud.databricks.com", + ".dev.databricks.com", +} + +var databricksAzureDomains []string = []string{ + ".azuredatabricks.net", + ".databricks.azure.cn", + ".databricks.azure.us", +} + +type CloudType int + +const ( + AWS = iota + Azure + Unknown +) + +func (cl CloudType) String() string { + switch cl { + case AWS: + return "AWS" + case Azure: + return "Azure" + } + + return "Unknown" +} + +func InferCloudFromHost(hostname string) CloudType { + + for _, d := range databricksAzureDomains { + if strings.Contains(hostname, d) { + return Azure + } + } + + for _, d := range databricksAWSDomains { + if strings.Contains(hostname, d) { + return AWS + } + } + + return Unknown +} diff --git a/auth/oauth/pkce/pkce.go b/auth/oauth/pkce/pkce.go new file mode 100644 index 0000000..b9f3368 --- /dev/null +++ b/auth/oauth/pkce/pkce.go @@ -0,0 +1,47 @@ +package pkce + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "fmt" + "io" + + "golang.org/x/oauth2" +) + +// Generate generates a new random PKCE code. +func Generate() (Code, error) { return generate(rand.Reader) } + +func generate(rand io.Reader) (Code, error) { + // From https://tools.ietf.org/html/rfc7636#section-4.1: + // code_verifier = high-entropy cryptographic random STRING using the + // unreserved characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~" + // from Section 2.3 of [RFC3986], with a minimum length of 43 characters + // and a maximum length of 128 characters. + var buf [32]byte + if _, err := io.ReadFull(rand, buf[:]); err != nil { + return "", fmt.Errorf("could not generate PKCE code: %w", err) + } + return Code(hex.EncodeToString(buf[:])), nil +} + +// Code implements the basic options required for RFC 7636: Proof Key for Code Exchange (PKCE). +type Code string + +// Challenge returns the OAuth2 auth code parameter for sending the PKCE code challenge. +func (p *Code) Challenge() oauth2.AuthCodeOption { + b := sha256.Sum256([]byte(*p)) + return oauth2.SetAuthURLParam("code_challenge", base64.RawURLEncoding.EncodeToString(b[:])) +} + +// Method returns the OAuth2 auth code parameter for sending the PKCE code challenge method. +func (p *Code) Method() oauth2.AuthCodeOption { + return oauth2.SetAuthURLParam("code_challenge_method", "S256") +} + +// Verifier returns the OAuth2 auth code parameter for sending the PKCE code verifier. +func (p *Code) Verifier() oauth2.AuthCodeOption { + return oauth2.SetAuthURLParam("code_verifier", string(*p)) +} diff --git a/auth/oauth/u2m/authenticator.go b/auth/oauth/u2m/authenticator.go new file mode 100644 index 0000000..29a973c --- /dev/null +++ b/auth/oauth/u2m/authenticator.go @@ -0,0 +1,268 @@ +package u2m + +import ( + "context" + "crypto/rand" + "encoding/base64" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "os" + "os/signal" + "strings" + "sync" + "time" + + "github.com/pkg/browser" + + "github.com/databricks/databricks-sql-go/auth" + "github.com/databricks/databricks-sql-go/auth/oauth" + "github.com/rs/zerolog/log" + "golang.org/x/oauth2" +) + +const ( + azureClientId = "96eecda7-19ea-49cc-abb5-240097d554f5" + azureRedirctURL = "localhost:8030" + + awsClientId = "databricks-sql-connector" + awsRedirctURL = "localhost:8030" +) + +func NewAuthenticator(hostName string, timeout time.Duration) (auth.Authenticator, error) { + + cloud := oauth.InferCloudFromHost(hostName) + + var clientID, redirectURL string + if cloud == oauth.AWS { + clientID = awsClientId + redirectURL = awsRedirctURL + } else if cloud == oauth.Azure { + clientID = azureClientId + redirectURL = azureRedirctURL + } else { + return nil, errors.New("unhandled cloud type: " + cloud.String()) + } + + // Get an oauth2 config + config, err := GetConfig(context.Background(), hostName, clientID, "", redirectURL, nil) + if err != nil { + return nil, fmt.Errorf("unable to generate oauth2.Config: %w", err) + } + + tsp, err := GetTokenSourceProvider(context.Background(), config, timeout) + + return &u2mAuthenticator{ + clientID: clientID, + hostName: hostName, + tsp: tsp, + }, err +} + +type u2mAuthenticator struct { + clientID string + hostName string + // scopes []string + tokenSource oauth2.TokenSource + tsp *tokenSourceProvider + mx sync.Mutex +} + +// Auth will start the OAuth Authorization Flow to authenticate the cli client +// using the users credentials in the browser. Compatible with SSO. +func (c *u2mAuthenticator) Authenticate(r *http.Request) error { + c.mx.Lock() + defer c.mx.Unlock() + if c.tokenSource != nil { + token, err := c.tokenSource.Token() + if err == nil { + token.SetAuthHeader(r) + return nil + } else if !strings.Contains(err.Error(), "invalid_grant") { + return err + } + + token.SetAuthHeader(r) + return nil + } + + tokenSource, err := c.tsp.GetTokenSource() + if err != nil { + return fmt.Errorf("unable to get token source: %w", err) + } + c.tokenSource = tokenSource + + token, err := tokenSource.Token() + if err != nil { + return fmt.Errorf("unable to get token source: %w", err) + } + + token.SetAuthHeader(r) + + return nil +} + +type authResponse struct { + err string + details string + state string + code string +} + +type tokenSourceProvider struct { + timeout time.Duration + state string + sigintCh chan os.Signal + authDoneCh chan authResponse + redirectURL *url.URL + config oauth2.Config +} + +func (tsp *tokenSourceProvider) GetTokenSource() (oauth2.TokenSource, error) { + state, err := randString(16) + if err != nil { + err = fmt.Errorf("unable to generate random number: %w", err) + return nil, err + } + + challenge, challengeMethod, verifier, err := GetAuthCodeOptions() + if err != nil { + return nil, err + } + + loginURL := tsp.config.AuthCodeURL(state, challenge, challengeMethod) + tsp.state = state + + log.Info().Msgf("listening on %s://%s/", tsp.redirectURL.Scheme, tsp.redirectURL.Host) + listener, err := net.Listen("tcp", tsp.redirectURL.Host) + if err != nil { + return nil, err + } + defer listener.Close() + + srv := &http.Server{ + ReadHeaderTimeout: 3 * time.Second, + WriteTimeout: 30 * time.Second, + } + + defer srv.Close() + + // Start local server to wait for callback + go func() { + err := srv.Serve(listener) + + // in case port is in use + if err != nil && err != http.ErrServerClosed { + tsp.authDoneCh <- authResponse{err: err.Error()} + } + }() + + fmt.Printf("\nOpen URL in Browser to Continue: %s\n\n", loginURL) + err = browser.OpenURL(loginURL) + if err != nil { + fmt.Println("Unable to open browser automatically. Please open manually: ", loginURL) + } + + ctx := context.Background() + // Wait for callback to be received, Wait for either the callback to finish, SIGINT to be received or up to 2 minutes + select { + case authResponse := <-tsp.authDoneCh: + if authResponse.err != "" { + return nil, fmt.Errorf("identity provider error: %s: %s", authResponse.err, authResponse.details) + } + token, err := tsp.config.Exchange(ctx, authResponse.code, verifier) + if err != nil { + return nil, fmt.Errorf("failed to exchange token: %w", err) + } + + return tsp.config.TokenSource(ctx, token), nil + + case <-tsp.sigintCh: + return nil, errors.New("interrupted while waiting for auth callback") + + case <-time.After(tsp.timeout): + return nil, errors.New("timed out waiting for response from provider") + } +} + +func (tsp *tokenSourceProvider) ServeHTTP(w http.ResponseWriter, r *http.Request) { + resp := authResponse{ + err: r.URL.Query().Get("error"), + details: r.URL.Query().Get("error_description"), + state: r.URL.Query().Get("state"), + code: r.URL.Query().Get("code"), + } + + // Send the response back to the to cli + defer func() { tsp.authDoneCh <- resp }() + + // Do some checking of the response here to show more relevant content + if resp.err != "" { + log.Error().Msg(resp.err) + w.WriteHeader(http.StatusBadRequest) + _, err := w.Write([]byte(errorHTML("Identity Provider returned an error: " + resp.err))) + if err != nil { + log.Error().Err(err).Msg("unable to write error response") + } + return + } + if resp.state != tsp.state && r.URL.String() != "/favicon.ico" { + msg := "Authentication state received did not match original request. Please try to login again." + log.Error().Msg(msg) + w.WriteHeader(http.StatusBadRequest) + _, err := w.Write([]byte(errorHTML(msg))) + if err != nil { + log.Error().Err(err).Msg("unable to write error response") + } + return + } + + _, err := w.Write([]byte(infoHTML("CLI Login Success", "You may close this window anytime now and go back to terminal"))) + if err != nil { + log.Error().Err(err).Msg("unable to write success response") + } +} + +var register sync.Once = sync.Once{} + +func GetTokenSourceProvider(ctx context.Context, config oauth2.Config, timeout time.Duration) (*tokenSourceProvider, error) { + if timeout == 0 { + timeout = 2 * time.Minute + } + + // handle ctrl-c while waiting for the callback + sigintCh := make(chan os.Signal, 1) + signal.Notify(sigintCh, os.Interrupt) + + // receive auth callback response + authDoneCh := make(chan authResponse) + + u, _ := url.Parse(config.RedirectURL) + if u.Path == "" { + u.Path = "/" + } + + tsp := &tokenSourceProvider{ + timeout: timeout, + sigintCh: sigintCh, + authDoneCh: authDoneCh, + redirectURL: u, + config: config, + } + + f := func() { http.Handle(u.Path, tsp) } + register.Do(f) + + return tsp, nil +} + +func randString(nByte int) (string, error) { + b := make([]byte, nByte) + if _, err := io.ReadFull(rand.Reader, b); err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(b), nil +} diff --git a/auth/oauth/u2m/html_template.go b/auth/oauth/u2m/html_template.go new file mode 100644 index 0000000..69107b3 --- /dev/null +++ b/auth/oauth/u2m/html_template.go @@ -0,0 +1,56 @@ +package u2m + +import ( + "bytes" + _ "embed" + "html/template" +) + +type SimplePage struct { + Title string + Heading string + Content string + Action ActionLink + Code string +} + +type ActionLink struct { + Label string + Link string +} + +var ( + //go:embed templates/simple.html + simpleHtmlPage string +) + +func renderHTML(data SimplePage) (string, error) { + var out bytes.Buffer + tmpl, err := template.New("name").Parse(simpleHtmlPage) + if err != nil { + return "", err + } + err = tmpl.Execute(&out, data) + return out.String(), err +} + +func infoHTML(title, content string) string { + data := SimplePage{ + Title: "Authentication Success", + Heading: title, + Content: content, + } + out, _ := renderHTML(data) + return out +} + +func errorHTML(msg string) string { + data := SimplePage{ + Title: "Authentication Error", + Heading: "Ooops!", + Content: "Sorry, Databricks could not authenticate to your account due to some server errors. Please try it later.", + Code: msg, + } + out, _ := renderHTML(data) + return out +} diff --git a/auth/oauth/u2m/templates/simple.html b/auth/oauth/u2m/templates/simple.html new file mode 100644 index 0000000..71b0336 --- /dev/null +++ b/auth/oauth/u2m/templates/simple.html @@ -0,0 +1,100 @@ + + + + + + {{ .Title }} + + + + + + + +
+
+ +
{{ .Heading }}
+
{{ .Content }}
+ + {{ .Action.Label }} + + + {{ .Code }} + +
+
+ + + \ No newline at end of file diff --git a/auth/oauth/u2m/u2m.go b/auth/oauth/u2m/u2m.go new file mode 100644 index 0000000..456e369 --- /dev/null +++ b/auth/oauth/u2m/u2m.go @@ -0,0 +1,51 @@ +package u2m + +import ( + "context" + "fmt" + "strings" + + "github.com/databricks/databricks-sql-go/auth/oauth" + "github.com/databricks/databricks-sql-go/auth/oauth/pkce" + "golang.org/x/oauth2" +) + +func GetConfig(ctx context.Context, hostName, clientID, clientSecret, callbackURL string, scopes []string) (oauth2.Config, error) { + // Add necessary scopes for AWS or Azure + scopes = oauth.GetScopes(hostName, scopes) + + // Get the endpoint based on the host name + endpoint, err := oauth.GetEndpoint(ctx, hostName) + if err != nil { + return oauth2.Config{}, fmt.Errorf("could not lookup provider details: %w", err) + } + + if !strings.HasPrefix(callbackURL, "http") { + callbackURL = fmt.Sprintf("http://%s", callbackURL) + } + + config := oauth2.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + Endpoint: endpoint, + RedirectURL: callbackURL, + Scopes: scopes, + } + + return config, nil +} + +func GetAuthCodeOptions() (challenge, challengeMethod, verifier oauth2.AuthCodeOption, err error) { + code, err := pkce.Generate() + if err != nil { + return + } + + return code.Challenge(), code.Method(), code.Verifier(), err +} + +func GetLoginURL(config oauth2.Config, state string, options ...oauth2.AuthCodeOption) string { + loginURL := config.AuthCodeURL(state, options...) + + return loginURL +} diff --git a/connector.go b/connector.go index 03a3bec..96a8831 100644 --- a/connector.go +++ b/connector.go @@ -9,6 +9,7 @@ import ( "time" "github.com/databricks/databricks-sql-go/auth" + "github.com/databricks/databricks-sql-go/auth/oauth/m2m" "github.com/databricks/databricks-sql-go/auth/pat" "github.com/databricks/databricks-sql-go/driverctx" dbsqlerr "github.com/databricks/databricks-sql-go/errors" @@ -259,3 +260,13 @@ func WithMaxDownloadThreads(numThreads int) connOption { c.MaxDownloadThreads = numThreads } } + +// Setup of Oauth M2m authentication +func WithClientCredentials(clientID, clientSecret string) connOption { + return func(c *config.Config) { + if clientID != "" && clientSecret != "" { + authr := m2m.NewAuthenticator(clientID, clientSecret, c.Host) + c.Authenticator = authr + } + } +} diff --git a/doc.go b/doc.go index 3fb432b..cdbcb13 100644 --- a/doc.go +++ b/doc.go @@ -39,6 +39,10 @@ Supported optional connection parameters can be specified in param=value and inc - userAgentEntry: Used to identify partners. Set as a string with format - useCloudFetch: Used to enable cloud fetch for the query execution. Default is false - maxDownloadThreads: Sets up the max number of concurrent workers for cloud fetch. Default is 10 + - authType: Specifies the desired authentication type. Valid values are: Pat, OauthM2M, OauthU2M + - accessToken: Personal access token. Required if authType set to Pat + - clientID: Specifies the client ID to use with OauthM2M + - clientSecret: Specifies the client secret to use with OauthM2M Supported optional session parameters can be specified in param=value and include: @@ -83,6 +87,8 @@ Supported functional options include: - WithUserAgentEntry( string). Used to identify partners. Optional - WithCloudFetch (bool). Used to enable cloud fetch for the query execution. Default is false. Optional - WithMaxDownloadThreads ( int). Sets up the max number of concurrent workers for cloud fetch. Default is 10. Optional + - WithAuthenticator ( auth.Authenticator). Sets up authentication. Required if neither access token or client credentials are provided. + - WithClientCredentials( string, string). Sets up Oauth M2M authentication. # Query cancellation and timeout diff --git a/errors/errors.go b/errors/errors.go index 5b4339b..a551853 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -22,12 +22,12 @@ const ( ErrInvalidURL = "invalid URL" ErrNoAuthenticationMethod = "no authentication method set" + ErrNoDefaultAuthenticator = "unable to create default authenticator" ErrInvalidDSNFormat = "invalid DSN: invalid format" ErrInvalidDSNPort = "invalid DSN: invalid DSN port" ErrInvalidDSNPATIsEmpty = "invalid DSN: empty token" ErrBasicAuthNotSupported = "invalid DSN: basic auth not enabled" - ErrInvalidDSNMaxRows = "invalid DSN: maxRows param is not an integer" - ErrInvalidDSNTimeout = "invalid DSN: timeout param is not an integer" + ErrInvalidDSNM2m = "invalid DSN: clientId and clientSecret params required" // Execution error messages (query failure) ErrQueryExecution = "failed to execute query" diff --git a/examples/oauth/main.go b/examples/oauth/main.go new file mode 100644 index 0000000..7393fbd --- /dev/null +++ b/examples/oauth/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "context" + "database/sql" + "fmt" + "log" + "os" + "time" + + dbsql "github.com/databricks/databricks-sql-go" + "github.com/databricks/databricks-sql-go/auth/oauth/u2m" + "github.com/joho/godotenv" +) + +func main() { + err := godotenv.Load() + + if err != nil { + log.Fatal(err.Error()) + } + + authenticator, err := u2m.NewAuthenticator(os.Getenv("DATABRICKS_HOST"), 1*time.Minute) + + if err != nil { + log.Fatal(err.Error()) + } + + connector, err := dbsql.NewConnector( + dbsql.WithServerHostname(os.Getenv("DATABRICKS_HOST")), + dbsql.WithHTTPPath(os.Getenv("DATABRICKS_HTTPPATH")), + dbsql.WithAuthenticator(authenticator), + ) + if err != nil { + log.Fatal(err) + } + + db := sql.OpenDB(connector) + defer db.Close() + + // Pinging should require logging in + if err := db.Ping(); err != nil { + fmt.Println(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) + defer cancel() + + var res int + + // Running query should not require logging in as we should have a token + // from when ping was called. + err1 := db.QueryRowContext(ctx, `select 1`).Scan(&res) + + if err1 != nil { + if err1 == sql.ErrNoRows { + fmt.Println("not found") + return + } else { + fmt.Printf("err: %v\n", err1) + } + } + fmt.Println(res) +} diff --git a/go.mod b/go.mod index d5a232d..e3862a8 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,12 @@ go 1.19 require ( github.com/apache/arrow/go/v12 v12.0.1 github.com/apache/thrift v0.17.0 + github.com/coreos/go-oidc/v3 v3.5.0 github.com/joho/godotenv v1.4.0 github.com/mattn/go-isatty v0.0.19 + github.com/pierrec/lz4/v4 v4.1.15 github.com/stretchr/testify v1.8.1 + golang.org/x/oauth2 v0.7.0 gotest.tools/gotestsum v1.8.2 ) @@ -17,7 +20,9 @@ require ( github.com/dnephin/pflag v1.0.7 // indirect github.com/fatih/color v1.13.0 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/goccy/go-json v0.9.11 // indirect + github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/flatbuffers v2.0.8+incompatible // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect @@ -29,20 +34,24 @@ require ( github.com/mattn/go-colorable v0.1.12 // indirect github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 // indirect - github.com/pierrec/lz4/v4 v4.1.15 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect + golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect golang.org/x/mod v0.8.0 // indirect + golang.org/x/net v0.9.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect + golang.org/x/term v0.7.0 // indirect golang.org/x/tools v0.6.0 // indirect golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.28.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( github.com/hashicorp/go-retryablehttp v0.7.1 + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.28.0 - golang.org/x/sys v0.6.0 // indirect + golang.org/x/sys v0.7.0 // indirect ) diff --git a/go.sum b/go.sum index 677161e..568bb27 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= @@ -5,6 +6,8 @@ github.com/apache/arrow/go/v12 v12.0.1 h1:JsR2+hzYYjgSUkBSaahpqCetqZMr76djX80fF/ github.com/apache/arrow/go/v12 v12.0.1/go.mod h1:weuTY7JvTG/HDPtMQxEUp7pU73vkLWMLpY67QwZ/WWw= github.com/apache/thrift v0.17.0 h1:cMd2aj52n+8VoAtvSvLn4kDC3aZ6IAkBuqWQ2IDu7wo= github.com/apache/thrift v0.17.0/go.mod h1:OLxhMRJxomX+1I/KUw03qoV3mMz16BwaKI+d4fPBx7Q= +github.com/coreos/go-oidc/v3 v3.5.0 h1:VxKtbccHZxs8juq7RdJntSqtXFtde9YpNpGn0yqgEHw= +github.com/coreos/go-oidc/v3 v3.5.0/go.mod h1:ecXRtV4romGPeO6ieExAsUK9cb/3fp9hXNz1tlv8PIM= github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -16,13 +19,20 @@ github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= +github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/flatbuffers v2.0.8+incompatible h1:ivUb1cGomAB101ZM1T0nOiWz9pSrTMoa9+EiY7igmkM= github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -43,7 +53,10 @@ github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQan github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -59,6 +72,8 @@ github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8D github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -72,33 +87,49 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc= +golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 h1:tnebWN09GYg9OLPss1KXj8txwZc6X6uMr6VFdcGNbHw= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk= +golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= +golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -110,24 +141,35 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -137,7 +179,15 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0= golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/client/client.go b/internal/client/client.go index 274a89d..2037240 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -253,7 +253,7 @@ func InitThriftClient(cfg *config.Config, httpclient *http.Client) (*ThriftServi case "http": if httpclient == nil { if cfg.Authenticator == nil { - return nil, dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.ErrNoAuthenticationMethod, nil) + return nil, dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.ErrNoDefaultAuthenticator, nil) } httpclient = RetryableClient(cfg) } diff --git a/internal/config/config.go b/internal/config/config.go index 3f4607e..4d2835d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -15,6 +15,8 @@ import ( "github.com/databricks/databricks-sql-go/auth" "github.com/databricks/databricks-sql-go/auth/noop" + "github.com/databricks/databricks-sql-go/auth/oauth/m2m" + "github.com/databricks/databricks-sql-go/auth/oauth/u2m" "github.com/databricks/databricks-sql-go/auth/pat" "github.com/databricks/databricks-sql-go/internal/cli_service" dbsqlerrint "github.com/databricks/databricks-sql-go/internal/errors" @@ -218,87 +220,72 @@ func ParseDSN(dsn string) (UserConfig, error) { return UserConfig{}, dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.ErrInvalidDSNPort, err) } ucfg.Port = port - name := parsedURL.User.Username() - if name == "token" { - pass, ok := parsedURL.User.Password() - if pass == "" { - return UserConfig{}, dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.ErrInvalidDSNPATIsEmpty, err) - } - if ok { - ucfg.AccessToken = pass - pat := &pat.PATAuth{ - AccessToken: pass, - } - ucfg.Authenticator = pat - } - } else { - if name != "" { - return UserConfig{}, dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.ErrBasicAuthNotSupported, err) - } - } + ucfg.HTTPPath = parsedURL.Path - params := parsedURL.Query() - maxRowsStr := params.Get("maxRows") - if maxRowsStr != "" { - maxRows, err := strconv.Atoi(maxRowsStr) + + // Any params that are not specifically handled are assumed to be session params. + // Use extractableParams so that the processed values are deleted as we go. + params := &extractableParams{Values: parsedURL.Query()} + + // Create an authenticator based on the url and params + err = makeAuthenticator(parsedURL, params, &ucfg) + if err != nil { + return UserConfig{}, err + } + + if maxRows, ok, err := params.extractAsInt("maxRows"); ok { if err != nil { - return UserConfig{}, dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.ErrInvalidDSNMaxRows, err) + return UserConfig{}, err } - // we should always have at least some page size - if maxRows != 0 { + if maxRows > 0 { ucfg.MaxRows = maxRows } } - params.Del("maxRows") - timeoutStr := params.Get("timeout") - if timeoutStr != "" { - timeoutSeconds, err := strconv.Atoi(timeoutStr) + if timeoutSeconds, ok, err := params.extractAsInt("timeout"); ok { if err != nil { - return UserConfig{}, dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.ErrInvalidDSNTimeout, err) + return UserConfig{}, err } ucfg.QueryTimeout = time.Duration(timeoutSeconds) * time.Second } - params.Del("timeout") - if params.Has("catalog") { - ucfg.Catalog = params.Get("catalog") - params.Del("catalog") + + if catalog, ok := params.extract("catalog"); ok { + ucfg.Catalog = catalog } - if params.Has("userAgentEntry") { - ucfg.UserAgentEntry = params.Get("userAgentEntry") + if userAgent, ok := params.extract("userAgentEntry"); ok { + ucfg.UserAgentEntry = userAgent params.Del("userAgentEntry") } - if params.Has("schema") { - ucfg.Schema = params.Get("schema") - params.Del("schema") + if schema, ok := params.extract("schema"); ok { + ucfg.Schema = schema } // Cloud Fetch parameters - if params.Has("useCloudFetch") { - useCloudFetch, err := strconv.ParseBool(params.Get("useCloudFetch")) + if useCloudFetch, ok, err := params.extractAsBool("useCloudFetch"); ok { if err != nil { - return UserConfig{}, dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.InvalidDSNFormat("useCloudFetch", params.Get("useCloudFetch"), "bool"), err) + return UserConfig{}, err } ucfg.UseCloudFetch = useCloudFetch } - params.Del("useCloudFetch") - if params.Has("maxDownloadThreads") { - numThreads, err := strconv.Atoi(params.Get("maxDownloadThreads")) + + if numThreads, ok, err := params.extractAsInt("maxDownloadThreads"); ok { if err != nil { - return UserConfig{}, dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.InvalidDSNFormat("maxDownloadThreads", params.Get("maxDownloadThreads"), "int"), err) + return UserConfig{}, err } ucfg.MaxDownloadThreads = numThreads } - params.Del("maxDownloadThreads") - for k := range params { - if strings.ToLower(k) == "timezone" { - ucfg.Location, err = time.LoadLocation(params.Get("timezone")) - } + // for timezone we do a case insensitive key match. + // We use getNoCase because we want to leave timezone in the params so that it will also + // be used as a session param. + if timezone, ok := params.getNoCase("timezone"); ok { + ucfg.Location, err = time.LoadLocation(timezone) } - if len(params) > 0 { + + // any left over params are treated as session params + if len(params.Values) > 0 { sessionParams := make(map[string]string) - for k := range params { + for k := range params.Values { sessionParams[k] = params.Get(k) } ucfg.SessionParams = sessionParams @@ -307,6 +294,149 @@ func ParseDSN(dsn string) (UserConfig, error) { return ucfg, err } +// update the config with an authenticator based on the value from the parsed DSN +func makeAuthenticator(parsedURL *url.URL, params *extractableParams, config *UserConfig) error { + name := parsedURL.User.Username() + // if the user name is set to 'token' we will interpret the password as an acess token + if name == "token" { + pass, _ := parsedURL.User.Password() + return addPatAuthenticator(pass, config) + } else if name != "" { + // Currently don't support user name/password authentication + return dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.ErrBasicAuthNotSupported, nil) + } else { + // Process parameters that specify the authentication type. They are removed from params + // Get the optional authentication type param + authTypeS, _ := params.extract("authType") + authType := auth.ParseAuthType(authTypeS) + + // Get optional parameters for creating an authenticator + clientId, hasClientId := params.extract("clientId") + if !hasClientId { + clientId, hasClientId = params.extract("clientID") + } + + clientSecret, hasClientSecret := params.extract("clientSecret") + accessToken, hasAccessToken := params.extract("accessToken") + + switch authType { + case auth.AuthTypeUnknown: + // if no authentication type is specified create an authenticator based on which + // params have values + if hasAccessToken { + return addPatAuthenticator(accessToken, config) + } + + if hasClientId || hasClientSecret { + return addOauthM2MAuthenticator(clientId, clientSecret, config) + } + case auth.AuthTypePat: + return addPatAuthenticator(accessToken, config) + case auth.AuthTypeOauthM2M: + return addOauthM2MAuthenticator(clientId, clientSecret, config) + case auth.AuthTypeOauthU2M: + return addOauthU2MAuthenticator(config) + } + + } + + return nil +} + +func addPatAuthenticator(accessToken string, config *UserConfig) error { + if accessToken == "" { + return dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.ErrInvalidDSNPATIsEmpty, nil) + } + config.AccessToken = accessToken + pat := &pat.PATAuth{ + AccessToken: accessToken, + } + config.Authenticator = pat + return nil +} + +func addOauthM2MAuthenticator(clientId, clientSecret string, config *UserConfig) error { + if clientId == "" || clientSecret == "" { + return dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.ErrInvalidDSNM2m, nil) + } + + m2m := m2m.NewAuthenticator(clientId, clientSecret, config.Host) + config.Authenticator = m2m + return nil +} + +func addOauthU2MAuthenticator(config *UserConfig) error { + u2m, err := u2m.NewAuthenticator(config.Host, 0) + if err == nil { + config.Authenticator = u2m + } + return err +} + +type extractableParams struct { + url.Values +} + +// returns the value corresponding to the key, if any, and a bool flag indicating if +// there was a set value and it is not the empty string +// deletes the key/value from params +func (params *extractableParams) extract(key string) (string, bool) { + return extractParam(key, params, false, true) +} + +func (params *extractableParams) extractAsInt(key string) (int, bool, error) { + if intString, ok := extractParam(key, params, false, true); ok { + i, err := strconv.Atoi(intString) + if err != nil { + return 0, true, dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.InvalidDSNFormat(key, intString, "int"), err) + } + + return i, true, nil + } + + return 0, false, nil +} + +func (params *extractableParams) extractAsBool(key string) (bool, bool, error) { + if boolString, ok := extractParam(key, params, false, true); ok { + b, err := strconv.ParseBool(boolString) + if err != nil { + return false, true, dbsqlerrint.NewRequestError(context.TODO(), dbsqlerr.InvalidDSNFormat(key, boolString, "bool"), err) + } + + return b, true, nil + } + return false, false, nil +} + +// returns the value corresponding to the key using case insensitive key matching and a bool flag +// indicating if the value was set and is not the empty string +func (params *extractableParams) getNoCase(key string) (string, bool) { + return extractParam(key, params, true, false) +} + +func extractParam(key string, params *extractableParams, ignoreCase bool, delValue bool) (string, bool) { + if ignoreCase { + key = strings.ToLower(key) + } + + for k := range params.Values { + kc := k + if ignoreCase { + kc = strings.ToLower(k) + } + if kc == key { + val := params.Get(k) + if delValue { + params.Del(k) + } + return val, val != "" + } + } + + return "", false +} + type ArrowConfig struct { UseArrowBatches bool UseArrowNativeDecimal bool diff --git a/internal/config/config_test.go b/internal/config/config_test.go index d891c36..df5e4bd 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -2,11 +2,13 @@ package config import ( "crypto/tls" + "fmt" "reflect" "testing" "time" "github.com/databricks/databricks-sql-go/auth/noop" + "github.com/databricks/databricks-sql-go/auth/oauth/m2m" "github.com/databricks/databricks-sql-go/auth/pat" "github.com/databricks/databricks-sql-go/internal/cli_service" ) @@ -15,6 +17,9 @@ func TestParseConfig(t *testing.T) { type args struct { dsn string } + + defCloudConfig := CloudFetchConfig{}.WithDefaults() + tz, _ := time.LoadLocation("America/Vancouver") tests := []struct { name string @@ -28,23 +33,18 @@ func TestParseConfig(t *testing.T) { name: "base case", args: args{dsn: "token:supersecret@example.cloud.databricks.com:443/sql/1.0/endpoints/12346a5b5b0e123a"}, wantCfg: UserConfig{ - Protocol: "https", - Host: "example.cloud.databricks.com", - Port: 443, - MaxRows: defaultMaxRows, - Authenticator: &pat.PATAuth{AccessToken: "supersecret"}, - AccessToken: "supersecret", - HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a", - SessionParams: make(map[string]string), - RetryMax: 4, - RetryWaitMin: 1 * time.Second, - RetryWaitMax: 30 * time.Second, - CloudFetchConfig: CloudFetchConfig{ - UseCloudFetch: false, - MaxDownloadThreads: 10, - MaxFilesInMemory: 10, - MinTimeToExpiry: 0 * time.Second, - }, + Protocol: "https", + Host: "example.cloud.databricks.com", + Port: 443, + MaxRows: defaultMaxRows, + Authenticator: &pat.PATAuth{AccessToken: "supersecret"}, + AccessToken: "supersecret", + HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a", + SessionParams: make(map[string]string), + RetryMax: 4, + RetryWaitMin: 1 * time.Second, + RetryWaitMax: 30 * time.Second, + CloudFetchConfig: defCloudConfig, }, wantURL: "https://example.cloud.databricks.com:443/sql/1.0/endpoints/12346a5b5b0e123a", wantErr: false, @@ -53,23 +53,18 @@ func TestParseConfig(t *testing.T) { name: "with https scheme", args: args{dsn: "https://token:supersecret@example.cloud.databricks.com:443/sql/1.0/endpoints/12346a5b5b0e123a"}, wantCfg: UserConfig{ - Protocol: "https", - Host: "example.cloud.databricks.com", - Port: 443, - MaxRows: defaultMaxRows, - AccessToken: "supersecret", - Authenticator: &pat.PATAuth{AccessToken: "supersecret"}, - HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a", - SessionParams: make(map[string]string), - RetryMax: 4, - RetryWaitMin: 1 * time.Second, - RetryWaitMax: 30 * time.Second, - CloudFetchConfig: CloudFetchConfig{ - UseCloudFetch: false, - MaxDownloadThreads: 10, - MaxFilesInMemory: 10, - MinTimeToExpiry: 0 * time.Second, - }, + Protocol: "https", + Host: "example.cloud.databricks.com", + Port: 443, + MaxRows: defaultMaxRows, + AccessToken: "supersecret", + Authenticator: &pat.PATAuth{AccessToken: "supersecret"}, + HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a", + SessionParams: make(map[string]string), + RetryMax: 4, + RetryWaitMin: 1 * time.Second, + RetryWaitMax: 30 * time.Second, + CloudFetchConfig: defCloudConfig, }, wantURL: "https://example.cloud.databricks.com:443/sql/1.0/endpoints/12346a5b5b0e123a", wantErr: false, @@ -78,22 +73,17 @@ func TestParseConfig(t *testing.T) { name: "with http scheme", args: args{dsn: "http://localhost:8080/sql/1.0/endpoints/12346a5b5b0e123a"}, wantCfg: UserConfig{ - Protocol: "http", - Host: "localhost", - Port: 8080, - MaxRows: defaultMaxRows, - Authenticator: &noop.NoopAuth{}, - HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a", - SessionParams: make(map[string]string), - RetryMax: 4, - RetryWaitMin: 1 * time.Second, - RetryWaitMax: 30 * time.Second, - CloudFetchConfig: CloudFetchConfig{ - UseCloudFetch: false, - MaxDownloadThreads: 10, - MaxFilesInMemory: 10, - MinTimeToExpiry: 0 * time.Second, - }, + Protocol: "http", + Host: "localhost", + Port: 8080, + MaxRows: defaultMaxRows, + Authenticator: &noop.NoopAuth{}, + HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a", + SessionParams: make(map[string]string), + RetryMax: 4, + RetryWaitMin: 1 * time.Second, + RetryWaitMax: 30 * time.Second, + CloudFetchConfig: defCloudConfig, }, wantErr: false, wantURL: "http://localhost:8080/sql/1.0/endpoints/12346a5b5b0e123a", @@ -102,21 +92,16 @@ func TestParseConfig(t *testing.T) { name: "with localhost", args: args{dsn: "http://localhost:8080"}, wantCfg: UserConfig{ - Protocol: "http", - Host: "localhost", - Port: 8080, - Authenticator: &noop.NoopAuth{}, - MaxRows: defaultMaxRows, - SessionParams: make(map[string]string), - RetryMax: 4, - RetryWaitMin: 1 * time.Second, - RetryWaitMax: 30 * time.Second, - CloudFetchConfig: CloudFetchConfig{ - UseCloudFetch: false, - MaxDownloadThreads: 10, - MaxFilesInMemory: 10, - MinTimeToExpiry: 0 * time.Second, - }, + Protocol: "http", + Host: "localhost", + Port: 8080, + Authenticator: &noop.NoopAuth{}, + MaxRows: defaultMaxRows, + SessionParams: make(map[string]string), + RetryMax: 4, + RetryWaitMin: 1 * time.Second, + RetryWaitMax: 30 * time.Second, + CloudFetchConfig: defCloudConfig, }, wantErr: false, wantURL: "http://localhost:8080", @@ -125,24 +110,19 @@ func TestParseConfig(t *testing.T) { name: "with query params", args: args{dsn: "token:supersecret@example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?timeout=100&maxRows=1000"}, wantCfg: UserConfig{ - Protocol: "https", - Host: "example.cloud.databricks.com", - Port: 8000, - AccessToken: "supersecret", - Authenticator: &pat.PATAuth{AccessToken: "supersecret"}, - HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a", - QueryTimeout: 100 * time.Second, - MaxRows: 1000, - SessionParams: make(map[string]string), - RetryMax: 4, - RetryWaitMin: 1 * time.Second, - RetryWaitMax: 30 * time.Second, - CloudFetchConfig: CloudFetchConfig{ - UseCloudFetch: false, - MaxDownloadThreads: 10, - MaxFilesInMemory: 10, - MinTimeToExpiry: 0 * time.Second, - }, + Protocol: "https", + Host: "example.cloud.databricks.com", + Port: 8000, + AccessToken: "supersecret", + Authenticator: &pat.PATAuth{AccessToken: "supersecret"}, + HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a", + QueryTimeout: 100 * time.Second, + MaxRows: 1000, + SessionParams: make(map[string]string), + RetryMax: 4, + RetryWaitMin: 1 * time.Second, + RetryWaitMax: 30 * time.Second, + CloudFetchConfig: defCloudConfig, }, wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a", wantErr: false, @@ -151,25 +131,20 @@ func TestParseConfig(t *testing.T) { name: "with query params and session params", args: args{dsn: "token:supersecret@example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?timeout=100&maxRows=1000&timezone=America/Vancouver"}, wantCfg: UserConfig{ - Protocol: "https", - Host: "example.cloud.databricks.com", - Port: 8000, - AccessToken: "supersecret", - Authenticator: &pat.PATAuth{AccessToken: "supersecret"}, - HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a", - QueryTimeout: 100 * time.Second, - MaxRows: 1000, - Location: tz, - SessionParams: map[string]string{"timezone": "America/Vancouver"}, - RetryMax: 4, - RetryWaitMin: 1 * time.Second, - RetryWaitMax: 30 * time.Second, - CloudFetchConfig: CloudFetchConfig{ - UseCloudFetch: false, - MaxDownloadThreads: 10, - MaxFilesInMemory: 10, - MinTimeToExpiry: 0 * time.Second, - }, + Protocol: "https", + Host: "example.cloud.databricks.com", + Port: 8000, + AccessToken: "supersecret", + Authenticator: &pat.PATAuth{AccessToken: "supersecret"}, + HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a", + QueryTimeout: 100 * time.Second, + MaxRows: 1000, + Location: tz, + SessionParams: map[string]string{"timezone": "America/Vancouver"}, + RetryMax: 4, + RetryWaitMin: 1 * time.Second, + RetryWaitMax: 30 * time.Second, + CloudFetchConfig: defCloudConfig, }, wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a", wantErr: false, @@ -178,22 +153,17 @@ func TestParseConfig(t *testing.T) { name: "bare", args: args{dsn: "example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a"}, wantCfg: UserConfig{ - Protocol: "https", - Host: "example.cloud.databricks.com", - Authenticator: &noop.NoopAuth{}, - Port: 8000, - MaxRows: defaultMaxRows, - HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a", - SessionParams: make(map[string]string), - RetryMax: 4, - RetryWaitMin: 1 * time.Second, - RetryWaitMax: 30 * time.Second, - CloudFetchConfig: CloudFetchConfig{ - UseCloudFetch: false, - MaxDownloadThreads: 10, - MaxFilesInMemory: 10, - MinTimeToExpiry: 0 * time.Second, - }, + Protocol: "https", + Host: "example.cloud.databricks.com", + Authenticator: &noop.NoopAuth{}, + Port: 8000, + MaxRows: defaultMaxRows, + HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a", + SessionParams: make(map[string]string), + RetryMax: 4, + RetryWaitMin: 1 * time.Second, + RetryWaitMax: 30 * time.Second, + CloudFetchConfig: defCloudConfig, }, wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a", wantErr: false, @@ -202,24 +172,19 @@ func TestParseConfig(t *testing.T) { name: "with catalog", args: args{dsn: "token:supersecret@example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123b?catalog=default"}, wantCfg: UserConfig{ - Protocol: "https", - Host: "example.cloud.databricks.com", - Port: 8000, - MaxRows: defaultMaxRows, - AccessToken: "supersecret", - Authenticator: &pat.PATAuth{AccessToken: "supersecret"}, - HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123b", - Catalog: "default", - SessionParams: make(map[string]string), - RetryMax: 4, - RetryWaitMin: 1 * time.Second, - RetryWaitMax: 30 * time.Second, - CloudFetchConfig: CloudFetchConfig{ - UseCloudFetch: false, - MaxDownloadThreads: 10, - MaxFilesInMemory: 10, - MinTimeToExpiry: 0 * time.Second, - }, + Protocol: "https", + Host: "example.cloud.databricks.com", + Port: 8000, + MaxRows: defaultMaxRows, + AccessToken: "supersecret", + Authenticator: &pat.PATAuth{AccessToken: "supersecret"}, + HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123b", + Catalog: "default", + SessionParams: make(map[string]string), + RetryMax: 4, + RetryWaitMin: 1 * time.Second, + RetryWaitMax: 30 * time.Second, + CloudFetchConfig: defCloudConfig, }, wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123b", wantErr: false, @@ -228,24 +193,19 @@ func TestParseConfig(t *testing.T) { name: "with user agent entry", args: args{dsn: "token:supersecret@example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123b?userAgentEntry=partner-name"}, wantCfg: UserConfig{ - Protocol: "https", - Host: "example.cloud.databricks.com", - Port: 8000, - MaxRows: defaultMaxRows, - AccessToken: "supersecret", - Authenticator: &pat.PATAuth{AccessToken: "supersecret"}, - HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123b", - UserAgentEntry: "partner-name", - SessionParams: make(map[string]string), - RetryMax: 4, - RetryWaitMin: 1 * time.Second, - RetryWaitMax: 30 * time.Second, - CloudFetchConfig: CloudFetchConfig{ - UseCloudFetch: false, - MaxDownloadThreads: 10, - MaxFilesInMemory: 10, - MinTimeToExpiry: 0 * time.Second, - }, + Protocol: "https", + Host: "example.cloud.databricks.com", + Port: 8000, + MaxRows: defaultMaxRows, + AccessToken: "supersecret", + Authenticator: &pat.PATAuth{AccessToken: "supersecret"}, + HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123b", + UserAgentEntry: "partner-name", + SessionParams: make(map[string]string), + RetryMax: 4, + RetryWaitMin: 1 * time.Second, + RetryWaitMax: 30 * time.Second, + CloudFetchConfig: defCloudConfig, }, wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123b", wantErr: false, @@ -254,24 +214,19 @@ func TestParseConfig(t *testing.T) { name: "with schema", args: args{dsn: "token:supersecret2@example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?schema=system"}, wantCfg: UserConfig{ - Protocol: "https", - Host: "example.cloud.databricks.com", - Port: 8000, - MaxRows: defaultMaxRows, - AccessToken: "supersecret2", - Authenticator: &pat.PATAuth{AccessToken: "supersecret2"}, - HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a", - Schema: "system", - SessionParams: make(map[string]string), - RetryMax: 4, - RetryWaitMin: 1 * time.Second, - RetryWaitMax: 30 * time.Second, - CloudFetchConfig: CloudFetchConfig{ - UseCloudFetch: false, - MaxDownloadThreads: 10, - MaxFilesInMemory: 10, - MinTimeToExpiry: 0 * time.Second, - }, + Protocol: "https", + Host: "example.cloud.databricks.com", + Port: 8000, + MaxRows: defaultMaxRows, + AccessToken: "supersecret2", + Authenticator: &pat.PATAuth{AccessToken: "supersecret2"}, + HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a", + Schema: "system", + SessionParams: make(map[string]string), + RetryMax: 4, + RetryWaitMin: 1 * time.Second, + RetryWaitMax: 30 * time.Second, + CloudFetchConfig: defCloudConfig, }, wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a", wantErr: false, @@ -295,7 +250,6 @@ func TestParseConfig(t *testing.T) { UseCloudFetch: true, MaxDownloadThreads: 10, MaxFilesInMemory: 10, - MinTimeToExpiry: 0 * time.Second, }, }, wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123b", @@ -320,7 +274,6 @@ func TestParseConfig(t *testing.T) { UseCloudFetch: true, MaxDownloadThreads: 15, MaxFilesInMemory: 10, - MinTimeToExpiry: 0 * time.Second, }, }, wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123b", @@ -349,7 +302,6 @@ func TestParseConfig(t *testing.T) { UseCloudFetch: true, MaxDownloadThreads: 15, MaxFilesInMemory: 10, - MinTimeToExpiry: 0 * time.Second, }, }, wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a", @@ -359,22 +311,17 @@ func TestParseConfig(t *testing.T) { name: "missing http path", args: args{dsn: "token:supersecret@example.cloud.databricks.com:443"}, wantCfg: UserConfig{ - Protocol: "https", - Host: "example.cloud.databricks.com", - Port: 443, - MaxRows: defaultMaxRows, - AccessToken: "supersecret", - Authenticator: &pat.PATAuth{AccessToken: "supersecret"}, - SessionParams: make(map[string]string), - RetryMax: 4, - RetryWaitMin: 1 * time.Second, - RetryWaitMax: 30 * time.Second, - CloudFetchConfig: CloudFetchConfig{ - UseCloudFetch: false, - MaxDownloadThreads: 10, - MaxFilesInMemory: 10, - MinTimeToExpiry: 0 * time.Second, - }, + Protocol: "https", + Host: "example.cloud.databricks.com", + Port: 443, + MaxRows: defaultMaxRows, + AccessToken: "supersecret", + Authenticator: &pat.PATAuth{AccessToken: "supersecret"}, + SessionParams: make(map[string]string), + RetryMax: 4, + RetryWaitMin: 1 * time.Second, + RetryWaitMax: 30 * time.Second, + CloudFetchConfig: defCloudConfig, }, wantURL: "https://example.cloud.databricks.com:443", wantErr: false, @@ -384,25 +331,20 @@ func TestParseConfig(t *testing.T) { name: "missing http path 2", args: args{dsn: "token:supersecret2@example.cloud.databricks.com:443?catalog=default&schema=system&timeout=100&maxRows=1000"}, wantCfg: UserConfig{ - Protocol: "https", - Host: "example.cloud.databricks.com", - Port: 443, - AccessToken: "supersecret2", - Authenticator: &pat.PATAuth{AccessToken: "supersecret2"}, - QueryTimeout: 100 * time.Second, - MaxRows: 1000, - Catalog: "default", - Schema: "system", - SessionParams: make(map[string]string), - RetryMax: 4, - RetryWaitMin: 1 * time.Second, - RetryWaitMax: 30 * time.Second, - CloudFetchConfig: CloudFetchConfig{ - UseCloudFetch: false, - MaxDownloadThreads: 10, - MaxFilesInMemory: 10, - MinTimeToExpiry: 0 * time.Second, - }, + Protocol: "https", + Host: "example.cloud.databricks.com", + Port: 443, + AccessToken: "supersecret2", + Authenticator: &pat.PATAuth{AccessToken: "supersecret2"}, + QueryTimeout: 100 * time.Second, + MaxRows: 1000, + Catalog: "default", + Schema: "system", + SessionParams: make(map[string]string), + RetryMax: 4, + RetryWaitMin: 1 * time.Second, + RetryWaitMax: 30 * time.Second, + CloudFetchConfig: defCloudConfig, }, wantURL: "https://example.cloud.databricks.com:443", wantErr: false, @@ -444,7 +386,6 @@ func TestParseConfig(t *testing.T) { wantCfg: UserConfig{}, wantErr: true, }, - { name: "missing host", args: args{dsn: "token:supersecret2@:443?catalog=default&schema=system&timeout=100&maxRows=1000"}, @@ -461,24 +402,158 @@ func TestParseConfig(t *testing.T) { RetryMax: 4, RetryWaitMin: 1 * time.Second, RetryWaitMax: 30 * time.Second, - CloudFetchConfig: CloudFetchConfig{}.WithDefaults(), + CloudFetchConfig: defCloudConfig, }, wantURL: "https://:443", wantErr: false, wantEndpointErr: true, }, + { + name: "with accessToken param", + args: args{dsn: "example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?catalog=default&schema=system&userAgentEntry=partner-name&timeout=100&maxRows=1000&ANSI_MODE=true&accessToken=supersecret2"}, + wantCfg: UserConfig{ + Protocol: "https", + Host: "example.cloud.databricks.com", + Port: 8000, + AccessToken: "supersecret2", + Authenticator: &pat.PATAuth{AccessToken: "supersecret2"}, + HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a", + QueryTimeout: 100 * time.Second, + MaxRows: 1000, + UserAgentEntry: "partner-name", + Catalog: "default", + Schema: "system", + SessionParams: map[string]string{"ANSI_MODE": "true"}, + RetryMax: 4, + RetryWaitMin: 1 * time.Second, + RetryWaitMax: 30 * time.Second, + CloudFetchConfig: defCloudConfig, + }, + wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a", + wantErr: false, + }, + { + name: "with accessToken param and client id/secret params", + args: args{dsn: "example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?catalog=default&schema=system&userAgentEntry=partner-name&timeout=100&maxRows=1000&ANSI_MODE=true&accessToken=supersecret2&clientId=client_id&clientSecret=client_secret"}, + wantCfg: UserConfig{ + Protocol: "https", + Host: "example.cloud.databricks.com", + Port: 8000, + AccessToken: "supersecret2", + Authenticator: &pat.PATAuth{AccessToken: "supersecret2"}, + HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a", + QueryTimeout: 100 * time.Second, + MaxRows: 1000, + UserAgentEntry: "partner-name", + Catalog: "default", + Schema: "system", + SessionParams: map[string]string{"ANSI_MODE": "true"}, + RetryMax: 4, + RetryWaitMin: 1 * time.Second, + RetryWaitMax: 30 * time.Second, + CloudFetchConfig: defCloudConfig, + }, + wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a", + wantErr: false, + }, + { + name: "authType unknown with accessTokenParam", + args: args{dsn: "example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?authType=unknown&catalog=default&schema=system&userAgentEntry=partner-name&timeout=100&maxRows=1000&ANSI_MODE=true&accessToken=supersecret2&clientId=client_id&clientSecret=client_secret"}, + wantCfg: UserConfig{ + Protocol: "https", + Host: "example.cloud.databricks.com", + Port: 8000, + AccessToken: "supersecret2", + Authenticator: &pat.PATAuth{AccessToken: "supersecret2"}, + HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a", + QueryTimeout: 100 * time.Second, + MaxRows: 1000, + UserAgentEntry: "partner-name", + Catalog: "default", + Schema: "system", + SessionParams: map[string]string{"ANSI_MODE": "true"}, + RetryMax: 4, + RetryWaitMin: 1 * time.Second, + RetryWaitMax: 30 * time.Second, + CloudFetchConfig: defCloudConfig, + }, + wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a", + wantErr: false, + }, + { + name: "client id no secret", + args: args{dsn: "example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?authType=unknown&clientId=client_id&catalog=default&schema=system&userAgentEntry=partner-name&timeout=100&maxRows=1000&ANSI_MODE=true"}, + wantCfg: UserConfig{}, + wantErr: true, + }, + { + name: "client secret no id", + args: args{dsn: "example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?authType=unknown&clientSecret=client_secret&catalog=default&schema=system&userAgentEntry=partner-name&timeout=100&maxRows=1000&ANSI_MODE=true"}, + wantCfg: UserConfig{}, + wantErr: true, + }, + { + name: "authType Pat, no accessToken", + args: args{dsn: "example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?authType=pat&clientSecret=client_secret&catalog=default&schema=system&userAgentEntry=partner-name&timeout=100&maxRows=1000&ANSI_MODE=true"}, + wantCfg: UserConfig{}, + wantErr: true, + }, + { + name: "authType m2m, no id", + args: args{dsn: "example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?authType=oauthm2m&clientSecret=client_secret&catalog=default&schema=system&userAgentEntry=partner-name&timeout=100&maxRows=1000&ANSI_MODE=true"}, + wantCfg: UserConfig{}, + wantErr: true, + }, + { + name: "authType m2m, no secret", + args: args{dsn: "example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?authType=oauthm2m&clientId=client_id&client_secret=&catalog=default&schema=system&userAgentEntry=partner-name&timeout=100&maxRows=1000&ANSI_MODE=true"}, + wantCfg: UserConfig{}, + wantErr: true, + }, + { + name: "authType unknown with client id/secret", + args: args{dsn: "example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?authType=unknown&clientId=client_id&clientSecret=client_secret&catalog=default&schema=system&userAgentEntry=partner-name&timeout=100&maxRows=1000&ANSI_MODE=true"}, + wantCfg: UserConfig{ + Protocol: "https", + Host: "example.cloud.databricks.com", + Port: 8000, + Authenticator: m2m.NewAuthenticator("client_id", "client_secret", "example.cloud.databricks.com"), + HTTPPath: "/sql/1.0/endpoints/12346a5b5b0e123a", + QueryTimeout: 100 * time.Second, + MaxRows: 1000, + UserAgentEntry: "partner-name", + Catalog: "default", + Schema: "system", + SessionParams: map[string]string{"ANSI_MODE": "true"}, + RetryMax: 4, + RetryWaitMin: 1 * time.Second, + RetryWaitMax: 30 * time.Second, + CloudFetchConfig: defCloudConfig, + }, + wantURL: "https://example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a", + wantErr: false, + }, + { + name: "authType m2m with accessToken", + args: args{dsn: "example.cloud.databricks.com:8000/sql/1.0/endpoints/12346a5b5b0e123a?authType=oauthm2m&accessToken=supersecret2&catalog=default&schema=system&userAgentEntry=partner-name&timeout=100&maxRows=1000&ANSI_MODE=true"}, + wantCfg: UserConfig{}, + wantErr: true, + }, } - for _, tt := range tests { + for i, tt := range tests { + fmt.Println(i) t.Run(tt.name, func(t *testing.T) { got, err := ParseDSN(tt.args.dsn) if (err != nil) != tt.wantErr { t.Errorf("ParseConfig() error = %v, wantErr %v", err, tt.wantErr) return } + if !reflect.DeepEqual(got, tt.wantCfg) { t.Errorf("ParseConfig() = %v, want %v", got, tt.wantCfg) return } + if err == nil { cfg := &Config{UserConfig: got} gotUrl, err := cfg.ToEndpointURL()