SecretsManager: Changes to specs as ref, description, system keeper (#105319)

* SecretsManager: Changes to specs as ref, description, system keeper

Co-authored-by: PoorlyDefinedBehaviour <brunotj2015@hotmail.com>
Co-authored-by: Dana Axinte <53751979+dana-axinte@users.noreply.github.com>
Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>

* SecretsManager: Changes to rest storage for spec ref, description, system keeper

Co-authored-by: PoorlyDefinedBehaviour <brunotj2015@hotmail.com>
Co-authored-by: Dana Axinte <53751979+dana-axinte@users.noreply.github.com>
Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>

* SecretsManager: Changes to rest storage for spec description

Co-authored-by: PoorlyDefinedBehaviour <brunotj2015@hotmail.com>
Co-authored-by: Dana Axinte <53751979+dana-axinte@users.noreply.github.com>
Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>

* SecretsManager: Changes to rest storage for spec description

Co-authored-by: PoorlyDefinedBehaviour <brunotj2015@hotmail.com>
Co-authored-by: Dana Axinte <53751979+dana-axinte@users.noreply.github.com>
Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>

---------

Co-authored-by: PoorlyDefinedBehaviour <brunotj2015@hotmail.com>
Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>
pull/105339/head
Dana Axinte 2 months ago committed by GitHub
parent 9d6ce37f68
commit 5158dce936
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      pkg/apis/secret/go.mod
  2. 89
      pkg/apis/secret/v0alpha1/keeper.go
  3. 8
      pkg/apis/secret/v0alpha1/register.go
  4. 36
      pkg/apis/secret/v0alpha1/secure_value.go
  5. 77
      pkg/apis/secret/v0alpha1/zz_generated.deepcopy.go
  6. 144
      pkg/apis/secret/v0alpha1/zz_generated.openapi.go
  7. 2
      pkg/apis/secret/v0alpha1/zz_generated.openapi_violation_exceptions.list
  8. 27
      pkg/registry/apis/secret/reststorage/keeper_rest.go
  9. 94
      pkg/registry/apis/secret/reststorage/keeper_rest_test.go
  10. 29
      pkg/registry/apis/secret/reststorage/secure_value_rest.go
  11. 91
      pkg/registry/apis/secret/reststorage/secure_value_rest_test.go

@ -11,6 +11,7 @@ require (
k8s.io/apimachinery v0.32.3
k8s.io/apiserver v0.32.3
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
)
require (
@ -94,7 +95,6 @@ require (
k8s.io/client-go v0.32.3 // indirect
k8s.io/component-base v0.32.3 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect

@ -16,26 +16,52 @@ type Keeper struct {
// This is the actual keeper schema.
// +patchStrategy=replace
// +patchMergeKey=name
Spec KeeperSpec `json:"spec,omitempty" patchStrategy:"replace" patchMergeKey:"name"`
Spec KeeperSpec `json:"spec" patchStrategy:"replace" patchMergeKey:"name"`
}
func (k *Keeper) IsSqlKeeper() bool {
return k.Spec.SQL != nil && k.Spec.SQL.Encryption != nil
// KeeperType represents the type of a Keeper.
type KeeperType string
const (
AWSKeeperType KeeperType = "aws"
AzureKeeperType KeeperType = "azure"
GCPKeeperType KeeperType = "gcp"
HashiCorpKeeperType KeeperType = "hashicorp"
)
func (kt KeeperType) String() string {
return string(kt)
}
// KeeperConfig is an interface that all keeper config types must implement.
type KeeperConfig interface {
Type() string
Type() KeeperType
}
type KeeperSpec struct {
// Human friendly name for the keeper.
Title string `json:"title"`
// You can only chose one of the following.
SQL *SQLKeeperConfig `json:"sql,omitempty"`
AWS *AWSKeeperConfig `json:"aws,omitempty"`
Azure *AzureKeeperConfig `json:"azurekeyvault,omitempty"`
GCP *GCPKeeperConfig `json:"gcp,omitempty"`
// Short description for the Keeper.
// +k8s:validation:minLength=1
// +k8s:validation:maxLength=253
Description string `json:"description"`
// AWS Keeper Configuration.
// +structType=atomic
// +optional
AWS *AWSKeeperConfig `json:"aws,omitempty"`
// Azure Keeper Configuration.
// +structType=atomic
// +optional
Azure *AzureKeeperConfig `json:"azurekeyvault,omitempty"`
// GCP Keeper Configuration.
// +structType=atomic
// +optional
GCP *GCPKeeperConfig `json:"gcp,omitempty"`
// HashiCorp Vault Keeper Configuration.
// +structType=atomic
// +optional
HashiCorp *HashiCorpKeeperConfig `json:"hashivault,omitempty"`
}
@ -52,25 +78,6 @@ type KeeperList struct {
Items []Keeper `json:"items,omitempty"`
}
// The default SQL keeper.
type SQLKeeperConfig struct {
Encryption *Encryption `json:"encryption,omitempty"`
}
func (s *SQLKeeperConfig) Type() string {
return "sql"
}
// Encryption of default SQL keeper.
type Encryption struct {
Envelope *Envelope `json:"envelope,omitempty"` // TODO: what would this be
AWS *AWSCredentials `json:"aws,omitempty"`
Azure *AzureCredentials `json:"azure,omitempty"`
GCP *GCPCredentials `json:"gcp,omitempty"`
HashiCorp *HashiCorpCredentials `json:"hashicorp,omitempty"`
}
// Credentials of remote keepers.
type AWSCredentials struct {
AccessKeyID CredentialValue `json:"accessKeyId"`
@ -99,15 +106,19 @@ type HashiCorpCredentials struct {
type Envelope struct{}
// Holds the way credentials are obtained.
// +union
type CredentialValue struct {
// The name of the secure value that holds the actual value.
// +optional
SecureValueName string `json:"secureValueName,omitempty"`
// The value is taken from the environment variable.
// +optional
ValueFromEnv string `json:"valueFromEnv,omitempty"`
// The value is taken from the Grafana config file.
// TODO: how do we explain that this is a path to the config file?
// +optional
ValueFromConfig string `json:"valueFromConfig,omitempty"`
}
@ -128,18 +139,18 @@ type HashiCorpKeeperConfig struct {
HashiCorpCredentials `json:",inline"`
}
func (s *AWSKeeperConfig) Type() string {
return "aws"
func (s *AWSKeeperConfig) Type() KeeperType {
return AWSKeeperType
}
func (s *AzureKeeperConfig) Type() string {
return "azure"
func (s *AzureKeeperConfig) Type() KeeperType {
return AzureKeeperType
}
func (s *GCPKeeperConfig) Type() string {
return "gcp"
func (s *GCPKeeperConfig) Type() KeeperType {
return GCPKeeperType
}
func (s *HashiCorpKeeperConfig) Type() string {
return "hashicorp"
func (s *HashiCorpKeeperConfig) Type() KeeperType {
return HashiCorpKeeperType
}

@ -30,7 +30,7 @@ var SecureValuesResourceInfo = utils.NewResourceInfo(
// This defines the fields we view in `kubectl get`. Not related with the storage layer.
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Title", Type: "string", Format: "string", Description: "The display name of the secure value"},
{Name: "Description", Type: "string", Format: "string", Description: "Short description that explains the purpose of this SecureValue"},
{Name: "Keeper", Type: "string", Format: "string", Description: "Storage of the secure value"},
{Name: "Ref", Type: "string", Format: "string", Description: "If present, the reference to a secret"},
{Name: "Status", Type: "string", Format: "string", Description: "The status of the secure value"},
@ -41,7 +41,7 @@ var SecureValuesResourceInfo = utils.NewResourceInfo(
if ok {
return []interface{}{
r.Name,
r.Spec.Title,
r.Spec.Description,
r.Spec.Keeper,
r.Spec.Ref,
r.Status.Phase,
@ -65,7 +65,7 @@ var KeeperResourceInfo = utils.NewResourceInfo(
// This defines the fields we view in `kubectl get`. Not related with the storage layer.
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Title", Type: "string", Format: "string", Description: "The display name of the keeper"},
{Name: "Description", Type: "string", Format: "string", Description: "Short description for the Keeper"},
},
// Decodes the object into a concrete type. Return order in the slice must be the same as in `Definition`.
Reader: func(obj any) ([]interface{}, error) {
@ -73,7 +73,7 @@ var KeeperResourceInfo = utils.NewResourceInfo(
if ok {
return []interface{}{
r.Name,
r.Spec.Title,
r.Spec.Description,
}, nil
}

@ -14,11 +14,11 @@ type SecureValue struct {
metav1.ObjectMeta `json:"metadata,omitempty"`
// This is the actual secure value schema.
Spec SecureValueSpec `json:"spec,omitempty"`
Spec SecureValueSpec `json:"spec"`
// Read-only observed status of the `SecureValue`.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
Status SecureValueStatus `json:"status"`
Status SecureValueStatus `json:"status,omitempty"`
}
// +enum
@ -46,32 +46,40 @@ type SecureValueStatus struct {
// Only applicable if the `phase=Failed`.
// +optional
Message string `json:"message,omitempty"`
// +optional
ExternalID string `json:"externalId,omitempty"`
}
type SecureValueSpec struct {
// Human friendly name for the secure value.
Title string `json:"title"`
// Short description that explains the purpose of this SecureValue.
// +k8s:validation:minLength=1
// +k8s:validation:maxLength=253
Description string `json:"description"`
// The raw value is only valid for write. Read/List will always be empty.
// There is no support for mixing `value` and `ref`, you can't create a secret in a third-party keeper with a specified `ref`.
// +k8s:validation:minLength=1
Value ExposedSecureValue `json:"value,omitempty"`
// When using a remote Key manager, the ref is used to reference a value inside the remote storage.
// When using a third-party keeper, the `ref` is used to reference a value inside the remote storage.
// This should not contain sensitive information.
Ref string `json:"ref,omitempty"`
// +k8s:validation:minLength=1
// +k8s:validation:maxLength=1024
// +optional
Ref *string `json:"ref,omitempty"`
// Name of the keeper, being the actual storage of the secure value.
Keeper string `json:"keeper,omitempty"`
// If not specified, the default keeper for the namespace will be used.
// +k8s:validation:minLength=1
// +k8s:validation:maxLength=253
// +optional
Keeper *string `json:"keeper,omitempty"`
// The Decrypters that are allowed to decrypt this secret.
// An empty list means no service can decrypt it.
// Support and behavior is still TBD, but could likely look like:
// * testdata.grafana.app/{name1}
// * testdata.grafana.app/{name2}
// * runner.k6.grafana.app/* -- allow any k6 test runner
// Rather than a string pattern, we may want a more explicit object:
// [{ group:"testdata.grafana.app", name="name1"},
// { group:"runner.k6.grafana.app"}]
// +k8s:validation:maxItems=64
// +k8s:validation:uniqueItems=true
// +listType=atomic
// +optional
Decrypters []string `json:"decrypters"`

@ -96,47 +96,6 @@ func (in *CredentialValue) DeepCopy() *CredentialValue {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Encryption) DeepCopyInto(out *Encryption) {
*out = *in
if in.Envelope != nil {
in, out := &in.Envelope, &out.Envelope
*out = new(Envelope)
**out = **in
}
if in.AWS != nil {
in, out := &in.AWS, &out.AWS
*out = new(AWSCredentials)
**out = **in
}
if in.Azure != nil {
in, out := &in.Azure, &out.Azure
*out = new(AzureCredentials)
**out = **in
}
if in.GCP != nil {
in, out := &in.GCP, &out.GCP
*out = new(GCPCredentials)
**out = **in
}
if in.HashiCorp != nil {
in, out := &in.HashiCorp, &out.HashiCorp
*out = new(HashiCorpCredentials)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Encryption.
func (in *Encryption) DeepCopy() *Encryption {
if in == nil {
return nil
}
out := new(Encryption)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Envelope) DeepCopyInto(out *Envelope) {
*out = *in
@ -283,11 +242,6 @@ func (in *KeeperList) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KeeperSpec) DeepCopyInto(out *KeeperSpec) {
*out = *in
if in.SQL != nil {
in, out := &in.SQL, &out.SQL
*out = new(SQLKeeperConfig)
(*in).DeepCopyInto(*out)
}
if in.AWS != nil {
in, out := &in.AWS, &out.AWS
*out = new(AWSKeeperConfig)
@ -321,27 +275,6 @@ func (in *KeeperSpec) DeepCopy() *KeeperSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SQLKeeperConfig) DeepCopyInto(out *SQLKeeperConfig) {
*out = *in
if in.Encryption != nil {
in, out := &in.Encryption, &out.Encryption
*out = new(Encryption)
(*in).DeepCopyInto(*out)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SQLKeeperConfig.
func (in *SQLKeeperConfig) DeepCopy() *SQLKeeperConfig {
if in == nil {
return nil
}
out := new(SQLKeeperConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecureValue) DeepCopyInto(out *SecureValue) {
*out = *in
@ -406,6 +339,16 @@ func (in *SecureValueList) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecureValueSpec) DeepCopyInto(out *SecureValueSpec) {
*out = *in
if in.Ref != nil {
in, out := &in.Ref, &out.Ref
*out = new(string)
**out = **in
}
if in.Keeper != nil {
in, out := &in.Keeper, &out.Keeper
*out = new(string)
**out = **in
}
if in.Decrypters != nil {
in, out := &in.Decrypters, &out.Decrypters
*out = make([]string, len(*in))

@ -10,6 +10,7 @@ package v0alpha1
import (
common "k8s.io/kube-openapi/pkg/common"
spec "k8s.io/kube-openapi/pkg/validation/spec"
ptr "k8s.io/utils/ptr"
)
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
@ -19,7 +20,6 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AzureCredentials": schema_pkg_apis_secret_v0alpha1_AzureCredentials(ref),
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AzureKeeperConfig": schema_pkg_apis_secret_v0alpha1_AzureKeeperConfig(ref),
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.CredentialValue": schema_pkg_apis_secret_v0alpha1_CredentialValue(ref),
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.Encryption": schema_pkg_apis_secret_v0alpha1_Encryption(ref),
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.Envelope": schema_pkg_apis_secret_v0alpha1_Envelope(ref),
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.GCPCredentials": schema_pkg_apis_secret_v0alpha1_GCPCredentials(ref),
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.GCPKeeperConfig": schema_pkg_apis_secret_v0alpha1_GCPKeeperConfig(ref),
@ -28,7 +28,6 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.Keeper": schema_pkg_apis_secret_v0alpha1_Keeper(ref),
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.KeeperList": schema_pkg_apis_secret_v0alpha1_KeeperList(ref),
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.KeeperSpec": schema_pkg_apis_secret_v0alpha1_KeeperSpec(ref),
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.SQLKeeperConfig": schema_pkg_apis_secret_v0alpha1_SQLKeeperConfig(ref),
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.SecureValue": schema_pkg_apis_secret_v0alpha1_SecureValue(ref),
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.SecureValueList": schema_pkg_apis_secret_v0alpha1_SecureValueList(ref),
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.SecureValueSpec": schema_pkg_apis_secret_v0alpha1_SecureValueSpec(ref),
@ -218,47 +217,20 @@ func schema_pkg_apis_secret_v0alpha1_CredentialValue(ref common.ReferenceCallbac
},
},
},
},
}
}
func schema_pkg_apis_secret_v0alpha1_Encryption(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Encryption of default SQL keeper.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"envelope": {
SchemaProps: spec.SchemaProps{
Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.Envelope"),
},
},
"aws": {
SchemaProps: spec.SchemaProps{
Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AWSCredentials"),
},
},
"azure": {
SchemaProps: spec.SchemaProps{
Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AzureCredentials"),
},
},
"gcp": {
SchemaProps: spec.SchemaProps{
Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.GCPCredentials"),
},
},
"hashicorp": {
SchemaProps: spec.SchemaProps{
Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.HashiCorpCredentials"),
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-unions": []interface{}{
map[string]interface{}{
"fields-to-discriminateBy": map[string]interface{}{
"secureValueName": "SecureValueName",
"valueFromConfig": "ValueFromConfig",
"valueFromEnv": "ValueFromEnv",
},
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AWSCredentials", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AzureCredentials", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.Envelope", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.GCPCredentials", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.HashiCorpCredentials"},
}
}
@ -424,6 +396,7 @@ func schema_pkg_apis_secret_v0alpha1_Keeper(ref common.ReferenceCallback) common
},
},
},
Required: []string{"spec"},
},
},
Dependencies: []string{
@ -486,66 +459,66 @@ func schema_pkg_apis_secret_v0alpha1_KeeperSpec(ref common.ReferenceCallback) co
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"title": {
"description": {
SchemaProps: spec.SchemaProps{
Description: "Human friendly name for the keeper.",
Description: "Short description for the Keeper.",
Default: "",
MinLength: ptr.To[int64](1),
MaxLength: ptr.To[int64](253),
Type: []string{"string"},
Format: "",
},
},
"sql": {
SchemaProps: spec.SchemaProps{
Description: "You can only chose one of the following.",
Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.SQLKeeperConfig"),
},
},
"aws": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-map-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AWSKeeperConfig"),
Description: "AWS Keeper Configuration.",
Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AWSKeeperConfig"),
},
},
"azurekeyvault": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-map-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AzureKeeperConfig"),
Description: "Azure Keeper Configuration.",
Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AzureKeeperConfig"),
},
},
"gcp": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-map-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.GCPKeeperConfig"),
Description: "GCP Keeper Configuration.",
Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.GCPKeeperConfig"),
},
},
"hashivault": {
SchemaProps: spec.SchemaProps{
Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.HashiCorpKeeperConfig"),
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-map-type": "atomic",
},
},
},
},
Required: []string{"title"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AWSKeeperConfig", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AzureKeeperConfig", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.GCPKeeperConfig", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.HashiCorpKeeperConfig", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.SQLKeeperConfig"},
}
}
func schema_pkg_apis_secret_v0alpha1_SQLKeeperConfig(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "The default SQL keeper.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"encryption": {
SchemaProps: spec.SchemaProps{
Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.Encryption"),
Description: "HashiCorp Vault Keeper Configuration.",
Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.HashiCorpKeeperConfig"),
},
},
},
Required: []string{"description"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.Encryption"},
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AWSKeeperConfig", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AzureKeeperConfig", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.GCPKeeperConfig", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.HashiCorpKeeperConfig"},
}
}
@ -591,7 +564,7 @@ func schema_pkg_apis_secret_v0alpha1_SecureValue(ref common.ReferenceCallback) c
},
},
},
Required: []string{"status"},
Required: []string{"spec"},
},
},
Dependencies: []string{
@ -654,10 +627,12 @@ func schema_pkg_apis_secret_v0alpha1_SecureValueSpec(ref common.ReferenceCallbac
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"title": {
"description": {
SchemaProps: spec.SchemaProps{
Description: "Human friendly name for the secure value.",
Description: "Short description that explains the purpose of this SecureValue.",
Default: "",
MinLength: ptr.To[int64](1),
MaxLength: ptr.To[int64](253),
Type: []string{"string"},
Format: "",
},
@ -665,20 +640,25 @@ func schema_pkg_apis_secret_v0alpha1_SecureValueSpec(ref common.ReferenceCallbac
"value": {
SchemaProps: spec.SchemaProps{
Description: "The raw value is only valid for write. Read/List will always be empty. There is no support for mixing `value` and `ref`, you can't create a secret in a third-party keeper with a specified `ref`.",
MinLength: ptr.To[int64](1),
Type: []string{"string"},
Format: "",
},
},
"ref": {
SchemaProps: spec.SchemaProps{
Description: "When using a remote Key manager, the ref is used to reference a value inside the remote storage. This should not contain sensitive information.",
Description: "When using a third-party keeper, the `ref` is used to reference a value inside the remote storage. This should not contain sensitive information.",
MinLength: ptr.To[int64](1),
MaxLength: ptr.To[int64](1024),
Type: []string{"string"},
Format: "",
},
},
"keeper": {
SchemaProps: spec.SchemaProps{
Description: "Name of the keeper, being the actual storage of the secure value.",
Description: "Name of the keeper, being the actual storage of the secure value. If not specified, the default keeper for the namespace will be used.",
MinLength: ptr.To[int64](1),
MaxLength: ptr.To[int64](253),
Type: []string{"string"},
Format: "",
},
@ -690,7 +670,9 @@ func schema_pkg_apis_secret_v0alpha1_SecureValueSpec(ref common.ReferenceCallbac
},
},
SchemaProps: spec.SchemaProps{
Description: "The Decrypters that are allowed to decrypt this secret. An empty list means no service can decrypt it. Support and behavior is still TBD, but could likely look like: * testdata.grafana.app/{name1} * testdata.grafana.app/{name2} * runner.k6.grafana.app/* -- allow any k6 test runner Rather than a string pattern, we may want a more explicit object: [{ group:\"testdata.grafana.app\", name=\"name1\"},\n { group:\"runner.k6.grafana.app\"}]",
Description: "The Decrypters that are allowed to decrypt this secret. An empty list means no service can decrypt it.",
MaxItems: ptr.To[int64](64),
UniqueItems: true,
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
@ -704,7 +686,7 @@ func schema_pkg_apis_secret_v0alpha1_SecureValueSpec(ref common.ReferenceCallbac
},
},
},
Required: []string{"title"},
Required: []string{"description"},
},
},
}
@ -732,6 +714,12 @@ func schema_pkg_apis_secret_v0alpha1_SecureValueStatus(ref common.ReferenceCallb
Format: "",
},
},
"externalId": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"phase"},
},

@ -2,7 +2,7 @@ API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alp
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,AWSCredentials,KMSKeyID
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,AzureCredentials,ClientID
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,AzureCredentials,TenantID
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,Encryption,HashiCorp
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,GCPCredentials,ProjectID
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,KeeperSpec,Azure
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,KeeperSpec,HashiCorp
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,SecureValueStatus,ExternalID

@ -209,8 +209,8 @@ func ValidateKeeper(keeper *secretv0alpha1.Keeper, operation admission.Operation
errs := make(field.ErrorList, 0)
if keeper.Spec.Title == "" {
errs = append(errs, field.Required(field.NewPath("spec", "title"), "a `title` is required"))
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.
@ -220,28 +220,6 @@ func ValidateKeeper(keeper *secretv0alpha1.Keeper, operation admission.Operation
return errs
}
// TODO: Improve SQL keeper validation.
// SQL keeper is not allowed to use `secureValueName` in credentials fields to avoid depending on another keeper.
if keeper.IsSqlKeeper() {
if keeper.Spec.SQL.Encryption.AWS != nil {
if keeper.Spec.SQL.Encryption.AWS.AccessKeyID.SecureValueName != "" {
errs = append(errs, field.Forbidden(field.NewPath("spec", "aws", "accessKeyId"), "secureValueName cannot be used with SQL keeper"))
}
if keeper.Spec.SQL.Encryption.AWS.SecretAccessKey.SecureValueName != "" {
errs = append(errs, field.Forbidden(field.NewPath("spec", "aws", "secretAccessKey"), "secureValueName cannot be used with SQL keeper"))
}
}
if keeper.Spec.SQL.Encryption.Azure != nil && keeper.Spec.SQL.Encryption.Azure.ClientSecret.SecureValueName != "" {
errs = append(errs, field.Forbidden(field.NewPath("spec", "azure", "clientSecret"), "secureValueName cannot be used with SQL keeper"))
}
if keeper.Spec.SQL.Encryption.HashiCorp != nil && keeper.Spec.SQL.Encryption.HashiCorp.Token.SecureValueName != "" {
errs = append(errs, field.Forbidden(field.NewPath("spec", "hashicorp", "token"), "secureValueName cannot be used with SQL keeper"))
}
}
if keeper.Spec.AWS != nil {
if err := validateCredentialValue(field.NewPath("spec", "aws", "accessKeyId"), keeper.Spec.AWS.AccessKeyID); err != nil {
errs = append(errs, err)
@ -295,7 +273,6 @@ func ValidateKeeper(keeper *secretv0alpha1.Keeper, operation admission.Operation
func validateKeepers(keeper *secretv0alpha1.Keeper) *field.Error {
availableKeepers := map[string]bool{
"sql": keeper.Spec.SQL != nil,
"aws": keeper.Spec.AWS != nil,
"azure": keeper.Spec.Azure != nil,
"gcp": keeper.Spec.GCP != nil,

@ -10,28 +10,33 @@ import (
func TestValidateKeeper(t *testing.T) {
t.Run("when creating a new keeper", func(t *testing.T) {
t.Run("the `title` must be present", func(t *testing.T) {
t.Run("the `description` must be present", func(t *testing.T) {
keeper := &secretv0alpha1.Keeper{
Spec: secretv0alpha1.KeeperSpec{
SQL: &secretv0alpha1.SQLKeeperConfig{},
AWS: &secretv0alpha1.AWSKeeperConfig{
AWSCredentials: secretv0alpha1.AWSCredentials{
AccessKeyID: secretv0alpha1.CredentialValue{ValueFromEnv: "some-value"},
SecretAccessKey: secretv0alpha1.CredentialValue{ValueFromEnv: "some-value"},
KMSKeyID: "kms-key-id",
},
},
},
}
errs := ValidateKeeper(keeper, admission.Create)
require.Len(t, errs, 1)
require.Equal(t, "spec.title", errs[0].Field)
require.Equal(t, "spec.description", errs[0].Field)
})
})
t.Run("only one `keeper` must be present", func(t *testing.T) {
keeper := &secretv0alpha1.Keeper{
Spec: secretv0alpha1.KeeperSpec{
Title: "title",
SQL: &secretv0alpha1.SQLKeeperConfig{},
AWS: &secretv0alpha1.AWSKeeperConfig{},
Azure: &secretv0alpha1.AzureKeeperConfig{},
GCP: &secretv0alpha1.GCPKeeperConfig{},
HashiCorp: &secretv0alpha1.HashiCorpKeeperConfig{},
Description: "short description",
AWS: &secretv0alpha1.AWSKeeperConfig{},
Azure: &secretv0alpha1.AzureKeeperConfig{},
GCP: &secretv0alpha1.GCPKeeperConfig{},
HashiCorp: &secretv0alpha1.HashiCorpKeeperConfig{},
},
}
@ -43,7 +48,7 @@ func TestValidateKeeper(t *testing.T) {
t.Run("at least one `keeper` must be present", func(t *testing.T) {
keeper := &secretv0alpha1.Keeper{
Spec: secretv0alpha1.KeeperSpec{
Title: "title",
Description: "description",
},
}
@ -55,7 +60,7 @@ func TestValidateKeeper(t *testing.T) {
t.Run("aws keeper validation", func(t *testing.T) {
validKeeperAWS := &secretv0alpha1.Keeper{
Spec: secretv0alpha1.KeeperSpec{
Title: "title",
Description: "description",
AWS: &secretv0alpha1.AWSKeeperConfig{
AWSCredentials: secretv0alpha1.AWSCredentials{
AccessKeyID: secretv0alpha1.CredentialValue{
@ -122,7 +127,7 @@ func TestValidateKeeper(t *testing.T) {
t.Run("azure keeper validation", func(t *testing.T) {
validKeeperAzure := &secretv0alpha1.Keeper{
Spec: secretv0alpha1.KeeperSpec{
Title: "title",
Description: "description",
Azure: &secretv0alpha1.AzureKeeperConfig{
AzureCredentials: secretv0alpha1.AzureCredentials{
KeyVaultName: "kv-name",
@ -191,7 +196,7 @@ func TestValidateKeeper(t *testing.T) {
t.Run("gcp keeper validation", func(t *testing.T) {
validKeeperGCP := &secretv0alpha1.Keeper{
Spec: secretv0alpha1.KeeperSpec{
Title: "title",
Description: "description",
GCP: &secretv0alpha1.GCPKeeperConfig{
GCPCredentials: secretv0alpha1.GCPCredentials{
ProjectID: "project-id",
@ -223,7 +228,7 @@ func TestValidateKeeper(t *testing.T) {
t.Run("hashicorp keeper validation", func(t *testing.T) {
validKeeperHashiCorp := &secretv0alpha1.Keeper{
Spec: secretv0alpha1.KeeperSpec{
Title: "title",
Description: "description",
HashiCorp: &secretv0alpha1.HashiCorpKeeperConfig{
HashiCorpCredentials: secretv0alpha1.HashiCorpCredentials{
Address: "http://address",
@ -268,65 +273,4 @@ func TestValidateKeeper(t *testing.T) {
})
})
})
t.Run("sql keeper validation", func(t *testing.T) {
t.Run("does not allow usage of `secureValueName` in credentials", func(t *testing.T) {
providers := []struct {
name string
enc secretv0alpha1.Encryption
expectedErrors int
}{
{
name: "aws",
enc: secretv0alpha1.Encryption{
AWS: &secretv0alpha1.AWSCredentials{
AccessKeyID: secretv0alpha1.CredentialValue{
SecureValueName: "not-empty",
},
SecretAccessKey: secretv0alpha1.CredentialValue{
SecureValueName: "not-empty",
},
},
},
expectedErrors: 2,
},
{
name: "azure",
enc: secretv0alpha1.Encryption{
Azure: &secretv0alpha1.AzureCredentials{
ClientSecret: secretv0alpha1.CredentialValue{
SecureValueName: "not-empty",
},
},
},
expectedErrors: 1,
},
{
name: "hashicorp",
enc: secretv0alpha1.Encryption{
HashiCorp: &secretv0alpha1.HashiCorpCredentials{
Token: secretv0alpha1.CredentialValue{
SecureValueName: "not-empty",
},
},
},
expectedErrors: 1,
},
}
for _, tc := range providers {
t.Run("when using credentials for "+tc.name, func(t *testing.T) {
keeper := &secretv0alpha1.Keeper{
Spec: secretv0alpha1.KeeperSpec{
Title: "title",
SQL: &secretv0alpha1.SQLKeeperConfig{Encryption: &tc.enc},
},
}
errs := ValidateKeeper(keeper, admission.Create)
require.Len(t, errs, tc.expectedErrors)
})
}
})
})
}

@ -218,19 +218,15 @@ func ValidateSecureValue(sv, oldSv *secretv0alpha1.SecureValue, operation admiss
func validateSecureValueCreate(sv *secretv0alpha1.SecureValue) field.ErrorList {
errs := make(field.ErrorList, 0)
if sv.Spec.Title == "" {
errs = append(errs, field.Required(field.NewPath("spec", "title"), "a `title` is required"))
if sv.Spec.Description == "" {
errs = append(errs, field.Required(field.NewPath("spec", "description"), "a `description` is required"))
}
if sv.Spec.Keeper == "" {
errs = append(errs, field.Required(field.NewPath("spec", "keeper"), "a `keeper` is required"))
}
if sv.Spec.Value == "" && sv.Spec.Ref == "" {
if 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 != "" && sv.Spec.Ref != "" {
if 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"))
}
@ -249,12 +245,12 @@ func validateSecureValueUpdate(sv, oldSv *secretv0alpha1.SecureValue) field.Erro
}
// Only validate if one of the fields is being changed/set.
if sv.Spec.Value != "" || sv.Spec.Ref != "" {
if oldSv.Spec.Ref != "" && sv.Spec.Value != "" {
if sv.Spec.Value != "" || (sv.Spec.Ref != nil && *sv.Spec.Ref != "") {
if (oldSv.Spec.Ref != nil && *oldSv.Spec.Ref != "") && sv.Spec.Value != "" {
errs = append(errs, field.Forbidden(field.NewPath("spec"), "cannot set `value` when `ref` was already previously set"))
}
if oldSv.Spec.Ref == "" && sv.Spec.Ref != "" {
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"))
}
}
@ -271,6 +267,17 @@ func validateSecureValueUpdate(sv, oldSv *secretv0alpha1.SecureValue) field.Erro
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 {

@ -14,44 +14,37 @@ import (
func TestValidateSecureValue(t *testing.T) {
t.Run("when creating a new securevalue", func(t *testing.T) {
keeper := "keeper"
validSecureValue := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{
Title: "title",
Value: "value",
Keeper: "keeper",
Decrypters: []string{"actor_app1", "actor_app2"},
Description: "description",
Value: "value",
Keeper: &keeper,
Decrypters: []string{"actor_app1", "actor_app2"},
},
}
t.Run("the `title` must be present", func(t *testing.T) {
t.Run("the `description` must be present", func(t *testing.T) {
sv := validSecureValue.DeepCopy()
sv.Spec.Title = ""
sv.Spec.Description = ""
errs := ValidateSecureValue(sv, nil, admission.Create, nil)
require.Len(t, errs, 1)
require.Equal(t, "spec.title", errs[0].Field)
})
t.Run("the `keeper` must be present", func(t *testing.T) {
sv := validSecureValue.DeepCopy()
sv.Spec.Keeper = ""
errs := ValidateSecureValue(sv, nil, admission.Create, nil)
require.Len(t, errs, 1)
require.Equal(t, "spec.keeper", errs[0].Field)
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) {
sv := validSecureValue.DeepCopy()
sv.Spec.Value = ""
sv.Spec.Ref = ""
sv.Spec.Ref = nil
errs := ValidateSecureValue(sv, nil, admission.Create, nil)
require.Len(t, errs, 1)
require.Equal(t, "spec", errs[0].Field)
ref := "value"
sv.Spec.Value = "value"
sv.Spec.Ref = "value"
sv.Spec.Ref = &ref
errs = ValidateSecureValue(sv, nil, admission.Create, nil)
require.Len(t, errs, 1)
@ -63,13 +56,14 @@ func TestValidateSecureValue(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 := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{
Ref: "", // empty `ref` means a `value` was present.
Ref: nil, // empty `ref` means a `value` was present.
},
}
ref := "ref"
sv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{
Ref: "ref",
Ref: &ref,
},
}
@ -79,9 +73,10 @@ func TestValidateSecureValue(t *testing.T) {
})
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 := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{
Ref: "non-empty",
Ref: &ref,
},
}
@ -97,16 +92,18 @@ func TestValidateSecureValue(t *testing.T) {
})
t.Run("when both `value` and `ref` are set, it returns an error", func(t *testing.T) {
refNonEmpty := "non-empty"
oldSv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{
Ref: "non-empty",
Ref: &refNonEmpty,
},
}
ref := "ref"
sv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{
Value: "value",
Ref: "ref",
Ref: &ref,
},
}
@ -128,13 +125,13 @@ func TestValidateSecureValue(t *testing.T) {
t.Run("when no changes are made, it returns no errors", func(t *testing.T) {
oldSv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{
Title: "old-title",
Description: "old-description",
},
}
sv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{
Title: "new-title",
Description: "new-description",
},
}
@ -151,15 +148,17 @@ func TestValidateSecureValue(t *testing.T) {
})
t.Run("when trying to change the `keeper`, it returns an error", func(t *testing.T) {
keeperA := "a-keeper"
keeperAnother := "another-keeper"
oldSv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{
Keeper: "a-keeper",
Keeper: &keeperA,
},
}
sv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{
Keeper: "another-keeper",
Keeper: &keeperAnother,
},
}
@ -170,9 +169,10 @@ func TestValidateSecureValue(t *testing.T) {
})
t.Run("`decrypters` must have unique items", func(t *testing.T) {
ref := "ref"
sv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{
Title: "title", Keeper: "keeper", Ref: "ref",
Description: "description", Ref: &ref,
Decrypters: []string{
"actor_app1",
@ -187,9 +187,10 @@ func TestValidateSecureValue(t *testing.T) {
})
t.Run("`decrypters` must match the expected format", func(t *testing.T) {
ref := "ref"
sv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{
Title: "title", Keeper: "keeper", Ref: "ref",
Description: "description", Ref: &ref,
Decrypters: []string{
"app1",
@ -215,9 +216,10 @@ func TestValidateSecureValue(t *testing.T) {
decrypters := slices.Collect(maps.Keys(allowList))
t.Run("no matches, returns an error", func(t *testing.T) {
ref := "ref"
sv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{
Title: "title", Keeper: "keeper", Ref: "ref",
Description: "description", Ref: &ref,
Decrypters: []string{"actor_app3"},
},
@ -228,9 +230,10 @@ func TestValidateSecureValue(t *testing.T) {
})
t.Run("no decrypters, returns no error", func(t *testing.T) {
ref := "ref"
sv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{
Title: "title", Keeper: "keeper", Ref: "ref",
Description: "description", Ref: &ref,
Decrypters: []string{},
},
@ -241,9 +244,10 @@ func TestValidateSecureValue(t *testing.T) {
})
t.Run("one match, returns no errors", func(t *testing.T) {
ref := "ref"
sv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{
Title: "title", Keeper: "keeper", Ref: "ref",
Description: "description", Ref: &ref,
Decrypters: []string{decrypters[0]},
},
@ -254,9 +258,10 @@ func TestValidateSecureValue(t *testing.T) {
})
t.Run("all matches, returns no errors", func(t *testing.T) {
ref := "ref"
sv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{
Title: "title", Keeper: "keeper", Ref: "ref",
Description: "description", Ref: &ref,
Decrypters: decrypters,
},
@ -266,4 +271,24 @@ func TestValidateSecureValue(t *testing.T) {
require.Empty(t, errs)
})
})
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("actor_app%d", i))
}
ref := "ref"
sv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{
Description: "description", Ref: &ref,
Decrypters: decrypters,
},
}
errs := ValidateSecureValue(sv, nil, admission.Create, nil)
require.Len(t, errs, 1)
require.Equal(t, "spec.decrypters", errs[0].Field)
})
}

Loading…
Cancel
Save