Files
grafana/pkg/registry/apis/secret/service/secure_value.go
T
Bruno 7698970f22 Secrets: changes to allow a 3rd party keeper / secret references (#115156)
* Secrets: changes to allow a 3rd party keeper / secret references

* fix test

* make gofmt

* lint

* fix tests

* assign aws secrets manager to @grafana/grafana-operator-experience-squad

* rename Keeper.Reference to Keeper.RetrieveReference

* rename ModelSecretsManager to ModelAWSSecretsManager

* validator: ensure that only one of keeper.Spec.Aws.AccessKey or keeper.Spec.Aws.AssumeRole are set

* move secrets manager dep / go mod tidy

* move secrets manager dep

* keeper validator: move 3rd party secret stores validation to their own functions

* add github.com/aws/aws-sdk-go-v2/service/secretsmanager pkg/extensions/enterprise_imports

* make update-workspace

* undo go.mod changes in /apps

* make update-workspace

* fix test

* add github.com/aws/aws-sdk-go-v2/service/secretsmanager to enterprise_imports

* make update-workspace

* gcworker: handle refs

* make update-workspace

* create toggle: FeatureStageExperimental

* allow features.IsEnabled for now

* format
2026-01-06 11:30:04 -03:00

400 lines
14 KiB
Go

package service
import (
"context"
"fmt"
"strconv"
"time"
claims "github.com/grafana/authlib/types"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"k8s.io/apiserver/pkg/admission"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/codes"
"github.com/grafana/grafana-app-sdk/logging"
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/service/metrics"
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
)
var _ contracts.SecureValueService = (*SecureValueService)(nil)
type SecureValueService struct {
tracer trace.Tracer
accessClient claims.AccessClient
secureValueMetadataStorage contracts.SecureValueMetadataStorage
secureValueValidator contracts.SecureValueValidator
secureValueMutator contracts.SecureValueMutator
keeperMetadataStorage contracts.KeeperMetadataStorage
keeperService contracts.KeeperService
metrics *metrics.SecureValueServiceMetrics
}
func ProvideSecureValueService(
tracer trace.Tracer,
accessClient claims.AccessClient,
secureValueMetadataStorage contracts.SecureValueMetadataStorage,
secureValueValidator contracts.SecureValueValidator,
secureValueMutator contracts.SecureValueMutator,
keeperMetadataStorage contracts.KeeperMetadataStorage,
keeperService contracts.KeeperService,
reg prometheus.Registerer,
) contracts.SecureValueService {
return &SecureValueService{
tracer: tracer,
accessClient: accessClient,
secureValueMetadataStorage: secureValueMetadataStorage,
secureValueValidator: secureValueValidator,
secureValueMutator: secureValueMutator,
keeperMetadataStorage: keeperMetadataStorage,
keeperService: keeperService,
metrics: metrics.NewSecureValueServiceMetrics(reg),
}
}
func (s *SecureValueService) Create(ctx context.Context, sv *secretv1beta1.SecureValue, actorUID string) (createdSv *secretv1beta1.SecureValue, createErr error) {
start := time.Now()
ctx, span := s.tracer.Start(ctx, "SecureValueService.Create", trace.WithAttributes(
attribute.String("namespace", sv.GetNamespace()),
attribute.String("actor", actorUID),
))
defer span.End()
defer func() {
args := []any{
"namespace", sv.GetNamespace(),
"actorUID", actorUID,
}
if createdSv != nil {
args = append(args, "name", createdSv.GetName())
span.SetAttributes(attribute.String("name", createdSv.GetName()))
}
success := createErr == nil
args = append(args, "success", success)
if !success {
span.SetStatus(codes.Error, "SecureValueService.Create failed")
span.RecordError(createErr)
args = append(args, "error", createErr)
}
logging.FromContext(ctx).Info("SecureValueService.Create finished", args...)
s.metrics.SecureValueCreateDuration.WithLabelValues(strconv.FormatBool(success)).Observe(time.Since(start).Seconds())
}()
// Secure value creation uses the active keeper
keeperName, keeperCfg, err := s.keeperMetadataStorage.GetActiveKeeperConfig(ctx, sv.Namespace)
if err != nil {
return nil, fmt.Errorf("fetching active keeper config: namespace=%+v %w", sv.Namespace, err)
}
return s.createNewVersion(ctx, keeperName, keeperCfg, sv, actorUID)
}
func (s *SecureValueService) Update(ctx context.Context, newSecureValue *secretv1beta1.SecureValue, actorUID string) (_ *secretv1beta1.SecureValue, sync bool, updateErr error) {
start := time.Now()
name, namespace := newSecureValue.GetName(), newSecureValue.GetNamespace()
ctx, span := s.tracer.Start(ctx, "SecureValueService.Update", trace.WithAttributes(
attribute.String("name", name),
attribute.String("namespace", namespace),
attribute.String("actor", actorUID),
))
defer span.End()
defer func() {
args := []any{
"name", name,
"namespace", namespace,
"actorUID", actorUID,
"sync", sync,
}
success := updateErr == nil
args = append(args, "success", success)
if !success {
span.SetStatus(codes.Error, "SecureValueService.Update failed")
span.RecordError(updateErr)
args = append(args, "error", updateErr)
}
logging.FromContext(ctx).Info("SecureValueService.Update finished", args...)
s.metrics.SecureValueUpdateDuration.WithLabelValues(strconv.FormatBool(success)).Observe(time.Since(start).Seconds())
}()
currentVersion, err := s.secureValueMetadataStorage.Read(ctx, xkube.Namespace(newSecureValue.Namespace), newSecureValue.Name, contracts.ReadOpts{})
if err != nil {
return nil, false, fmt.Errorf("reading secure value secret: %+w", err)
}
keeperCfg, err := s.keeperMetadataStorage.GetKeeperConfig(ctx, currentVersion.Namespace, currentVersion.Status.Keeper, contracts.ReadOpts{})
if err != nil {
return nil, false, fmt.Errorf("fetching keeper config: namespace=%+v keeper: %q %w", newSecureValue.Namespace, currentVersion.Status.Keeper, err)
}
if newSecureValue.Spec.Value == nil && newSecureValue.Spec.Ref == nil {
keeper, err := s.keeperService.KeeperForConfig(keeperCfg)
if err != nil {
return nil, false, fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", newSecureValue.Namespace, newSecureValue.Status.Keeper, err)
}
logging.FromContext(ctx).Debug("retrieved keeper", "namespace", newSecureValue.Namespace, "type", keeperCfg.Type())
secret, err := keeper.Expose(ctx, keeperCfg, xkube.Namespace(newSecureValue.Namespace), newSecureValue.Name, currentVersion.Status.Version)
if err != nil {
return nil, false, fmt.Errorf("reading secret value from keeper: %w %w", contracts.ErrSecureValueMissingSecretAndRef, err)
}
newSecureValue.Spec.Value = &secret
}
// Secure value updates use the keeper used to create the secure value
const updateIsSync = true
createdSv, err := s.createNewVersion(ctx, currentVersion.Status.Keeper, keeperCfg, newSecureValue, actorUID)
return createdSv, updateIsSync, err
}
func (s *SecureValueService) createNewVersion(ctx context.Context, keeperName string, keeperCfg secretv1beta1.KeeperConfig, sv *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, error) {
if keeperName == "" {
return nil, fmt.Errorf("keeper name is required, got empty string")
}
if err := s.secureValueMutator.Mutate(sv, admission.Create); err != nil {
return nil, err
}
if errorList := s.secureValueValidator.Validate(sv, nil, admission.Create); len(errorList) > 0 {
return nil, contracts.NewErrValidateSecureValue(errorList)
}
if sv.Spec.Ref != nil && keeperCfg.Type() == secretv1beta1.SystemKeeperType {
return nil, contracts.ErrReferenceWithSystemKeeper
}
createdSv, err := s.secureValueMetadataStorage.Create(ctx, keeperName, sv, actorUID)
if err != nil {
return nil, fmt.Errorf("creating secure value: %w", err)
}
createdSv.Status = secretv1beta1.SecureValueStatus{
Version: createdSv.Status.Version,
Keeper: keeperName,
}
keeper, err := s.keeperService.KeeperForConfig(keeperCfg)
if err != nil {
return nil, fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", createdSv.Namespace, keeperName, err)
}
logging.FromContext(ctx).Debug("retrieved keeper", "namespace", createdSv.Namespace, "type", keeperCfg.Type())
// TODO: can we stop using external id?
// TODO: store uses only the namespace and returns and id. It could be a kv instead.
// TODO: check that the encrypted store works with multiple versions
switch {
case sv.Spec.Value != nil:
externalID, err := keeper.Store(ctx, keeperCfg, xkube.Namespace(createdSv.Namespace), createdSv.Name, createdSv.Status.Version, sv.Spec.Value.DangerouslyExposeAndConsumeValue())
if err != nil {
return nil, fmt.Errorf("storing secure value in keeper: %w", err)
}
createdSv.Status.ExternalID = string(externalID)
if err := s.secureValueMetadataStorage.SetExternalID(ctx, xkube.Namespace(createdSv.Namespace), createdSv.Name, createdSv.Status.Version, externalID); err != nil {
return nil, fmt.Errorf("setting secure value external id: %w", err)
}
case sv.Spec.Ref != nil:
// No-op, there's nothing to store in the keeper since the
// secret is already stored in the 3rd party secret store
// and it's being referenced.
default:
return nil, fmt.Errorf("secure value doesn't specify either a secret value or a reference")
}
if err := s.secureValueMetadataStorage.SetVersionToActive(ctx, xkube.Namespace(createdSv.Namespace), createdSv.Name, createdSv.Status.Version); err != nil {
return nil, fmt.Errorf("marking secure value version as active: %w", err)
}
// In a single query:
// TODO: set external id
// TODO: set to active
return createdSv, nil
}
func (s *SecureValueService) Read(ctx context.Context, namespace xkube.Namespace, name string) (_ *secretv1beta1.SecureValue, readErr error) {
if namespace == "" {
return nil, fmt.Errorf("namespace cannot be empty")
}
if name == "" {
return nil, fmt.Errorf("name cannot be empty")
}
start := time.Now()
ctx, span := s.tracer.Start(ctx, "SecureValueService.Read", trace.WithAttributes(
attribute.String("name", name),
attribute.String("namespace", namespace.String()),
))
defer func() {
args := []any{
"name", name,
"namespace", namespace.String(),
}
success := readErr == nil
args = append(args, "success", success)
if !success {
span.SetStatus(codes.Error, "SecureValueService.Read failed")
span.RecordError(readErr)
args = append(args, "error", readErr)
}
logging.FromContext(ctx).Info("SecureValueService.Read finished", args...)
s.metrics.SecureValueReadDuration.WithLabelValues(strconv.FormatBool(success)).Observe(time.Since(start).Seconds())
}()
defer span.End()
return s.secureValueMetadataStorage.Read(ctx, namespace, name, contracts.ReadOpts{ForUpdate: false})
}
func (s *SecureValueService) List(ctx context.Context, namespace xkube.Namespace) (_ *secretv1beta1.SecureValueList, listErr error) {
if namespace == "" {
return nil, fmt.Errorf("namespace cannot be empty")
}
start := time.Now()
ctx, span := s.tracer.Start(ctx, "SecureValueService.List", trace.WithAttributes(
attribute.String("namespace", namespace.String()),
))
defer span.End()
defer func() {
args := []any{
"namespace", namespace,
}
success := listErr == nil
args = append(args, "success", success)
if !success {
span.SetStatus(codes.Error, "SecureValueService.List failed")
span.RecordError(listErr)
args = append(args, "error", listErr)
}
logging.FromContext(ctx).Info("SecureValueService.List finished", args...)
s.metrics.SecureValueListDuration.WithLabelValues(strconv.FormatBool(success)).Observe(time.Since(start).Seconds())
}()
user, ok := claims.AuthInfoFrom(ctx)
if !ok {
return nil, fmt.Errorf("missing auth info in context")
}
hasPermissionFor, _, err := s.accessClient.Compile(ctx, user, claims.ListRequest{
Group: secretv1beta1.APIGroup,
Resource: secretv1beta1.SecureValuesResourceInfo.GetName(),
Namespace: namespace.String(),
Verb: utils.VerbGet, // Why not VerbList?
})
if err != nil {
return nil, fmt.Errorf("failed to compile checker: %w", err)
}
secureValuesMetadata, err := s.secureValueMetadataStorage.List(ctx, namespace)
if err != nil {
return nil, fmt.Errorf("fetching secure values from storage: %+w", err)
}
out := make([]secretv1beta1.SecureValue, 0)
for _, metadata := range secureValuesMetadata {
// Check whether the user has permission to access this specific SecureValue in the namespace.
if !hasPermissionFor(metadata.Name, "") {
continue
}
out = append(out, metadata)
}
return &secretv1beta1.SecureValueList{
Items: out,
}, nil
}
func (s *SecureValueService) Delete(ctx context.Context, namespace xkube.Namespace, name string) (_ *secretv1beta1.SecureValue, deleteErr error) {
if namespace == "" {
return nil, fmt.Errorf("namespace cannot be empty")
}
if name == "" {
return nil, fmt.Errorf("name cannot be empty")
}
start := time.Now()
ctx, span := s.tracer.Start(ctx, "SecureValueService.Delete", trace.WithAttributes(
attribute.String("name", name),
attribute.String("namespace", namespace.String()),
))
defer span.End()
defer func() {
args := []any{
"name", name,
"namespace", namespace,
}
success := deleteErr == nil
args = append(args, "success", success)
if !success {
span.SetStatus(codes.Error, "SecureValueService.Delete failed")
span.RecordError(deleteErr)
args = append(args, "error", deleteErr)
}
logging.FromContext(ctx).Info("SecureValueService.Delete finished", args...)
s.metrics.SecureValueDeleteDuration.WithLabelValues(strconv.FormatBool(success)).Observe(time.Since(start).Seconds())
}()
// TODO: does this need to be for update?
sv, err := s.secureValueMetadataStorage.Read(ctx, namespace, name, contracts.ReadOpts{ForUpdate: true})
if err != nil {
return nil, fmt.Errorf("fetching secure value: %+w", err)
}
if err := s.secureValueMetadataStorage.SetVersionToInactive(ctx, namespace, name, sv.Status.Version); err != nil {
return nil, fmt.Errorf("setting secure value version to inactive: %+w", err)
}
return sv, nil
}
func (s *SecureValueService) SetKeeperAsActive(ctx context.Context, namespace xkube.Namespace, name string) error {
// The system keeper is not in the database, so skip checking it exists.
// TODO: should the system keeper be in the database?
if name != contracts.SystemKeeperName {
// Check keeper exists. No need to worry about time of check to time of use
// since trying to activate a just deleted keeper will result in all
// keepers being inactive and defaulting to the system keeper.
if _, err := s.keeperMetadataStorage.Read(ctx, namespace, name, contracts.ReadOpts{}); err != nil {
return fmt.Errorf("reading keeper before setting as active: %w", err)
}
}
if err := s.keeperMetadataStorage.SetAsActive(ctx, namespace, name); err != nil {
return fmt.Errorf("calling keeper metadata storage to set keeper as active: %w", err)
}
return nil
}