7698970f22
* 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
400 lines
14 KiB
Go
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
|
|
}
|