Secrets: Provide grpc and local inline secure value client (#109179)

* TestUtils: Add helper to create certs

* InlineSecureValue: Move to its own package rather than service

* Settings: Rename decrypt server configs to grpc prefixed as it is more generic

* InlineSecureValue: Add gRPC client

* InlineSecureValue: Add service provider grpc/local depending on setting.Cfg

* Wire: Add ProvideInlineSecureValueService to basic wireset

* InlineSecureValue: Re-export in root secret pkg and generate mocks for interface

* Settings: Unify decrypt+inline setting into generic grpc client
This commit is contained in:
Matheus Macabu
2025-08-06 09:04:55 +02:00
committed by GitHub
parent f3b8a891af
commit fc117afebc
14 changed files with 802 additions and 153 deletions
@@ -76,8 +76,8 @@ func (s *secretsService) Encrypt(ctx context.Context, namespace, name string, da
}
decrypters := []string{svcName}
if s.cfg.SecretsManagement.DecryptGrafanaServiceName != "" {
decrypters = append(decrypters, s.cfg.SecretsManagement.DecryptGrafanaServiceName)
if s.cfg.SecretsManagement.GrpcGrafanaServiceName != "" {
decrypters = append(decrypters, s.cfg.SecretsManagement.GrpcGrafanaServiceName)
}
// Create the secret directly as unstructured
+9 -22
View File
@@ -4,7 +4,6 @@ import (
"fmt"
authnlib "github.com/grafana/authlib/authn"
claims "github.com/grafana/authlib/types"
"go.opentelemetry.io/otel/trace"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
@@ -13,24 +12,15 @@ import (
)
func ProvideDecryptService(cfg *setting.Cfg, tracer trace.Tracer, decryptStorage contracts.DecryptStorage) (contracts.DecryptService, error) {
switch cfg.SecretsManagement.DecryptServerType {
case "grpc":
if cfg.SecretsManagement.GrpcClientEnable {
grpcClientConfig := grpcutils.ReadGrpcClientConfig(cfg)
if cfg.SecretsManagement.DecryptServerAddress == "" {
return nil, fmt.Errorf("decrypt_server_address is required when decrypt_server_type is grpc")
if cfg.SecretsManagement.GrpcServerAddress == "" {
return nil, fmt.Errorf("grpc_server_address is required when grpc client is enabled")
}
if grpcClientConfig.Token == "" || grpcClientConfig.TokenExchangeURL == "" {
return nil, fmt.Errorf("grpc_client_authentication.token and grpc_client_authentication.token_exchange_url are required when secrets_manager.decrypt_server_type is grpc")
}
nsInfo, err := claims.ParseNamespace(grpcClientConfig.TokenNamespace)
if err != nil {
return nil, fmt.Errorf("failed to parse token namespace %v: %w", grpcClientConfig.TokenNamespace, err)
}
if nsInfo.OrgID < 1 {
return nil, fmt.Errorf("invalid token namepsace %v", grpcClientConfig.TokenNamespace)
return nil, fmt.Errorf("grpc_client_authentication.token and grpc_client_authentication.token_exchange_url are required when grpc client is enabled")
}
tokenExchangeClient, err := authnlib.NewTokenExchangeClient(authnlib.TokenExchangeConfig{
@@ -43,22 +33,19 @@ func ProvideDecryptService(cfg *setting.Cfg, tracer trace.Tracer, decryptStorage
tlsConfig := readTLSFromConfig(cfg)
client, err := NewGRPCDecryptClientWithTLS(tokenExchangeClient, tracer, cfg.SecretsManagement.DecryptServerAddress, tlsConfig)
client, err := NewGRPCDecryptClientWithTLS(tokenExchangeClient, tracer, cfg.SecretsManagement.GrpcServerAddress, tlsConfig)
if err != nil {
return nil, fmt.Errorf("failed to create grpc decrypt client: %w", err)
}
return client, nil
case "local", "":
return NewLocalDecryptClient(decryptStorage)
}
return nil, fmt.Errorf("unsupported storage type: %s", cfg.SecretsManagement.DecryptServerType)
return NewLocalDecryptClient(decryptStorage)
}
func readTLSFromConfig(cfg *setting.Cfg) TLSConfig {
if !cfg.SecretsManagement.DecryptServerUseTLS {
if !cfg.SecretsManagement.GrpcServerUseTLS {
return TLSConfig{
UseTLS: false,
InsecureSkipVerify: true,
@@ -72,7 +59,7 @@ func readTLSFromConfig(cfg *setting.Cfg) TLSConfig {
CertFile: apiServer.Key("proxy_client_cert_file").MustString(""),
KeyFile: apiServer.Key("proxy_client_key_file").MustString(""),
CAFile: apiServer.Key("apiservice_ca_bundle_file").MustString(""),
ServerName: cfg.SecretsManagement.DecryptServerTLSServerName,
InsecureSkipVerify: cfg.SecretsManagement.DecryptServerTLSSkipVerify,
ServerName: cfg.SecretsManagement.GrpcServerTLSServerName,
InsecureSkipVerify: cfg.SecretsManagement.GrpcServerTLSSkipVerify,
}
}
@@ -1,4 +1,4 @@
package decrypt
package decrypt_test
import (
"context"
@@ -11,7 +11,6 @@ import (
decryptv1beta1 "github.com/grafana/grafana/apps/secret/decrypt/v1beta1"
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
"github.com/madflojo/testcerts"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace/noop"
@@ -21,6 +20,8 @@ import (
"google.golang.org/grpc/metadata"
"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"
@@ -43,9 +44,8 @@ func TestDecryptService(t *testing.T) {
}
cfg := setting.NewCfg()
cfg.SecretsManagement.DecryptServerType = "local"
decryptService, err := ProvideDecryptService(cfg, tracer, mockStorage)
decryptService, err := decrypt.ProvideDecryptService(cfg, tracer, mockStorage)
require.NoError(t, err)
resp, err := decryptService.Decrypt(ctx, "svc-name", "default", []string{"secure-value-1"})
@@ -72,9 +72,8 @@ func TestDecryptService(t *testing.T) {
}
cfg := setting.NewCfg()
cfg.SecretsManagement.DecryptServerType = "local"
decryptService, err := ProvideDecryptService(cfg, tracer, mockStorage)
decryptService, err := decrypt.ProvideDecryptService(cfg, tracer, mockStorage)
require.NoError(t, err)
resp, err := decryptService.Decrypt(ctx, "svc-name", "default", []string{"secure-value-1", "secure-value-2"})
@@ -100,9 +99,9 @@ func TestDecryptService(t *testing.T) {
}
cfg := setting.NewCfg()
cfg.SecretsManagement.DecryptServerType = "local"
cfg.SecretsManagement.GrpcClientEnable = false
decryptService, err := ProvideDecryptService(cfg, tracer, mockStorage)
decryptService, err := decrypt.ProvideDecryptService(cfg, tracer, mockStorage)
require.NoError(t, err)
resp, err := decryptService.Decrypt(ctx, "svc-name", "default", []string{"secure-value-1", "secure-value-2"})
@@ -111,39 +110,27 @@ func TestDecryptService(t *testing.T) {
require.EqualValues(t, decryptedValuesResp, resp)
})
t.Run("when storage type is unsupported, it returns an error", func(t *testing.T) {
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.DecryptServerType = "unsupported"
cfg.SecretsManagement.GrpcClientEnable = true
cfg.SecretsManagement.GrpcServerAddress = "127.0.0.1:10000"
decryptService, err := ProvideDecryptService(cfg, tracer, nil)
require.Error(t, err)
require.Contains(t, err.Error(), "unsupported storage type")
require.Nil(t, decryptService)
})
t.Run("when storage type is grpc but token exchange config is missing, it returns an error", func(t *testing.T) {
t.Parallel()
cfg := setting.NewCfg()
cfg.SecretsManagement.DecryptServerType = "grpc"
cfg.SecretsManagement.DecryptServerAddress = "127.0.0.1:10000"
_, err := ProvideDecryptService(cfg, tracer, nil)
_, 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 storage type is grpc but storage address is missing, it returns an error", func(t *testing.T) {
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.DecryptServerType = "grpc"
cfg.SecretsManagement.GrpcClientEnable = true
_, err := ProvideDecryptService(cfg, tracer, nil)
_, err := decrypt.ProvideDecryptService(cfg, tracer, nil)
require.Error(t, err)
require.Contains(t, err.Error(), "decrypt_server_address is required")
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) {
@@ -165,9 +152,9 @@ func TestDecryptService(t *testing.T) {
listener, err := nettest.NewLocalListener("tcp")
require.NoError(t, err)
certPaths := createX509TestDir(t)
certPaths := testutils.CreateX509TestDir(t)
serverCert, err := tls.LoadX509KeyPair(certPaths.serverCert, certPaths.serverKey)
serverCert, err := tls.LoadX509KeyPair(certPaths.ServerCert, certPaths.ServerKey)
require.NoError(t, err)
tlsConfig := &tls.Config{
@@ -204,12 +191,12 @@ func TestDecryptService(t *testing.T) {
namespace := "stacks-1234"
cfg := setting.NewCfg()
cfg.SecretsManagement.DecryptServerType = "grpc"
cfg.SecretsManagement.DecryptServerAddress = listener.Addr().String()
cfg.SecretsManagement.DecryptGrafanaServiceName = grafanaSvcIdentity
cfg.SecretsManagement.DecryptServerUseTLS = true
cfg.SecretsManagement.DecryptServerTLSServerName = "localhost"
cfg.SecretsManagement.DecryptServerTLSSkipVerify = false
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")
@@ -220,15 +207,15 @@ func TestDecryptService(t *testing.T) {
require.NoError(t, err)
apiServer := cfg.Raw.Section("grafana-apiserver")
_, err = apiServer.NewKey("proxy_client_cert_file", certPaths.clientCert)
_, err = apiServer.NewKey("proxy_client_cert_file", certPaths.ClientCert)
require.NoError(t, err)
_, err = apiServer.NewKey("proxy_client_key_file", certPaths.clientKey)
_, err = apiServer.NewKey("proxy_client_key_file", certPaths.ClientKey)
require.NoError(t, err)
_, err = apiServer.NewKey("apiservice_ca_bundle_file", certPaths.ca)
_, 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 := ProvideDecryptService(cfg, tracer, nil)
decryptService, err := decrypt.ProvideDecryptService(cfg, tracer, nil)
require.NoError(t, err)
require.NotNil(t, decryptService)
@@ -271,40 +258,3 @@ func (m *mockDecryptServer) DecryptSecureValues(ctx context.Context, req *decryp
args := m.Called(ctx, req)
return args.Get(0).(*decryptv1beta1.SecureValueDecryptResponseCollection), args.Error(1)
}
type certPaths struct {
clientCert string
clientKey string
serverCert string
serverKey string
ca string
}
func createX509TestDir(t *testing.T) certPaths {
t.Helper()
tmpDir := t.TempDir()
ca := testcerts.NewCA()
caCertFile, _, err := ca.ToTempFile(tmpDir)
require.NoError(t, err)
serverKp, err := ca.NewKeyPair("localhost")
require.NoError(t, err)
serverCertFile, serverKeyFile, err := serverKp.ToTempFile(tmpDir)
require.NoError(t, err)
clientKp, err := ca.NewKeyPair()
require.NoError(t, err)
clientCertFile, clientKeyFile, err := clientKp.ToTempFile(tmpDir)
require.NoError(t, err)
return certPaths{
clientCert: clientCertFile.Name(),
clientKey: clientKeyFile.Name(),
serverCert: serverCertFile.Name(),
serverKey: serverKeyFile.Name(),
ca: caCertFile.Name(),
}
}
+8
View File
@@ -0,0 +1,8 @@
package secret
import "github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
// InlineSecureValueSupport allows resources to manage secrets inline
//
//go:generate mockery --name InlineSecureValueSupport --structname MockInlineSecureValueSupport --inpackage --filename inline_mock.go --with-expecter
type InlineSecureValueSupport = contracts.InlineSecureValueSupport
@@ -0,0 +1,200 @@
package inline
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"os"
"github.com/fullstorydev/grpchan"
authnlib "github.com/grafana/authlib/authn"
"github.com/grafana/authlib/types"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
inlinev1beta1 "github.com/grafana/grafana/apps/secret/inline/v1beta1"
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
)
type GRPCInlineClient struct {
conn *grpc.ClientConn
tracer trace.Tracer
tokenExchanger authnlib.TokenExchanger
}
var _ contracts.InlineSecureValueSupport = &GRPCInlineClient{}
type TLSConfig struct {
UseTLS bool
CertFile string
KeyFile string
CAFile string
ServerName string
InsecureSkipVerify bool
}
func NewGRPCInlineClient(tokenExchanger authnlib.TokenExchanger, tracer trace.Tracer, address string, tlsConfig TLSConfig) (*GRPCInlineClient, error) {
var opts []grpc.DialOption
if tlsConfig.UseTLS {
creds, err := createTLSCredentials(tlsConfig)
if err != nil {
return nil, fmt.Errorf("failed to setup TLS: %w", err)
}
opts = append(opts, grpc.WithTransportCredentials(creds))
} else {
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
}
conn, err := grpc.NewClient(address, opts...)
if err != nil {
return nil, fmt.Errorf("failed to connect to grpc server at %s: %w", address, err)
}
return &GRPCInlineClient{
conn: conn,
tracer: tracer,
tokenExchanger: tokenExchanger,
}, nil
}
func createTLSCredentials(config TLSConfig) (credentials.TransportCredentials, error) {
tlsConfig := &tls.Config{}
if config.CAFile != "" {
caCert, err := os.ReadFile(config.CAFile)
if err != nil {
return nil, fmt.Errorf("failed to read CA: %w", err)
}
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
return nil, fmt.Errorf("failed to append CA")
}
tlsConfig.RootCAs = caCertPool
}
if config.CertFile != "" && config.KeyFile != "" {
cert, err := tls.LoadX509KeyPair(config.CertFile, config.KeyFile)
if err != nil {
return nil, fmt.Errorf("failed to load client certificate: %w", err)
}
tlsConfig.Certificates = []tls.Certificate{cert}
}
if config.ServerName != "" {
tlsConfig.ServerName = config.ServerName
}
if config.InsecureSkipVerify {
tlsConfig.InsecureSkipVerify = true
}
return credentials.NewTLS(tlsConfig), nil
}
// Close will close the underlying gRPC connection. After it is closed, the client cannot be used anymore.
func (g *GRPCInlineClient) Close() error {
if g.conn != nil {
return g.conn.Close()
}
return nil
}
func (g *GRPCInlineClient) CanReference(ctx context.Context, owner v0alpha1.ObjectReference, names ...string) error {
client, err := g.getClient(owner.Namespace)
if err != nil {
return err
}
req := &inlinev1beta1.CanReferenceRequest{
Owner: &inlinev1beta1.ObjectReference{
ApiGroup: owner.APIGroup,
ApiVersion: owner.APIVersion,
Kind: owner.Kind,
Namespace: owner.Namespace,
Name: owner.Name,
},
Names: names,
}
_, err = client.CanReference(ctx, req)
return err
}
func (g *GRPCInlineClient) CreateInline(ctx context.Context, owner v0alpha1.ObjectReference, value v0alpha1.RawSecureValue) (string, error) {
client, err := g.getClient(owner.Namespace)
if err != nil {
return "", err
}
if value.IsZero() {
return "", fmt.Errorf("empty value provided for CreateInline")
}
req := &inlinev1beta1.CreateInlineRequest{
Owner: &inlinev1beta1.ObjectReference{
ApiGroup: owner.APIGroup,
ApiVersion: owner.APIVersion,
Kind: owner.Kind,
Namespace: owner.Namespace,
Name: owner.Name,
},
Value: value.DangerouslyExposeAndConsumeValue(),
}
resp, err := client.CreateInline(ctx, req)
if err != nil {
return "", err
}
return resp.GetName(), nil
}
func (g *GRPCInlineClient) DeleteWhenOwnedByResource(ctx context.Context, owner v0alpha1.ObjectReference, name string) error {
client, err := g.getClient(owner.Namespace)
if err != nil {
return err
}
req := &inlinev1beta1.DeleteWhenOwnedByResourceRequest{
Owner: &inlinev1beta1.ObjectReference{
ApiGroup: owner.APIGroup,
ApiVersion: owner.APIVersion,
Kind: owner.Kind,
Namespace: owner.Namespace,
Name: owner.Name,
},
Name: name,
}
_, err = client.DeleteWhenOwnedByResource(ctx, req)
return err
}
func (g *GRPCInlineClient) getClient(namespace string) (inlinev1beta1.InlineSecureValueServiceClient, error) {
_, err := types.ParseNamespace(namespace)
if err != nil {
return nil, err
}
tokenExchangerInterceptor := authnlib.NewGrpcClientInterceptor(
g.tokenExchanger,
authnlib.WithClientInterceptorTracer(g.tracer),
authnlib.WithClientInterceptorNamespace(namespace),
authnlib.WithClientInterceptorAudience([]string{secretv1beta1.APIGroup}),
)
clientConn := grpchan.InterceptClientConn(
g.conn,
tokenExchangerInterceptor.UnaryClientInterceptor,
tokenExchangerInterceptor.StreamClientInterceptor,
)
return inlinev1beta1.NewInlineSecureValueServiceClient(clientConn), nil
}
@@ -1,4 +1,4 @@
package service
package inline
import (
"context"
@@ -20,25 +20,25 @@ import (
"github.com/grafana/grafana/pkg/util"
)
type inlineSecureValueService struct {
type LocalInlineSecureValueService struct {
tracer trace.Tracer
secureValueService contracts.SecureValueService
accessChecker authlib.AccessChecker
}
func ProvideInlineSecureValueService(
func NewLocalInlineSecureValueService(
tracer trace.Tracer,
secureValueService contracts.SecureValueService,
accessClient authlib.AccessClient,
) contracts.InlineSecureValueSupport {
return &inlineSecureValueService{
return &LocalInlineSecureValueService{
tracer: tracer,
secureValueService: secureValueService,
accessChecker: accessClient,
}
}
func (s *inlineSecureValueService) CanReference(ctx context.Context, owner common.ObjectReference, names ...string) error {
func (s *LocalInlineSecureValueService) CanReference(ctx context.Context, owner common.ObjectReference, names ...string) error {
ctx, span := s.tracer.Start(ctx, "InlineSecureValueService.CanReference", trace.WithAttributes(
attribute.String("owner.namespace", owner.Namespace),
attribute.String("owner.apiGroup", owner.APIGroup),
@@ -86,7 +86,7 @@ func (s *inlineSecureValueService) CanReference(ctx context.Context, owner commo
return nil
}
func (s *inlineSecureValueService) isSecureValueOwnedByResource(ctx context.Context, owner common.ObjectReference, name string) (bool, error) {
func (s *LocalInlineSecureValueService) isSecureValueOwnedByResource(ctx context.Context, owner common.ObjectReference, name string) (bool, error) {
sv, err := s.secureValueService.Read(ctx, xkube.Namespace(owner.Namespace), name)
if err != nil {
if errors.Is(err, contracts.ErrSecureValueNotFound) {
@@ -127,7 +127,7 @@ func (s *inlineSecureValueService) isSecureValueOwnedByResource(ctx context.Cont
return false, nil
}
func (s *inlineSecureValueService) canIdentityReadSecureValue(ctx context.Context, namespace xkube.Namespace, name string) error {
func (s *LocalInlineSecureValueService) canIdentityReadSecureValue(ctx context.Context, namespace xkube.Namespace, name string) error {
authInfo, ok := authlib.AuthInfoFrom(ctx)
if !ok {
return fmt.Errorf("missing auth info in context")
@@ -156,7 +156,7 @@ func (s *inlineSecureValueService) canIdentityReadSecureValue(ctx context.Contex
return nil
}
func (s *inlineSecureValueService) CreateInline(ctx context.Context, owner common.ObjectReference, value common.RawSecureValue) (string, error) {
func (s *LocalInlineSecureValueService) CreateInline(ctx context.Context, owner common.ObjectReference, value common.RawSecureValue) (string, error) {
ctx, span := s.tracer.Start(ctx, "InlineSecureValueService.CreateInline", trace.WithAttributes(
attribute.String("owner.namespace", owner.Namespace),
attribute.String("owner.apiGroup", owner.APIGroup),
@@ -219,7 +219,7 @@ func (s *inlineSecureValueService) CreateInline(ctx context.Context, owner commo
return createdSv.GetName(), nil
}
func (s *inlineSecureValueService) DeleteWhenOwnedByResource(ctx context.Context, owner common.ObjectReference, name string) error {
func (s *LocalInlineSecureValueService) DeleteWhenOwnedByResource(ctx context.Context, owner common.ObjectReference, name string) error {
ctx, span := s.tracer.Start(ctx, "InlineSecureValueService.DeleteWhenOwnedByResource", trace.WithAttributes(
attribute.String("owner.namespace", owner.Namespace),
attribute.String("owner.apiGroup", owner.APIGroup),
@@ -1,4 +1,4 @@
package service_test
package inline_test
import (
"testing"
@@ -6,7 +6,7 @@ import (
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/service"
"github.com/grafana/grafana/pkg/registry/apis/secret/inline"
"github.com/grafana/grafana/pkg/registry/apis/secret/testutils"
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
"github.com/stretchr/testify/require"
@@ -54,7 +54,7 @@ func TestIntegration_InlineSecureValue_CanReference(t *testing.T) {
"securevalues:read": {"securevalues:uid:" + sv2},
})
svc := service.ProvideInlineSecureValueService(tracer, tu.SecureValueService, tu.AccessClient)
svc := inline.NewLocalInlineSecureValueService(tracer, tu.SecureValueService, tu.AccessClient)
err = svc.CanReference(ctx, owner, sv1, sv2)
require.NoError(t, err)
@@ -63,7 +63,7 @@ func TestIntegration_InlineSecureValue_CanReference(t *testing.T) {
t.Run("when the auth info is missing it returns an error", func(t *testing.T) {
t.Parallel()
svc := service.ProvideInlineSecureValueService(tracer, nil, nil)
svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil)
err := svc.CanReference(t.Context(), common.ObjectReference{})
require.Error(t, err)
})
@@ -71,7 +71,7 @@ func TestIntegration_InlineSecureValue_CanReference(t *testing.T) {
t.Run("when the owner namespace does not match auth info namespace it returns an error", func(t *testing.T) {
t.Parallel()
svc := service.ProvideInlineSecureValueService(tracer, nil, nil)
svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil)
reqNs := "org-2345"
ctx := testutils.CreateUserAuthContext(t.Context(), reqNs, map[string][]string{})
@@ -83,7 +83,7 @@ func TestIntegration_InlineSecureValue_CanReference(t *testing.T) {
t.Run("when the owner namespace is empty it returns an error", func(t *testing.T) {
t.Parallel()
svc := service.ProvideInlineSecureValueService(tracer, nil, nil)
svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil)
ctx := testutils.CreateUserAuthContext(t.Context(), defaultNs, map[string][]string{})
@@ -94,7 +94,7 @@ func TestIntegration_InlineSecureValue_CanReference(t *testing.T) {
t.Run("when the owner reference has empty fields it returns an error", func(t *testing.T) {
t.Parallel()
svc := service.ProvideInlineSecureValueService(tracer, nil, nil)
svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil)
owner := common.ObjectReference{
Namespace: defaultNs,
@@ -125,7 +125,7 @@ func TestIntegration_InlineSecureValue_CanReference(t *testing.T) {
t.Run("when no secure values are provided it returns an error", func(t *testing.T) {
t.Parallel()
svc := service.ProvideInlineSecureValueService(tracer, nil, nil)
svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil)
ctx := testutils.CreateUserAuthContext(t.Context(), defaultNs, map[string][]string{})
@@ -137,7 +137,7 @@ func TestIntegration_InlineSecureValue_CanReference(t *testing.T) {
t.Parallel()
tu := testutils.Setup(t)
svc := service.ProvideInlineSecureValueService(tracer, tu.SecureValueService, nil)
svc := inline.NewLocalInlineSecureValueService(tracer, tu.SecureValueService, nil)
ctx := testutils.CreateUserAuthContext(t.Context(), defaultNs, map[string][]string{})
@@ -169,7 +169,7 @@ func TestIntegration_InlineSecureValue_CanReference(t *testing.T) {
ctx := testutils.CreateUserAuthContext(t.Context(), defaultNs, map[string][]string{})
svc := service.ProvideInlineSecureValueService(tracer, tu.SecureValueService, nil)
svc := inline.NewLocalInlineSecureValueService(tracer, tu.SecureValueService, nil)
err = svc.CanReference(ctx, owner, sv1)
require.Error(t, err)
@@ -189,7 +189,7 @@ func TestIntegration_InlineSecureValue_CanReference(t *testing.T) {
ctx := identity.WithServiceIdentityContext(t.Context(), 1234)
svc := service.ProvideInlineSecureValueService(tracer, tu.SecureValueService, nil)
svc := inline.NewLocalInlineSecureValueService(tracer, tu.SecureValueService, nil)
err = svc.CanReference(ctx, owner, sv1)
require.Error(t, err)
@@ -207,7 +207,7 @@ func TestIntegration_InlineSecureValue_CanReference(t *testing.T) {
})
require.NoError(t, err)
svc := service.ProvideInlineSecureValueService(tracer, tu.SecureValueService, tu.AccessClient)
svc := inline.NewLocalInlineSecureValueService(tracer, tu.SecureValueService, tu.AccessClient)
ctx := testutils.CreateUserAuthContext(t.Context(), defaultNs, map[string][]string{
"securevalues:read": {"securevalues:uid:another-sv"}, // can read, but another resource!
@@ -248,7 +248,7 @@ func TestIntegration_InlineSecureValue_CreateInline(t *testing.T) {
createAuthCtx := testutils.CreateOBOAuthContext(t.Context(), serviceIdentity, owner.Namespace, nil, nil)
svc := service.ProvideInlineSecureValueService(tracer, tu.SecureValueService, nil)
svc := inline.NewLocalInlineSecureValueService(tracer, tu.SecureValueService, nil)
createdName, err := svc.CreateInline(createAuthCtx, owner, secret)
require.NoError(t, err)
@@ -265,7 +265,7 @@ func TestIntegration_InlineSecureValue_CreateInline(t *testing.T) {
t.Run("when the auth info is missing it returns an error", func(t *testing.T) {
t.Parallel()
svc := service.ProvideInlineSecureValueService(tracer, nil, nil)
svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil)
_, err := svc.CreateInline(t.Context(), common.ObjectReference{}, "")
require.Error(t, err)
})
@@ -273,7 +273,7 @@ func TestIntegration_InlineSecureValue_CreateInline(t *testing.T) {
t.Run("when the request identity is not a user nor a service account, it returns an error", func(t *testing.T) {
t.Parallel()
svc := service.ProvideInlineSecureValueService(tracer, nil, nil)
svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil)
createAuthCtx := testutils.CreateServiceAuthContext(t.Context(), "service-identity", defaultNs, nil)
@@ -284,7 +284,7 @@ func TestIntegration_InlineSecureValue_CreateInline(t *testing.T) {
t.Run("when the owner namespace does not match auth info namespace it returns an error", func(t *testing.T) {
t.Parallel()
svc := service.ProvideInlineSecureValueService(tracer, nil, nil)
svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil)
reqNs := "org-2345"
createAuthCtx := testutils.CreateOBOAuthContext(t.Context(), "service-identity", reqNs, nil, nil)
@@ -296,7 +296,7 @@ func TestIntegration_InlineSecureValue_CreateInline(t *testing.T) {
t.Run("when the owner namespace is empty it returns an error", func(t *testing.T) {
t.Parallel()
svc := service.ProvideInlineSecureValueService(tracer, nil, nil)
svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil)
createAuthCtx := testutils.CreateOBOAuthContext(t.Context(), "service-identity", defaultNs, nil, nil)
@@ -307,7 +307,7 @@ func TestIntegration_InlineSecureValue_CreateInline(t *testing.T) {
t.Run("when the owner reference has empty fields it returns an error", func(t *testing.T) {
t.Parallel()
svc := service.ProvideInlineSecureValueService(tracer, nil, nil)
svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil)
owner := common.ObjectReference{
Namespace: defaultNs,
@@ -339,7 +339,7 @@ func TestIntegration_InlineSecureValue_CreateInline(t *testing.T) {
t.Run("when an empty secret is provided it returns an error", func(t *testing.T) {
t.Parallel()
svc := service.ProvideInlineSecureValueService(tracer, nil, nil)
svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil)
createAuthCtx := testutils.CreateOBOAuthContext(t.Context(), "service-identity", defaultNs, nil, nil)
@@ -376,7 +376,7 @@ func TestIntegration_InlineSecureValue_DeleteWhenOwnedByResource(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, createdSv1)
svc := service.ProvideInlineSecureValueService(tracer, tu.SecureValueService, nil)
svc := inline.NewLocalInlineSecureValueService(tracer, tu.SecureValueService, nil)
ctx := testutils.CreateServiceAuthContext(t.Context(), "", defaultNs, nil)
@@ -392,7 +392,7 @@ func TestIntegration_InlineSecureValue_DeleteWhenOwnedByResource(t *testing.T) {
t.Run("when the auth info is missing it returns an error", func(t *testing.T) {
t.Parallel()
svc := service.ProvideInlineSecureValueService(tracer, nil, nil)
svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil)
err := svc.DeleteWhenOwnedByResource(t.Context(), common.ObjectReference{}, "")
require.Error(t, err)
})
@@ -400,7 +400,7 @@ func TestIntegration_InlineSecureValue_DeleteWhenOwnedByResource(t *testing.T) {
t.Run("when the owner namespace does not match auth info namespace it returns an error", func(t *testing.T) {
t.Parallel()
svc := service.ProvideInlineSecureValueService(tracer, nil, nil)
svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil)
reqNs := "org-2345"
ctx := testutils.CreateUserAuthContext(t.Context(), reqNs, map[string][]string{})
@@ -412,7 +412,7 @@ func TestIntegration_InlineSecureValue_DeleteWhenOwnedByResource(t *testing.T) {
t.Run("when the owner namespace is empty it returns an error", func(t *testing.T) {
t.Parallel()
svc := service.ProvideInlineSecureValueService(tracer, nil, nil)
svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil)
ctx := testutils.CreateUserAuthContext(t.Context(), defaultNs, map[string][]string{})
@@ -423,7 +423,7 @@ func TestIntegration_InlineSecureValue_DeleteWhenOwnedByResource(t *testing.T) {
t.Run("when the owner reference has empty fields it returns an error", func(t *testing.T) {
t.Parallel()
svc := service.ProvideInlineSecureValueService(tracer, nil, nil)
svc := inline.NewLocalInlineSecureValueService(tracer, nil, nil)
owner := common.ObjectReference{
Namespace: defaultNs,
@@ -467,7 +467,7 @@ func TestIntegration_InlineSecureValue_DeleteWhenOwnedByResource(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, createdSv1)
svc := service.ProvideInlineSecureValueService(tracer, tu.SecureValueService, nil)
svc := inline.NewLocalInlineSecureValueService(tracer, tu.SecureValueService, nil)
ctx := testutils.CreateServiceAuthContext(t.Context(), "", defaultNs, nil)
@@ -494,7 +494,7 @@ func TestIntegration_InlineSecureValue_DeleteWhenOwnedByResource(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, createdSv1)
svc := service.ProvideInlineSecureValueService(tracer, tu.SecureValueService, nil)
svc := inline.NewLocalInlineSecureValueService(tracer, tu.SecureValueService, nil)
ctx := testutils.CreateServiceAuthContext(t.Context(), "", defaultNs, nil)
@@ -0,0 +1,71 @@
package inline
import (
"fmt"
authnlib "github.com/grafana/authlib/authn"
authlib "github.com/grafana/authlib/types"
"go.opentelemetry.io/otel/trace"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/services/authn/grpcutils"
"github.com/grafana/grafana/pkg/setting"
)
func ProvideInlineSecureValueService(
cfg *setting.Cfg,
tracer trace.Tracer,
secureValueService contracts.SecureValueService,
accessClient authlib.AccessClient,
) (contracts.InlineSecureValueSupport, error) {
if cfg.SecretsManagement.GrpcClientEnable {
grpcClientConfig := grpcutils.ReadGrpcClientConfig(cfg)
if cfg.SecretsManagement.GrpcServerAddress == "" {
return nil, fmt.Errorf("grpc_server_address is required when grpc client is enabled")
}
if grpcClientConfig.Token == "" || grpcClientConfig.TokenExchangeURL == "" {
return nil, fmt.Errorf("grpc_client_authentication.token and grpc_client_authentication.token_exchange_url are required when grpc client is enabled")
}
tokenExchangeClient, err := authnlib.NewTokenExchangeClient(authnlib.TokenExchangeConfig{
Token: grpcClientConfig.Token,
TokenExchangeURL: grpcClientConfig.TokenExchangeURL,
})
if err != nil {
return nil, fmt.Errorf("failed to create token exchange client: %w", err)
}
tlsConfig := readTLSFromConfig(cfg)
client, err := NewGRPCInlineClient(tokenExchangeClient, tracer, cfg.SecretsManagement.GrpcServerAddress, tlsConfig)
if err != nil {
return nil, fmt.Errorf("failed to create grpc inline secure value client: %w", err)
}
return client, nil
}
return NewLocalInlineSecureValueService(tracer, secureValueService, accessClient), nil
}
func readTLSFromConfig(cfg *setting.Cfg) TLSConfig {
if !cfg.SecretsManagement.GrpcServerUseTLS {
return TLSConfig{
UseTLS: false,
InsecureSkipVerify: true,
}
}
apiServer := cfg.SectionWithEnvOverrides("grafana-apiserver")
return TLSConfig{
UseTLS: true,
CertFile: apiServer.Key("proxy_client_cert_file").MustString(""),
KeyFile: apiServer.Key("proxy_client_key_file").MustString(""),
CAFile: apiServer.Key("apiservice_ca_bundle_file").MustString(""),
ServerName: cfg.SecretsManagement.GrpcServerTLSServerName,
InsecureSkipVerify: cfg.SecretsManagement.GrpcServerTLSSkipVerify,
}
}
@@ -0,0 +1,190 @@
package inline_test
import (
"context"
"crypto/tls"
"net/http"
"net/http/httptest"
"strings"
"testing"
inlinev1beta1 "github.com/grafana/grafana/apps/secret/inline/v1beta1"
"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"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/secret/inline"
"github.com/grafana/grafana/pkg/registry/apis/secret/testutils"
"github.com/grafana/grafana/pkg/services/authn/clients"
"github.com/grafana/grafana/pkg/setting"
)
func TestProvideInlineSecureValueService(t *testing.T) {
t.Parallel()
tracer := noop.NewTracerProvider().Tracer("test")
t.Run("when the grpc server is disabled, it returns local inline service", func(t *testing.T) {
t.Parallel()
cfg := setting.NewCfg()
cfg.SecretsManagement.GrpcClientEnable = false
service, err := inline.ProvideInlineSecureValueService(cfg, nil, nil, nil)
require.NoError(t, err)
require.IsType(t, &inline.LocalInlineSecureValueService{}, service)
})
t.Run("when grpc server is enabled but server address is missing, it returns an error", func(t *testing.T) {
t.Parallel()
cfg := setting.NewCfg()
cfg.SecretsManagement.GrpcClientEnable = true
service, err := inline.ProvideInlineSecureValueService(cfg, nil, nil, nil)
require.Error(t, err)
require.Nil(t, service)
})
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"
service, err := inline.ProvideInlineSecureValueService(cfg, nil, nil, nil)
require.Error(t, err)
require.Nil(t, service)
})
t.Run("happy path with grpc+tls server with fake token 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)
inlineServer := &mockInlineServer{}
inlineServer.On("CreateInline", mock.Anything, mock.Anything).Return(&inlinev1beta1.CreateInlineResponse{Name: "test-value"}, nil)
inlineServer.On("CanReference", mock.Anything, mock.Anything).Return(&inlinev1beta1.CanReferenceResponse{}, nil)
inlineServer.On("DeleteWhenOwnedByResource", mock.Anything, mock.Anything).Return(&inlinev1beta1.DeleteWhenOwnedByResourceResponse{}, nil)
inlinev1beta1.RegisterInlineSecureValueServiceServer(grpcServer, inlineServer)
go func() {
_ = grpcServer.Serve(listener)
<-t.Context().Done()
}()
// Populate configuration with gRPC+TLS options and mock token exchanger
namespace := "stacks-1234"
cfg := setting.NewCfg()
cfg.SecretsManagement.GrpcClientEnable = true
cfg.SecretsManagement.GrpcServerAddress = listener.Addr().String()
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("proxy_client_cert_file", certPaths.ClientCert)
require.NoError(t, err)
_, err = apiServer.NewKey("proxy_client_key_file", certPaths.ClientKey)
require.NoError(t, err)
_, err = apiServer.NewKey("apiservice_ca_bundle_file", certPaths.CA)
require.NoError(t, err)
inlineService, err := inline.ProvideInlineSecureValueService(cfg, tracer, nil, nil)
require.NoError(t, err)
require.IsType(t, &inline.GRPCInlineClient{}, inlineService)
owner := common.ObjectReference{
APIGroup: "example.com",
APIVersion: "v1",
Kind: "TestResource",
Namespace: namespace,
Name: "test-resource",
}
name, err := inlineService.CreateInline(t.Context(), owner, common.NewSecretValue("test-value"))
require.NoError(t, err)
require.Equal(t, "test-value", name)
err = inlineService.CanReference(t.Context(), owner, "test-value")
require.NoError(t, err)
err = inlineService.DeleteWhenOwnedByResource(t.Context(), owner, "test-value")
require.NoError(t, err)
mock.AssertExpectationsForObjects(t, inlineServer)
requestContext := inlineServer.Calls[0].Arguments[0].(context.Context)
md, ok := metadata.FromIncomingContext(requestContext)
require.True(t, ok)
require.NotEmpty(t, md)
require.Equal(t, respTokenExchanged, md[strings.ToLower(clients.ExtJWTAuthenticationHeaderName)][0])
})
}
type mockInlineServer struct {
mock.Mock
}
var _ inlinev1beta1.InlineSecureValueServiceServer = (*mockInlineServer)(nil)
func (m *mockInlineServer) CanReference(ctx context.Context, req *inlinev1beta1.CanReferenceRequest) (*inlinev1beta1.CanReferenceResponse, error) {
args := m.Called(ctx, req)
return args.Get(0).(*inlinev1beta1.CanReferenceResponse), args.Error(1)
}
func (m *mockInlineServer) CreateInline(ctx context.Context, req *inlinev1beta1.CreateInlineRequest) (*inlinev1beta1.CreateInlineResponse, error) {
args := m.Called(ctx, req)
return args.Get(0).(*inlinev1beta1.CreateInlineResponse), args.Error(1)
}
func (m *mockInlineServer) DeleteWhenOwnedByResource(ctx context.Context, req *inlinev1beta1.DeleteWhenOwnedByResourceRequest) (*inlinev1beta1.DeleteWhenOwnedByResourceResponse, error) {
args := m.Called(ctx, req)
return args.Get(0).(*inlinev1beta1.DeleteWhenOwnedByResourceResponse), args.Error(1)
}
+206
View File
@@ -0,0 +1,206 @@
// Code generated by mockery v2.52.4. DO NOT EDIT.
package secret
import (
context "context"
mock "github.com/stretchr/testify/mock"
v0alpha1 "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
)
// MockInlineSecureValueSupport is an autogenerated mock type for the InlineSecureValueSupport type
type MockInlineSecureValueSupport struct {
mock.Mock
}
type MockInlineSecureValueSupport_Expecter struct {
mock *mock.Mock
}
func (_m *MockInlineSecureValueSupport) EXPECT() *MockInlineSecureValueSupport_Expecter {
return &MockInlineSecureValueSupport_Expecter{mock: &_m.Mock}
}
// CanReference provides a mock function with given fields: ctx, owner, names
func (_m *MockInlineSecureValueSupport) CanReference(ctx context.Context, owner v0alpha1.ObjectReference, names ...string) error {
_va := make([]interface{}, len(names))
for _i := range names {
_va[_i] = names[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, owner)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
if len(ret) == 0 {
panic("no return value specified for CanReference")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, v0alpha1.ObjectReference, ...string) error); ok {
r0 = rf(ctx, owner, names...)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockInlineSecureValueSupport_CanReference_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CanReference'
type MockInlineSecureValueSupport_CanReference_Call struct {
*mock.Call
}
// CanReference is a helper method to define mock.On call
// - ctx context.Context
// - owner v0alpha1.ObjectReference
// - names ...string
func (_e *MockInlineSecureValueSupport_Expecter) CanReference(ctx interface{}, owner interface{}, names ...interface{}) *MockInlineSecureValueSupport_CanReference_Call {
return &MockInlineSecureValueSupport_CanReference_Call{Call: _e.mock.On("CanReference",
append([]interface{}{ctx, owner}, names...)...)}
}
func (_c *MockInlineSecureValueSupport_CanReference_Call) Run(run func(ctx context.Context, owner v0alpha1.ObjectReference, names ...string)) *MockInlineSecureValueSupport_CanReference_Call {
_c.Call.Run(func(args mock.Arguments) {
variadicArgs := make([]string, len(args)-2)
for i, a := range args[2:] {
if a != nil {
variadicArgs[i] = a.(string)
}
}
run(args[0].(context.Context), args[1].(v0alpha1.ObjectReference), variadicArgs...)
})
return _c
}
func (_c *MockInlineSecureValueSupport_CanReference_Call) Return(_a0 error) *MockInlineSecureValueSupport_CanReference_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockInlineSecureValueSupport_CanReference_Call) RunAndReturn(run func(context.Context, v0alpha1.ObjectReference, ...string) error) *MockInlineSecureValueSupport_CanReference_Call {
_c.Call.Return(run)
return _c
}
// CreateInline provides a mock function with given fields: ctx, owner, value
func (_m *MockInlineSecureValueSupport) CreateInline(ctx context.Context, owner v0alpha1.ObjectReference, value v0alpha1.RawSecureValue) (string, error) {
ret := _m.Called(ctx, owner, value)
if len(ret) == 0 {
panic("no return value specified for CreateInline")
}
var r0 string
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, v0alpha1.ObjectReference, v0alpha1.RawSecureValue) (string, error)); ok {
return rf(ctx, owner, value)
}
if rf, ok := ret.Get(0).(func(context.Context, v0alpha1.ObjectReference, v0alpha1.RawSecureValue) string); ok {
r0 = rf(ctx, owner, value)
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func(context.Context, v0alpha1.ObjectReference, v0alpha1.RawSecureValue) error); ok {
r1 = rf(ctx, owner, value)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockInlineSecureValueSupport_CreateInline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateInline'
type MockInlineSecureValueSupport_CreateInline_Call struct {
*mock.Call
}
// CreateInline is a helper method to define mock.On call
// - ctx context.Context
// - owner v0alpha1.ObjectReference
// - value v0alpha1.RawSecureValue
func (_e *MockInlineSecureValueSupport_Expecter) CreateInline(ctx interface{}, owner interface{}, value interface{}) *MockInlineSecureValueSupport_CreateInline_Call {
return &MockInlineSecureValueSupport_CreateInline_Call{Call: _e.mock.On("CreateInline", ctx, owner, value)}
}
func (_c *MockInlineSecureValueSupport_CreateInline_Call) Run(run func(ctx context.Context, owner v0alpha1.ObjectReference, value v0alpha1.RawSecureValue)) *MockInlineSecureValueSupport_CreateInline_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(v0alpha1.ObjectReference), args[2].(v0alpha1.RawSecureValue))
})
return _c
}
func (_c *MockInlineSecureValueSupport_CreateInline_Call) Return(_a0 string, _a1 error) *MockInlineSecureValueSupport_CreateInline_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockInlineSecureValueSupport_CreateInline_Call) RunAndReturn(run func(context.Context, v0alpha1.ObjectReference, v0alpha1.RawSecureValue) (string, error)) *MockInlineSecureValueSupport_CreateInline_Call {
_c.Call.Return(run)
return _c
}
// DeleteWhenOwnedByResource provides a mock function with given fields: ctx, owner, name
func (_m *MockInlineSecureValueSupport) DeleteWhenOwnedByResource(ctx context.Context, owner v0alpha1.ObjectReference, name string) error {
ret := _m.Called(ctx, owner, name)
if len(ret) == 0 {
panic("no return value specified for DeleteWhenOwnedByResource")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, v0alpha1.ObjectReference, string) error); ok {
r0 = rf(ctx, owner, name)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockInlineSecureValueSupport_DeleteWhenOwnedByResource_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteWhenOwnedByResource'
type MockInlineSecureValueSupport_DeleteWhenOwnedByResource_Call struct {
*mock.Call
}
// DeleteWhenOwnedByResource is a helper method to define mock.On call
// - ctx context.Context
// - owner v0alpha1.ObjectReference
// - name string
func (_e *MockInlineSecureValueSupport_Expecter) DeleteWhenOwnedByResource(ctx interface{}, owner interface{}, name interface{}) *MockInlineSecureValueSupport_DeleteWhenOwnedByResource_Call {
return &MockInlineSecureValueSupport_DeleteWhenOwnedByResource_Call{Call: _e.mock.On("DeleteWhenOwnedByResource", ctx, owner, name)}
}
func (_c *MockInlineSecureValueSupport_DeleteWhenOwnedByResource_Call) Run(run func(ctx context.Context, owner v0alpha1.ObjectReference, name string)) *MockInlineSecureValueSupport_DeleteWhenOwnedByResource_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(v0alpha1.ObjectReference), args[2].(string))
})
return _c
}
func (_c *MockInlineSecureValueSupport_DeleteWhenOwnedByResource_Call) Return(_a0 error) *MockInlineSecureValueSupport_DeleteWhenOwnedByResource_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockInlineSecureValueSupport_DeleteWhenOwnedByResource_Call) RunAndReturn(run func(context.Context, v0alpha1.ObjectReference, string) error) *MockInlineSecureValueSupport_DeleteWhenOwnedByResource_Call {
_c.Call.Return(run)
return _c
}
// NewMockInlineSecureValueSupport creates a new instance of MockInlineSecureValueSupport. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockInlineSecureValueSupport(t interface {
mock.TestingT
Cleanup(func())
}) *MockInlineSecureValueSupport {
mock := &MockInlineSecureValueSupport{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
@@ -6,6 +6,7 @@ import (
"github.com/grafana/authlib/authn"
"github.com/grafana/authlib/types"
"github.com/madflojo/testcerts"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace/noop"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -80,7 +81,6 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut {
defaultKey := "SdlklWklckeLS"
cfg := setting.NewCfg()
cfg.SecretsManagement = setting.SecretsManagerSettings{
DecryptServerType: "local",
CurrentEncryptionProvider: "secret_key.v1",
ConfiguredKMSProviders: map[string]map[string]string{"secret_key.v1": {"secret_key": defaultKey}},
}
@@ -131,9 +131,6 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut {
require.NoError(t, err)
testCfg := setting.NewCfg()
testCfg.SecretsManagement = setting.SecretsManagerSettings{
DecryptServerType: "local",
}
decryptService, err := decrypt.ProvideDecryptService(testCfg, tracer, decryptStorage)
require.NoError(t, err)
@@ -287,3 +284,40 @@ func CreateOBOAuthContext(
return types.WithAuthInfo(ctx, requester)
}
type TestCertPaths struct {
ClientCert string
ClientKey string
ServerCert string
ServerKey string
CA string
}
func CreateX509TestDir(t *testing.T) TestCertPaths {
t.Helper()
tmpDir := t.TempDir()
ca := testcerts.NewCA()
caCertFile, _, err := ca.ToTempFile(tmpDir)
require.NoError(t, err)
serverKp, err := ca.NewKeyPair("localhost")
require.NoError(t, err)
serverCertFile, serverKeyFile, err := serverKp.ToTempFile(tmpDir)
require.NoError(t, err)
clientKp, err := ca.NewKeyPair()
require.NoError(t, err)
clientCertFile, clientKeyFile, err := clientKp.ToTempFile(tmpDir)
require.NoError(t, err)
return TestCertPaths{
ClientCert: clientCertFile.Name(),
ClientKey: clientKeyFile.Name(),
ServerCert: serverCertFile.Name(),
ServerKey: serverKeyFile.Name(),
CA: caCertFile.Name(),
}
}
+2
View File
@@ -44,6 +44,7 @@ import (
secretdecrypt "github.com/grafana/grafana/pkg/registry/apis/secret/decrypt"
cipher "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/service"
encryptionManager "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager"
secretinline "github.com/grafana/grafana/pkg/registry/apis/secret/inline"
secretsecurevalueservice "github.com/grafana/grafana/pkg/registry/apis/secret/service"
secretvalidator "github.com/grafana/grafana/pkg/registry/apis/secret/validator"
appregistry "github.com/grafana/grafana/pkg/registry/apps"
@@ -428,6 +429,7 @@ var wireBasicSet = wire.NewSet(
secretmetadata.ProvideDecryptStorage,
secretdecrypt.ProvideDecryptAuthorizer,
secretdecrypt.ProvideDecryptService,
secretinline.ProvideInlineSecureValueService,
secretencryption.ProvideDataKeyStorage,
secretencryption.ProvideGlobalDataKeyStorage,
secretencryption.ProvideEncryptedValueStorage,
File diff suppressed because one or more lines are too long
+12 -12
View File
@@ -16,24 +16,24 @@ type SecretsManagerSettings struct {
// In OSS, the provider type can only be "secret_key". In Enterprise, it can additionally be one of: "aws_kms", "azure_keyvault", "google_kms", "hashicorp_vault"
ConfiguredKMSProviders map[string]map[string]string
DecryptServerType string // "local" or "grpc"
DecryptServerUseTLS bool // Applicable for decrypt_server_type=grpc. Whether to use TLS for the decrypt server
DecryptServerTLSSkipVerify bool // Applicable for decrypt_server_type=grpc. Whether to skip TLS verification for the decrypt server
DecryptServerTLSServerName string // Applicable for decrypt_server_type=grpc. Server name to use for TLS verification
DecryptServerAddress string // Applicable for decrypt_server_type=grpc. Address for external secrets server
DecryptGrafanaServiceName string // Service name to use for background grafana decryption
GrpcClientEnable bool // Whether to enable the gRPC client. If disabled, it will use the in-process services implementations.
GrpcServerUseTLS bool // Whether to use TLS when communicating with the gRPC server
GrpcServerTLSSkipVerify bool // Whether to skip TLS verification when communicating with the gRPC server
GrpcServerTLSServerName string // Server name to use for TLS verification
GrpcServerAddress string // Address for gRPC secrets server
GrpcGrafanaServiceName string // Service name to use for background grafana decryption/inline
}
func (cfg *Cfg) readSecretsManagerSettings() {
secretsMgmt := cfg.Raw.Section("secrets_manager")
cfg.SecretsManagement.CurrentEncryptionProvider = secretsMgmt.Key("encryption_provider").MustString(MisconfiguredProvider)
cfg.SecretsManagement.DecryptServerType = valueAsString(secretsMgmt, "decrypt_server_type", "local")
cfg.SecretsManagement.DecryptServerUseTLS = secretsMgmt.Key("decrypt_server_use_tls").MustBool(false)
cfg.SecretsManagement.DecryptServerTLSSkipVerify = secretsMgmt.Key("decrypt_server_tls_skip_verify").MustBool(false)
cfg.SecretsManagement.DecryptServerTLSServerName = valueAsString(secretsMgmt, "decrypt_server_tls_server_name", "")
cfg.SecretsManagement.DecryptServerAddress = valueAsString(secretsMgmt, "decrypt_server_address", "")
cfg.SecretsManagement.DecryptGrafanaServiceName = valueAsString(secretsMgmt, "decrypt_grafana_service_name", "")
cfg.SecretsManagement.GrpcClientEnable = secretsMgmt.Key("grpc_client_enable").MustBool(false)
cfg.SecretsManagement.GrpcServerUseTLS = secretsMgmt.Key("grpc_server_use_tls").MustBool(false)
cfg.SecretsManagement.GrpcServerTLSSkipVerify = secretsMgmt.Key("grpc_server_tls_skip_verify").MustBool(false)
cfg.SecretsManagement.GrpcServerTLSServerName = valueAsString(secretsMgmt, "grpc_server_tls_server_name", "")
cfg.SecretsManagement.GrpcServerAddress = valueAsString(secretsMgmt, "grpc_server_address", "")
cfg.SecretsManagement.GrpcGrafanaServiceName = valueAsString(secretsMgmt, "grpc_grafana_service_name", "")
// Extract available KMS providers from configuration sections
providers := make(map[string]map[string]string)