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 <francis.chu...@boostport.com>
Authored: Mon Jul 17 17:04:04 2017 +1000
Committer: Julian Hyde <jh...@apache.org>
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)

Reply via email to