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)