diff --git a/go.mod b/go.mod index a4f043aae..6ae3908f5 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,7 @@ require ( github.com/hashicorp/vault-plugin-auth-jwt v0.13.2-0.20221012184020-28cc68ee722b github.com/hashicorp/vault-plugin-auth-kerberos v0.8.0 github.com/hashicorp/vault-plugin-auth-oci v0.13.0-pre - github.com/hashicorp/vault/api v1.8.1 + github.com/hashicorp/vault/api v1.9.3-0.20230628215639-3ca33976762c github.com/hashicorp/vault/sdk v0.6.0 github.com/hashicorp/yamux v0.1.1 // indirect github.com/jcmturner/gokrb5/v8 v8.4.2 @@ -46,7 +46,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/pointerstructure v1.2.1 // indirect go.uber.org/atomic v1.10.0 // indirect - golang.org/x/crypto v0.0.0-20221012134737-56aed061732a + golang.org/x/crypto v0.6.0 golang.org/x/net v0.7.0 golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1 golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect diff --git a/go.sum b/go.sum index 64dab3390..af22099c2 100644 --- a/go.sum +++ b/go.sum @@ -762,6 +762,8 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= +github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -1238,8 +1240,8 @@ github.com/hashicorp/vault/api v1.3.1/go.mod h1:QeJoWxMFt+MsuWcYhmwRLwKEXrjwAFFy github.com/hashicorp/vault/api v1.5.0/go.mod h1:LkMdrZnWNrFaQyYYazWVn7KshilfDidgVBq6YiTq/bM= github.com/hashicorp/vault/api v1.7.2/go.mod h1:xbfA+1AvxFseDzxxdWaL0uO99n1+tndus4GCrtouy0M= github.com/hashicorp/vault/api v1.8.0/go.mod h1:uJrw6D3y9Rv7hhmS17JQC50jbPDAZdjZoTtrCCxxs7E= -github.com/hashicorp/vault/api v1.8.1 h1:bMieWIe6dAlqAAPReZO/8zYtXaWUg/21umwqGZpEjCI= -github.com/hashicorp/vault/api v1.8.1/go.mod h1:uJrw6D3y9Rv7hhmS17JQC50jbPDAZdjZoTtrCCxxs7E= +github.com/hashicorp/vault/api v1.9.3-0.20230628215639-3ca33976762c h1:JhuUjg3NzAQczN4/ji+JltY06545IAJI+djmdDBpHUo= +github.com/hashicorp/vault/api v1.9.3-0.20230628215639-3ca33976762c/go.mod h1:jo5Y/ET+hNyz+JnKDt8XLAdKs+AM0G5W0Vp1IrFI8N8= github.com/hashicorp/vault/api/auth/approle v0.1.0/go.mod h1:mHOLgh//xDx4dpqXoq6tS8Ob0FoCFWLU2ibJ26Lfmag= github.com/hashicorp/vault/api/auth/kubernetes v0.2.0/go.mod h1:2BKADs9mwqAycDK/6tiHRh2sX0SPnC0DN4wHjJoAirw= github.com/hashicorp/vault/api/auth/userpass v0.1.0/go.mod h1:0orUbtkEwbEPmaQ+wvfrOddGBimLJnuN8A/J0PNfBks= @@ -1996,6 +1998,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191119213627-4f8c1d86b1ba/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -2023,8 +2026,8 @@ golang.org/x/crypto v0.0.0-20220208050332-20e1d8d225ab/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20221012134737-56aed061732a h1:NmSIgad6KjE6VvHciPZuNRTKxGhlPfD6OA87W/PLkqg= -golang.org/x/crypto v0.0.0-20221012134737-56aed061732a/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -2156,6 +2159,7 @@ golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= diff --git a/helper/transport.go b/helper/transport.go index aff8c5106..8c3fbe113 100644 --- a/helper/transport.go +++ b/helper/transport.go @@ -7,13 +7,16 @@ package helper import ( "bytes" + "crypto/tls" "encoding/json" + "fmt" "log" "net/http" "net/http/httputil" "os" "strconv" "strings" + "sync" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging" "github.com/hashicorp/vault/sdk/helper/salt" @@ -29,7 +32,7 @@ const ( EnvLogResponseBody = "TERRAFORM_VAULT_LOG_RESPONSE_BODY" ) -// TransportOptions for transport. +// TransportOptions for TransportWrapper. type TransportOptions struct { // HMACRequestHeaders ensure that any configured header's value is // never revealed during logging operations. @@ -42,7 +45,7 @@ type TransportOptions struct { LogResponseBody bool } -// DefaultTransportOptions for setting up the HTTP transport wrapper. +// DefaultTransportOptions for setting up the HTTP TransportWrapper wrapper. func DefaultTransportOptions() *TransportOptions { opts := &TransportOptions{ HMACRequestHeaders: []string{ @@ -65,13 +68,27 @@ func DefaultTransportOptions() *TransportOptions { return opts } -type transport struct { +type TransportWrapper struct { name string transport http.RoundTripper options *TransportOptions + m sync.RWMutex } -func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { +func (t *TransportWrapper) SetTLSConfig(c *tls.Config) error { + t.m.Lock() + defer t.m.Unlock() + transport, ok := t.transport.(*http.Transport) + if !ok { + return fmt.Errorf("type assertion failed for %T", t.transport) + } + + transport.TLSClientConfig = c + + return nil +} + +func (t *TransportWrapper) RoundTrip(req *http.Request) (*http.Response, error) { if logging.IsDebugOrHigher() { var origHeaders http.Header if len(t.options.HMACRequestHeaders) > 0 && len(req.Header) > 0 { @@ -118,8 +135,8 @@ func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { return resp, nil } -func NewTransport(name string, t http.RoundTripper, opts *TransportOptions) *transport { - return &transport{ +func NewTransport(name string, t http.RoundTripper, opts *TransportOptions) *TransportWrapper { + return &TransportWrapper{ name: name, transport: t, options: opts, diff --git a/internal/provider/auth_azure_test.go b/internal/provider/auth_azure_test.go index f40356a47..da2bc1949 100644 --- a/internal/provider/auth_azure_test.go +++ b/internal/provider/auth_azure_test.go @@ -154,7 +154,11 @@ func TestAuthLoginAzure_LoginPath(t *testing.T) { func TestAuthLoginAzure_Login(t *testing.T) { handlerFunc := func(t *testLoginHandler, w http.ResponseWriter, req *http.Request) { m, err := json.Marshal( - &api.Secret{}, + &api.Secret{ + Data: map[string]interface{}{ + "auth_login": "azure", + }, + }, ) if err != nil { w.WriteHeader(http.StatusInternalServerError) @@ -198,7 +202,11 @@ func TestAuthLoginAzure_Login(t *testing.T) { consts.FieldResourceGroupName: "res1", }, }, - want: &api.Secret{}, + want: &api.Secret{ + Data: map[string]interface{}{ + "auth_login": "azure", + }, + }, wantErr: false, }, { @@ -225,7 +233,11 @@ func TestAuthLoginAzure_Login(t *testing.T) { skipFunc: func(t *testing.T) { testutil.SkipTestEnvUnset(t, envVarTFAccAzureAuth) }, - want: &api.Secret{}, + want: &api.Secret{ + Data: map[string]interface{}{ + "auth_login": "azure", + }, + }, wantErr: false, }, { diff --git a/internal/provider/auth_cert.go b/internal/provider/auth_cert.go index 106b15524..abbdea424 100644 --- a/internal/provider/auth_cert.go +++ b/internal/provider/auth_cert.go @@ -4,11 +4,14 @@ package provider import ( + "crypto/tls" "fmt" + "net/http" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/vault/api" + "github.com/hashicorp/terraform-provider-vault/helper" "github.com/hashicorp/terraform-provider-vault/internal/consts" ) @@ -75,7 +78,11 @@ func (l *AuthLoginCert) LoginPath() string { } func (l *AuthLoginCert) Init(d *schema.ResourceData, authField string) (AuthLogin, error) { - if err := l.AuthLoginCommon.Init(d, authField); err != nil { + if err := l.AuthLoginCommon.Init(d, authField, + func(data *schema.ResourceData) error { + return l.checkRequiredFields(d, consts.FieldCertFile, consts.FieldKeyFile) + }, + ); err != nil { return nil, err } @@ -111,46 +118,44 @@ func (l *AuthLoginCert) Login(client *api.Client) (*api.Secret, error) { return nil, err } - tlsConfig := &api.TLSConfig{ - Insecure: false, - } - - if v, ok := l.params[consts.FieldCACertFile]; ok { - tlsConfig.CACert = v.(string) - } - - if v, ok := l.params[consts.FieldCACertDir]; ok { - tlsConfig.CAPath = v.(string) + config := client.CloneConfig() + tlsConfig := config.TLSConfig() + if tlsConfig == nil { + return nil, fmt.Errorf("clone api.Config's TLSConfig is nil") } + var clientCertFile string + var clientKeyFile string if v, ok := l.params[consts.FieldCertFile]; ok { - tlsConfig.ClientCert = v.(string) + clientCertFile = v.(string) } if v, ok := l.params[consts.FieldKeyFile]; ok { - tlsConfig.ClientKey = v.(string) - } - - if v, ok := l.params[consts.FieldTLSServerName]; ok { - tlsConfig.TLSServerName = v.(string) + clientKeyFile = v.(string) } if v, ok := l.params[consts.FieldSkipTLSVerify]; ok { - tlsConfig.Insecure = v.(bool) + tlsConfig.InsecureSkipVerify = v.(bool) } - config := c.CloneConfig() - if err := config.ConfigureTLS(tlsConfig); err != nil { + clientCert, err := tls.LoadX509KeyPair(clientCertFile, clientKeyFile) + if err != nil { return nil, err } - c, err = api.NewClient(config) - if err != nil { - return nil, err + tlsConfig.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { + return &clientCert, nil } - if config.CloneHeaders { - c.SetHeaders(client.Headers()) + switch t := config.HttpClient.Transport.(type) { + case *helper.TransportWrapper: + if err := t.SetTLSConfig(tlsConfig); err != nil { + return nil, err + } + case *http.Transport: + t.TLSClientConfig = tlsConfig + default: + return nil, fmt.Errorf("HTTPClient has unsupported Transport type %T", t) } params := make(map[string]interface{}) diff --git a/internal/provider/auth_cert_test.go b/internal/provider/auth_cert_test.go index fd8529af8..166a9cd16 100644 --- a/internal/provider/auth_cert_test.go +++ b/internal/provider/auth_cert_test.go @@ -7,12 +7,15 @@ import ( "encoding/json" "fmt" "net/http" + "os" + "path" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/vault/api" "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/testutil" ) func TestAuthLoginCert_Init(t *testing.T) { @@ -207,13 +210,34 @@ func TestAuthLoginCert_Login(t *testing.T) { w.Write(m) } + tempDir := t.TempDir() + + b, k, err := testutil.GenerateCA() + if err != nil { + t.Fatal(err) + } + + certFile := path.Join(tempDir, "cert.crt") + if err := os.WriteFile(certFile, b, 0o400); err != nil { + t.Fatal(err) + } + + keyFile := path.Join(tempDir, "cert.key") + if err := os.WriteFile(keyFile, k, 0o400); err != nil { + t.Fatal(err) + } + tests := []authLoginTest{ { name: "default", authLogin: &AuthLoginCert{ AuthLoginCommon{ - authField: "baz", - params: map[string]interface{}{}, + authField: "baz", + params: map[string]interface{}{ + consts.FieldCertFile: certFile, + consts.FieldKeyFile: keyFile, + consts.FieldSkipTLSVerify: true, + }, initialized: true, }, }, @@ -232,6 +256,7 @@ func TestAuthLoginCert_Login(t *testing.T) { }, }, }, + tls: true, wantErr: false, }, { @@ -241,7 +266,10 @@ func TestAuthLoginCert_Login(t *testing.T) { authField: "baz", mount: "qux", params: map[string]interface{}{ - consts.FieldName: "bob", + consts.FieldName: "bob", + consts.FieldCertFile: certFile, + consts.FieldKeyFile: keyFile, + consts.FieldSkipTLSVerify: true, }, initialized: true, }, @@ -263,6 +291,7 @@ func TestAuthLoginCert_Login(t *testing.T) { }, }, }, + tls: true, wantErr: false, }, { @@ -277,6 +306,7 @@ func TestAuthLoginCert_Login(t *testing.T) { }, want: nil, wantErr: true, + tls: true, expectErr: authLoginInitCheckError, }, } diff --git a/internal/provider/auth_jwt_test.go b/internal/provider/auth_jwt_test.go index 6e6221c53..df933c241 100644 --- a/internal/provider/auth_jwt_test.go +++ b/internal/provider/auth_jwt_test.go @@ -108,7 +108,11 @@ func TestAuthLoginJWT_LoginPath(t *testing.T) { func TestAuthLoginJWT_Login(t *testing.T) { handlerFunc := func(t *testLoginHandler, w http.ResponseWriter, req *http.Request) { m, err := json.Marshal( - &api.Secret{}, + &api.Secret{ + Data: map[string]interface{}{ + "auth_login": "jwt", + }, + }, ) if err != nil { w.WriteHeader(http.StatusInternalServerError) @@ -146,7 +150,11 @@ func TestAuthLoginJWT_Login(t *testing.T) { consts.FieldJWT: "jwt1", }, }, - want: &api.Secret{}, + want: &api.Secret{ + Data: map[string]interface{}{ + "auth_login": "jwt", + }, + }, wantErr: false, }, { diff --git a/internal/provider/auth_kerberos_test.go b/internal/provider/auth_kerberos_test.go index b371daa2e..3243b6715 100644 --- a/internal/provider/auth_kerberos_test.go +++ b/internal/provider/auth_kerberos_test.go @@ -120,7 +120,11 @@ func TestAuthLoginKerberos_LoginPath(t *testing.T) { func TestAuthLoginKerberos_Login(t *testing.T) { handlerFunc := func(t *testLoginHandler, w http.ResponseWriter, req *http.Request) { m, err := json.Marshal( - &api.Secret{}, + &api.Secret{ + Data: map[string]interface{}{ + "auth_login": "kerberos", + }, + }, ) if err != nil { w.WriteHeader(http.StatusInternalServerError) @@ -167,7 +171,11 @@ func TestAuthLoginKerberos_Login(t *testing.T) { consts.FieldAuthorization: fmt.Sprintf("Negotiate %s", testNegTokenInit), }, }, - want: &api.Secret{}, + want: &api.Secret{ + Data: map[string]interface{}{ + "auth_login": "kerberos", + }, + }, wantErr: false, }, { @@ -206,7 +214,11 @@ func TestAuthLoginKerberos_Login(t *testing.T) { consts.FieldAuthorization: fmt.Sprintf("Negotiate %s", testNegTokenInit), }, }, - want: &api.Secret{}, + want: &api.Secret{ + Data: map[string]interface{}{ + "auth_login": "kerberos", + }, + }, wantErr: false, }, } diff --git a/internal/provider/auth_radius_test.go b/internal/provider/auth_radius_test.go index b1129063b..784ea5f29 100644 --- a/internal/provider/auth_radius_test.go +++ b/internal/provider/auth_radius_test.go @@ -108,7 +108,11 @@ func TestAuthLoginRadius_LoginPath(t *testing.T) { func TestAuthLoginRadius_Login(t *testing.T) { handlerFunc := func(t *testLoginHandler, w http.ResponseWriter, req *http.Request) { m, err := json.Marshal( - &api.Secret{}, + &api.Secret{ + Data: map[string]interface{}{ + "auth_login": "radius", + }, + }, ) if err != nil { w.WriteHeader(http.StatusInternalServerError) @@ -146,7 +150,11 @@ func TestAuthLoginRadius_Login(t *testing.T) { consts.FieldPassword: "password1", }, }, - want: &api.Secret{}, + want: &api.Secret{ + Data: map[string]interface{}{ + "auth_login": "radius", + }, + }, wantErr: false, }, { diff --git a/internal/provider/auth_test.go b/internal/provider/auth_test.go index b8cab6b8b..55075172c 100644 --- a/internal/provider/auth_test.go +++ b/internal/provider/auth_test.go @@ -5,8 +5,8 @@ package provider import ( "encoding/json" - "fmt" "io" + "net" "net/http" "reflect" "testing" @@ -33,6 +33,7 @@ type authLoginTest struct { wantErr bool expectErr error skipFunc func(t *testing.T) + tls bool preLoginFunc func(t *testing.T) } @@ -105,10 +106,15 @@ func testAuthLogin(t *testing.T, tt authLoginTest) { tt.preLoginFunc(t) } - config, ln := testutil.TestHTTPServer(t, tt.handler.handler()) + var config *api.Config + var ln net.Listener + if tt.tls { + config, ln = testutil.TestHTTPSServer(t, tt.handler.handler()) + } else { + config, ln = testutil.TestHTTPServer(t, tt.handler.handler()) + } defer ln.Close() - config.Address = fmt.Sprintf("http://%s", ln.Addr()) c, err := api.NewClient(config) if err != nil { t.Fatal(err) @@ -231,7 +237,6 @@ func assertAuthLoginInit(t *testing.T, tt authLoginInitTest, s map[string]*schem } d := schema.TestResourceDataRaw(t, s, tt.raw) - actual, err := l.Init(d, tt.authField) if (err != nil) != tt.wantErr { t.Fatalf("Init() error = %v, wantErr %v", err, tt.wantErr) diff --git a/internal/provider/meta.go b/internal/provider/meta.go index b4693a42d..d6dda5396 100644 --- a/internal/provider/meta.go +++ b/internal/provider/meta.go @@ -154,29 +154,26 @@ func NewProviderMeta(d *schema.ResourceData) (interface{}, error) { if addr != "" { clientConfig.Address = addr } + clientConfig.CloneTLSConfig = true - clientAuthI := d.Get(consts.FieldClientAuth).([]interface{}) - if len(clientAuthI) > 1 { - return nil, fmt.Errorf("client_auth block may appear only once") - } - - clientAuthCert := "" - clientAuthKey := "" - if len(clientAuthI) == 1 { - clientAuth := clientAuthI[0].(map[string]interface{}) - clientAuthCert = clientAuth[consts.FieldCertFile].(string) - clientAuthKey = clientAuth[consts.FieldKeyFile].(string) - } - - err := clientConfig.ConfigureTLS(&api.TLSConfig{ + tlsConfig := &api.TLSConfig{ CACert: d.Get(consts.FieldCACertFile).(string), CAPath: d.Get(consts.FieldCACertDir).(string), Insecure: d.Get(consts.FieldSkipTLSVerify).(bool), TLSServerName: d.Get(consts.FieldTLSServerName).(string), + } - ClientCert: clientAuthCert, - ClientKey: clientAuthKey, - }) + if _, ok := d.GetOk(consts.FieldClientAuth); ok { + prefix := fmt.Sprintf("%s.0.", consts.FieldClientAuth) + if v, ok := d.GetOk(prefix + consts.FieldCertFile); ok { + tlsConfig.ClientCert = v.(string) + } + if v, ok := d.GetOk(prefix + consts.FieldKeyFile); ok { + tlsConfig.ClientKey = v.(string) + } + } + + err := clientConfig.ConfigureTLS(tlsConfig) if err != nil { return nil, fmt.Errorf("failed to configure TLS for Vault API: %s", err) } diff --git a/internal/provider/meta_test.go b/internal/provider/meta_test.go index 59ce26b6b..fba0c71a3 100644 --- a/internal/provider/meta_test.go +++ b/internal/provider/meta_test.go @@ -757,3 +757,222 @@ func TestNewProviderMeta(t *testing.T) { }) } } + +func TestNewProviderMeta_Cert(t *testing.T) { + testutil.SkipTestAcc(t) + testutil.SkipTestAccEnt(t) + testutil.TestAccPreCheck(t) + + nsPrefix := acctest.RandomWithPrefix("ns") + + defaultUser := "alice" + defaultPassword := "f00bazB1ff" + + rootProvider := NewProvider(nil, nil) + pr := &schema.Resource{ + Schema: rootProvider.Schema, + } + + tests := []struct { + name string + d *schema.ResourceData + data map[string]interface{} + wantNamespace string + tokenNamespace string + authLoginNamespace string + wantErr bool + }{ + { + name: "invalid-nil-ResourceData", + d: nil, + wantErr: true, + }, + { + // expect provider namespace set. + name: "with-provider-ns-only", + d: pr.TestResourceData(), + data: map[string]interface{}{ + consts.FieldNamespace: nsPrefix + "prov", + consts.FieldSkipGetVaultVersion: true, + }, + wantNamespace: nsPrefix + "prov", + wantErr: false, + }, + { + // expect token namespace set + name: "with-token-ns-only", + d: pr.TestResourceData(), + data: map[string]interface{}{ + consts.FieldSkipGetVaultVersion: true, + consts.FieldSkipChildToken: true, + }, + tokenNamespace: nsPrefix + "token-ns-only", + wantNamespace: nsPrefix + "token-ns-only", + wantErr: false, + }, + { + // expect provider namespace set. + name: "with-provider-ns-and-token-ns", + d: pr.TestResourceData(), + data: map[string]interface{}{ + consts.FieldNamespace: nsPrefix + "prov-and-token", + consts.FieldSkipGetVaultVersion: true, + consts.FieldSkipChildToken: true, + }, + tokenNamespace: nsPrefix + "token-ns", + wantNamespace: nsPrefix + "prov-and-token", + wantErr: false, + }, + { + // expect auth_login namespace set. + name: "with-auth-login-and-ns", + d: pr.TestResourceData(), + data: map[string]interface{}{ + consts.FieldSkipGetVaultVersion: true, + consts.FieldSkipChildToken: true, + consts.FieldAuthLoginUserpass: []map[string]interface{}{ + { + consts.FieldNamespace: nsPrefix + "auth-ns", + consts.FieldMount: consts.MountTypeUserpass, + consts.FieldUsername: defaultUser, + consts.FieldPassword: defaultPassword, + }, + }, + }, + authLoginNamespace: nsPrefix + "auth-ns", + wantNamespace: nsPrefix + "auth-ns", + wantErr: false, + }, + { + // expect provider namespace set. + name: "with-provider-ns-and-auth-login-with-ns", + d: pr.TestResourceData(), + data: map[string]interface{}{ + consts.FieldNamespace: nsPrefix + "prov-ns-auth-ns", + consts.FieldSkipGetVaultVersion: true, + consts.FieldSkipChildToken: true, + consts.FieldAuthLoginUserpass: []map[string]interface{}{ + { + consts.FieldNamespace: nsPrefix + "auth-ns-prov-ns", + consts.FieldMount: consts.MountTypeUserpass, + consts.FieldUsername: defaultUser, + consts.FieldPassword: defaultPassword, + }, + }, + }, + authLoginNamespace: nsPrefix + "auth-ns-prov-ns", + wantNamespace: nsPrefix + "prov-ns-auth-ns", + wantErr: false, + }, + } + + createNamespace := func(t *testing.T, client *api.Client, ns string) { + t.Helper() + t.Cleanup(func() { + err := backoff.Retry(func() error { + _, err := client.Logical().Delete(consts.SysNamespaceRoot + ns) + return err + }, backoff.WithMaxRetries(backoff.NewConstantBackOff(time.Microsecond*500), 10)) + if err != nil { + t.Fatalf("failed to delete namespace %q, err=%s", ns, err) + } + }) + if _, err := client.Logical().Write( + consts.SysNamespaceRoot+ns, nil); err != nil { + t.Fatalf("failed to create namespace, err=%s", err) + } + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config := api.DefaultConfig() + config.CloneToken = true + client, err := api.NewClient(config) + if err != nil { + t.Fatalf("failed to create Vault client, err=%s", err) + } + + if tt.authLoginNamespace != "" { + createNamespace(t, client, tt.authLoginNamespace) + options := &api.EnableAuthOptions{ + Type: consts.MountTypeUserpass, + Description: "test auth_userpass", + Local: true, + } + + clone, err := client.Clone() + if err != nil { + t.Fatalf("failed to clone Vault client, err=%s", err) + } + + clone.SetNamespace(tt.authLoginNamespace) + if err := clone.Sys().EnableAuthWithOptions(consts.MountTypeUserpass, options); err != nil { + t.Fatalf("failed to enable auth, err=%s", err) + } + + if _, err := clone.Logical().Write("auth/userpass/users/alice", + map[string]interface{}{ + consts.FieldPassword: defaultPassword, + consts.FieldTokenPolicies: []string{"admin", "default"}, + }); err != nil { + t.Fatalf("failed to create user, err=%s", err) + } + } + + if tt.tokenNamespace != "" { + if tt.data == nil { + t.Fatal("test data cannot be nil when tokenNamespace set") + } + + createNamespace(t, client, tt.tokenNamespace) + clone, err := client.Clone() + if err != nil { + t.Fatalf("failed to clone Vault client, err=%s", err) + } + + // in order not to trigger the min TTL warning we can add some time to the min. + tokenTTL := TokenTTLMinRecommended + time.Second*10 + clone.SetNamespace(tt.tokenNamespace) + resp, err := clone.Auth().Token().Create(&api.TokenCreateRequest{ + TTL: tokenTTL.String(), + }) + if err != nil { + t.Fatalf("failed to create Vault token, err=%s", err) + } + tt.data[consts.FieldToken] = resp.Auth.ClientToken + } + + for k, v := range tt.data { + if err := tt.d.Set(k, v); err != nil { + t.Fatalf("failed to set resource data, key=%s, value=%#v", k, v) + } + } + + got, err := NewProviderMeta(tt.d) + if (err != nil) != tt.wantErr { + t.Errorf("NewProviderMeta() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if err != nil { + if got != nil { + t.Errorf("NewProviderMeta() got = %v, want nil", got) + } + return + } + + p, ok := got.(*ProviderMeta) + if !ok { + t.Fatalf("invalid type got %T, expected %T", got, &ProviderMeta{}) + } + + if !reflect.DeepEqual(p.client.Namespace(), tt.wantNamespace) { + t.Errorf("NewProviderMeta() got ns = %v, want ns %v", p.client.Namespace(), tt.wantNamespace) + } + + if client.Token() == "" { + t.Errorf("NewProviderMeta() got empty Client token") + } + }) + } +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 7e780c22f..3b7a38fc4 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -116,6 +116,7 @@ func NewProvider( Optional: true, Description: "Client authentication credentials.", MaxItems: 1, + Deprecated: fmt.Sprintf("Use %s instead", consts.FieldAuthLoginCert), Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ consts.FieldCertFile: { diff --git a/testutil/testutil.go b/testutil/testutil.go index 915aba057..319702141 100644 --- a/testutil/testutil.go +++ b/testutil/testutil.go @@ -4,10 +4,21 @@ package testutil import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" "encoding/json" + "encoding/pem" "fmt" "io" "io/ioutil" + "math/big" "net" "net/http" "os" @@ -16,6 +27,7 @@ import ( "strings" "sync" "testing" + "time" "github.com/coreos/pkg/multierror" "github.com/hashicorp/go-retryablehttp" @@ -331,16 +343,17 @@ func (c *ghRESTClient) do(method, path string, v interface{}) error { return nil } -// testHTTPServer creates a test HTTP server that handles requests until +// TestHTTPServer creates a test HTTP server that handles requests until // the listener returned is closed. // XXX: copied from github.com/hashicorp/vault/api/client_test.go func TestHTTPServer(t *testing.T, handler http.Handler) (*api.Config, net.Listener) { - ln, err := net.Listen("tcp", "127.0.0.1:0") + t.Helper() + + server, ln, err := testHTTPServer(handler, nil) if err != nil { t.Fatalf("err: %s", err) } - server := &http.Server{Handler: handler} go server.Serve(ln) config := api.DefaultConfig() @@ -349,6 +362,63 @@ func TestHTTPServer(t *testing.T, handler http.Handler) (*api.Config, net.Listen return config, ln } +// TestHTTPSServer creates a test HTTP server that handles requests until +// the listener returned is closed. +// XXX: copied from github.com/hashicorp/vault/api/client_test.go +func TestHTTPSServer(t *testing.T, handler http.Handler) (*api.Config, net.Listener) { + t.Helper() + + var ca []byte + var key []byte + var err error + var serverTLSConfig *tls.Config + ca, key, err = GenerateCA() + if err != nil { + t.Fatal(err) + } + + cert, err := tls.X509KeyPair(ca, key) + if err != nil { + t.Fatal(err) + } + + serverTLSConfig = &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + + server, ln, err := testHTTPServer(handler, serverTLSConfig) + if err != nil { + t.Fatalf("err: %s", err) + } + + go server.ServeTLS(ln, "", "") + + config := api.DefaultConfig() + config.CloneTLSConfig = true + if err := config.ConfigureTLS(&api.TLSConfig{ + CACertBytes: ca, + }); err != nil { + t.Fatal(err) + } + + config.Address = fmt.Sprintf("https://%s", ln.Addr()) + return config, ln +} + +func testHTTPServer(handler http.Handler, tlsConfig *tls.Config) (*http.Server, net.Listener, error) { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, nil, err + } + + server := &http.Server{ + Handler: handler, + TLSConfig: tlsConfig, + } + + return server, ln, err +} + func GetDynamicTCPListeners(host string, count int) ([]net.Listener, func() error, error) { _, p, err := net.SplitHostPort(host) if err != nil { @@ -728,3 +798,112 @@ func GetNamespaceImportStateCheck(ns string) resource.ImportStateCheckFunc { return nil } } + +// Stashing functions here for generating a CA cert in the tests. Pulled mostly +// from the vault-k8s cert package. + +func GenerateCA() ([]byte, []byte, error) { + // Create the private key we'll use for this CA cert. + signer, key, err := PrivateKey() + if err != nil { + return nil, nil, err + } + + // The serial number for the cert + sn, err := serialNumber() + if err != nil { + return nil, nil, err + } + + signerKeyId, err := keyId(signer.Public()) + if err != nil { + return nil, nil, err + } + + // Create the CA cert + template := x509.Certificate{ + SerialNumber: sn, + Subject: pkix.Name{CommonName: "Testing CA"}, + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + IsCA: true, + NotAfter: time.Now().Add(1 * time.Hour), + NotBefore: time.Now().Add(-1 * time.Minute), + AuthorityKeyId: signerKeyId, + SubjectKeyId: signerKeyId, + } + + bs, err := x509.CreateCertificate( + rand.Reader, &template, &template, signer.Public(), signer) + if err != nil { + return nil, nil, err + } + + var buf bytes.Buffer + err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs}) + if err != nil { + return nil, nil, err + } + + return buf.Bytes(), key, nil +} + +// PrivateKey returns a new ECDSA-based private key. Both a crypto.Signer +// and the key are returned. +func PrivateKey() (crypto.Signer, []byte, error) { + pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + + bs, err := x509.MarshalECPrivateKey(pk) + if err != nil { + return nil, nil, err + } + + var buf bytes.Buffer + err = pem.Encode(&buf, &pem.Block{Type: "EC PRIVATE KEY", Bytes: bs}) + if err != nil { + return nil, nil, err + } + + return pk, buf.Bytes(), nil +} + +// serialNumber generates a new random serial number. +func serialNumber() (*big.Int, error) { + return rand.Int(rand.Reader, (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil)) +} + +// keyId returns a x509 KeyId from the given signing key. The key must be +// an *ecdsa.PublicKey currently, but may support more types in the future. +func keyId(raw interface{}) ([]byte, error) { + switch raw.(type) { + case *ecdsa.PublicKey: + default: + return nil, fmt.Errorf("invalid key type: %T", raw) + } + + // This is not standard; RFC allows any unique identifier as long as they + // match in subject/authority chains but suggests specific hashing of DER + // bytes of public key including DER tags. + bs, err := x509.MarshalPKIXPublicKey(raw) + if err != nil { + return nil, err + } + + // String formatted + kID := sha256.Sum256(bs) + return []byte(strings.Replace(fmt.Sprintf("% x", kID), " ", ":", -1)), nil +} + +func GetTestCertPool(t *testing.T, cert []byte) *x509.CertPool { + t.Helper() + + pool := x509.NewCertPool() + if ok := pool.AppendCertsFromPEM(cert); !ok { + t.Fatal("test certificate contains no valid certificates") + } + return pool +}