Secrets service: Allow decrypt through gRPC connection (#108365)
This commit is contained in:
@@ -7,12 +7,13 @@ import (
|
||||
"time"
|
||||
|
||||
claims "github.com/grafana/authlib/types"
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
|
||||
@@ -65,14 +66,33 @@ func (s *decryptStorage) Decrypt(ctx context.Context, namespace xkube.Namespace,
|
||||
defer func() {
|
||||
span.SetAttributes(attribute.String("decrypter.identity", decrypterIdentity))
|
||||
|
||||
args := []any{
|
||||
"namespace", namespace.String(),
|
||||
"secret_name", name,
|
||||
"decrypter_identity", decrypterIdentity,
|
||||
}
|
||||
|
||||
// The service identity used for decryption is always what is from the signed token, but if the request is
|
||||
// coming from grafana, the service identity will be grafana, but the request metadata will contain
|
||||
// additional service identity information (such as coming from the provisioning service in grafana).
|
||||
// we do this for auditing purposes.
|
||||
if md, ok := metadata.FromIncomingContext(ctx); ok {
|
||||
if svcIdentities := md.Get(contracts.HeaderGrafanaServiceIdentityName); len(svcIdentities) > 0 {
|
||||
args = append(args, "grafana_decrypter_identity", svcIdentities[0])
|
||||
span.SetAttributes(attribute.String("grafana_decrypter.identity", svcIdentities[0]))
|
||||
}
|
||||
}
|
||||
|
||||
if decryptErr == nil {
|
||||
logging.FromContext(ctx).Info("Audit log:", "operation", "decrypt_secret_success", "namespace", namespace, "secret_name", name, "decrypter_identity", decrypterIdentity)
|
||||
args = append(args, "operation", "decrypt_secret_success")
|
||||
} else {
|
||||
span.SetStatus(codes.Error, "Decrypt failed")
|
||||
span.RecordError(decryptErr)
|
||||
|
||||
logging.FromContext(ctx).Info("Audit log:", "operation", "decrypt_secret_error", "namespace", namespace, "secret_name", name, "decrypter_identity", decrypterIdentity, "error", decryptErr)
|
||||
args = append(args, "operation", "decrypt_secret_error", "error", decryptErr.Error())
|
||||
}
|
||||
|
||||
logging.FromContext(ctx).Info("Secrets Audit Log", args...)
|
||||
|
||||
success := decryptErr == nil
|
||||
s.metrics.DecryptDuration.WithLabelValues(strconv.FormatBool(success)).Observe(time.Since(start).Seconds())
|
||||
s.metrics.DecryptRequestCount.WithLabelValues(strconv.FormatBool(success)).Inc()
|
||||
|
||||
@@ -6,7 +6,9 @@ import (
|
||||
|
||||
"github.com/grafana/authlib/authn"
|
||||
"github.com/grafana/authlib/types"
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
"github.com/stretchr/testify/require"
|
||||
grpcmetadata "google.golang.org/grpc/metadata"
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
|
||||
@@ -248,10 +250,67 @@ func TestIntegrationDecrypt(t *testing.T) {
|
||||
require.Empty(t, exposed)
|
||||
})
|
||||
|
||||
// TODO: add more tests for keeper failure scenarios, lets see how the async work will change this though.
|
||||
t.Run("happy path with grpc metadata in request, also record the metadata as part of the service identity", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
tokenSvcIdentity := "svc"
|
||||
stSvcIdentity := "st-svc"
|
||||
|
||||
// Create auth context with proper permissions that match the decrypters
|
||||
authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues:decrypt"}, tokenSvcIdentity, types.TypeUser)
|
||||
|
||||
// Needs to be incoming because we are pretending we received the metadata from a gRPC request
|
||||
ctx = grpcmetadata.NewIncomingContext(authCtx, grpcmetadata.New(map[string]string{
|
||||
contracts.HeaderGrafanaServiceIdentityName: stSvcIdentity,
|
||||
}))
|
||||
|
||||
// Setup service
|
||||
sut := testutils.Setup(t)
|
||||
|
||||
// Create a secure value
|
||||
spec := secretv1beta1.SecureValueSpec{
|
||||
Description: "description",
|
||||
Decrypters: []string{tokenSvcIdentity},
|
||||
Value: ptr.To(secretv1beta1.NewExposedSecureValue("value")),
|
||||
}
|
||||
sv := &secretv1beta1.SecureValue{Spec: spec}
|
||||
sv.Name = "sv-test"
|
||||
sv.Namespace = "default"
|
||||
|
||||
_, err := sut.CreateSv(ctx, testutils.CreateSvWithSv(sv))
|
||||
require.NoError(t, err)
|
||||
|
||||
fakeLogger := &mockLogger{}
|
||||
|
||||
loggerCtx := logging.Context(ctx, fakeLogger)
|
||||
|
||||
exposed, err := sut.DecryptStorage.Decrypt(loggerCtx, "default", "sv-test")
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, exposed)
|
||||
require.Equal(t, "value", exposed.DangerouslyExposeAndConsumeValue())
|
||||
|
||||
require.Len(t, fakeLogger.InfoArgs, 1)
|
||||
args := fakeLogger.InfoArgs[0]
|
||||
require.Contains(t, args, "grafana_decrypter_identity")
|
||||
require.Contains(t, args, "decrypter_identity")
|
||||
for i, arg := range args {
|
||||
if arg == "grafana_decrypter_identity" {
|
||||
require.Equal(t, stSvcIdentity, args[i+1].(string))
|
||||
}
|
||||
|
||||
if arg == "decrypter_identity" {
|
||||
require.Equal(t, tokenSvcIdentity, args[i+1].(string))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func createAuthContext(ctx context.Context, namespace string, permissions []string, svc string, identityType types.IdentityType) context.Context {
|
||||
ctx = logging.Context(ctx, logging.DefaultLogger)
|
||||
|
||||
requester := &identity.StaticRequester{
|
||||
Type: identityType,
|
||||
Namespace: namespace,
|
||||
@@ -269,3 +328,18 @@ func createAuthContext(ctx context.Context, namespace string, permissions []stri
|
||||
|
||||
return types.WithAuthInfo(ctx, requester)
|
||||
}
|
||||
|
||||
type mockLogger struct {
|
||||
logging.Logger
|
||||
InfoMsgs []string
|
||||
InfoArgs [][]any
|
||||
}
|
||||
|
||||
func (m *mockLogger) Info(msg string, args ...any) {
|
||||
m.InfoMsgs = append(m.InfoMsgs, msg)
|
||||
m.InfoArgs = append(m.InfoArgs, args)
|
||||
}
|
||||
|
||||
func (m *mockLogger) WithContext(ctx context.Context) logging.Logger {
|
||||
return m
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user