You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by jh...@apache.org on 2017/08/11 01:50:37 UTC

[40/50] calcite-avatica-go git commit: Add support for Kerberos/SPNEGO authentication

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 <fr...@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)