github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/pkg/certloader/loader_test.go (about)

     1  package certloader
     2  
     3  import (
     4  	"context"
     5  	"crypto/rand"
     6  	"crypto/rsa"
     7  	"crypto/x509"
     8  	"crypto/x509/pkix"
     9  	"encoding/pem"
    10  	"math/big"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/kyma-incubator/compass/components/director/pkg/certloader/automock"
    15  	"github.com/stretchr/testify/assert"
    16  	"github.com/stretchr/testify/mock"
    17  	"github.com/stretchr/testify/require"
    18  	v1 "k8s.io/api/core/v1"
    19  	"k8s.io/apimachinery/pkg/runtime"
    20  	"k8s.io/apimachinery/pkg/watch"
    21  )
    22  
    23  const (
    24  	secretName    = "secretName"
    25  	secretCertKey = "tls.crt"
    26  	secretKeyKey  = "tls.key"
    27  	testCN        = "test-common-name"
    28  )
    29  
    30  type testWatch struct {
    31  	events chan watch.Event
    32  }
    33  
    34  func (tw *testWatch) close() {
    35  	close(tw.events)
    36  }
    37  
    38  func (tw *testWatch) putEvent(ev watch.Event) {
    39  	tw.events <- ev
    40  }
    41  
    42  func (tw *testWatch) Stop() {}
    43  func (tw *testWatch) ResultChan() <-chan watch.Event {
    44  	return tw.events
    45  }
    46  
    47  func Test_CertificateLoaderWatch(t *testing.T) {
    48  	config := Config{
    49  		ExternalClientCertSecret:  "namespace/resource-name",
    50  		ExternalClientCertCertKey: "tls.crt",
    51  		ExternalClientCertKeyKey:  "tls.key",
    52  		ExtSvcClientCertSecret:    "namespace/resource-name",
    53  		ExtSvcClientCertCertKey:   "tls.crt",
    54  		ExtSvcClientCertKeyKey:    "tls.key"}
    55  
    56  	t.Run("should insert secret data on add event", func(t *testing.T) {
    57  		// given
    58  		ctx, cancel := context.WithCancel(context.Background())
    59  		defer cancel()
    60  		cache, watcher, secretManagerMock := preparation(ctx, 1, config)
    61  		certBytes, keyBytes := generateTestCertAndKey(t, testCN)
    62  
    63  		// when
    64  		watcher.putEvent(watch.Event{
    65  			Type: watch.Added,
    66  			Object: &v1.Secret{
    67  				Data: map[string][]byte{secretCertKey: certBytes, secretKeyKey: keyBytes},
    68  			},
    69  		})
    70  
    71  		// then
    72  		assert.Eventually(t, func() bool {
    73  			tlsCert := cache.Get()
    74  			require.NotNil(t, tlsCert)
    75  			return true
    76  		}, 2*time.Second, 100*time.Millisecond)
    77  		cancel()
    78  		assert.Eventually(t, func() bool {
    79  			<-ctx.Done()
    80  			return true
    81  		}, time.Second, 100*time.Millisecond)
    82  		secretManagerMock.AssertExpectations(t)
    83  	})
    84  
    85  	t.Run("should insert secret data on modify event", func(t *testing.T) {
    86  		// given
    87  		ctx, cancel := context.WithCancel(context.Background())
    88  		defer cancel()
    89  		cache, watcher, secretManagerMock := preparation(ctx, 1, config)
    90  		certBytes, keyBytes := generateTestCertAndKey(t, testCN)
    91  
    92  		// when
    93  		watcher.putEvent(watch.Event{
    94  			Type: watch.Modified,
    95  			Object: &v1.Secret{
    96  				Data: map[string][]byte{secretCertKey: certBytes, secretKeyKey: keyBytes},
    97  			},
    98  		})
    99  
   100  		// then
   101  		assert.Eventually(t, func() bool {
   102  			tlsCert := cache.Get()
   103  			require.NotNil(t, tlsCert)
   104  			return true
   105  		}, 2*time.Second, 100*time.Millisecond)
   106  		cancel()
   107  		assert.Eventually(t, func() bool {
   108  			<-ctx.Done()
   109  			return true
   110  		}, time.Second, 100*time.Millisecond)
   111  		secretManagerMock.AssertExpectations(t)
   112  	})
   113  
   114  	t.Run("should not insert secret data if the event object is not secret", func(t *testing.T) {
   115  		// given
   116  		ctx, cancel := context.WithCancel(context.Background())
   117  		defer cancel()
   118  		cache, watcher, secretManagerMock := preparation(ctx, 1, config)
   119  
   120  		// when
   121  		watcher.putEvent(watch.Event{
   122  			Type:   watch.Added,
   123  			Object: &runtime.Unknown{},
   124  		})
   125  
   126  		// then
   127  		assert.Eventually(t, func() bool {
   128  			tlsCert := cache.Get()
   129  			require.Nil(t, tlsCert["resource-name"])
   130  			return true
   131  		}, 2*time.Second, 100*time.Millisecond)
   132  		cancel()
   133  		assert.Eventually(t, func() bool {
   134  			<-ctx.Done()
   135  			return true
   136  		}, time.Second, 100*time.Millisecond)
   137  		secretManagerMock.AssertExpectations(t)
   138  	})
   139  
   140  	t.Run("should return empty cache after delete event", func(t *testing.T) {
   141  		// given
   142  		ctx, cancel := context.WithCancel(context.Background())
   143  		defer cancel()
   144  		cache, watcher, secretManagerMock := preparation(ctx, 1, config)
   145  		certBytes, keyBytes := generateTestCertAndKey(t, testCN)
   146  
   147  		// when
   148  		watcher.putEvent(watch.Event{
   149  			Type: watch.Added,
   150  			Object: &v1.Secret{
   151  				Data: map[string][]byte{secretCertKey: certBytes, secretKeyKey: keyBytes},
   152  			},
   153  		})
   154  
   155  		assert.Eventually(t, func() bool {
   156  			tlsCert := cache.Get()
   157  			require.NotNil(t, tlsCert)
   158  			return true
   159  		}, 2*time.Second, 100*time.Millisecond)
   160  
   161  		watcher.putEvent(watch.Event{
   162  			Type: watch.Deleted,
   163  		})
   164  
   165  		// then
   166  		assert.Eventually(t, func() bool {
   167  			tlsCert := cache.Get()
   168  			require.Nil(t, tlsCert["resource-name"])
   169  			return true
   170  		}, 2*time.Second, 100*time.Millisecond)
   171  		cancel()
   172  		assert.Eventually(t, func() bool {
   173  			<-ctx.Done()
   174  			return true
   175  		}, time.Second, 100*time.Millisecond)
   176  		secretManagerMock.AssertExpectations(t)
   177  	})
   178  
   179  	t.Run("should try reconnect when there is error event", func(t *testing.T) {
   180  		// given
   181  		ctx, cancel := context.WithCancel(context.Background())
   182  		defer cancel()
   183  		cache, watcher, secretManagerMock := preparation(ctx, 2, config)
   184  		certBytes, keyBytes := generateTestCertAndKey(t, testCN)
   185  
   186  		// when
   187  		watcher.putEvent(watch.Event{
   188  			Type: watch.Error,
   189  		})
   190  
   191  		watcher.putEvent(watch.Event{
   192  			Type: watch.Added,
   193  			Object: &v1.Secret{
   194  				Data: map[string][]byte{secretCertKey: certBytes, secretKeyKey: keyBytes},
   195  			},
   196  		})
   197  
   198  		// then
   199  		assert.Eventually(t, func() bool {
   200  			tlsCert := cache.Get()
   201  			require.NotNil(t, tlsCert)
   202  			return true
   203  		}, 2*time.Second, 100*time.Millisecond)
   204  
   205  		watcher.putEvent(watch.Event{
   206  			Type: watch.Deleted,
   207  		})
   208  		cancel()
   209  		assert.Eventually(t, func() bool {
   210  			<-ctx.Done()
   211  			return true
   212  		}, time.Second, 100*time.Millisecond)
   213  		secretManagerMock.AssertExpectations(t)
   214  	})
   215  
   216  	t.Run("should try reconnect when event channel is closed", func(t *testing.T) {
   217  		// given
   218  		ctx, cancel := context.WithCancel(context.Background())
   219  		defer cancel()
   220  		cache, watcher, secretManagerMock := preparation(ctx, 2, config)
   221  		certBytes, keyBytes := generateTestCertAndKey(t, testCN)
   222  
   223  		// when
   224  		watcher.close()
   225  		newWatcher := &testWatch{
   226  			events: make(chan watch.Event, 50),
   227  		}
   228  
   229  		secretManagerMock.On("Watch", mock.Anything, mock.AnythingOfType("v1.ListOptions")).Return(newWatcher, nil).Once()
   230  
   231  		newWatcher.putEvent(watch.Event{
   232  			Type: watch.Added,
   233  			Object: &v1.Secret{
   234  				Data: map[string][]byte{secretCertKey: certBytes, secretKeyKey: keyBytes},
   235  			},
   236  		})
   237  
   238  		// then
   239  		assert.Eventually(t, func() bool {
   240  			tlsCert := cache.Get()
   241  			require.NotNil(t, tlsCert)
   242  			return true
   243  		}, 2*time.Second, 100*time.Millisecond)
   244  		cancel()
   245  		assert.Eventually(t, func() bool {
   246  			<-ctx.Done()
   247  			return true
   248  		}, time.Second, 100*time.Millisecond)
   249  		secretManagerMock.AssertExpectations(t)
   250  	})
   251  }
   252  
   253  func Test_CertificateParsing(t *testing.T) {
   254  	ctx := context.Background()
   255  	config := Config{
   256  		ExternalClientCertCertKey: "tls.crt",
   257  		ExternalClientCertKeyKey:  "tls.key",
   258  	}
   259  	certBytes, keyBytes := generateTestCertAndKey(t, testCN)
   260  	invalidCert := "-----BEGIN CERTIFICATE-----\naZOCUHlJ1wKwnYiLnOofB1xyIUZhVLaJy7Ob\n-----END CERTIFICATE-----\n"
   261  	invalidKey := "-----BEGIN RSA PRIVATE KEY-----\n7qFmWkbkOAM9CUPx5RwSRt45oxlQjvDniZALWqbYxgO5f8cYZsEAyOU1n2DXgiei\n-----END RSA PRIVATE KEY-----\n"
   262  
   263  	testCases := []struct {
   264  		Name             string
   265  		SecretData       map[string][]byte
   266  		ExpectedErrorMsg string
   267  		Cfg              Config
   268  	}{
   269  		{
   270  			Name:       "Successfully get certificate from cache",
   271  			SecretData: map[string][]byte{secretCertKey: certBytes, secretKeyKey: keyBytes},
   272  			Cfg:        config,
   273  		},
   274  		{
   275  			Name:       "Successfully get ext svc certificate from cache",
   276  			SecretData: map[string][]byte{secretCertKey: certBytes, secretKeyKey: keyBytes},
   277  			Cfg: Config{
   278  				ExtSvcClientCertCertKey: "tls.crt",
   279  				ExtSvcClientCertKeyKey:  "tls.key",
   280  			},
   281  		},
   282  		{
   283  			Name:             "Error when secret data is empty",
   284  			SecretData:       map[string][]byte{},
   285  			ExpectedErrorMsg: "There is no certificate data provided",
   286  			Cfg:              config,
   287  		},
   288  		{
   289  			Name:             "Error when certificate data is invalid",
   290  			SecretData:       map[string][]byte{secretCertKey: []byte("invalid"), secretKeyKey: []byte("invalid")},
   291  			ExpectedErrorMsg: "Error while decoding certificate pem block",
   292  			Cfg:              config,
   293  		},
   294  		{
   295  			Name:             "Error when parsing certificate",
   296  			SecretData:       map[string][]byte{secretCertKey: []byte(invalidCert), secretKeyKey: []byte("invalid")},
   297  			ExpectedErrorMsg: "malformed certificate",
   298  			Cfg:              config,
   299  		},
   300  		{
   301  			Name:             "Error when private key is invalid",
   302  			SecretData:       map[string][]byte{secretCertKey: certBytes, secretKeyKey: []byte("invalid")},
   303  			ExpectedErrorMsg: "Error while decoding private key pem block",
   304  			Cfg:              config,
   305  		},
   306  		{
   307  			Name:             "Error when parsing private key",
   308  			SecretData:       map[string][]byte{secretCertKey: certBytes, secretKeyKey: []byte(invalidKey)},
   309  			ExpectedErrorMsg: "structure error",
   310  			Cfg:              config,
   311  		},
   312  	}
   313  
   314  	for _, testCase := range testCases {
   315  		t.Run(testCase.Name, func(t *testing.T) {
   316  			tlsCert, err := parseCertificate(ctx, testCase.SecretData, testCase.Cfg)
   317  
   318  			if testCase.ExpectedErrorMsg != "" {
   319  				require.Error(t, err)
   320  				require.Contains(t, err.Error(), testCase.ExpectedErrorMsg)
   321  				require.Nil(t, tlsCert)
   322  			} else {
   323  				require.NoError(t, err)
   324  				require.NotNil(t, tlsCert)
   325  			}
   326  		})
   327  	}
   328  }
   329  
   330  func preparation(ctx context.Context, number int, config Config) (Cache, *testWatch, *automock.Manager) {
   331  	cache := NewCertificateCache()
   332  	watcher := &testWatch{
   333  		events: make(chan watch.Event, 50),
   334  	}
   335  	secretManagerMock := &automock.Manager{}
   336  	secretManagerMock.On("Watch", mock.Anything, mock.AnythingOfType("v1.ListOptions")).Return(watcher, nil).Times(number)
   337  	loader := NewCertificateLoader(config, cache, []Manager{secretManagerMock}, []string{secretName}, time.Millisecond)
   338  	go loader.Run(ctx)
   339  
   340  	return cache, watcher, secretManagerMock
   341  }
   342  
   343  func generateTestCertAndKey(t *testing.T, commonName string) (crtPem, keyPem []byte) {
   344  	clientKey, err := rsa.GenerateKey(rand.Reader, 2048)
   345  	require.NoError(t, err)
   346  
   347  	template := &x509.Certificate{
   348  		IsCA:         true,
   349  		SerialNumber: big.NewInt(1234),
   350  		Subject: pkix.Name{
   351  			CommonName: commonName,
   352  		},
   353  		NotBefore:   time.Now(),
   354  		NotAfter:    time.Now().Add(time.Hour),
   355  		KeyUsage:    x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
   356  		ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
   357  	}
   358  
   359  	parent := template
   360  	certRaw, err := x509.CreateCertificate(rand.Reader, template, parent, &clientKey.PublicKey, clientKey)
   361  	require.NoError(t, err)
   362  
   363  	crtPem = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certRaw})
   364  	keyPem = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(clientKey)})
   365  
   366  	return
   367  }