Files
grafana/pkg/registry/apis/secret/validator/secure_value.go
BrunoandMatheus Macabu 0d67442f1a Secrets manager: create secure value using the active keeper (#114039)
* Secrets manager: create secure value using the active keeper

* SecureValueService.Update: fetch secure value from db to get the keeper

* fix keeper_store_test.go

* SecureValueService: fix bug in update where the current version keeper wasn't being passed to the createNewVersion method

* make gofmt

* remove outdated test

* update TestModel

* undo enterprise_imports changes

* use xkube.Namespace

* migrator: set secret_secure_value.keeper to 'system' when the column is null

* indent cue

* fix tests

* fix enterprise imports

* properly fix enterprise imports

* make update-workspace

* go mod tidy

---------

Co-authored-by: Matheus Macabu <macabu.matheus@gmail.com>
2025-11-21 11:20:16 -03:00

170 lines
5.0 KiB
Go

package validator
import (
"errors"
"strconv"
"strings"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/admission"
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
)
type secureValueValidator struct{}
var _ contracts.SecureValueValidator = &secureValueValidator{}
func ProvideSecureValueValidator() contracts.SecureValueValidator {
return &secureValueValidator{}
}
func (v *secureValueValidator) Validate(sv, oldSv *secretv1beta1.SecureValue, operation admission.Operation) field.ErrorList {
errs := make(field.ErrorList, 0)
// Operation-specific field validation.
switch operation {
case admission.Create:
errs = validateSecureValueCreate(sv)
// If we plan to support PATCH-style updates, we shouldn't be requiring fields to be set.
case admission.Update:
errs = validateSecureValueUpdate(sv, oldSv)
case admission.Delete:
case admission.Connect:
}
// General validations.
if err := validation.IsDNS1123Subdomain(sv.Name); len(err) > 0 {
errs = append(
errs,
field.Invalid(field.NewPath("metadata", "name"), sv.Name, strings.Join(err, ",")),
)
return errs
}
if err := validation.IsDNS1123Subdomain(sv.Namespace); len(err) > 0 {
errs = append(
errs,
field.Invalid(field.NewPath("metadata", "namespace"), sv.Name, strings.Join(err, ",")),
)
return errs
}
if sv.Spec.Value != nil && len(*sv.Spec.Value) > contracts.SecureValueRawInputMaxSizeBytes {
errs = append(
errs,
field.TooLong(field.NewPath("spec", "value"), len(*sv.Spec.Value), contracts.SecureValueRawInputMaxSizeBytes),
)
}
if errs := validateDecrypters(sv.Spec.Decrypters); len(errs) > 0 {
return errs
}
return errs
}
// validateSecureValueCreate does basic spec validation of a securevalue for the Create operation.
func validateSecureValueCreate(sv *secretv1beta1.SecureValue) field.ErrorList {
errs := make(field.ErrorList, 0)
if sv.Spec.Description == "" {
errs = append(errs, field.Required(field.NewPath("spec", "description"), "a `description` is required"))
}
if (sv.Spec.Value == nil || (sv.Spec.Value != nil && *sv.Spec.Value == "")) && (sv.Spec.Ref == nil || (sv.Spec.Ref != nil && *sv.Spec.Ref == "")) {
errs = append(errs, field.Required(field.NewPath("spec"), "either a `value` or `ref` is required"))
}
if (sv.Spec.Value != nil && *sv.Spec.Value != "") && (sv.Spec.Ref != nil && *sv.Spec.Ref != "") {
errs = append(errs, field.Forbidden(field.NewPath("spec"), "only one of `value` or `ref` can be set"))
}
return errs
}
// validateSecureValueUpdate does basic spec validation of a securevalue for the Update operation.
func validateSecureValueUpdate(sv, oldSv *secretv1beta1.SecureValue) field.ErrorList {
errs := make(field.ErrorList, 0)
// For updates, an `old` object is required.
if oldSv == nil {
errs = append(errs, field.InternalError(field.NewPath("spec"), errors.New("old object is nil")))
return errs
}
// Only validate if one of the fields is being changed/set.
if (sv.Spec.Value != nil && *sv.Spec.Value != "") || (sv.Spec.Ref != nil && *sv.Spec.Ref != "") {
if (oldSv.Spec.Ref != nil && *oldSv.Spec.Ref != "") && (sv.Spec.Value != nil && *sv.Spec.Value != "") {
errs = append(errs, field.Forbidden(field.NewPath("spec"), "cannot set `value` when `ref` was already previously set"))
}
if (oldSv.Spec.Ref == nil || (oldSv.Spec.Ref != nil && *oldSv.Spec.Ref == "")) && (sv.Spec.Ref != nil && *sv.Spec.Ref != "") {
errs = append(errs, field.Forbidden(field.NewPath("spec"), "cannot set `ref` when `value` was already previously set"))
}
}
return errs
}
// validateDecrypters validates that (if populated) the `decrypters` must be unique.
func validateDecrypters(decrypters []string) field.ErrorList {
errs := make(field.ErrorList, 0)
// Limit the number of decrypters to 64 to not have it unbounded.
// The number was chosen arbitrarily and should be enough.
if len(decrypters) > 64 {
errs = append(
errs,
field.TooMany(field.NewPath("spec", "decrypters"), len(decrypters), 64),
)
return errs
}
decrypterNames := make(map[string]struct{}, 0)
for i, decrypter := range decrypters {
decrypter = strings.TrimSpace(decrypter)
if decrypter == "" {
errs = append(
errs,
field.Invalid(field.NewPath("spec", "decrypters", "["+strconv.Itoa(i)+"]"), decrypter, "decrypters cannot be empty if specified"),
)
continue
}
// Use the same validation as labels for the decrypters.
if verrs := validation.IsValidLabelValue(decrypter); len(verrs) > 0 {
for _, verr := range verrs {
errs = append(
errs,
field.Invalid(field.NewPath("spec", "decrypters", "["+strconv.Itoa(i)+"]"), decrypter, verr),
)
}
continue
}
if _, exists := decrypterNames[decrypter]; exists {
errs = append(
errs,
field.Invalid(field.NewPath("spec", "decrypters", "["+strconv.Itoa(i)+"]"), decrypter, "decrypters must be unique"),
)
continue
}
decrypterNames[decrypter] = struct{}{}
}
return errs
}