diff --git a/pkg/registry/apis/secret/contracts/secure_value.go b/pkg/registry/apis/secret/contracts/secure_value.go index f3bc1d3c45c..33637fb2cba 100644 --- a/pkg/registry/apis/secret/contracts/secure_value.go +++ b/pkg/registry/apis/secret/contracts/secure_value.go @@ -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) +} diff --git a/pkg/registry/apis/secret/contracts/validator.go b/pkg/registry/apis/secret/contracts/validator.go new file mode 100644 index 00000000000..a0f326ab268 --- /dev/null +++ b/pkg/registry/apis/secret/contracts/validator.go @@ -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 +} diff --git a/pkg/registry/apis/secret/service/secure_value.go b/pkg/registry/apis/secret/service/secure_value.go index 3b9de019458..295ab09a7bd 100644 --- a/pkg/registry/apis/secret/service/secure_value.go +++ b/pkg/registry/apis/secret/service/secure_value.go @@ -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, diff --git a/pkg/registry/apis/secret/validator/keeper.go b/pkg/registry/apis/secret/validator/keeper.go new file mode 100644 index 00000000000..64afa94821a --- /dev/null +++ b/pkg/registry/apis/secret/validator/keeper.go @@ -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 +} diff --git a/pkg/registry/apis/secret/validator/keeper_test.go b/pkg/registry/apis/secret/validator/keeper_test.go new file mode 100644 index 00000000000..3c4b4015da6 --- /dev/null +++ b/pkg/registry/apis/secret/validator/keeper_test.go @@ -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) + }) + }) + }) +} diff --git a/pkg/registry/apis/secret/validator/secure_value.go b/pkg/registry/apis/secret/validator/secure_value.go new file mode 100644 index 00000000000..1f4130607b8 --- /dev/null +++ b/pkg/registry/apis/secret/validator/secure_value.go @@ -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 +} diff --git a/pkg/registry/apis/secret/validator/secure_value_test.go b/pkg/registry/apis/secret/validator/secure_value_test.go new file mode 100644 index 00000000000..d1c60834524 --- /dev/null +++ b/pkg/registry/apis/secret/validator/secure_value_test.go @@ -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) + }) +} diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 87983d28c6d..ff3bb2fa9b5 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -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)), diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index a6d947b18a1..bf627c84716 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -64,6 +64,7 @@ import ( encryption3 "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" manager4 "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager" service11 "github.com/grafana/grafana/pkg/registry/apis/secret/service" + validator3 "github.com/grafana/grafana/pkg/registry/apis/secret/validator" "github.com/grafana/grafana/pkg/registry/apis/userstorage" "github.com/grafana/grafana/pkg/registry/apps" advisor2 "github.com/grafana/grafana/pkg/registry/apps/advisor" @@ -1427,7 +1428,7 @@ var withOTelSet = wire.NewSet( otelTracer, grpcserver.ProvideService, interceptors.ProvideAuthenticator, ) -var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator2.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service10.ProvideService, wire.Bind(new(service10.LDAP), new(*service10.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service7.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service7.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets.Store), new(*database.SecretsStoreImpl)), grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database4.DashboardSnapshotStore)), database4.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service8.ServiceImpl)), service8.ProvideService, service7.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service7.Service)), service7.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager2.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, service5.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service5.DashboardServiceImpl)), service5.ProvideDashboardService, service5.ProvideDashboardProvisioningService, service5.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), folderimpl.ProvideDashboardFolderStore, wire.Bind(new(folder.FolderStore), new(*folderimpl.DashboardFolderStoreImpl)), service9.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service9.ImportDashboardService)), service6.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service6.Service)), service6.ProvideDashboardUpdater, sanitizer.ProvideService, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideDecryptStorage, decrypt.ProvideDecryptAuthorizer, decrypt.ProvideDecryptAllowList, encryption2.ProvideDataKeyStorage, encryption2.ProvideEncryptedValueStorage, service11.ProvideSecureValueService, migrator2.NewWithEngine, database5.ProvideDatabase, wire.Bind(new(contracts.Database), new(*database5.Database)), manager4.ProvideEncryptionManager, encryption3.ProvideThirdPartyProviderMap, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet) +var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator2.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service10.ProvideService, wire.Bind(new(service10.LDAP), new(*service10.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service7.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service7.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets.Store), new(*database.SecretsStoreImpl)), grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database4.DashboardSnapshotStore)), database4.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service8.ServiceImpl)), service8.ProvideService, service7.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service7.Service)), service7.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager2.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, service5.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service5.DashboardServiceImpl)), service5.ProvideDashboardService, service5.ProvideDashboardProvisioningService, service5.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), folderimpl.ProvideDashboardFolderStore, wire.Bind(new(folder.FolderStore), new(*folderimpl.DashboardFolderStoreImpl)), service9.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service9.ImportDashboardService)), service6.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service6.Service)), service6.ProvideDashboardUpdater, sanitizer.ProvideService, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideDecryptStorage, decrypt.ProvideDecryptAuthorizer, decrypt.ProvideDecryptAllowList, encryption2.ProvideDataKeyStorage, encryption2.ProvideEncryptedValueStorage, service11.ProvideSecureValueService, validator3.ProvideKeeperValidator, validator3.ProvideSecureValueValidator, migrator2.NewWithEngine, database5.ProvideDatabase, wire.Bind(new(contracts.Database), new(*database5.Database)), manager4.ProvideEncryptionManager, encryption3.ProvideThirdPartyProviderMap, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet) var wireSet = wire.NewSet( wireBasicSet, metrics.WireSet, sqlstore.ProvideService, metrics2.ProvideService, wire.Bind(new(notifications.Service), new(*notifications.NotificationService)), wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationService)), wire.Bind(new(notifications.EmailSender), new(*notifications.NotificationService)), wire.Bind(new(db.DB), new(*sqlstore.SQLStore)), prefimpl.ProvideService, oauthtoken.ProvideService, wire.Bind(new(oauthtoken.OAuthTokenService), new(*oauthtoken.Service)), wire.Bind(new(cleanup.AlertRuleService), new(*store2.DBstore)),