Implement Avatica HTTP authentication
Project: http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/repo Commit: http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/commit/472802ba Tree: http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/tree/472802ba Diff: http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/diff/472802ba Branch: refs/heads/master Commit: 472802ba29f522092fad657b586415e3604f7481 Parents: b1a6e6b Author: Francis Chuang <[email protected]> Authored: Mon May 29 14:04:52 2017 +1000 Committer: Julian Hyde <[email protected]> Committed: Thu Aug 10 18:47:11 2017 -0700 ---------------------------------------------------------------------- README.md | 10 +++++++ driver.go | 6 +++- dsn.go | 54 +++++++++++++++++++++++++++++++---- dsn_test.go | 75 +++++++++++++++++++++++++++++++++++++++++++++---- http_client.go | 41 +++++++++++++++++++++++---- vendor/vendor.json | 6 ++++ 6 files changed, 175 insertions(+), 17 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/blob/472802ba/README.md ---------------------------------------------------------------------- diff --git a/README.md b/README.md index e211292..429e43f 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,16 @@ If schema is set, you can still work on tables in other schemas by supplying a s The following parameters are supported: +#### avaticaUser +The user to use when authenticating against Avatica. + +#### avaticaPassword +The password to use when authentication against Avatica. + +#### authentication +The authentication type to use when authenticating against Avatica. Valid values are `BASIC` for HTTP Basic authentication +and `DIGEST` for HTTP Digest authentication. + #### location The `location` will be set as the location of unserialized `time.Time` values. It must be a valid timezone. http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/blob/472802ba/driver.go ---------------------------------------------------------------------- diff --git a/driver.go b/driver.go index 537aa16..0dd87e8 100644 --- a/driver.go +++ b/driver.go @@ -38,7 +38,11 @@ func (a *Driver) Open(dsn string) (driver.Conn, error) { return nil, fmt.Errorf("Unable to open connection: %s", err) } - httpClient := NewHTTPClient(config.endpoint) + httpClient := NewHTTPClient(config.endpoint, httpClientAuthConfig{ + username: config.avaticaUser, + password: config.avaticaPassword, + authenticationType: config.authentication, + }) connectionId := uuid.NewV4().String() info := map[string]string{ http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/blob/472802ba/dsn.go ---------------------------------------------------------------------- diff --git a/dsn.go b/dsn.go index 1311026..1bfa542 100644 --- a/dsn.go +++ b/dsn.go @@ -8,6 +8,14 @@ import ( "time" ) +type authentication int + +const ( + none authentication = iota + basic + digest +) + // Config is a configuration parsed from a DSN string type Config struct { endpoint string @@ -16,8 +24,13 @@ type Config struct { location *time.Location schema string transactionIsolation uint32 - user string - password string + + user string + password string + + authentication authentication + avaticaUser string + avaticaPassword string } // ParseDSN parses a DSN string to a Config @@ -83,10 +96,6 @@ func ParseDSN(dsn string) (*Config, error) { conf.location = loc } - if parsed.Path != "" { - conf.schema = strings.TrimPrefix(parsed.Path, "/") - } - if v := queries.Get("transactionIsolation"); v != "" { isolation, err := strconv.Atoi(v) @@ -102,6 +111,39 @@ func ParseDSN(dsn string) (*Config, error) { conf.transactionIsolation = uint32(isolation) } + if v := queries.Get("authentication"); v != "" { + + auth := strings.ToUpper(v) + + if auth == "BASIC" { + conf.authentication = basic + } else if auth == "DIGEST" { + conf.authentication = digest + } else { + return nil, fmt.Errorf("authentication must be either BASIC or DIGEST") + } + + user := queries.Get("avaticaUser") + + if user == "" { + return nil, fmt.Errorf("authentication is set to %s, but avaticaUser is empty", v) + } + + conf.avaticaUser = user + + pass := queries.Get("avaticaPassword") + + if pass == "" { + return nil, fmt.Errorf("authentication is set to %s, but avaticaPassword is empty", v) + } + + conf.avaticaPassword = pass + } + + if parsed.Path != "" { + conf.schema = strings.TrimPrefix(parsed.Path, "/") + } + parsed.User = nil parsed.RawQuery = "" parsed.Fragment = "" http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/blob/472802ba/dsn_test.go ---------------------------------------------------------------------- diff --git a/dsn_test.go b/dsn_test.go index 8d10f7d..3e0ecf9 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -8,17 +8,17 @@ import ( func TestParseDSN(t *testing.T) { - config, err := ParseDSN("http://username:password@localhost:8765/myschema?maxRowsTotal=1&frameMaxSize=1&location=Australia/Melbourne&transactionIsolation=8") + config, err := ParseDSN("http://username:password@localhost:8765/myschema?maxRowsTotal=1&frameMaxSize=1&location=Australia/Melbourne&transactionIsolation=8&authentication=BASIC&avaticaUser=someuser&avaticaPassword=somepassword") if err != nil { t.Fatalf("Unexpected error: %s", err) } - if config.user != "username"{ + if config.user != "username" { t.Errorf("Expected username to be %s, got %s", "username", config.user) } - if config.password != "password"{ + if config.password != "password" { t.Errorf("Expected password to be %s, got %s", "password", config.password) } @@ -45,6 +45,18 @@ func TestParseDSN(t *testing.T) { if config.transactionIsolation != 8 { t.Errorf("Expected transactionIsolation to be %d, got %d", 8, config.transactionIsolation) } + + if config.authentication != basic { + t.Errorf("Expected authentication to be BASIC (%d) got %d", basic, config.authentication) + } + + if config.avaticaUser != "someuser" { + t.Errorf("Expected avaticaUser to be %s, got %s", "someuser", config.avaticaUser) + } + + if config.avaticaPassword != "somepassword" { + t.Errorf("Expected avaticaPassword to be %s, got %s", "somepassword", config.avaticaPassword) + } } func TestParseEmptyDSN(t *testing.T) { @@ -64,11 +76,11 @@ func TestDSNDefaults(t *testing.T) { t.Fatalf("Unexpected error: %s", err) } - if config.user != ""{ + if config.user != "" { t.Errorf("Default username should be empty, got %s", config.user) } - if config.password != ""{ + if config.password != "" { t.Errorf("Default password should be empty, got %s", config.password) } @@ -91,6 +103,18 @@ func TestDSNDefaults(t *testing.T) { if config.transactionIsolation != 0 { t.Errorf("Default transaction level should be %d, got %d.", 0, config.transactionIsolation) } + + if config.authentication != none { + t.Errorf("Default authentication should be NONE (%d), got %d", none, config.authentication) + } + + if config.avaticaUser != "" { + t.Errorf("Default avaticaUser should be empty, got %s", config.avaticaUser) + } + + if config.avaticaPassword != "" { + t.Errorf("Default avaticaPassword should be empty, got %s", config.avaticaPassword) + } } func TestLocallocation(t *testing.T) { @@ -154,3 +178,44 @@ func TestValidTransactionIsolation(t *testing.T) { } } } + +func TestInvalidAuthentication(t *testing.T) { + + _, err := ParseDSN("http://localhost:8765?authentication=ASDF") + + if err == nil { + t.Fatal("Expected error due to invalid authentication, but did not receive any.") + } + + _, err = ParseDSN("http://localhost:8765?authentication=BASIC") + + if err == nil { + t.Fatal("Expected error due to missing avaticaUser and avaticaPassword, but did not receive any.") + } + + _, err = ParseDSN("http://localhost:8765?authentication=BASIC&avaticaUser=test") + + if err == nil { + t.Fatal("Expected error due to missing avaticaPassword, but did not receive any.") + } + + _, err = ParseDSN("http://localhost:8765?authentication=BASIC&avaticaPassword=test") + + if err == nil { + t.Fatal("Expected error due to missing avaticaUser, but did not receive any.") + } +} + +func TestValidAuthentication(t *testing.T) { + _, err := ParseDSN("http://localhost:8765?authentication=BASIC&avaticaUser=test&avaticaPassword=test") + + if err != nil { + t.Fatal("Unexpected error when DSN contains an authentication method, avaticaUser and avaticaPassword") + } + + _, err = ParseDSN("http://localhost:8765?authentication=DIGEST&avaticaUser=test&avaticaPassword=test") + + if err != nil { + t.Fatal("Unexpected error when DSN contains an authentication method, avaticaUser and avaticaPassword") + } +} http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/blob/472802ba/http_client.go ---------------------------------------------------------------------- diff --git a/http_client.go b/http_client.go index fd9b850..6613cd0 100644 --- a/http_client.go +++ b/http_client.go @@ -2,27 +2,46 @@ package avatica import ( "bytes" + "io/ioutil" + "net/http" + avaticaMessage "github.com/Boostport/avatica/message" "github.com/golang/protobuf/proto" "github.com/hashicorp/go-cleanhttp" + "github.com/xinsnake/go-http-digest-auth-client" "golang.org/x/net/context" "golang.org/x/net/context/ctxhttp" - "io/ioutil" - "net/http" ) +type httpClientAuthConfig struct { + username string + password string + authenticationType authentication +} + // httpClient wraps the default http.Client to communicate with the Avatica server. type httpClient struct { host string + authConfig httpClientAuthConfig + httpClient *http.Client } // NewHTTPClient creates a new httpClient from a host. -func NewHTTPClient(host string) *httpClient { +func NewHTTPClient(host string, authenticationConf httpClientAuthConfig) *httpClient { + + client := cleanhttp.DefaultPooledClient() + + if authenticationConf.authenticationType == digest { + rt := digest_auth_client.NewTransport(authenticationConf.username, authenticationConf.password) + client.Transport = &rt + } return &httpClient{ host: host, - httpClient: cleanhttp.DefaultPooledClient(), + authConfig: authenticationConf, + + httpClient: client, } } @@ -46,7 +65,19 @@ func (c *httpClient) post(ctx context.Context, message proto.Message) (proto.Mes return nil, err } - res, err := ctxhttp.Post(ctx, c.httpClient, c.host, "application/x-google-protobuf", bytes.NewReader(body)) + req, err := http.NewRequest("POST", c.host, bytes.NewReader(body)) + + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/x-google-protobuf") + + if c.authConfig.authenticationType == basic { + req.SetBasicAuth(c.authConfig.username, c.authConfig.password) + } + + res, err := ctxhttp.Do(ctx, c.httpClient, req) if err != nil { return nil, err http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/blob/472802ba/vendor/vendor.json ---------------------------------------------------------------------- diff --git a/vendor/vendor.json b/vendor/vendor.json index 9bee373..f66753a 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -157,6 +157,12 @@ "revisionTime": "2016-09-27T10:08:44Z" }, { + "checksumSHA1": "pZz/7xcCYgBXbV0T8s+7722/ZOQ=", + "path": "github.com/xinsnake/go-http-digest-auth-client", + "revision": "ddd37fe1722021e526546a269b5b5829a3d7b109", + "revisionTime": "2017-05-25T13:53:53Z" + }, + { "checksumSHA1": "D34ZqOW907kHAGTRU6lmxJs1ToU=", "path": "go4.org/syncutil/singleflight", "revision": "7ce08ca145dbe0e66a127c447b80ee7914f3e4f9",
