package decrypt_test import ( "context" "crypto/tls" "errors" "net/http" "net/http/httptest" "strings" "testing" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace/noop" "golang.org/x/net/nettest" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/metadata" decryptv1beta1 "github.com/grafana/grafana/apps/secret/decrypt/v1beta1" secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" decryptcontracts "github.com/grafana/grafana/apps/secret/pkg/decrypt" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/decrypt" "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/services/authn/clients" "github.com/grafana/grafana/pkg/setting" ) func TestDecryptService(t *testing.T) { t.Parallel() ctx := context.Background() tracer := noop.NewTracerProvider().Tracer("test") t.Run("when there are only errors from the storage, the service returns them in the map", func(t *testing.T) { t.Parallel() mockErr := errors.New("mock error") mockStorage := &mockDecryptStorage{} mockStorage.On("Decrypt", mock.Anything, mock.Anything, mock.Anything).Return(secretv1beta1.ExposedSecureValue(""), mockErr) decryptedValuesResp := map[string]decryptcontracts.DecryptResult{ "secure-value-1": decryptcontracts.NewDecryptResultErr(mockErr), } cfg := setting.NewCfg() decryptService, err := decrypt.ProvideDecryptService(cfg, tracer, mockStorage) require.NoError(t, err) resp, err := decryptService.Decrypt(ctx, "svc-name", "default", "secure-value-1") require.NotNil(t, resp) require.NoError(t, err) require.EqualValues(t, decryptedValuesResp, resp) }) t.Run("when there is no error from the storage, it returns a map of the decrypted values", func(t *testing.T) { t.Parallel() mockStorage := &mockDecryptStorage{} // Set up the mock to return a different value for each name in the test exposedSecureValue1 := secretv1beta1.NewExposedSecureValue("value1") exposedSecureValue2 := secretv1beta1.NewExposedSecureValue("value2") mockStorage.On("Decrypt", mock.Anything, xkube.Namespace("default"), "secure-value-1"). Return(exposedSecureValue1, nil) mockStorage.On("Decrypt", mock.Anything, xkube.Namespace("default"), "secure-value-2"). Return(exposedSecureValue2, nil) decryptedValuesResp := map[string]decryptcontracts.DecryptResult{ "secure-value-1": decryptcontracts.NewDecryptResultValue(&exposedSecureValue1), "secure-value-2": decryptcontracts.NewDecryptResultValue(&exposedSecureValue2), } cfg := setting.NewCfg() decryptService, err := decrypt.ProvideDecryptService(cfg, tracer, mockStorage) require.NoError(t, err) resp, err := decryptService.Decrypt(ctx, "svc-name", "default", "secure-value-1", "secure-value-2") require.NotNil(t, resp) require.NoError(t, err) require.EqualValues(t, decryptedValuesResp, resp) }) t.Run("when there is an error from the storage, the service returns a map of errors and decrypted values", func(t *testing.T) { t.Parallel() mockErr := errors.New("mock error") mockStorage := &mockDecryptStorage{} exposedSecureValue := secretv1beta1.NewExposedSecureValue("value") mockStorage.On("Decrypt", mock.Anything, xkube.Namespace("default"), "secure-value-1"). Return(exposedSecureValue, nil) mockStorage.On("Decrypt", mock.Anything, xkube.Namespace("default"), "secure-value-2"). Return(secretv1beta1.ExposedSecureValue(""), mockErr) decryptedValuesResp := map[string]decryptcontracts.DecryptResult{ "secure-value-1": decryptcontracts.NewDecryptResultValue(&exposedSecureValue), "secure-value-2": decryptcontracts.NewDecryptResultErr(mockErr), } cfg := setting.NewCfg() cfg.SecretsManagement.GrpcClientEnable = false decryptService, err := decrypt.ProvideDecryptService(cfg, tracer, mockStorage) require.NoError(t, err) resp, err := decryptService.Decrypt(ctx, "svc-name", "default", "secure-value-1", "secure-value-2") require.NotNil(t, resp) require.NoError(t, err) require.EqualValues(t, decryptedValuesResp, resp) }) t.Run("when grpc server is enabled but token exchange config is missing, it returns an error", func(t *testing.T) { t.Parallel() cfg := setting.NewCfg() cfg.SecretsManagement.GrpcClientEnable = true cfg.SecretsManagement.GrpcServerAddress = "127.0.0.1:10000" _, err := decrypt.ProvideDecryptService(cfg, tracer, nil) require.Error(t, err) require.Contains(t, err.Error(), "grpc_client_authentication.token and grpc_client_authentication.token_exchange_url are required") }) t.Run("when grpc server is enabled but storage address is missing, it returns an error", func(t *testing.T) { t.Parallel() cfg := setting.NewCfg() cfg.SecretsManagement.GrpcClientEnable = true _, err := decrypt.ProvideDecryptService(cfg, tracer, nil) require.Error(t, err) require.Contains(t, err.Error(), "grpc_server_address is required") }) t.Run("happy path with grpc+tls server with fake toke exchanger and server", func(t *testing.T) { t.Parallel() respTokenExchanged := "test-token" tokenExchangeServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { response := `{ "data": { "token": "` + respTokenExchanged + `" } }` w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(response)) })) t.Cleanup(tokenExchangeServer.Close) // Set up gRPC Server with TLS listener, err := nettest.NewLocalListener("tcp") require.NoError(t, err) certPaths := testutils.CreateX509TestDir(t) serverCert, err := tls.LoadX509KeyPair(certPaths.ServerCert, certPaths.ServerKey) require.NoError(t, err) tlsConfig := &tls.Config{ Certificates: []tls.Certificate{serverCert}, ClientAuth: tls.NoClientCert, InsecureSkipVerify: false, ServerName: "localhost", } grpcServer := grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsConfig))) t.Cleanup(grpcServer.Stop) decryptServer := &mockDecryptServer{} decryptServer.On("DecryptSecureValues", mock.Anything, mock.Anything).Return( &decryptv1beta1.SecureValueDecryptResponseCollection{ DecryptedValues: map[string]*decryptv1beta1.Result{ "secure-value-1": { Result: &decryptv1beta1.Result_Value{Value: "decrypted-value-1"}, }, }, }, nil, ) decryptv1beta1.RegisterSecureValueDecrypterServer(grpcServer, decryptServer) go func() { _ = grpcServer.Serve(listener) <-t.Context().Done() }() // Populate configuration with gRPC+TLS options and mock token exchanger grafanaSvcIdentity := "svc-identity-decrypter" namespace := "stacks-1234" cfg := setting.NewCfg() cfg.SecretsManagement.GrpcClientEnable = true cfg.SecretsManagement.GrpcServerAddress = listener.Addr().String() cfg.SecretsManagement.GrpcGrafanaServiceName = grafanaSvcIdentity cfg.SecretsManagement.GrpcServerUseTLS = true cfg.SecretsManagement.GrpcServerTLSServerName = "localhost" cfg.SecretsManagement.GrpcServerTLSSkipVerify = false grpcClientAuth := cfg.Raw.Section("grpc_client_authentication") _, err = grpcClientAuth.NewKey("token", "test-token") require.NoError(t, err) _, err = grpcClientAuth.NewKey("token_exchange_url", tokenExchangeServer.URL) require.NoError(t, err) _, err = grpcClientAuth.NewKey("token_namespace", namespace) require.NoError(t, err) apiServer := cfg.Raw.Section("grafana-apiserver") _, err = apiServer.NewKey("apiservice_ca_bundle_file", certPaths.CA) require.NoError(t, err) // Create and test decryption, using the mock grpc server as we dont test the business logic here decryptService, err := decrypt.ProvideDecryptService(cfg, tracer, nil) require.NoError(t, err) require.NotNil(t, decryptService) svcIdentity := "provsysoning-test" result, err := decryptService.Decrypt(t.Context(), svcIdentity, namespace, "secure-value-1") require.NoError(t, err) require.NotNil(t, result) require.Len(t, result, 1) require.NotEmpty(t, result["secure-value-1"]) requestContext := decryptServer.Calls[0].Arguments[0].(context.Context) md, ok := metadata.FromIncomingContext(requestContext) require.True(t, ok) require.NotEmpty(t, md) require.Equal(t, svcIdentity, md[strings.ToLower(contracts.HeaderGrafanaServiceIdentityName)][0]) require.Equal(t, respTokenExchanged, md[strings.ToLower(clients.ExtJWTAuthenticationHeaderName)][0]) }) } type mockDecryptStorage struct { mock.Mock } func (m *mockDecryptStorage) Decrypt(ctx context.Context, namespace xkube.Namespace, name string) (secretv1beta1.ExposedSecureValue, error) { args := m.Called(ctx, namespace, name) return args.Get(0).(secretv1beta1.ExposedSecureValue), args.Error(1) } type mockDecryptServer struct { mock.Mock } var _ decryptv1beta1.SecureValueDecrypterServer = (*mockDecryptServer)(nil) func (m *mockDecryptServer) DecryptSecureValues(ctx context.Context, req *decryptv1beta1.SecureValueDecryptRequest) (*decryptv1beta1.SecureValueDecryptResponseCollection, error) { args := m.Called(ctx, req) return args.Get(0).(*decryptv1beta1.SecureValueDecryptResponseCollection), args.Error(1) }