Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add http client and tlsSecretRef #25

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

# ====================================================================================
Expand Down
3 changes: 3 additions & 0 deletions apis/disposablerequest/v1alpha2/disposablerequest_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"'
Expand Down
1 change: 1 addition & 0 deletions apis/disposablerequest/v1alpha2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions apis/request/v1alpha2/request_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand Down
1 change: 1 addition & 0 deletions apis/request/v1alpha2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions examples/sample/disposablerequest_mtls.yaml
Original file line number Diff line number Diff line change
@@ -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
62 changes: 45 additions & 17 deletions internal/clients/http/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
Expand All @@ -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 {
Expand All @@ -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{
Expand All @@ -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,
Expand Down Expand Up @@ -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
}

Expand All @@ -128,3 +132,27 @@ func toJSON(request HttpRequest) string {

return string(jsonBytes)
}

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)
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
}
162 changes: 162 additions & 0 deletions internal/clients/http/client_test.go
Original file line number Diff line number Diff line change
@@ -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, 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)
}
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
}
21 changes: 18 additions & 3 deletions internal/controller/disposablerequest/disposablerequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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) {
Expand All @@ -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)
}
Expand Down Expand Up @@ -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{
Expand Down
Loading