Secrets: Add separate package for resource validation (#108097)
* Secrets: Add SecureValueService interface * Secrets: Move resource validators to their own package/structs for reusing
This commit is contained in:
@@ -38,3 +38,11 @@ type SecureValueMetadataStorage interface {
|
||||
SetExternalID(ctx context.Context, namespace xkube.Namespace, name string, version int64, externalID ExternalID) error
|
||||
ReadForDecrypt(ctx context.Context, namespace xkube.Namespace, name string) (*DecryptSecureValue, error)
|
||||
}
|
||||
|
||||
type SecureValueService interface {
|
||||
Create(ctx context.Context, sv *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, error)
|
||||
Read(ctx context.Context, namespace xkube.Namespace, name string) (*secretv1beta1.SecureValue, error)
|
||||
List(ctx context.Context, namespace xkube.Namespace) (*secretv1beta1.SecureValueList, error)
|
||||
Update(ctx context.Context, newSecureValue *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, bool, error)
|
||||
Delete(ctx context.Context, namespace xkube.Namespace, name string) (*secretv1beta1.SecureValue, error)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
|
||||
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
|
||||
)
|
||||
|
||||
type SecureValueValidator interface {
|
||||
Validate(sv *secretv1beta1.SecureValue, oldSv *secretv1beta1.SecureValue, operation admission.Operation) field.ErrorList
|
||||
}
|
||||
|
||||
type KeeperValidator interface {
|
||||
Validate(keeper *secretv1beta1.Keeper, oldKeeper *secretv1beta1.Keeper, operation admission.Operation) field.ErrorList
|
||||
}
|
||||
@@ -5,13 +5,14 @@ import (
|
||||
"fmt"
|
||||
|
||||
claims "github.com/grafana/authlib/types"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"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/xkube"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
type SecureValueService struct {
|
||||
@@ -23,6 +24,8 @@ type SecureValueService struct {
|
||||
keeperService contracts.KeeperService
|
||||
}
|
||||
|
||||
var _ contracts.SecureValueService = &SecureValueService{}
|
||||
|
||||
func ProvideSecureValueService(
|
||||
tracer trace.Tracer,
|
||||
accessClient claims.AccessClient,
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"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 keeperValidator struct{}
|
||||
|
||||
var _ contracts.KeeperValidator = &keeperValidator{}
|
||||
|
||||
func ProvideKeeperValidator() contracts.KeeperValidator {
|
||||
return &keeperValidator{}
|
||||
}
|
||||
|
||||
func (v *keeperValidator) Validate(keeper *secretv1beta1.Keeper, oldKeeper *secretv1beta1.Keeper, operation admission.Operation) field.ErrorList {
|
||||
// Only validate Create and Update for now.
|
||||
if operation != admission.Create && operation != admission.Update {
|
||||
return nil
|
||||
}
|
||||
|
||||
errs := make(field.ErrorList, 0)
|
||||
|
||||
if keeper.Spec.Description == "" {
|
||||
errs = append(errs, field.Required(field.NewPath("spec", "description"), "a `description` is required"))
|
||||
}
|
||||
|
||||
// Only one keeper type can be configured. Return early and don't validate the specific keeper fields.
|
||||
if err := validateKeepers(keeper); err != nil {
|
||||
errs = append(errs, err)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
if keeper.Spec.Aws != nil {
|
||||
if err := validateCredentialValue(field.NewPath("spec", "aws", "accessKeyID"), keeper.Spec.Aws.AccessKeyID); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if err := validateCredentialValue(field.NewPath("spec", "aws", "secretAccessKey"), keeper.Spec.Aws.SecretAccessKey); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if keeper.Spec.Azure != nil {
|
||||
if keeper.Spec.Azure.KeyVaultName == "" {
|
||||
errs = append(errs, field.Required(field.NewPath("spec", "azure", "keyVaultName"), "a `keyVaultName` is required"))
|
||||
}
|
||||
|
||||
if keeper.Spec.Azure.TenantID == "" {
|
||||
errs = append(errs, field.Required(field.NewPath("spec", "azure", "tenantID"), "a `tenantID` is required"))
|
||||
}
|
||||
|
||||
if keeper.Spec.Azure.ClientID == "" {
|
||||
errs = append(errs, field.Required(field.NewPath("spec", "azure", "clientID"), "a `clientID` is required"))
|
||||
}
|
||||
|
||||
if err := validateCredentialValue(field.NewPath("spec", "azure", "clientSecret"), keeper.Spec.Azure.ClientSecret); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if keeper.Spec.Gcp != nil {
|
||||
if keeper.Spec.Gcp.ProjectID == "" {
|
||||
errs = append(errs, field.Required(field.NewPath("spec", "gcp", "projectID"), "a `projectID` is required"))
|
||||
}
|
||||
|
||||
if keeper.Spec.Gcp.CredentialsFile == "" {
|
||||
errs = append(errs, field.Required(field.NewPath("spec", "gcp", "credentialsFile"), "a `credentialsFile` is required"))
|
||||
}
|
||||
}
|
||||
|
||||
if keeper.Spec.HashiCorpVault != nil {
|
||||
if keeper.Spec.HashiCorpVault.Address == "" {
|
||||
errs = append(errs, field.Required(field.NewPath("spec", "hashiCorpVault", "address"), "an `address` is required"))
|
||||
}
|
||||
|
||||
if err := validateCredentialValue(field.NewPath("spec", "hashiCorpVault", "token"), keeper.Spec.HashiCorpVault.Token); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func validateKeepers(keeper *secretv1beta1.Keeper) *field.Error {
|
||||
availableKeepers := map[string]bool{
|
||||
"aws": keeper.Spec.Aws != nil,
|
||||
"azure": keeper.Spec.Azure != nil,
|
||||
"gcp": keeper.Spec.Gcp != nil,
|
||||
"hashiCorpVault": keeper.Spec.HashiCorpVault != nil,
|
||||
}
|
||||
|
||||
configuredKeepers := make([]string, 0)
|
||||
|
||||
for keeperKind, notNil := range availableKeepers {
|
||||
if notNil {
|
||||
configuredKeepers = append(configuredKeepers, keeperKind)
|
||||
}
|
||||
}
|
||||
|
||||
if len(configuredKeepers) == 0 {
|
||||
return field.Required(field.NewPath("spec"), "at least one `keeper` must be present")
|
||||
}
|
||||
|
||||
if len(configuredKeepers) > 1 {
|
||||
return field.Invalid(
|
||||
field.NewPath("spec"),
|
||||
strings.Join(configuredKeepers, " & "),
|
||||
"only one `keeper` can be present at a time but found more",
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCredentialValue(path *field.Path, credentials secretv1beta1.KeeperCredentialValue) *field.Error {
|
||||
availableOptions := map[string]bool{
|
||||
"secureValueName": credentials.SecureValueName != "",
|
||||
"valueFromEnv": credentials.ValueFromEnv != "",
|
||||
"valueFromConfig": credentials.ValueFromConfig != "",
|
||||
}
|
||||
|
||||
configuredCredentials := make([]string, 0)
|
||||
|
||||
for credentialKind, notEmpty := range availableOptions {
|
||||
if notEmpty {
|
||||
configuredCredentials = append(configuredCredentials, credentialKind)
|
||||
}
|
||||
}
|
||||
|
||||
if len(configuredCredentials) == 0 {
|
||||
return field.Required(path, "one of `secureValueName`, `valueFromEnv` or `valueFromConfig` must be present")
|
||||
}
|
||||
|
||||
if len(configuredCredentials) > 1 {
|
||||
return field.Invalid(
|
||||
path,
|
||||
strings.Join(configuredCredentials, " & "),
|
||||
"only one of `secureValueName`, `valueFromEnv` or `valueFromConfig` must be present at a time but found more",
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
|
||||
)
|
||||
|
||||
func TestValidateKeeper(t *testing.T) {
|
||||
t.Run("when creating a new keeper", func(t *testing.T) {
|
||||
t.Run("the `description` must be present", func(t *testing.T) {
|
||||
keeper := &secretv1beta1.Keeper{
|
||||
Spec: secretv1beta1.KeeperSpec{
|
||||
Aws: &secretv1beta1.KeeperAWSConfig{
|
||||
AccessKeyID: secretv1beta1.KeeperCredentialValue{ValueFromEnv: "some-value"},
|
||||
SecretAccessKey: secretv1beta1.KeeperCredentialValue{ValueFromEnv: "some-value"},
|
||||
KmsKeyID: ptr.To("kms-key-id"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
validator := ProvideKeeperValidator()
|
||||
errs := validator.Validate(keeper, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.description", errs[0].Field)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("only one `keeper` must be present", func(t *testing.T) {
|
||||
keeper := &secretv1beta1.Keeper{
|
||||
Spec: secretv1beta1.KeeperSpec{
|
||||
Description: "short description",
|
||||
Aws: &secretv1beta1.KeeperAWSConfig{},
|
||||
Azure: &secretv1beta1.KeeperAzureConfig{},
|
||||
Gcp: &secretv1beta1.KeeperGCPConfig{},
|
||||
HashiCorpVault: &secretv1beta1.KeeperHashiCorpConfig{},
|
||||
},
|
||||
}
|
||||
|
||||
validator := ProvideKeeperValidator()
|
||||
errs := validator.Validate(keeper, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec", errs[0].Field)
|
||||
})
|
||||
|
||||
t.Run("at least one `keeper` must be present", func(t *testing.T) {
|
||||
keeper := &secretv1beta1.Keeper{
|
||||
Spec: secretv1beta1.KeeperSpec{
|
||||
Description: "description",
|
||||
},
|
||||
}
|
||||
|
||||
validator := ProvideKeeperValidator()
|
||||
errs := validator.Validate(keeper, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec", errs[0].Field)
|
||||
})
|
||||
|
||||
t.Run("aws keeper validation", func(t *testing.T) {
|
||||
validKeeperAWS := &secretv1beta1.Keeper{
|
||||
Spec: secretv1beta1.KeeperSpec{
|
||||
Description: "description",
|
||||
Aws: &secretv1beta1.KeeperAWSConfig{
|
||||
AccessKeyID: secretv1beta1.KeeperCredentialValue{
|
||||
ValueFromEnv: "some-value",
|
||||
},
|
||||
SecretAccessKey: secretv1beta1.KeeperCredentialValue{
|
||||
SecureValueName: "some-value",
|
||||
},
|
||||
KmsKeyID: ptr.To("optional"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("`accessKeyID` must be present", func(t *testing.T) {
|
||||
t.Run("at least one of the credential value must be present", func(t *testing.T) {
|
||||
keeper := validKeeperAWS.DeepCopy()
|
||||
keeper.Spec.Aws.AccessKeyID = secretv1beta1.KeeperCredentialValue{}
|
||||
|
||||
validator := ProvideKeeperValidator()
|
||||
errs := validator.Validate(keeper, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.aws.accessKeyID", errs[0].Field)
|
||||
})
|
||||
|
||||
t.Run("at most one of the credential value must be present", func(t *testing.T) {
|
||||
keeper := validKeeperAWS.DeepCopy()
|
||||
keeper.Spec.Aws.AccessKeyID = secretv1beta1.KeeperCredentialValue{
|
||||
SecureValueName: "a",
|
||||
ValueFromEnv: "b",
|
||||
ValueFromConfig: "c",
|
||||
}
|
||||
|
||||
validator := ProvideKeeperValidator()
|
||||
errs := validator.Validate(keeper, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.aws.accessKeyID", errs[0].Field)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("`secretAccessKey` must be present", func(t *testing.T) {
|
||||
t.Run("at least one of the credential value must be present", func(t *testing.T) {
|
||||
keeper := validKeeperAWS.DeepCopy()
|
||||
keeper.Spec.Aws.SecretAccessKey = secretv1beta1.KeeperCredentialValue{}
|
||||
|
||||
validator := ProvideKeeperValidator()
|
||||
errs := validator.Validate(keeper, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.aws.secretAccessKey", errs[0].Field)
|
||||
})
|
||||
|
||||
t.Run("at most one of the credential value must be present", func(t *testing.T) {
|
||||
keeper := validKeeperAWS.DeepCopy()
|
||||
keeper.Spec.Aws.SecretAccessKey = secretv1beta1.KeeperCredentialValue{
|
||||
SecureValueName: "a",
|
||||
ValueFromEnv: "b",
|
||||
ValueFromConfig: "c",
|
||||
}
|
||||
|
||||
validator := ProvideKeeperValidator()
|
||||
errs := validator.Validate(keeper, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.aws.secretAccessKey", errs[0].Field)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("azure keeper validation", func(t *testing.T) {
|
||||
validKeeperAzure := &secretv1beta1.Keeper{
|
||||
Spec: secretv1beta1.KeeperSpec{
|
||||
Description: "description",
|
||||
Azure: &secretv1beta1.KeeperAzureConfig{
|
||||
KeyVaultName: "kv-name",
|
||||
TenantID: "tenant-id",
|
||||
ClientID: "client-id",
|
||||
ClientSecret: secretv1beta1.KeeperCredentialValue{
|
||||
ValueFromConfig: "config.path.value",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("`keyVaultName` must be present", func(t *testing.T) {
|
||||
keeper := validKeeperAzure.DeepCopy()
|
||||
keeper.Spec.Azure.KeyVaultName = ""
|
||||
|
||||
validator := ProvideKeeperValidator()
|
||||
errs := validator.Validate(keeper, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.azure.keyVaultName", errs[0].Field)
|
||||
})
|
||||
|
||||
t.Run("`tenantID` must be present", func(t *testing.T) {
|
||||
keeper := validKeeperAzure.DeepCopy()
|
||||
keeper.Spec.Azure.TenantID = ""
|
||||
|
||||
validator := ProvideKeeperValidator()
|
||||
errs := validator.Validate(keeper, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.azure.tenantID", errs[0].Field)
|
||||
})
|
||||
|
||||
t.Run("`clientID` must be present", func(t *testing.T) {
|
||||
keeper := validKeeperAzure.DeepCopy()
|
||||
keeper.Spec.Azure.ClientID = ""
|
||||
|
||||
validator := ProvideKeeperValidator()
|
||||
errs := validator.Validate(keeper, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.azure.clientID", errs[0].Field)
|
||||
})
|
||||
|
||||
t.Run("`clientSecret` must be present", func(t *testing.T) {
|
||||
t.Run("at least one of the credential value must be present", func(t *testing.T) {
|
||||
keeper := validKeeperAzure.DeepCopy()
|
||||
keeper.Spec.Azure.ClientSecret = secretv1beta1.KeeperCredentialValue{}
|
||||
|
||||
validator := ProvideKeeperValidator()
|
||||
errs := validator.Validate(keeper, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.azure.clientSecret", errs[0].Field)
|
||||
})
|
||||
|
||||
t.Run("at most one of the credential value must be present", func(t *testing.T) {
|
||||
keeper := validKeeperAzure.DeepCopy()
|
||||
keeper.Spec.Azure.ClientSecret = secretv1beta1.KeeperCredentialValue{
|
||||
SecureValueName: "a",
|
||||
ValueFromEnv: "b",
|
||||
ValueFromConfig: "c",
|
||||
}
|
||||
|
||||
validator := ProvideKeeperValidator()
|
||||
errs := validator.Validate(keeper, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.azure.clientSecret", errs[0].Field)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("gcp keeper validation", func(t *testing.T) {
|
||||
validKeeperGCP := &secretv1beta1.Keeper{
|
||||
Spec: secretv1beta1.KeeperSpec{
|
||||
Description: "description",
|
||||
Gcp: &secretv1beta1.KeeperGCPConfig{
|
||||
ProjectID: "project-id",
|
||||
CredentialsFile: "/path/to/credentials/file.json",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("`projectID` must be present", func(t *testing.T) {
|
||||
keeper := validKeeperGCP.DeepCopy()
|
||||
keeper.Spec.Gcp.ProjectID = ""
|
||||
|
||||
validator := ProvideKeeperValidator()
|
||||
errs := validator.Validate(keeper, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.gcp.projectID", errs[0].Field)
|
||||
})
|
||||
|
||||
t.Run("`credentialsFile` must be present", func(t *testing.T) {
|
||||
keeper := validKeeperGCP.DeepCopy()
|
||||
keeper.Spec.Gcp.CredentialsFile = ""
|
||||
|
||||
validator := ProvideKeeperValidator()
|
||||
errs := validator.Validate(keeper, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.gcp.credentialsFile", errs[0].Field)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("hashicorp keeper validation", func(t *testing.T) {
|
||||
validKeeperHashiCorp := &secretv1beta1.Keeper{
|
||||
Spec: secretv1beta1.KeeperSpec{
|
||||
Description: "description",
|
||||
HashiCorpVault: &secretv1beta1.KeeperHashiCorpConfig{
|
||||
Address: "http://address",
|
||||
Token: secretv1beta1.KeeperCredentialValue{
|
||||
ValueFromConfig: "config.path.value",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("`address` must be present", func(t *testing.T) {
|
||||
keeper := validKeeperHashiCorp.DeepCopy()
|
||||
keeper.Spec.HashiCorpVault.Address = ""
|
||||
|
||||
validator := ProvideKeeperValidator()
|
||||
errs := validator.Validate(keeper, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.hashiCorpVault.address", errs[0].Field)
|
||||
})
|
||||
|
||||
t.Run("`token` must be present", func(t *testing.T) {
|
||||
t.Run("at least one of the credential value must be present", func(t *testing.T) {
|
||||
keeper := validKeeperHashiCorp.DeepCopy()
|
||||
keeper.Spec.HashiCorpVault.Token = secretv1beta1.KeeperCredentialValue{}
|
||||
|
||||
validator := ProvideKeeperValidator()
|
||||
errs := validator.Validate(keeper, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.hashiCorpVault.token", errs[0].Field)
|
||||
})
|
||||
|
||||
t.Run("at most one of the credential value must be present", func(t *testing.T) {
|
||||
keeper := validKeeperHashiCorp.DeepCopy()
|
||||
keeper.Spec.HashiCorpVault.Token = secretv1beta1.KeeperCredentialValue{
|
||||
SecureValueName: "a",
|
||||
ValueFromEnv: "b",
|
||||
ValueFromConfig: "c",
|
||||
}
|
||||
|
||||
validator := ProvideKeeperValidator()
|
||||
errs := validator.Validate(keeper, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.hashiCorpVault.token", errs[0].Field)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"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 {
|
||||
decryptersAllowList contracts.DecryptAllowList
|
||||
}
|
||||
|
||||
var _ contracts.SecureValueValidator = &secureValueValidator{}
|
||||
|
||||
func ProvideSecureValueValidator(decryptersAllowList contracts.DecryptAllowList) contracts.SecureValueValidator {
|
||||
return &secureValueValidator{
|
||||
decryptersAllowList: decryptersAllowList,
|
||||
}
|
||||
}
|
||||
|
||||
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 sv.Spec.Value != nil && len(*sv.Spec.Value) > contracts.SECURE_VALUE_RAW_INPUT_MAX_SIZE_BYTES {
|
||||
errs = append(
|
||||
errs,
|
||||
field.TooLong(field.NewPath("spec", "value"), len(*sv.Spec.Value), contracts.SECURE_VALUE_RAW_INPUT_MAX_SIZE_BYTES),
|
||||
)
|
||||
}
|
||||
|
||||
if errs := validateDecrypters(sv.Spec.Decrypters, v.decryptersAllowList); 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"))
|
||||
}
|
||||
}
|
||||
|
||||
// Keeper cannot be changed.
|
||||
if sv.Spec.Keeper != oldSv.Spec.Keeper {
|
||||
errs = append(errs, field.Forbidden(field.NewPath("spec"), "the `keeper` cannot be changed"))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
// validateDecrypters validates that (if populated) the `decrypters` must be unique.
|
||||
func validateDecrypters(decrypters []string, decryptersAllowList map[string]struct{}) 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
|
||||
}
|
||||
|
||||
// Allow List: decrypters must match exactly and be in the allowed list to be able to decrypt.
|
||||
if len(decryptersAllowList) > 0 {
|
||||
if _, exists := decryptersAllowList[decrypter]; !exists {
|
||||
errs = append(
|
||||
errs,
|
||||
field.Invalid(field.NewPath("spec", "decrypters", "["+strconv.Itoa(i)+"]"), decrypter, fmt.Sprintf("allowed values: %v", decryptersAllowList)),
|
||||
)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
)
|
||||
|
||||
func TestValidateSecureValue(t *testing.T) {
|
||||
t.Run("when creating a new securevalue", func(t *testing.T) {
|
||||
keeper := "keeper"
|
||||
validSecureValue := &secretv1beta1.SecureValue{
|
||||
Spec: secretv1beta1.SecureValueSpec{
|
||||
Description: "description",
|
||||
Value: ptr.To(secretv1beta1.NewExposedSecureValue("value")),
|
||||
Keeper: &keeper,
|
||||
Decrypters: []string{"app1", "app2"},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("the `description` must be present", func(t *testing.T) {
|
||||
sv := validSecureValue.DeepCopy()
|
||||
sv.Spec.Description = ""
|
||||
|
||||
validator := ProvideSecureValueValidator(nil)
|
||||
errs := validator.Validate(sv, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.description", errs[0].Field)
|
||||
})
|
||||
|
||||
t.Run("either a `value` or `ref` must be present but not both", func(t *testing.T) {
|
||||
// nil
|
||||
sv := validSecureValue.DeepCopy()
|
||||
sv.Spec.Value = nil
|
||||
sv.Spec.Ref = nil
|
||||
|
||||
validator := ProvideSecureValueValidator(nil)
|
||||
errs := validator.Validate(sv, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec", errs[0].Field)
|
||||
|
||||
// empty value
|
||||
sv.Spec.Value = ptr.To(secretv1beta1.NewExposedSecureValue(""))
|
||||
sv.Spec.Ref = nil
|
||||
|
||||
validator = ProvideSecureValueValidator(nil)
|
||||
errs = validator.Validate(sv, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec", errs[0].Field)
|
||||
|
||||
// present value and ref
|
||||
ref := "value"
|
||||
sv.Spec.Value = ptr.To(secretv1beta1.NewExposedSecureValue("value"))
|
||||
sv.Spec.Ref = &ref
|
||||
|
||||
validator = ProvideSecureValueValidator(nil)
|
||||
errs = validator.Validate(sv, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec", errs[0].Field)
|
||||
})
|
||||
|
||||
t.Run("`value` cannot exceed 24576 bytes", func(t *testing.T) {
|
||||
sv := validSecureValue.DeepCopy()
|
||||
sv.Spec.Value = ptr.To(secretv1beta1.NewExposedSecureValue(strings.Repeat("a", contracts.SECURE_VALUE_RAW_INPUT_MAX_SIZE_BYTES+1)))
|
||||
sv.Spec.Ref = nil
|
||||
|
||||
validator := ProvideSecureValueValidator(nil)
|
||||
errs := validator.Validate(sv, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.value", errs[0].Field)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("when updating a securevalue", func(t *testing.T) {
|
||||
t.Run("when trying to switch from a `value` (old) to a `ref` (new), it returns an error", func(t *testing.T) {
|
||||
oldSv := &secretv1beta1.SecureValue{
|
||||
Spec: secretv1beta1.SecureValueSpec{
|
||||
Ref: nil, // empty `ref` means a `value` was present.
|
||||
},
|
||||
}
|
||||
|
||||
ref := "ref"
|
||||
sv := &secretv1beta1.SecureValue{
|
||||
Spec: secretv1beta1.SecureValueSpec{
|
||||
Ref: &ref,
|
||||
},
|
||||
}
|
||||
|
||||
validator := ProvideSecureValueValidator(nil)
|
||||
errs := validator.Validate(sv, oldSv, admission.Update)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec", errs[0].Field)
|
||||
})
|
||||
|
||||
t.Run("when trying to switch from a `ref` (old) to a `value` (new), it returns an error", func(t *testing.T) {
|
||||
ref := "non-empty"
|
||||
oldSv := &secretv1beta1.SecureValue{
|
||||
Spec: secretv1beta1.SecureValueSpec{
|
||||
Ref: &ref,
|
||||
},
|
||||
}
|
||||
|
||||
sv := &secretv1beta1.SecureValue{
|
||||
Spec: secretv1beta1.SecureValueSpec{
|
||||
Value: ptr.To(secretv1beta1.NewExposedSecureValue("value")),
|
||||
},
|
||||
}
|
||||
|
||||
validator := ProvideSecureValueValidator(nil)
|
||||
errs := validator.Validate(sv, oldSv, admission.Update)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec", errs[0].Field)
|
||||
})
|
||||
|
||||
t.Run("when both `value` and `ref` are set, it returns an error", func(t *testing.T) {
|
||||
refNonEmpty := "non-empty"
|
||||
oldSv := &secretv1beta1.SecureValue{
|
||||
Spec: secretv1beta1.SecureValueSpec{
|
||||
Ref: &refNonEmpty,
|
||||
},
|
||||
}
|
||||
|
||||
ref := "ref"
|
||||
sv := &secretv1beta1.SecureValue{
|
||||
Spec: secretv1beta1.SecureValueSpec{
|
||||
Value: ptr.To(secretv1beta1.NewExposedSecureValue("value")),
|
||||
Ref: &ref,
|
||||
},
|
||||
}
|
||||
|
||||
validator := ProvideSecureValueValidator(nil)
|
||||
errs := validator.Validate(sv, oldSv, admission.Update)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec", errs[0].Field)
|
||||
|
||||
oldSv = &secretv1beta1.SecureValue{
|
||||
Spec: secretv1beta1.SecureValueSpec{
|
||||
Value: ptr.To(secretv1beta1.NewExposedSecureValue("non-empty")),
|
||||
},
|
||||
}
|
||||
|
||||
validator = ProvideSecureValueValidator(nil)
|
||||
errs = validator.Validate(sv, oldSv, admission.Update)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec", errs[0].Field)
|
||||
})
|
||||
|
||||
t.Run("when no changes are made, it returns no errors", func(t *testing.T) {
|
||||
oldSv := &secretv1beta1.SecureValue{
|
||||
Spec: secretv1beta1.SecureValueSpec{
|
||||
Description: "old-description",
|
||||
},
|
||||
}
|
||||
|
||||
sv := &secretv1beta1.SecureValue{
|
||||
Spec: secretv1beta1.SecureValueSpec{
|
||||
Description: "new-description",
|
||||
},
|
||||
}
|
||||
|
||||
validator := ProvideSecureValueValidator(nil)
|
||||
errs := validator.Validate(sv, oldSv, admission.Update)
|
||||
require.Empty(t, errs)
|
||||
})
|
||||
|
||||
t.Run("when the old object is `nil` it returns an error", func(t *testing.T) {
|
||||
sv := &secretv1beta1.SecureValue{}
|
||||
|
||||
validator := ProvideSecureValueValidator(nil)
|
||||
errs := validator.Validate(sv, nil, admission.Update)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec", errs[0].Field)
|
||||
})
|
||||
|
||||
t.Run("when trying to change the `keeper`, it returns an error", func(t *testing.T) {
|
||||
keeperA := "a-keeper"
|
||||
keeperAnother := "another-keeper"
|
||||
oldSv := &secretv1beta1.SecureValue{
|
||||
Spec: secretv1beta1.SecureValueSpec{
|
||||
Keeper: &keeperA,
|
||||
},
|
||||
}
|
||||
|
||||
sv := &secretv1beta1.SecureValue{
|
||||
Spec: secretv1beta1.SecureValueSpec{
|
||||
Keeper: &keeperAnother,
|
||||
},
|
||||
}
|
||||
|
||||
validator := ProvideSecureValueValidator(nil)
|
||||
errs := validator.Validate(sv, oldSv, admission.Update)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec", errs[0].Field)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("`decrypters` must have unique items", func(t *testing.T) {
|
||||
ref := "ref"
|
||||
sv := &secretv1beta1.SecureValue{
|
||||
Spec: secretv1beta1.SecureValueSpec{
|
||||
Description: "description", Ref: &ref,
|
||||
|
||||
Decrypters: []string{
|
||||
"app1",
|
||||
"app1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
validator := ProvideSecureValueValidator(nil)
|
||||
errs := validator.Validate(sv, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.decrypters.[1]", errs[0].Field)
|
||||
})
|
||||
|
||||
t.Run("when set, the `decrypters` must be one of the allowed in the allow list", func(t *testing.T) {
|
||||
allowList := map[string]struct{}{"app1": {}, "app2": {}}
|
||||
decrypters := slices.Collect(maps.Keys(allowList))
|
||||
|
||||
t.Run("no matches, returns an error", func(t *testing.T) {
|
||||
ref := "ref"
|
||||
sv := &secretv1beta1.SecureValue{
|
||||
Spec: secretv1beta1.SecureValueSpec{
|
||||
Description: "description", Ref: &ref,
|
||||
|
||||
Decrypters: []string{"app3"},
|
||||
},
|
||||
}
|
||||
|
||||
validator := ProvideSecureValueValidator(allowList)
|
||||
errs := validator.Validate(sv, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
})
|
||||
|
||||
t.Run("no decrypters, returns no error", func(t *testing.T) {
|
||||
ref := "ref"
|
||||
sv := &secretv1beta1.SecureValue{
|
||||
Spec: secretv1beta1.SecureValueSpec{
|
||||
Description: "description", Ref: &ref,
|
||||
|
||||
Decrypters: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
validator := ProvideSecureValueValidator(allowList)
|
||||
errs := validator.Validate(sv, nil, admission.Create)
|
||||
require.Empty(t, errs)
|
||||
})
|
||||
|
||||
t.Run("one match, returns no errors", func(t *testing.T) {
|
||||
ref := "ref"
|
||||
sv := &secretv1beta1.SecureValue{
|
||||
Spec: secretv1beta1.SecureValueSpec{
|
||||
Description: "description", Ref: &ref,
|
||||
|
||||
Decrypters: []string{decrypters[0]},
|
||||
},
|
||||
}
|
||||
|
||||
validator := ProvideSecureValueValidator(allowList)
|
||||
errs := validator.Validate(sv, nil, admission.Create)
|
||||
require.Empty(t, errs)
|
||||
})
|
||||
|
||||
t.Run("all matches, returns no errors", func(t *testing.T) {
|
||||
ref := "ref"
|
||||
sv := &secretv1beta1.SecureValue{
|
||||
Spec: secretv1beta1.SecureValueSpec{
|
||||
Description: "description", Ref: &ref,
|
||||
|
||||
Decrypters: decrypters,
|
||||
},
|
||||
}
|
||||
|
||||
validator := ProvideSecureValueValidator(allowList)
|
||||
errs := validator.Validate(sv, nil, admission.Create)
|
||||
require.Empty(t, errs)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("`decrypters` must be a valid label value", func(t *testing.T) {
|
||||
decrypters := []string{
|
||||
"", // invalid
|
||||
"is/this/valid", // invalid
|
||||
"is this valid", // invalid
|
||||
"is.this.valid",
|
||||
"is-this-valid",
|
||||
"is_this_valid",
|
||||
"0isthisvalid9",
|
||||
"isthisvalid9",
|
||||
"0isthisvalid",
|
||||
"isthisvalid",
|
||||
}
|
||||
|
||||
ref := "ref"
|
||||
sv := &secretv1beta1.SecureValue{
|
||||
Spec: secretv1beta1.SecureValueSpec{
|
||||
Description: "description", Ref: &ref,
|
||||
|
||||
Decrypters: decrypters,
|
||||
},
|
||||
}
|
||||
|
||||
validator := ProvideSecureValueValidator(nil)
|
||||
errs := validator.Validate(sv, nil, admission.Create)
|
||||
require.Len(t, errs, 3)
|
||||
})
|
||||
|
||||
t.Run("`decrypters` cannot have more than 64 items", func(t *testing.T) {
|
||||
decrypters := make([]string, 0, 64+1)
|
||||
for i := 0; i < 64+1; i++ {
|
||||
decrypters = append(decrypters, fmt.Sprintf("app%d", i))
|
||||
}
|
||||
|
||||
ref := "ref"
|
||||
sv := &secretv1beta1.SecureValue{
|
||||
Spec: secretv1beta1.SecureValueSpec{
|
||||
Description: "description", Ref: &ref,
|
||||
|
||||
Decrypters: decrypters,
|
||||
},
|
||||
}
|
||||
|
||||
validator := ProvideSecureValueValidator(nil)
|
||||
errs := validator.Validate(sv, nil, admission.Create)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.decrypters", errs[0].Field)
|
||||
})
|
||||
}
|
||||
@@ -46,6 +46,7 @@ import (
|
||||
gsmEncryption "github.com/grafana/grafana/pkg/registry/apis/secret/encryption"
|
||||
encryptionManager "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager"
|
||||
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"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
@@ -429,6 +430,8 @@ var wireBasicSet = wire.NewSet(
|
||||
secretencryption.ProvideDataKeyStorage,
|
||||
secretencryption.ProvideEncryptedValueStorage,
|
||||
secretsecurevalueservice.ProvideSecureValueService,
|
||||
secretvalidator.ProvideKeeperValidator,
|
||||
secretvalidator.ProvideSecureValueValidator,
|
||||
secretmigrator.NewWithEngine,
|
||||
secretdatabase.ProvideDatabase,
|
||||
wire.Bind(new(secretcontracts.Database), new(*secretdatabase.Database)),
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user