Add support for Kerberos/SPNEGO 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/38a538fe Tree: http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/tree/38a538fe Diff: http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/diff/38a538fe Branch: refs/heads/master Commit: 38a538fe605e754c888d8a955427ff1dcae79de5 Parents: 7a1093f Author: Francis Chuang <[email protected]> Authored: Mon Jul 17 17:04:04 2017 +1000 Committer: Julian Hyde <[email protected]> Committed: Thu Aug 10 18:47:12 2017 -0700 ---------------------------------------------------------------------- Gopkg.lock | 19 +++++++++++- Gopkg.toml | 4 +++ README.md | 27 +++++++++++++---- driver.go | 17 ++++++++--- dsn.go | 83 +++++++++++++++++++++++++++++++++++++++++++--------- dsn_test.go | 56 ++++++++++++++++++++++++++++++++++- http_client.go | 84 ++++++++++++++++++++++++++++++++++++++++++++++------- 7 files changed, 255 insertions(+), 35 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/blob/38a538fe/Gopkg.lock ---------------------------------------------------------------------- diff --git a/Gopkg.lock b/Gopkg.lock index c58d646..3fd77a2 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -14,6 +14,17 @@ revision = "3573b8b52aa7b37b9358d966a898feb387f62437" [[projects]] + branch = "master" + name = "github.com/jcmturner/asn1" + packages = ["."] + revision = "478ccf09c45d824f741022c79e542624952a83c5" + +[[projects]] + name = "github.com/jcmturner/gokrb5" + packages = ["asn1tools","client","config","credentials","crypto","crypto/aescts","crypto/common","crypto/etype","crypto/rfc3961","crypto/rfc3962","crypto/rfc8009","gssapi","iana","iana/adtype","iana/asnAppTag","iana/chksumtype","iana/errorcode","iana/etypeID","iana/flags","iana/keyusage","iana/msgtype","iana/nametype","iana/patype","keytab","krberror","messages","mstypes","ndr","pac","types"] + revision = "c26bda0a3bb400baa018645465f49407ef530f27" + +[[projects]] name = "github.com/satori/go.uuid" packages = ["."] revision = "879c5887cd475cd7864858769793b2ceb0d44feb" @@ -26,6 +37,12 @@ [[projects]] branch = "master" + name = "golang.org/x/crypto" + packages = ["pbkdf2"] + revision = "7f7c0c2d75ebb4e32a21396ce36e87b6dadc91c9" + +[[projects]] + branch = "master" name = "golang.org/x/net" packages = ["context","context/ctxhttp"] revision = "054b33e6527139ad5b1ec2f6232c3b175bd9a30c" @@ -33,6 +50,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "65536a41be5ba5e7160432be7dc25c788d7df51bef4b2524a63de2608860179d" + inputs-digest = "b97d946f979b64b669b1fe36fbf3566976593958b26305dc1efab8490eccbfee" solver-name = "gps-cdcl" solver-version = 1 http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/blob/38a538fe/Gopkg.toml ---------------------------------------------------------------------- diff --git a/Gopkg.toml b/Gopkg.toml index 55f5de7..d4b2c51 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -32,3 +32,7 @@ [[constraint]] name = "github.com/xinsnake/go-http-digest-auth-client" revision = "ddd37fe1722021e526546a269b5b5829a3d7b109" + +[[constraint]] + name = "github.com/jcmturner/gokrb5" + revision = "c26bda0a3bb400baa018645465f49407ef530f27" \ No newline at end of file http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/blob/38a538fe/README.md ---------------------------------------------------------------------- diff --git a/README.md b/README.md index 429e43f..57c899e 100644 --- a/README.md +++ b/README.md @@ -56,15 +56,32 @@ If schema is set, you can still work on tables in other schemas by supplying a s The following parameters are supported: +#### authentication +The authentication type to use when authenticating against Avatica. Valid values are `BASIC` for HTTP Basic authentication, +`DIGEST` for HTTP Digest authentication, and `SPNEGO` for Kerberos with SPNEGO authentication. + #### avaticaUser -The user to use when authenticating against Avatica. +The user to use when authenticating against Avatica. This parameter is required if `authentication` is `BASIC` or `DIGEST`. #### avaticaPassword -The password to use when authentication against Avatica. +The password to use when authenticating against Avatica. This parameter is required if `authentication` is `BASIC` or `DIGEST`. -#### authentication -The authentication type to use when authenticating against Avatica. Valid values are `BASIC` for HTTP Basic authentication -and `DIGEST` for HTTP Digest authentication. +#### principal +The Kerberos principal to use when authenticating against Avatica. It should be in the form `primary/instance@realm`, where +the instance is optional. This parameter is required if `authentication` is `SPNEGO` and you want the driver to perform the +Kerberos login. + +#### keytab +The path to the Kerberos keytab to use when authenticating against Avatica. This parameter is required if `authentication` +is `SPNEGO` and you want the driver to perform the Kerberos login. + +#### krb5Conf +The path to the Kerberos configuration to use when authenticating against Avatica. This parameter is required if `authentication` +is `SPNEGO` and you want the driver to perform the Kerberos login. + +#### krb5CredentialsCache +The path to the Kerberos credential cache file to use when authenticating against Avatica. This parameter is required if +`authentication` is `SPNEGO` and you have logged into Kerberos already and want the driver to use the existing credentials. #### location http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/blob/38a538fe/driver.go ---------------------------------------------------------------------- diff --git a/driver.go b/driver.go index 0dd87e8..b9bfa7c 100644 --- a/driver.go +++ b/driver.go @@ -38,11 +38,20 @@ func (a *Driver) Open(dsn string) (driver.Conn, error) { return nil, fmt.Errorf("Unable to open connection: %s", err) } - httpClient := NewHTTPClient(config.endpoint, httpClientAuthConfig{ - username: config.avaticaUser, - password: config.avaticaPassword, - authenticationType: config.authentication, + httpClient, err := NewHTTPClient(config.endpoint, httpClientAuthConfig{ + authenticationType: config.authentication, + username: config.avaticaUser, + password: config.avaticaPassword, + principal: config.principal, + keytab: config.keytab, + krb5Conf: config.krb5Conf, + krb5CredentialCache: config.krb5CredentialCache, }) + + if err != nil { + return nil, fmt.Errorf("Unable to create HTTP client: %s", err) + } + connectionId := uuid.NewV4().String() info := map[string]string{ http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/blob/38a538fe/dsn.go ---------------------------------------------------------------------- diff --git a/dsn.go b/dsn.go index d20919d..d96647a 100644 --- a/dsn.go +++ b/dsn.go @@ -14,6 +14,7 @@ const ( none authentication = iota basic digest + spnego ) // Config is a configuration parsed from a DSN string @@ -28,9 +29,18 @@ type Config struct { user string password string - authentication authentication - avaticaUser string - avaticaPassword string + authentication authentication + avaticaUser string + avaticaPassword string + principal krb5Principal + keytab string + krb5Conf string + krb5CredentialCache string +} + +type krb5Principal struct { + username string + realm string } // ParseDSN parses a DSN string to a Config @@ -119,25 +129,70 @@ func ParseDSN(dsn string) (*Config, error) { conf.authentication = basic } else if auth == "DIGEST" { conf.authentication = digest + } else if auth == "SPNEGO" { + conf.authentication = spnego } else { - return nil, fmt.Errorf("authentication must be either BASIC or DIGEST") + return nil, fmt.Errorf("authentication must be either BASIC, DIGEST or SPNEGO") } - user := queries.Get("avaticaUser") + if conf.authentication == basic || conf.authentication == digest { - if user == "" { - return nil, fmt.Errorf("authentication is set to %s, but avaticaUser is empty", v) - } + user := queries.Get("avaticaUser") - conf.avaticaUser = user + if user == "" { + return nil, fmt.Errorf("authentication is set to %s, but avaticaUser is empty", v) + } - pass := queries.Get("avaticaPassword") + conf.avaticaUser = user - if pass == "" { - return nil, fmt.Errorf("authentication is set to %s, but avaticaPassword is empty", v) - } + pass := queries.Get("avaticaPassword") + + if pass == "" { + return nil, fmt.Errorf("authentication is set to %s, but avaticaPassword is empty", v) + } + + conf.avaticaPassword = pass + + } else if conf.authentication == spnego { + principal := queries.Get("principal") + + keytab := queries.Get("keytab") + + krb5Conf := queries.Get("krb5Conf") - conf.avaticaPassword = pass + krb5CredentialCache := queries.Get("krb5CredentialCache") + + if principal == "" && keytab == "" && krb5Conf == "" && krb5CredentialCache == "" { + return nil, fmt.Errorf("when using SPNEGO authetication, you must provide the principal, keytab and krb5Conf parameters or a krb5TicketCache parameter") + } + + if !((principal != "" && keytab != "" && krb5Conf != "") || (principal == "" && keytab == "" && krb5Conf == "")) { + return nil, fmt.Errorf("when using SPNEGO authentication with a principal and keytab, the principal, keytab and krb5Conf parameters are required") + } + + if (principal != "" || keytab != "" || krb5Conf != "") && krb5CredentialCache != "" { + return nil, fmt.Errorf("ambigious configuration for SPNEGO authentication: use either pricipal, keytab and krb5Conf or krb5TicketCache") + } + + if principal != "" { + + splittedPrincipal := strings.Split(principal, "@") + + if len(splittedPrincipal) != 2 { + return nil, fmt.Errorf("invalid kerberos principal (%s): the principal should be in the format primary/instance@realm where instance is optional", principal) + } + + conf.principal = krb5Principal{ + username: splittedPrincipal[0], + realm: splittedPrincipal[1], + } + + conf.keytab = keytab + conf.krb5Conf = krb5Conf + } else if krb5CredentialCache != "" { + conf.krb5CredentialCache = krb5CredentialCache + } + } } if parsed.Path != "" { http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/blob/38a538fe/dsn_test.go ---------------------------------------------------------------------- diff --git a/dsn_test.go b/dsn_test.go index 3e0ecf9..90828d6 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -115,9 +115,27 @@ func TestDSNDefaults(t *testing.T) { if config.avaticaPassword != "" { t.Errorf("Default avaticaPassword should be empty, got %s", config.avaticaPassword) } + + principal := krb5Principal{} + + if config.principal != principal { + t.Errorf("Default principal should be empty, got %s", config.principal) + } + + if config.keytab != "" { + t.Errorf("Default keytab should be empty, got %s", config.keytab) + } + + if config.krb5Conf != "" { + t.Errorf("Default krb5Conf should be empty, got %s", config.krb5Conf) + } + + if config.krb5CredentialCache != "" { + t.Errorf("Default krb5CredentialCache should be empty, got %s", config.krb5CredentialCache) + } } -func TestLocallocation(t *testing.T) { +func TestLocalLocation(t *testing.T) { config, err := ParseDSN("http://localhost:8765?location=Local") @@ -204,6 +222,30 @@ func TestInvalidAuthentication(t *testing.T) { if err == nil { t.Fatal("Expected error due to missing avaticaUser, but did not receive any.") } + + _, err = ParseDSN("http://localhost:8765?authentication=SPNEGO&principal=test/test@realm&krb5Conf=/path/to/krb5.conf") + + if err == nil { + t.Fatal("Expected error due to missing keytab, but did not receive any.") + } + + _, err = ParseDSN("http://localhost:8765?authentication=SPNEGO&keytab=/path/to/file.keytab&krb5Conf=/path/to/krb5.conf") + + if err == nil { + t.Fatal("Expected error due to missing principal, but did not receive any.") + } + + _, err = ParseDSN("http://localhost:8765?authentication=SPNEGO&principal=test/test@realm&keytab=/path/to/file.keytab") + + if err == nil { + t.Fatal("Expected error due to missing krb5Conf, but did not receive any.") + } + + _, err = ParseDSN("http://localhost:8765?authentication=SPNEGO") + + if err == nil { + t.Fatal("Expected error due to invalid SPNEGO config, but did not receive any.") + } } func TestValidAuthentication(t *testing.T) { @@ -218,4 +260,16 @@ func TestValidAuthentication(t *testing.T) { if err != nil { t.Fatal("Unexpected error when DSN contains an authentication method, avaticaUser and avaticaPassword") } + + _, err = ParseDSN("http://localhost:8765?authentication=SPNEGO&principal=test/test@realm&keytab=/path/to/file.keytab&krb5Conf=/path/to/krb5.conf") + + if err != nil { + t.Fatal("Unexpected error when DSN contains an authentication method, principal and keytab and krb5Conf") + } + + _, err = ParseDSN("http://localhost:8765?authentication=SPNEGO&krb5CredentialCache=/path/to/cache") + + if err != nil { + t.Fatal("Unexpected error when DSN contains an authentication method with path to the credential cache") + } } http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/blob/38a538fe/http_client.go ---------------------------------------------------------------------- diff --git a/http_client.go b/http_client.go index 6613cd0..418ab5c 100644 --- a/http_client.go +++ b/http_client.go @@ -5,18 +5,30 @@ import ( "io/ioutil" "net/http" + "fmt" + avaticaMessage "github.com/Boostport/avatica/message" "github.com/golang/protobuf/proto" "github.com/hashicorp/go-cleanhttp" + "github.com/jcmturner/gokrb5/client" + "github.com/jcmturner/gokrb5/config" + "github.com/jcmturner/gokrb5/credentials" + "github.com/jcmturner/gokrb5/keytab" "github.com/xinsnake/go-http-digest-auth-client" "golang.org/x/net/context" "golang.org/x/net/context/ctxhttp" ) type httpClientAuthConfig struct { - username string - password string authenticationType authentication + + username string + password string + + principal krb5Principal + keytab string + krb5Conf string + krb5CredentialCache string } // httpClient wraps the default http.Client to communicate with the Avatica server. @@ -25,24 +37,74 @@ type httpClient struct { authConfig httpClientAuthConfig httpClient *http.Client + + kerberosClient client.Client } // NewHTTPClient creates a new httpClient from a host. -func NewHTTPClient(host string, authenticationConf httpClientAuthConfig) *httpClient { +func NewHTTPClient(host string, authenticationConf httpClientAuthConfig) (*httpClient, error) { + + hc := cleanhttp.DefaultPooledClient() + + c := &httpClient{ + host: host, + authConfig: authenticationConf, - client := cleanhttp.DefaultPooledClient() + httpClient: hc, + } if authenticationConf.authenticationType == digest { rt := digest_auth_client.NewTransport(authenticationConf.username, authenticationConf.password) - client.Transport = &rt - } + c.httpClient.Transport = &rt - return &httpClient{ - host: host, - authConfig: authenticationConf, + } else if authenticationConf.authenticationType == spnego { + + if authenticationConf.krb5CredentialCache != "" { + + tc, err := credentials.LoadCCache(authenticationConf.krb5CredentialCache) - httpClient: client, + if err != nil { + return nil, fmt.Errorf("error reading kerberos ticket cache: %s", err) + } + + kc, err := client.NewClientFromCCache(tc) + + if err != nil { + return nil, fmt.Errorf("error creating kerberos client: %s", err) + } + + c.kerberosClient = kc + + } else { + + cfg, err := config.Load(authenticationConf.krb5Conf) + + if err != nil { + return nil, fmt.Errorf("error reading kerberos config: %s", err) + } + + kt, err := keytab.Load(authenticationConf.keytab) + + if err != nil { + return nil, fmt.Errorf("error reading kerberos keytab: %s", err) + } + + kc := client.NewClientWithKeytab(authenticationConf.principal.username, authenticationConf.principal.realm, kt) + kc.WithConfig(cfg) + + err = kc.Login() + + if err != nil { + return nil, fmt.Errorf("error performing kerberos login with keytab: %s", err) + } + + kc.EnableAutoSessionRenewal() + + c.kerberosClient = kc + } } + + return c, nil } // post posts a protocol buffer message to the Avatica server. @@ -75,6 +137,8 @@ func (c *httpClient) post(ctx context.Context, message proto.Message) (proto.Mes if c.authConfig.authenticationType == basic { req.SetBasicAuth(c.authConfig.username, c.authConfig.password) + } else if c.authConfig.authenticationType == spnego { + c.kerberosClient.SetSPNEGOHeader(req, "") } res, err := ctxhttp.Do(ctx, c.httpClient, req)
