From 83359dce3fbe18f96482c8f3fcbe22660a033ccd Mon Sep 17 00:00:00 2001 From: Tim <32556895+Avarei@users.noreply.github.com> Date: Wed, 11 Sep 2024 22:23:22 +0200 Subject: [PATCH 1/6] add mtls Signed-off-by: Tim <32556895+Avarei@users.noreply.github.com> --- .../v1alpha2/disposablerequest_types.go | 3 + .../v1alpha2/zz_generated.deepcopy.go | 1 + apis/request/v1alpha2/request_types.go | 3 + .../request/v1alpha2/zz_generated.deepcopy.go | 1 + internal/clients/http/client.go | 61 +++++++++++++------ .../disposablerequest/disposablerequest.go | 21 ++++++- .../disposablerequest_test.go | 20 +++--- internal/controller/request/observe.go | 2 +- internal/controller/request/observe_test.go | 14 ++--- internal/controller/request/request.go | 22 ++++++- internal/controller/request/request_test.go | 18 +++--- .../requestgen/request_generator_test.go | 4 ++ internal/json/util_test.go | 4 ++ ...http.crossplane.io_disposablerequests.yaml | 14 +++++ package/crds/http.crossplane.io_requests.yaml | 14 +++++ 15 files changed, 152 insertions(+), 50 deletions(-) diff --git a/apis/disposablerequest/v1alpha2/disposablerequest_types.go b/apis/disposablerequest/v1alpha2/disposablerequest_types.go index ab6c5eb..72beda7 100644 --- a/apis/disposablerequest/v1alpha2/disposablerequest_types.go +++ b/apis/disposablerequest/v1alpha2/disposablerequest_types.go @@ -45,6 +45,9 @@ type DisposableRequestParameters struct { // InsecureSkipTLSVerify, when set to true, skips TLS certificate checks for the HTTP request InsecureSkipTLSVerify bool `json:"insecureSkipTLSVerify,omitempty"` + // TlsSecretRef expects a reference to an opaque secret containing tls.crt and tls.key or/and ca.crt + TlsSecretRef xpv1.SecretReference `json:"tlsSecretRef,omitempty"` + // ExpectedResponse is a jq filter expression used to evaluate the HTTP response and determine if it matches the expected criteria. // The expression should return a boolean; if true, the response is considered expected. // Example: '.body.job_status == "success"' diff --git a/apis/disposablerequest/v1alpha2/zz_generated.deepcopy.go b/apis/disposablerequest/v1alpha2/zz_generated.deepcopy.go index 7a363eb..9128574 100644 --- a/apis/disposablerequest/v1alpha2/zz_generated.deepcopy.go +++ b/apis/disposablerequest/v1alpha2/zz_generated.deepcopy.go @@ -113,6 +113,7 @@ func (in *DisposableRequestParameters) DeepCopyInto(out *DisposableRequestParame *out = new(int32) **out = **in } + out.TlsSecretRef = in.TlsSecretRef if in.NextReconcile != nil { in, out := &in.NextReconcile, &out.NextReconcile *out = new(v1.Duration) diff --git a/apis/request/v1alpha2/request_types.go b/apis/request/v1alpha2/request_types.go index b685c0f..a1aee4e 100644 --- a/apis/request/v1alpha2/request_types.go +++ b/apis/request/v1alpha2/request_types.go @@ -42,6 +42,9 @@ type RequestParameters struct { // InsecureSkipTLSVerify, when set to true, skips TLS certificate checks for the HTTP request InsecureSkipTLSVerify bool `json:"insecureSkipTLSVerify,omitempty"` + // TlsSecretRef expects a reference to an opaque secret containing tls.crt and tls.key or/and ca.crt + TlsSecretRef xpv1.SecretReference `json:"tlsSecretRef,omitempty"` + // SecretInjectionConfig specifies the secrets receiving patches for response data. SecretInjectionConfigs []SecretInjectionConfig `json:"secretInjectionConfigs,omitempty"` } diff --git a/apis/request/v1alpha2/zz_generated.deepcopy.go b/apis/request/v1alpha2/zz_generated.deepcopy.go index 6301c31..8ed214e 100644 --- a/apis/request/v1alpha2/zz_generated.deepcopy.go +++ b/apis/request/v1alpha2/zz_generated.deepcopy.go @@ -178,6 +178,7 @@ func (in *RequestParameters) DeepCopyInto(out *RequestParameters) { *out = new(v1.Duration) **out = **in } + out.TlsSecretRef = in.TlsSecretRef if in.SecretInjectionConfigs != nil { in, out := &in.SecretInjectionConfigs, &out.SecretInjectionConfigs *out = make([]SecretInjectionConfig, len(*in)) diff --git a/internal/clients/http/client.go b/internal/clients/http/client.go index b99d229..2126c79 100644 --- a/internal/clients/http/client.go +++ b/internal/clients/http/client.go @@ -4,7 +4,9 @@ import ( "bytes" "context" "crypto/tls" + "crypto/x509" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -15,12 +17,12 @@ import ( // Client is the interface to interact with Http type Client interface { - SendRequest(ctx context.Context, method string, url string, body Data, headers Data, skipTLSVerify bool) (resp HttpDetails, err error) + SendRequest(ctx context.Context, method string, url string, body Data, headers Data) (resp HttpDetails, err error) } type client struct { - log logging.Logger - timeout time.Duration + client http.Client + log logging.Logger } type HttpResponse struct { @@ -46,7 +48,7 @@ type HttpDetails struct { HttpRequest HttpRequest } -func (hc *client) SendRequest(ctx context.Context, method string, url string, body Data, headers Data, skipTLSVerify bool) (details HttpDetails, err error) { +func (hc *client) SendRequest(ctx context.Context, method string, url string, body Data, headers Data) (details HttpDetails, err error) { requestBody := []byte(body.Decrypted.(string)) request, err := http.NewRequestWithContext(ctx, method, url, bytes.NewBuffer(requestBody)) requestDetails := HttpRequest{ @@ -68,16 +70,7 @@ func (hc *client) SendRequest(ctx context.Context, method string, url string, bo } } - client := &http.Client{ - Transport: &http.Transport{ - // #nosec G402 - TLSClientConfig: &tls.Config{InsecureSkipVerify: skipTLSVerify}, - Proxy: http.ProxyFromEnvironment, // Use proxy settings from environment - }, - Timeout: hc.timeout, - } - - response, err := client.Do(request) + response, err := hc.client.Do(request) if err != nil { return HttpDetails{ HttpRequest: requestDetails, @@ -113,10 +106,21 @@ func (hc *client) SendRequest(ctx context.Context, method string, url string, bo } // NewClient returns a new Http Client -func NewClient(log logging.Logger, timeout time.Duration) (Client, error) { +func NewClient(log logging.Logger, timeout time.Duration, certPEMBlock, keyPEMBlock, caPEMBlock []byte, insecureSkipVerify bool) (Client, error) { + tlsConfig, err := tlsConfig(certPEMBlock, keyPEMBlock, caPEMBlock, insecureSkipVerify) + if err != nil { + return nil, err + } + httpClient := http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + Proxy: http.ProxyFromEnvironment, // Use proxy settings from environment + }, + Timeout: timeout, + } return &client{ - log: log, - timeout: timeout, + client: httpClient, + log: log, }, nil } @@ -128,3 +132,26 @@ func toJSON(request HttpRequest) string { return string(jsonBytes) } + +func tlsConfig(certPEMBlock, keyPEMBlock, caPEMBlock []byte, insecureSkipVerify bool) (*tls.Config, error) { + tlsConfig := &tls.Config{} + if len(certPEMBlock) > 0 && len(keyPEMBlock) > 0 { + certificate, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock) + if err != nil { + return nil, err + } + tlsConfig.Certificates = []tls.Certificate{certificate} + } + + if len(caPEMBlock) > 0 { + caPool := x509.NewCertPool() + if !caPool.AppendCertsFromPEM(caPEMBlock) { + return nil, errors.New("some error appending the ca.crt") + } + tlsConfig.RootCAs = caPool + } + + tlsConfig.InsecureSkipVerify = insecureSkipVerify + + return tlsConfig, nil +} diff --git a/internal/controller/disposablerequest/disposablerequest.go b/internal/controller/disposablerequest/disposablerequest.go index 4a27a07..5887d16 100644 --- a/internal/controller/disposablerequest/disposablerequest.go +++ b/internal/controller/disposablerequest/disposablerequest.go @@ -42,6 +42,7 @@ import ( apisv1alpha1 "github.com/crossplane-contrib/provider-http/apis/v1alpha1" httpClient "github.com/crossplane-contrib/provider-http/internal/clients/http" "github.com/crossplane-contrib/provider-http/internal/utils" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -94,7 +95,7 @@ type connector struct { logger logging.Logger kube client.Client usage resource.Tracker - newHttpClientFn func(log logging.Logger, timeout time.Duration) (httpClient.Client, error) + newHttpClientFn func(log logging.Logger, timeout time.Duration, certPEMBlock, keyPEMBlock, caPEMBlock []byte, insecureSkipVerify bool) (httpClient.Client, error) } func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) { @@ -115,7 +116,21 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E return nil, errors.Wrap(err, errProviderNotRetrieved) } - h, err := c.newHttpClientFn(l, utils.WaitTimeout(cr.Spec.ForProvider.WaitTimeout)) + secret := &corev1.Secret{} + + if cr.Spec.ForProvider.TlsSecretRef.Name != "" && cr.Spec.ForProvider.TlsSecretRef.Namespace != "" { + if err := c.kube.Get(ctx, types.NamespacedName{ + Namespace: cr.Spec.ForProvider.TlsSecretRef.Namespace, + Name: cr.Spec.ForProvider.TlsSecretRef.Name, + }, secret); err != nil { + return nil, errors.Wrap(err, errGetReferencedSecret) + } + } + certPEMBlock := secret.Data["tls.crt"] + keyPEMBlock := secret.Data["tls.key"] + caPEMBlock := secret.Data["ca.crt"] + + h, err := c.newHttpClientFn(l, utils.WaitTimeout(cr.Spec.ForProvider.WaitTimeout), certPEMBlock, keyPEMBlock, caPEMBlock, cr.Spec.ForProvider.InsecureSkipTLSVerify) if err != nil { return nil, errors.Wrap(err, errNewHttpClient) } @@ -184,7 +199,7 @@ func (c *external) deployAction(ctx context.Context, cr *v1alpha2.DisposableRequ bodyData := httpClient.Data{Encrypted: cr.Spec.ForProvider.Body, Decrypted: sensitiveBody} headersData := httpClient.Data{Encrypted: cr.Spec.ForProvider.Headers, Decrypted: sensitiveHeaders} - details, err := c.http.SendRequest(ctx, cr.Spec.ForProvider.Method, cr.Spec.ForProvider.URL, bodyData, headersData, cr.Spec.ForProvider.InsecureSkipTLSVerify) + details, err := c.http.SendRequest(ctx, cr.Spec.ForProvider.Method, cr.Spec.ForProvider.URL, bodyData, headersData) sensitiveResponse := details.HttpResponse resource := &utils.RequestResource{ diff --git a/internal/controller/disposablerequest/disposablerequest_test.go b/internal/controller/disposablerequest/disposablerequest_test.go index eabd46b..c9b00c4 100644 --- a/internal/controller/disposablerequest/disposablerequest_test.go +++ b/internal/controller/disposablerequest/disposablerequest_test.go @@ -103,14 +103,14 @@ func httpDisposableRequest(rm ...httpDisposableRequestModifier) *v1alpha2.Dispos return r } -type MockSendRequestFn func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) +type MockSendRequestFn func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data) (resp httpClient.HttpDetails, err error) type MockHttpClient struct { MockSendRequest MockSendRequestFn } -func (c *MockHttpClient) SendRequest(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { - return c.MockSendRequest(ctx, method, url, body, headers, skipTLSVerify) +func (c *MockHttpClient) SendRequest(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data) (resp httpClient.HttpDetails, err error) { + return c.MockSendRequest(ctx, method, url, body, headers) } type notHttpDisposableRequest struct { @@ -143,7 +143,7 @@ func Test_httpExternal_Create(t *testing.T) { "DisposableRequestFailed": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, errBoom }, }, @@ -161,7 +161,7 @@ func Test_httpExternal_Create(t *testing.T) { "Success": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, nil }, }, @@ -219,7 +219,7 @@ func Test_httpExternal_Update(t *testing.T) { "DisposableRequestFailed": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, errBoom }, }, @@ -236,7 +236,7 @@ func Test_httpExternal_Update(t *testing.T) { "Success": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, nil }, }, @@ -290,7 +290,7 @@ func Test_deployAction(t *testing.T) { "SuccessUpdateStatusRequestFailure": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, errors.Errorf(utils.ErrInvalidURL, "invalid-url") }, }, @@ -318,7 +318,7 @@ func Test_deployAction(t *testing.T) { "SuccessUpdateStatusCodeError": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{ HttpResponse: httpClient.HttpResponse{ StatusCode: 400, @@ -356,7 +356,7 @@ func Test_deployAction(t *testing.T) { "SuccessUpdateStatusSuccessfulRequest": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{ HttpResponse: httpClient.HttpResponse{ StatusCode: 200, diff --git a/internal/controller/request/observe.go b/internal/controller/request/observe.go index b2d41b6..c3305b8 100644 --- a/internal/controller/request/observe.go +++ b/internal/controller/request/observe.go @@ -54,7 +54,7 @@ func (c *external) isUpToDate(ctx context.Context, cr *v1alpha2.Request) (Observ return FailedObserve(), err } - details, responseErr := c.http.SendRequest(ctx, http.MethodGet, requestDetails.Url, requestDetails.Body, requestDetails.Headers, cr.Spec.ForProvider.InsecureSkipTLSVerify) + details, responseErr := c.http.SendRequest(ctx, http.MethodGet, requestDetails.Url, requestDetails.Body, requestDetails.Headers) if details.HttpResponse.StatusCode == http.StatusNotFound { return FailedObserve(), errors.New(errObjectNotFound) } diff --git a/internal/controller/request/observe_test.go b/internal/controller/request/observe_test.go index add3044..d07a964 100644 --- a/internal/controller/request/observe_test.go +++ b/internal/controller/request/observe_test.go @@ -36,7 +36,7 @@ func Test_isUpToDate(t *testing.T) { "ObjectNotFoundEmptyBody": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, nil }, }, @@ -54,7 +54,7 @@ func Test_isUpToDate(t *testing.T) { "ObjectNotFoundPostFailed": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, nil }, }, @@ -73,7 +73,7 @@ func Test_isUpToDate(t *testing.T) { "ObjectNotFound404StatusCode": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, nil }, }, @@ -91,7 +91,7 @@ func Test_isUpToDate(t *testing.T) { "FailBodyNotJSON": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{ HttpResponse: httpClient.HttpResponse{ Body: "not a JSON", @@ -113,7 +113,7 @@ func Test_isUpToDate(t *testing.T) { "SuccessNotSynced": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{ HttpResponse: httpClient.HttpResponse{ Body: `{"username":"old_name"}`, @@ -147,7 +147,7 @@ func Test_isUpToDate(t *testing.T) { "SuccessNoPUTMapping": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{ HttpResponse: httpClient.HttpResponse{ Body: `{"username":"old_name"}`, @@ -187,7 +187,7 @@ func Test_isUpToDate(t *testing.T) { "SuccessJSONBody": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body, headers httpClient.Data) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{ HttpResponse: httpClient.HttpResponse{ Body: `{"username":"john_doe_new_username"}`, diff --git a/internal/controller/request/request.go b/internal/controller/request/request.go index 369a1e0..55b8e99 100644 --- a/internal/controller/request/request.go +++ b/internal/controller/request/request.go @@ -42,6 +42,7 @@ import ( "github.com/crossplane-contrib/provider-http/internal/controller/request/statushandler" datapatcher "github.com/crossplane-contrib/provider-http/internal/data-patcher" "github.com/crossplane-contrib/provider-http/internal/utils" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -57,6 +58,7 @@ const ( errMappingNotFound = "%s mapping doesn't exist in request, skipping operation" errPatchDataToSecret = "Warning, couldn't patch data from request to secret %s:%s:%s, error: %s" errGetLatestVersion = "failed to get the latest version of the resource" + errGetReferencedSecret = "cannot get referenced secret" ) // Setup adds a controller that reconciles Request managed resources. @@ -92,7 +94,7 @@ type connector struct { logger logging.Logger kube client.Client usage resource.Tracker - newHttpClientFn func(log logging.Logger, timeout time.Duration) (httpClient.Client, error) + newHttpClientFn func(log logging.Logger, timeout time.Duration, certPEMBlock, keyPEMBlock, caPEMBlock []byte, insecureSkipVerify bool) (httpClient.Client, error) } // Connect typically produces an ExternalClient by: @@ -118,7 +120,21 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E return nil, errors.Wrap(err, errProviderNotRetrieved) } - h, err := c.newHttpClientFn(l, utils.WaitTimeout(cr.Spec.ForProvider.WaitTimeout)) + secret := &corev1.Secret{} + + if cr.Spec.ForProvider.TlsSecretRef.Name != "" && cr.Spec.ForProvider.TlsSecretRef.Namespace != "" { + if err := c.kube.Get(ctx, types.NamespacedName{ + Namespace: cr.Spec.ForProvider.TlsSecretRef.Namespace, + Name: cr.Spec.ForProvider.TlsSecretRef.Name, + }, secret); err != nil { + return nil, errors.Wrap(err, errGetReferencedSecret) + } + } + certPEMBlock := secret.Data["tls.crt"] + keyPEMBlock := secret.Data["tls.key"] + caPEMBlock := secret.Data["ca.crt"] + + h, err := c.newHttpClientFn(l, utils.WaitTimeout(cr.Spec.ForProvider.WaitTimeout), certPEMBlock, keyPEMBlock, caPEMBlock, cr.Spec.ForProvider.InsecureSkipTLSVerify) if err != nil { return nil, errors.Wrap(err, errNewHttpClient) } @@ -195,7 +211,7 @@ func (c *external) deployAction(ctx context.Context, cr *v1alpha2.Request, metho return err } - details, err := c.http.SendRequest(ctx, mapping.Method, requestDetails.Url, requestDetails.Body, requestDetails.Headers, cr.Spec.ForProvider.InsecureSkipTLSVerify) + details, err := c.http.SendRequest(ctx, mapping.Method, requestDetails.Url, requestDetails.Body, requestDetails.Headers) c.patchResponseToSecret(ctx, cr, &details.HttpResponse) statusHandler, err := statushandler.NewStatusHandler(ctx, cr, details, err, c.localKube, c.logger) diff --git a/internal/controller/request/request_test.go b/internal/controller/request/request_test.go index cae539d..01350e4 100644 --- a/internal/controller/request/request_test.go +++ b/internal/controller/request/request_test.go @@ -72,14 +72,14 @@ type notHttpRequest struct { resource.Managed } -type MockSendRequestFn func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) +type MockSendRequestFn func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data) (resp httpClient.HttpDetails, err error) type MockHttpClient struct { MockSendRequest MockSendRequestFn } -func (c *MockHttpClient) SendRequest(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { - return c.MockSendRequest(ctx, method, url, body, headers, skipTLSVerify) +func (c *MockHttpClient) SendRequest(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data) (resp httpClient.HttpDetails, err error) { + return c.MockSendRequest(ctx, method, url, body, headers) } type MockSetRequestStatusFn func() error @@ -126,7 +126,7 @@ func Test_httpExternal_Create(t *testing.T) { "RequestFailed": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, errBoom }, }, @@ -143,7 +143,7 @@ func Test_httpExternal_Create(t *testing.T) { "Success": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, nil }, }, @@ -201,7 +201,7 @@ func Test_httpExternal_Update(t *testing.T) { "RequestFailed": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, errBoom }, }, @@ -218,7 +218,7 @@ func Test_httpExternal_Update(t *testing.T) { "Success": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, nil }, }, @@ -276,7 +276,7 @@ func Test_httpExternal_Delete(t *testing.T) { "RequestFailed": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, errBoom }, }, @@ -293,7 +293,7 @@ func Test_httpExternal_Delete(t *testing.T) { "Success": { args: args{ http: &MockHttpClient{ - MockSendRequest: func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data, skipTLSVerify bool) (resp httpClient.HttpDetails, err error) { + MockSendRequest: func(ctx context.Context, method string, url string, body httpClient.Data, headers httpClient.Data) (resp httpClient.HttpDetails, err error) { return httpClient.HttpDetails{}, nil }, }, diff --git a/internal/controller/request/requestgen/request_generator_test.go b/internal/controller/request/requestgen/request_generator_test.go index 3947ae1..28d1f04 100644 --- a/internal/controller/request/requestgen/request_generator_test.go +++ b/internal/controller/request/requestgen/request_generator_test.go @@ -424,6 +424,10 @@ func Test_generateRequestObject(t *testing.T) { "body": map[string]any{"id": "123"}, "statusCode": float64(200), }, + "tlsSecretRef": map[string]any{ + "name": "", + "namespace": "", + }, }, }, }, diff --git a/internal/json/util_test.go b/internal/json/util_test.go index ed3c698..a52a7e1 100644 --- a/internal/json/util_test.go +++ b/internal/json/util_test.go @@ -293,6 +293,10 @@ func Test_StructToMap(t *testing.T) { "baseUrl": "https://api.example.com/users", "body": `{"username": "john_doe", "email": "john.doe@example.com"}`, }, + "tlsSecretRef": map[string]any{ + "name": "", + "namespace": "", + }, }, errMessage: "", }, diff --git a/package/crds/http.crossplane.io_disposablerequests.yaml b/package/crds/http.crossplane.io_disposablerequests.yaml index 8ac26a5..eaa1d1b 100644 --- a/package/crds/http.crossplane.io_disposablerequests.yaml +++ b/package/crds/http.crossplane.io_disposablerequests.yaml @@ -528,6 +528,20 @@ spec: description: ShouldLoopInfinitely specifies whether the reconciliation should loop indefinitely. type: boolean + tlsSecretRef: + description: TlsSecretRef expects a reference to an opaque secret + containing tls.crt and tls.key or/and ca.crt + properties: + name: + description: Name of the secret. + type: string + namespace: + description: Namespace of the secret. + type: string + required: + - name + - namespace + type: object url: type: string x-kubernetes-validations: diff --git a/package/crds/http.crossplane.io_requests.yaml b/package/crds/http.crossplane.io_requests.yaml index ef57d72..05648a9 100644 --- a/package/crds/http.crossplane.io_requests.yaml +++ b/package/crds/http.crossplane.io_requests.yaml @@ -556,6 +556,20 @@ spec: - secretRef type: object type: array + tlsSecretRef: + description: TlsSecretRef expects a reference to an opaque secret + containing tls.crt and tls.key or/and ca.crt + properties: + name: + description: Name of the secret. + type: string + namespace: + description: Namespace of the secret. + type: string + required: + - name + - namespace + type: object waitTimeout: description: WaitTimeout specifies the maximum time duration for waiting. From d2f02a6467aa994a5362532450117adf1ffe2623 Mon Sep 17 00:00:00 2001 From: Tim <32556895+Avarei@users.noreply.github.com> Date: Sun, 15 Sep 2024 12:14:53 +0200 Subject: [PATCH 2/6] add tests for newClient and mTLS Signed-off-by: Tim <32556895+Avarei@users.noreply.github.com> --- internal/clients/http/client_test.go | 162 +++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 internal/clients/http/client_test.go diff --git a/internal/clients/http/client_test.go b/internal/clients/http/client_test.go new file mode 100644 index 0000000..a44a9f8 --- /dev/null +++ b/internal/clients/http/client_test.go @@ -0,0 +1,162 @@ +package http + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "errors" + "math/big" + "net" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/crossplane/crossplane-runtime/pkg/logging" + "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/google/go-cmp/cmp" +) + +func Test_newClient(t *testing.T) { + log := logging.NewNopLogger() + clientTlsCrt, clientTlsKey, err := createCertBundle() + if err != nil { + t.Fatal(err) + } + serverTlsCrt, serverTlsKey, err := createCertBundle() + if err != nil { + t.Fatal(err) + } + + type args struct { + cert []byte + key []byte + ca []byte + insecure bool + serverRequiresMTLS bool + } + type want struct { + newClientErr error + sendRequestHasErr bool + } + cases := map[string]struct { + args args + want want + }{ + "NoMTLSConfig": { + args: args{ + insecure: true, + serverRequiresMTLS: false, + }, + want: want{}, + }, + "ValidMTLSConfig": { + args: args{ + cert: clientTlsCrt, + key: clientTlsKey, + ca: serverTlsCrt, + insecure: false, + serverRequiresMTLS: true, + }, + want: want{}, + }, + "InvalidMTLSConfig": { + args: args{ + cert: []byte("invalid cert"), + key: []byte("invalid key"), + insecure: false, + }, + want: want{ + newClientErr: errors.New("tls: failed to find any PEM data in certificate input"), + }, + }, + "ServerNotInCA": { + args: args{ + cert: clientTlsCrt, + key: clientTlsKey, + insecure: false, + serverRequiresMTLS: true, + }, + want: want{ + sendRequestHasErr: true, + }, + }, + } + + for name, tc := range cases { + tc := tc // Create local copies of loop variables + + t.Run(name, func(t *testing.T) { + client, gotErr := NewClient(log, 10*time.Second, []byte(tc.args.cert), []byte(tc.args.key), []byte(tc.args.ca), tc.args.insecure) + if diff := cmp.Diff(tc.want.newClientErr, gotErr, test.EquateErrors()); diff != "" { + t.Fatalf("NewClient(...): -want error, +got error: %s", diff) + } + if gotErr != nil { + return + } + + server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + cert, err := tls.X509KeyPair(serverTlsCrt, serverTlsKey) + if err != nil { + t.Fatalf("invalid server certificates: %s", err) + } + cas := x509.NewCertPool() + cas.AppendCertsFromPEM(clientTlsCrt) + server.TLS = &tls.Config{ + Certificates: []tls.Certificate{cert}, + ClientCAs: cas, + } + if tc.args.serverRequiresMTLS { + server.TLS.ClientAuth = tls.RequireAndVerifyClientCert + } + + server.StartTLS() + defer server.Close() + + _, err = client.SendRequest(context.Background(), http.MethodGet, server.URL, Data{Decrypted: "", Encrypted: ""}, Data{Decrypted: map[string][]string{}, Encrypted: map[string][]string{}}) + if tc.want.sendRequestHasErr == (err == nil) { + t.Fatal(err) + } + }) + } +} + +func createCertBundle() ([]byte, []byte, error) { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, err + } + + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + NotBefore: time.Now(), + NotAfter: time.Now().Add(365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, + BasicConstraintsValid: true, + DNSNames: []string{"localhost"}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + } + + // Create a self-signed certificate + certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return nil, nil, err + } + encodedCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes}) + if encodedCert == nil { + return nil, nil, errors.New("could not PEM encode certificate") + } + encodedKey := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) + if encodedKey == nil { + return nil, nil, errors.New("could not PEM encode private key") + } + + return encodedCert, encodedKey, nil +} From 127baa4728d230636ac2ac944f1a9017ee2eeb0c Mon Sep 17 00:00:00 2001 From: Tim <32556895+Avarei@users.noreply.github.com> Date: Sun, 15 Sep 2024 12:51:56 +0200 Subject: [PATCH 3/6] upgrade golangci-lint to 1.61.0 Signed-off-by: Tim <32556895+Avarei@users.noreply.github.com> --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index c647e3e..4b8e266 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ GO_TEST_PARALLEL := $(shell echo $$(( $(NPROCS) / 2 ))) GO_STATIC_PACKAGES = $(GO_PROJECT)/cmd/provider GO_SUBDIRS += cmd internal apis GO111MODULE = on -GOLANGCILINT_VERSION = 1.51.2 +GOLANGCILINT_VERSION = 1.61.0 -include build/makelib/golang.mk # ==================================================================================== From f3794f09affaf9daf43c8574090f56122195647c Mon Sep 17 00:00:00 2001 From: Tim <32556895+Avarei@users.noreply.github.com> Date: Sun, 15 Sep 2024 13:00:29 +0200 Subject: [PATCH 4/6] add lint ignore for not setting minTLSversion Signed-off-by: Tim <32556895+Avarei@users.noreply.github.com> --- internal/clients/http/client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/clients/http/client.go b/internal/clients/http/client.go index 2126c79..183373f 100644 --- a/internal/clients/http/client.go +++ b/internal/clients/http/client.go @@ -134,6 +134,7 @@ func toJSON(request HttpRequest) string { } func tlsConfig(certPEMBlock, keyPEMBlock, caPEMBlock []byte, insecureSkipVerify bool) (*tls.Config, error) { + // #nosec G402 tlsConfig := &tls.Config{} if len(certPEMBlock) > 0 && len(keyPEMBlock) > 0 { certificate, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock) From da40291d0258ca64938e669185e4f7fc80419165 Mon Sep 17 00:00:00 2001 From: Tim <32556895+Avarei@users.noreply.github.com> Date: Sun, 15 Sep 2024 13:01:01 +0200 Subject: [PATCH 5/6] remove obsolete conversion Signed-off-by: Tim <32556895+Avarei@users.noreply.github.com> --- internal/clients/http/client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/clients/http/client_test.go b/internal/clients/http/client_test.go index a44a9f8..4cb9b27 100644 --- a/internal/clients/http/client_test.go +++ b/internal/clients/http/client_test.go @@ -90,7 +90,7 @@ func Test_newClient(t *testing.T) { tc := tc // Create local copies of loop variables t.Run(name, func(t *testing.T) { - client, gotErr := NewClient(log, 10*time.Second, []byte(tc.args.cert), []byte(tc.args.key), []byte(tc.args.ca), tc.args.insecure) + client, gotErr := NewClient(log, 10*time.Second, tc.args.cert, tc.args.key, tc.args.ca, tc.args.insecure) if diff := cmp.Diff(tc.want.newClientErr, gotErr, test.EquateErrors()); diff != "" { t.Fatalf("NewClient(...): -want error, +got error: %s", diff) } From ac2488ef8145661edcaa9d3c4539e2d49447c53b Mon Sep 17 00:00:00 2001 From: Tim <32556895+Avarei@users.noreply.github.com> Date: Sun, 15 Sep 2024 15:15:53 +0200 Subject: [PATCH 6/6] add minimal example Signed-off-by: Tim <32556895+Avarei@users.noreply.github.com> --- examples/sample/disposablerequest_mtls.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 examples/sample/disposablerequest_mtls.yaml diff --git a/examples/sample/disposablerequest_mtls.yaml b/examples/sample/disposablerequest_mtls.yaml new file mode 100644 index 0000000..baa25e4 --- /dev/null +++ b/examples/sample/disposablerequest_mtls.yaml @@ -0,0 +1,15 @@ +apiVersion: http.crossplane.io/v1alpha2 +kind: DisposableRequest +metadata: + name: mtls-get +spec: + deletionPolicy: Orphan + forProvider: + tlsSecretRef: # reference to a secret containing tls.crt, tls.key and ca.crt + name: client-tls + namespace: default + method: GET + url: https://example.com + waitTimeout: 5m + providerConfigRef: + name: http-conf