mirror of https://github.com/grafana/grafana
Secrets: Only register dependencies to start up (#107504)
parent
d55541735a
commit
7614089077
@ -1,386 +0,0 @@ |
|||||||
package reststorage |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
"strings" |
|
||||||
|
|
||||||
claims "github.com/grafana/authlib/types" |
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors" |
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/internalversion" |
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
|
||||||
"k8s.io/apimachinery/pkg/labels" |
|
||||||
"k8s.io/apimachinery/pkg/runtime" |
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field" |
|
||||||
"k8s.io/apiserver/pkg/admission" |
|
||||||
"k8s.io/apiserver/pkg/endpoints/request" |
|
||||||
"k8s.io/apiserver/pkg/registry/rest" |
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/utils" |
|
||||||
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" |
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts" |
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube" |
|
||||||
) |
|
||||||
|
|
||||||
var ( |
|
||||||
_ rest.Scoper = (*KeeperRest)(nil) |
|
||||||
_ rest.SingularNameProvider = (*KeeperRest)(nil) |
|
||||||
_ rest.Getter = (*KeeperRest)(nil) |
|
||||||
_ rest.Lister = (*KeeperRest)(nil) |
|
||||||
_ rest.Storage = (*KeeperRest)(nil) |
|
||||||
_ rest.Creater = (*KeeperRest)(nil) |
|
||||||
_ rest.Updater = (*KeeperRest)(nil) |
|
||||||
_ rest.GracefulDeleter = (*KeeperRest)(nil) |
|
||||||
) |
|
||||||
|
|
||||||
// KeeperRest is an implementation of CRUDL operations on a `keeper` backed by TODO.
|
|
||||||
type KeeperRest struct { |
|
||||||
storage contracts.KeeperMetadataStorage |
|
||||||
accessClient claims.AccessClient |
|
||||||
resource utils.ResourceInfo |
|
||||||
tableConverter rest.TableConvertor |
|
||||||
} |
|
||||||
|
|
||||||
// NewKeeperRest is a returns a constructed `*KeeperRest`.
|
|
||||||
func NewKeeperRest(storage contracts.KeeperMetadataStorage, accessClient claims.AccessClient, resource utils.ResourceInfo) *KeeperRest { |
|
||||||
return &KeeperRest{storage, accessClient, resource, resource.TableConverter()} |
|
||||||
} |
|
||||||
|
|
||||||
// New returns an empty `*Keeper` that is used by the `Create` method.
|
|
||||||
func (s *KeeperRest) New() runtime.Object { |
|
||||||
return s.resource.NewFunc() |
|
||||||
} |
|
||||||
|
|
||||||
// Destroy is called when? [TODO]
|
|
||||||
func (s *KeeperRest) Destroy() {} |
|
||||||
|
|
||||||
// NamespaceScoped returns `true` because the storage is namespaced (== org).
|
|
||||||
func (s *KeeperRest) NamespaceScoped() bool { |
|
||||||
return true |
|
||||||
} |
|
||||||
|
|
||||||
// GetSingularName is used by `kubectl` discovery to have singular name representation of resources.
|
|
||||||
func (s *KeeperRest) GetSingularName() string { |
|
||||||
return s.resource.GetSingularName() |
|
||||||
} |
|
||||||
|
|
||||||
// NewList returns an empty `*KeeperList` that is used by the `List` method.
|
|
||||||
func (s *KeeperRest) NewList() runtime.Object { |
|
||||||
return s.resource.NewListFunc() |
|
||||||
} |
|
||||||
|
|
||||||
// ConvertToTable is used by Kubernetes and converts objects to `metav1.Table`.
|
|
||||||
func (s *KeeperRest) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { |
|
||||||
return s.tableConverter.ConvertToTable(ctx, object, tableOptions) |
|
||||||
} |
|
||||||
|
|
||||||
// List calls the inner `store` (persistence) and returns a list of `Keepers` within a `namespace` filtered by the `options`.
|
|
||||||
func (s *KeeperRest) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { |
|
||||||
namespace, ok := request.NamespaceFrom(ctx) |
|
||||||
if !ok { |
|
||||||
return nil, fmt.Errorf("missing namespace") |
|
||||||
} |
|
||||||
|
|
||||||
user, ok := claims.AuthInfoFrom(ctx) |
|
||||||
if !ok { |
|
||||||
return nil, fmt.Errorf("missing auth info in context") |
|
||||||
} |
|
||||||
|
|
||||||
hasPermissionFor, err := s.accessClient.Compile(ctx, user, claims.ListRequest{ |
|
||||||
Group: secretv0alpha1.GROUP, |
|
||||||
Resource: secretv0alpha1.KeeperResourceInfo.GetName(), |
|
||||||
Namespace: namespace, |
|
||||||
Verb: utils.VerbGet, |
|
||||||
}) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("failed to compile checker: %w", err) |
|
||||||
} |
|
||||||
|
|
||||||
labelSelector := options.LabelSelector |
|
||||||
if labelSelector == nil { |
|
||||||
labelSelector = labels.Everything() |
|
||||||
} |
|
||||||
|
|
||||||
keepersList, err := s.storage.List(ctx, xkube.Namespace(namespace)) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("failed to list keepers: %w", err) |
|
||||||
} |
|
||||||
|
|
||||||
allowedKeepers := make([]secretv0alpha1.Keeper, 0) |
|
||||||
|
|
||||||
for _, keeper := range keepersList { |
|
||||||
// Check whether the user has permission to access this specific Keeper in the namespace.
|
|
||||||
if !hasPermissionFor(keeper.Name, "") { |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
if labelSelector.Matches(labels.Set(keeper.Labels)) { |
|
||||||
allowedKeepers = append(allowedKeepers, keeper) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return &secretv0alpha1.KeeperList{ |
|
||||||
Items: allowedKeepers, |
|
||||||
}, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Get calls the inner `store` (persistence) and returns a `Keeper` by `name`.
|
|
||||||
func (s *KeeperRest) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { |
|
||||||
namespace, ok := request.NamespaceFrom(ctx) |
|
||||||
if !ok { |
|
||||||
return nil, fmt.Errorf("missing namespace") |
|
||||||
} |
|
||||||
|
|
||||||
// TODO: readopts
|
|
||||||
kp, err := s.storage.Read(ctx, xkube.Namespace(namespace), name, contracts.ReadOpts{}) |
|
||||||
if err != nil { |
|
||||||
if errors.Is(err, contracts.ErrKeeperNotFound) { |
|
||||||
return nil, s.resource.NewNotFound(name) |
|
||||||
} |
|
||||||
return nil, fmt.Errorf("failed to read keeper: %w", err) |
|
||||||
} |
|
||||||
|
|
||||||
return kp, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Create a new `Keeper`. Does some validation and allows empty `name` (generated).
|
|
||||||
func (s *KeeperRest) Create( |
|
||||||
ctx context.Context, |
|
||||||
obj runtime.Object, |
|
||||||
createValidation rest.ValidateObjectFunc, |
|
||||||
options *metav1.CreateOptions, |
|
||||||
) (runtime.Object, error) { |
|
||||||
kp, ok := obj.(*secretv0alpha1.Keeper) |
|
||||||
if !ok { |
|
||||||
return nil, fmt.Errorf("expected Keeper for create") |
|
||||||
} |
|
||||||
|
|
||||||
user, ok := claims.AuthInfoFrom(ctx) |
|
||||||
if !ok { |
|
||||||
return nil, fmt.Errorf("missing auth info in context") |
|
||||||
} |
|
||||||
|
|
||||||
if err := createValidation(ctx, obj); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
createdKeeper, err := s.storage.Create(ctx, kp, user.GetUID()) |
|
||||||
if err != nil { |
|
||||||
var kErr xkube.ErrorLister |
|
||||||
if errors.As(err, &kErr) { |
|
||||||
return nil, apierrors.NewInvalid(kp.GroupVersionKind().GroupKind(), kp.Name, kErr.ErrorList()) |
|
||||||
} |
|
||||||
|
|
||||||
return nil, fmt.Errorf("failed to create keeper: %w", err) |
|
||||||
} |
|
||||||
|
|
||||||
return createdKeeper, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Update a `Keeper`'s `value`. The second return parameter indicates whether the resource was newly created.
|
|
||||||
func (s *KeeperRest) Update( |
|
||||||
ctx context.Context, |
|
||||||
name string, |
|
||||||
objInfo rest.UpdatedObjectInfo, |
|
||||||
createValidation rest.ValidateObjectFunc, |
|
||||||
updateValidation rest.ValidateObjectUpdateFunc, |
|
||||||
forceAllowCreate bool, |
|
||||||
options *metav1.UpdateOptions, |
|
||||||
) (runtime.Object, bool, error) { |
|
||||||
user, ok := claims.AuthInfoFrom(ctx) |
|
||||||
if !ok { |
|
||||||
return nil, false, fmt.Errorf("missing auth info in context") |
|
||||||
} |
|
||||||
|
|
||||||
oldObj, err := s.Get(ctx, name, &metav1.GetOptions{}) |
|
||||||
if err != nil { |
|
||||||
return nil, false, err |
|
||||||
} |
|
||||||
|
|
||||||
// Makes sure the UID and ResourceVersion are OK.
|
|
||||||
// TODO: this also makes it so the labels and annotations are additive, unless we check and remove manually.
|
|
||||||
newObj, err := objInfo.UpdatedObject(ctx, oldObj) |
|
||||||
if err != nil { |
|
||||||
return nil, false, fmt.Errorf("k8s updated object: %w", err) |
|
||||||
} |
|
||||||
|
|
||||||
// The current supported behavior for `Update` is to replace the entire `spec` with the new one.
|
|
||||||
// Each provider-specific setting of a keeper lives at the top-level, so it makes it possible to change a provider
|
|
||||||
// during an update. Otherwise both old and new providers would be merged in the `newObj` which is not allowed.
|
|
||||||
if err := updateValidation(ctx, newObj, oldObj); err != nil { |
|
||||||
return nil, false, err |
|
||||||
} |
|
||||||
|
|
||||||
newKeeper, ok := newObj.(*secretv0alpha1.Keeper) |
|
||||||
if !ok { |
|
||||||
return nil, false, fmt.Errorf("expected Keeper for update") |
|
||||||
} |
|
||||||
|
|
||||||
// TODO: do we need to do this here again? Probably not, but double-check!
|
|
||||||
newKeeper.Annotations = xkube.CleanAnnotations(newKeeper.Annotations) |
|
||||||
|
|
||||||
// Current implementation replaces everything passed in the spec, so it is not a PATCH. Do we want/need to support that?
|
|
||||||
updatedKeeper, err := s.storage.Update(ctx, newKeeper, user.GetUID()) |
|
||||||
if err != nil { |
|
||||||
var kErr xkube.ErrorLister |
|
||||||
if errors.As(err, &kErr) { |
|
||||||
return nil, false, apierrors.NewInvalid(newKeeper.GroupVersionKind().GroupKind(), newKeeper.Name, kErr.ErrorList()) |
|
||||||
} |
|
||||||
|
|
||||||
return nil, false, fmt.Errorf("failed to update keeper: %w", err) |
|
||||||
} |
|
||||||
|
|
||||||
return updatedKeeper, false, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Delete calls the inner `store` (persistence) in order to delete the `Keeper`.
|
|
||||||
// The second return parameter `bool` indicates whether the delete was intant or not. It always is for `Keepers`.
|
|
||||||
func (s *KeeperRest) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { |
|
||||||
namespace, ok := request.NamespaceFrom(ctx) |
|
||||||
if !ok { |
|
||||||
return nil, false, fmt.Errorf("missing namespace") |
|
||||||
} |
|
||||||
|
|
||||||
err := s.storage.Delete(ctx, xkube.Namespace(namespace), name) |
|
||||||
if err != nil { |
|
||||||
if errors.Is(err, contracts.ErrKeeperNotFound) { |
|
||||||
return nil, false, s.resource.NewNotFound(name) |
|
||||||
} |
|
||||||
return nil, false, fmt.Errorf("failed to delete keeper: %w", err) |
|
||||||
} |
|
||||||
|
|
||||||
return nil, true, nil |
|
||||||
} |
|
||||||
|
|
||||||
// ValidateKeeper does basic spec validation of a keeper.
|
|
||||||
func ValidateKeeper(keeper *secretv0alpha1.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.HashiCorp != nil { |
|
||||||
if keeper.Spec.HashiCorp.Address == "" { |
|
||||||
errs = append(errs, field.Required(field.NewPath("spec", "hashicorp", "address"), "a `address` is required")) |
|
||||||
} |
|
||||||
|
|
||||||
if err := validateCredentialValue(field.NewPath("spec", "hashicorp", "token"), keeper.Spec.HashiCorp.Token); err != nil { |
|
||||||
errs = append(errs, err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return errs |
|
||||||
} |
|
||||||
|
|
||||||
func validateKeepers(keeper *secretv0alpha1.Keeper) *field.Error { |
|
||||||
availableKeepers := map[string]bool{ |
|
||||||
"aws": keeper.Spec.AWS != nil, |
|
||||||
"azure": keeper.Spec.Azure != nil, |
|
||||||
"gcp": keeper.Spec.GCP != nil, |
|
||||||
"hashicorp": keeper.Spec.HashiCorp != 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 secretv0alpha1.CredentialValue) *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 |
|
||||||
} |
|
@ -1,276 +0,0 @@ |
|||||||
package reststorage |
|
||||||
|
|
||||||
import ( |
|
||||||
"testing" |
|
||||||
|
|
||||||
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" |
|
||||||
"github.com/stretchr/testify/require" |
|
||||||
"k8s.io/apiserver/pkg/admission" |
|
||||||
) |
|
||||||
|
|
||||||
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 := &secretv0alpha1.Keeper{ |
|
||||||
Spec: secretv0alpha1.KeeperSpec{ |
|
||||||
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.description", errs[0].Field) |
|
||||||
}) |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("only one `keeper` must be present", func(t *testing.T) { |
|
||||||
keeper := &secretv0alpha1.Keeper{ |
|
||||||
Spec: secretv0alpha1.KeeperSpec{ |
|
||||||
Description: "short description", |
|
||||||
AWS: &secretv0alpha1.AWSKeeperConfig{}, |
|
||||||
Azure: &secretv0alpha1.AzureKeeperConfig{}, |
|
||||||
GCP: &secretv0alpha1.GCPKeeperConfig{}, |
|
||||||
HashiCorp: &secretv0alpha1.HashiCorpKeeperConfig{}, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
errs := ValidateKeeper(keeper, 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 := &secretv0alpha1.Keeper{ |
|
||||||
Spec: secretv0alpha1.KeeperSpec{ |
|
||||||
Description: "description", |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
errs := ValidateKeeper(keeper, admission.Create) |
|
||||||
require.Len(t, errs, 1) |
|
||||||
require.Equal(t, "spec", errs[0].Field) |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("aws keeper validation", func(t *testing.T) { |
|
||||||
validKeeperAWS := &secretv0alpha1.Keeper{ |
|
||||||
Spec: secretv0alpha1.KeeperSpec{ |
|
||||||
Description: "description", |
|
||||||
AWS: &secretv0alpha1.AWSKeeperConfig{ |
|
||||||
AWSCredentials: secretv0alpha1.AWSCredentials{ |
|
||||||
AccessKeyID: secretv0alpha1.CredentialValue{ |
|
||||||
ValueFromEnv: "some-value", |
|
||||||
}, |
|
||||||
SecretAccessKey: secretv0alpha1.CredentialValue{ |
|
||||||
SecureValueName: "some-value", |
|
||||||
}, |
|
||||||
KMSKeyID: "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 = secretv0alpha1.CredentialValue{} |
|
||||||
|
|
||||||
errs := ValidateKeeper(keeper, 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 = secretv0alpha1.CredentialValue{ |
|
||||||
SecureValueName: "a", |
|
||||||
ValueFromEnv: "b", |
|
||||||
ValueFromConfig: "c", |
|
||||||
} |
|
||||||
|
|
||||||
errs := ValidateKeeper(keeper, 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 = secretv0alpha1.CredentialValue{} |
|
||||||
|
|
||||||
errs := ValidateKeeper(keeper, 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 = secretv0alpha1.CredentialValue{ |
|
||||||
SecureValueName: "a", |
|
||||||
ValueFromEnv: "b", |
|
||||||
ValueFromConfig: "c", |
|
||||||
} |
|
||||||
|
|
||||||
errs := ValidateKeeper(keeper, 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 := &secretv0alpha1.Keeper{ |
|
||||||
Spec: secretv0alpha1.KeeperSpec{ |
|
||||||
Description: "description", |
|
||||||
Azure: &secretv0alpha1.AzureKeeperConfig{ |
|
||||||
AzureCredentials: secretv0alpha1.AzureCredentials{ |
|
||||||
KeyVaultName: "kv-name", |
|
||||||
TenantID: "tenant-id", |
|
||||||
ClientID: "client-id", |
|
||||||
ClientSecret: secretv0alpha1.CredentialValue{ |
|
||||||
ValueFromConfig: "config.path.value", |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
t.Run("`keyVaultName` must be present", func(t *testing.T) { |
|
||||||
keeper := validKeeperAzure.DeepCopy() |
|
||||||
keeper.Spec.Azure.KeyVaultName = "" |
|
||||||
|
|
||||||
errs := ValidateKeeper(keeper, 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 = "" |
|
||||||
|
|
||||||
errs := ValidateKeeper(keeper, 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 = "" |
|
||||||
|
|
||||||
errs := ValidateKeeper(keeper, 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 = secretv0alpha1.CredentialValue{} |
|
||||||
|
|
||||||
errs := ValidateKeeper(keeper, 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 = secretv0alpha1.CredentialValue{ |
|
||||||
SecureValueName: "a", |
|
||||||
ValueFromEnv: "b", |
|
||||||
ValueFromConfig: "c", |
|
||||||
} |
|
||||||
|
|
||||||
errs := ValidateKeeper(keeper, 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 := &secretv0alpha1.Keeper{ |
|
||||||
Spec: secretv0alpha1.KeeperSpec{ |
|
||||||
Description: "description", |
|
||||||
GCP: &secretv0alpha1.GCPKeeperConfig{ |
|
||||||
GCPCredentials: secretv0alpha1.GCPCredentials{ |
|
||||||
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 = "" |
|
||||||
|
|
||||||
errs := ValidateKeeper(keeper, 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 = "" |
|
||||||
|
|
||||||
errs := ValidateKeeper(keeper, 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 := &secretv0alpha1.Keeper{ |
|
||||||
Spec: secretv0alpha1.KeeperSpec{ |
|
||||||
Description: "description", |
|
||||||
HashiCorp: &secretv0alpha1.HashiCorpKeeperConfig{ |
|
||||||
HashiCorpCredentials: secretv0alpha1.HashiCorpCredentials{ |
|
||||||
Address: "http://address", |
|
||||||
Token: secretv0alpha1.CredentialValue{ |
|
||||||
ValueFromConfig: "config.path.value", |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
t.Run("`address` must be present", func(t *testing.T) { |
|
||||||
keeper := validKeeperHashiCorp.DeepCopy() |
|
||||||
keeper.Spec.HashiCorp.Address = "" |
|
||||||
|
|
||||||
errs := ValidateKeeper(keeper, admission.Create) |
|
||||||
require.Len(t, errs, 1) |
|
||||||
require.Equal(t, "spec.hashicorp.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.HashiCorp.Token = secretv0alpha1.CredentialValue{} |
|
||||||
|
|
||||||
errs := ValidateKeeper(keeper, admission.Create) |
|
||||||
require.Len(t, errs, 1) |
|
||||||
require.Equal(t, "spec.hashicorp.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.HashiCorp.Token = secretv0alpha1.CredentialValue{ |
|
||||||
SecureValueName: "a", |
|
||||||
ValueFromEnv: "b", |
|
||||||
ValueFromConfig: "c", |
|
||||||
} |
|
||||||
|
|
||||||
errs := ValidateKeeper(keeper, admission.Create) |
|
||||||
require.Len(t, errs, 1) |
|
||||||
require.Equal(t, "spec.hashicorp.token", errs[0].Field) |
|
||||||
}) |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
@ -1,379 +0,0 @@ |
|||||||
package reststorage |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
"strconv" |
|
||||||
"strings" |
|
||||||
|
|
||||||
claims "github.com/grafana/authlib/types" |
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/internalversion" |
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
|
||||||
"k8s.io/apimachinery/pkg/fields" |
|
||||||
"k8s.io/apimachinery/pkg/labels" |
|
||||||
"k8s.io/apimachinery/pkg/runtime" |
|
||||||
"k8s.io/apimachinery/pkg/util/validation" |
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field" |
|
||||||
"k8s.io/apiserver/pkg/admission" |
|
||||||
"k8s.io/apiserver/pkg/endpoints/request" |
|
||||||
"k8s.io/apiserver/pkg/registry/rest" |
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/utils" |
|
||||||
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" |
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts" |
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube" |
|
||||||
) |
|
||||||
|
|
||||||
var ( |
|
||||||
_ rest.Scoper = (*SecureValueRest)(nil) |
|
||||||
_ rest.SingularNameProvider = (*SecureValueRest)(nil) |
|
||||||
_ rest.Getter = (*SecureValueRest)(nil) |
|
||||||
_ rest.Lister = (*SecureValueRest)(nil) |
|
||||||
_ rest.Storage = (*SecureValueRest)(nil) |
|
||||||
_ rest.Creater = (*SecureValueRest)(nil) |
|
||||||
_ rest.Updater = (*SecureValueRest)(nil) |
|
||||||
_ rest.GracefulDeleter = (*SecureValueRest)(nil) |
|
||||||
) |
|
||||||
|
|
||||||
// SecureValueRest is an implementation of CRUDL operations on a `securevalue` backed by a persistence layer `store`.
|
|
||||||
type SecureValueRest struct { |
|
||||||
storage contracts.SecureValueMetadataStorage |
|
||||||
resource utils.ResourceInfo |
|
||||||
tableConverter rest.TableConvertor |
|
||||||
} |
|
||||||
|
|
||||||
// NewSecureValueRest is a returns a constructed `*SecureValueRest`.
|
|
||||||
func NewSecureValueRest(storage contracts.SecureValueMetadataStorage, resource utils.ResourceInfo) *SecureValueRest { |
|
||||||
return &SecureValueRest{storage, resource, resource.TableConverter()} |
|
||||||
} |
|
||||||
|
|
||||||
// New returns an empty `*SecureValue` that is used by the `Create` method.
|
|
||||||
func (s *SecureValueRest) New() runtime.Object { |
|
||||||
return s.resource.NewFunc() |
|
||||||
} |
|
||||||
|
|
||||||
// Destroy is called when? [TODO]
|
|
||||||
func (s *SecureValueRest) Destroy() {} |
|
||||||
|
|
||||||
// NamespaceScoped returns `true` because the storage is namespaced (== org).
|
|
||||||
func (s *SecureValueRest) NamespaceScoped() bool { |
|
||||||
return true |
|
||||||
} |
|
||||||
|
|
||||||
// GetSingularName is used by `kubectl` discovery to have singular name representation of resources.
|
|
||||||
func (s *SecureValueRest) GetSingularName() string { |
|
||||||
return s.resource.GetSingularName() |
|
||||||
} |
|
||||||
|
|
||||||
// NewList returns an empty `*SecureValueList` that is used by the `List` method.
|
|
||||||
func (s *SecureValueRest) NewList() runtime.Object { |
|
||||||
return s.resource.NewListFunc() |
|
||||||
} |
|
||||||
|
|
||||||
// ConvertToTable is used by Kubernetes and converts objects to `metav1.Table`.
|
|
||||||
func (s *SecureValueRest) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { |
|
||||||
return s.tableConverter.ConvertToTable(ctx, object, tableOptions) |
|
||||||
} |
|
||||||
|
|
||||||
// List calls the inner `store` (persistence) and returns a list of `securevalues` within a `namespace` filtered by the `options`.
|
|
||||||
func (s *SecureValueRest) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { |
|
||||||
namespace, ok := request.NamespaceFrom(ctx) |
|
||||||
if !ok { |
|
||||||
return nil, fmt.Errorf("missing namespace") |
|
||||||
} |
|
||||||
|
|
||||||
secureValueList, err := s.storage.List(ctx, xkube.Namespace(namespace)) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("failed to list secure values: %w", err) |
|
||||||
} |
|
||||||
|
|
||||||
labelSelector := options.LabelSelector |
|
||||||
|
|
||||||
if labelSelector == nil { |
|
||||||
labelSelector = labels.Everything() |
|
||||||
} |
|
||||||
|
|
||||||
fieldSelector := options.FieldSelector |
|
||||||
if fieldSelector == nil { |
|
||||||
fieldSelector = fields.Everything() |
|
||||||
} |
|
||||||
|
|
||||||
allowedSecureValues := make([]secretv0alpha1.SecureValue, 0, len(secureValueList)) |
|
||||||
|
|
||||||
for _, secureValue := range secureValueList { |
|
||||||
// Filter by label
|
|
||||||
if labelSelector.Matches(labels.Set(secureValue.Labels)) { |
|
||||||
// Filter by status.phase
|
|
||||||
if fieldSelector.Matches(fields.Set{"status.phase": string(secureValue.Status.Phase)}) { |
|
||||||
allowedSecureValues = append(allowedSecureValues, secureValue) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return &secretv0alpha1.SecureValueList{Items: allowedSecureValues}, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Get calls the inner `store` (persistence) and returns a `securevalue` by `name`. It will NOT return the decrypted `value`.
|
|
||||||
func (s *SecureValueRest) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { |
|
||||||
namespace, ok := request.NamespaceFrom(ctx) |
|
||||||
if !ok { |
|
||||||
return nil, fmt.Errorf("missing namespace") |
|
||||||
} |
|
||||||
|
|
||||||
sv, err := s.storage.Read(ctx, xkube.Namespace(namespace), name, contracts.ReadOpts{}) |
|
||||||
if err != nil { |
|
||||||
if errors.Is(err, contracts.ErrSecureValueNotFound) { |
|
||||||
return nil, s.resource.NewNotFound(name) |
|
||||||
} |
|
||||||
|
|
||||||
return nil, fmt.Errorf("failed to read secure value: %w", err) |
|
||||||
} |
|
||||||
|
|
||||||
return sv, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Create a new `securevalue`. Does some validation and allows empty `name` (generated).
|
|
||||||
func (s *SecureValueRest) Create( |
|
||||||
ctx context.Context, |
|
||||||
obj runtime.Object, |
|
||||||
createValidation rest.ValidateObjectFunc, |
|
||||||
_ *metav1.CreateOptions, |
|
||||||
) (runtime.Object, error) { |
|
||||||
sv, ok := obj.(*secretv0alpha1.SecureValue) |
|
||||||
if !ok { |
|
||||||
return nil, fmt.Errorf("expected SecureValue for create") |
|
||||||
} |
|
||||||
|
|
||||||
if err := createValidation(ctx, obj); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
user, ok := claims.AuthInfoFrom(ctx) |
|
||||||
if !ok { |
|
||||||
return nil, fmt.Errorf("missing auth info in context") |
|
||||||
} |
|
||||||
|
|
||||||
createdSecureValueMetadata, err := s.storage.Create(ctx, sv, user.GetUID()) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("creating secure value %+w", err) |
|
||||||
} |
|
||||||
|
|
||||||
return createdSecureValueMetadata, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Update a `securevalue`'s `value`. The second return parameter indicates whether the resource was newly created.
|
|
||||||
// Currently does not support "create on update" functionality. If the securevalue does not yet exist, it returns an error.
|
|
||||||
func (s *SecureValueRest) Update( |
|
||||||
ctx context.Context, |
|
||||||
name string, |
|
||||||
objInfo rest.UpdatedObjectInfo, |
|
||||||
_ rest.ValidateObjectFunc, |
|
||||||
updateValidation rest.ValidateObjectUpdateFunc, |
|
||||||
_forceAllowCreate bool, |
|
||||||
_ *metav1.UpdateOptions, |
|
||||||
) (runtime.Object, bool, error) { |
|
||||||
oldObj, err := s.Get(ctx, name, &metav1.GetOptions{}) |
|
||||||
if err != nil { |
|
||||||
return nil, false, err |
|
||||||
} |
|
||||||
|
|
||||||
// Makes sure the UID and ResourceVersion are OK.
|
|
||||||
// TODO: this also makes it so the labels and annotations are additive, unless we check and remove manually.
|
|
||||||
newObj, err := objInfo.UpdatedObject(ctx, oldObj) |
|
||||||
if err != nil { |
|
||||||
return nil, false, fmt.Errorf("k8s updated object: %w", err) |
|
||||||
} |
|
||||||
|
|
||||||
if err := updateValidation(ctx, newObj, oldObj); err != nil { |
|
||||||
return nil, false, err |
|
||||||
} |
|
||||||
|
|
||||||
newSecureValue, ok := newObj.(*secretv0alpha1.SecureValue) |
|
||||||
if !ok { |
|
||||||
return nil, false, fmt.Errorf("expected SecureValue for update") |
|
||||||
} |
|
||||||
|
|
||||||
// TODO: do we need to do this here again? Probably not, but double-check!
|
|
||||||
newSecureValue.Annotations = xkube.CleanAnnotations(newSecureValue.Annotations) |
|
||||||
|
|
||||||
user, ok := claims.AuthInfoFrom(ctx) |
|
||||||
if !ok { |
|
||||||
return nil, false, fmt.Errorf("missing auth info in context") |
|
||||||
} |
|
||||||
|
|
||||||
// Current implementation replaces everything passed in the spec, so it is not a PATCH. Do we want/need to support that?
|
|
||||||
updatedSecureValueMetadata, err := s.storage.Update(ctx, newSecureValue, user.GetUID()) |
|
||||||
if err != nil { |
|
||||||
return updatedSecureValueMetadata, false, fmt.Errorf("updating secure value metadata: %+w", err) |
|
||||||
} |
|
||||||
|
|
||||||
return updatedSecureValueMetadata, false, nil |
|
||||||
} |
|
||||||
|
|
||||||
// The second return parameter `bool` indicates whether the delete was instant or not. It always is for `securevalues`.
|
|
||||||
func (s *SecureValueRest) Delete(ctx context.Context, name string, _ rest.ValidateObjectFunc, _ *metav1.DeleteOptions) (runtime.Object, bool, error) { |
|
||||||
namespace, ok := request.NamespaceFrom(ctx) |
|
||||||
if !ok { |
|
||||||
return nil, false, fmt.Errorf("missing namespace") |
|
||||||
} |
|
||||||
|
|
||||||
if err := s.storage.Delete(ctx, xkube.Namespace(namespace), name); err != nil { |
|
||||||
if errors.Is(err, contracts.ErrSecureValueNotFound) { |
|
||||||
return nil, false, s.resource.NewNotFound(name) |
|
||||||
} |
|
||||||
return nil, false, fmt.Errorf("deleting secure value: %+w", err) |
|
||||||
} |
|
||||||
|
|
||||||
return nil, false, nil |
|
||||||
} |
|
||||||
|
|
||||||
// ValidateSecureValue does basic spec validation of a securevalue.
|
|
||||||
func ValidateSecureValue(sv, oldSv *secretv0alpha1.SecureValue, operation admission.Operation, decryptersAllowList map[string]struct{}) 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 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, decryptersAllowList); len(errs) > 0 { |
|
||||||
return errs |
|
||||||
} |
|
||||||
|
|
||||||
return errs |
|
||||||
} |
|
||||||
|
|
||||||
// validateSecureValueCreate does basic spec validation of a securevalue for the Create operation.
|
|
||||||
func validateSecureValueCreate(sv *secretv0alpha1.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 == "" && (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 != 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 *secretv0alpha1.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 != "" || (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 == 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 |
|
||||||
} |
|
@ -1,308 +0,0 @@ |
|||||||
package reststorage |
|
||||||
|
|
||||||
import ( |
|
||||||
"fmt" |
|
||||||
"maps" |
|
||||||
"slices" |
|
||||||
"strings" |
|
||||||
"testing" |
|
||||||
|
|
||||||
"github.com/stretchr/testify/require" |
|
||||||
"k8s.io/apiserver/pkg/admission" |
|
||||||
|
|
||||||
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" |
|
||||||
"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 := &secretv0alpha1.SecureValue{ |
|
||||||
Spec: secretv0alpha1.SecureValueSpec{ |
|
||||||
Description: "description", |
|
||||||
Value: "value", |
|
||||||
Keeper: &keeper, |
|
||||||
Decrypters: []string{"app1", "app2"}, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
t.Run("the `description` must be present", func(t *testing.T) { |
|
||||||
sv := validSecureValue.DeepCopy() |
|
||||||
sv.Spec.Description = "" |
|
||||||
|
|
||||||
errs := ValidateSecureValue(sv, nil, admission.Create, nil) |
|
||||||
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) { |
|
||||||
sv := validSecureValue.DeepCopy() |
|
||||||
sv.Spec.Value = "" |
|
||||||
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 = &ref |
|
||||||
|
|
||||||
errs = ValidateSecureValue(sv, nil, admission.Create, nil) |
|
||||||
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 = secretv0alpha1.NewExposedSecureValue(strings.Repeat("a", contracts.SECURE_VALUE_RAW_INPUT_MAX_SIZE_BYTES+1)) |
|
||||||
sv.Spec.Ref = nil |
|
||||||
|
|
||||||
errs := ValidateSecureValue(sv, nil, admission.Create, nil) |
|
||||||
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 := &secretv0alpha1.SecureValue{ |
|
||||||
Spec: secretv0alpha1.SecureValueSpec{ |
|
||||||
Ref: nil, // empty `ref` means a `value` was present.
|
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
ref := "ref" |
|
||||||
sv := &secretv0alpha1.SecureValue{ |
|
||||||
Spec: secretv0alpha1.SecureValueSpec{ |
|
||||||
Ref: &ref, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
errs := ValidateSecureValue(sv, oldSv, admission.Update, nil) |
|
||||||
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 := &secretv0alpha1.SecureValue{ |
|
||||||
Spec: secretv0alpha1.SecureValueSpec{ |
|
||||||
Ref: &ref, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
sv := &secretv0alpha1.SecureValue{ |
|
||||||
Spec: secretv0alpha1.SecureValueSpec{ |
|
||||||
Value: "value", |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
errs := ValidateSecureValue(sv, oldSv, admission.Update, nil) |
|
||||||
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 := &secretv0alpha1.SecureValue{ |
|
||||||
Spec: secretv0alpha1.SecureValueSpec{ |
|
||||||
Ref: &refNonEmpty, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
ref := "ref" |
|
||||||
sv := &secretv0alpha1.SecureValue{ |
|
||||||
Spec: secretv0alpha1.SecureValueSpec{ |
|
||||||
Value: "value", |
|
||||||
Ref: &ref, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
errs := ValidateSecureValue(sv, oldSv, admission.Update, nil) |
|
||||||
require.Len(t, errs, 1) |
|
||||||
require.Equal(t, "spec", errs[0].Field) |
|
||||||
|
|
||||||
oldSv = &secretv0alpha1.SecureValue{ |
|
||||||
Spec: secretv0alpha1.SecureValueSpec{ |
|
||||||
Value: "non-empty", |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
errs = ValidateSecureValue(sv, oldSv, admission.Update, nil) |
|
||||||
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 := &secretv0alpha1.SecureValue{ |
|
||||||
Spec: secretv0alpha1.SecureValueSpec{ |
|
||||||
Description: "old-description", |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
sv := &secretv0alpha1.SecureValue{ |
|
||||||
Spec: secretv0alpha1.SecureValueSpec{ |
|
||||||
Description: "new-description", |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
errs := ValidateSecureValue(sv, oldSv, admission.Update, nil) |
|
||||||
require.Empty(t, errs) |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("when the old object is `nil` it returns an error", func(t *testing.T) { |
|
||||||
sv := &secretv0alpha1.SecureValue{} |
|
||||||
|
|
||||||
errs := ValidateSecureValue(sv, nil, admission.Update, nil) |
|
||||||
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 := &secretv0alpha1.SecureValue{ |
|
||||||
Spec: secretv0alpha1.SecureValueSpec{ |
|
||||||
Keeper: &keeperA, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
sv := &secretv0alpha1.SecureValue{ |
|
||||||
Spec: secretv0alpha1.SecureValueSpec{ |
|
||||||
Keeper: &keeperAnother, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
errs := ValidateSecureValue(sv, oldSv, admission.Update, nil) |
|
||||||
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 := &secretv0alpha1.SecureValue{ |
|
||||||
Spec: secretv0alpha1.SecureValueSpec{ |
|
||||||
Description: "description", Ref: &ref, |
|
||||||
|
|
||||||
Decrypters: []string{ |
|
||||||
"app1", |
|
||||||
"app1", |
|
||||||
}, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
errs := ValidateSecureValue(sv, nil, admission.Create, nil) |
|
||||||
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 := &secretv0alpha1.SecureValue{ |
|
||||||
Spec: secretv0alpha1.SecureValueSpec{ |
|
||||||
Description: "description", Ref: &ref, |
|
||||||
|
|
||||||
Decrypters: []string{"app3"}, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
errs := ValidateSecureValue(sv, nil, admission.Create, allowList) |
|
||||||
require.Len(t, errs, 1) |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("no decrypters, returns no error", func(t *testing.T) { |
|
||||||
ref := "ref" |
|
||||||
sv := &secretv0alpha1.SecureValue{ |
|
||||||
Spec: secretv0alpha1.SecureValueSpec{ |
|
||||||
Description: "description", Ref: &ref, |
|
||||||
|
|
||||||
Decrypters: []string{}, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
errs := ValidateSecureValue(sv, nil, admission.Create, allowList) |
|
||||||
require.Empty(t, errs) |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("one match, returns no errors", func(t *testing.T) { |
|
||||||
ref := "ref" |
|
||||||
sv := &secretv0alpha1.SecureValue{ |
|
||||||
Spec: secretv0alpha1.SecureValueSpec{ |
|
||||||
Description: "description", Ref: &ref, |
|
||||||
|
|
||||||
Decrypters: []string{decrypters[0]}, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
errs := ValidateSecureValue(sv, nil, admission.Create, allowList) |
|
||||||
require.Empty(t, errs) |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("all matches, returns no errors", func(t *testing.T) { |
|
||||||
ref := "ref" |
|
||||||
sv := &secretv0alpha1.SecureValue{ |
|
||||||
Spec: secretv0alpha1.SecureValueSpec{ |
|
||||||
Description: "description", Ref: &ref, |
|
||||||
|
|
||||||
Decrypters: decrypters, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
errs := ValidateSecureValue(sv, nil, admission.Create, allowList) |
|
||||||
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 := &secretv0alpha1.SecureValue{ |
|
||||||
Spec: secretv0alpha1.SecureValueSpec{ |
|
||||||
Description: "description", Ref: &ref, |
|
||||||
|
|
||||||
Decrypters: decrypters, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
errs := ValidateSecureValue(sv, nil, admission.Create, nil) |
|
||||||
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 := &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) |
|
||||||
}) |
|
||||||
} |
|
@ -1,567 +0,0 @@ |
|||||||
package secret |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"errors" |
|
||||||
"math/rand/v2" |
|
||||||
"net/http" |
|
||||||
"strconv" |
|
||||||
"strings" |
|
||||||
"testing" |
|
||||||
|
|
||||||
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" |
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt" |
|
||||||
"github.com/grafana/grafana/pkg/tests/apis" |
|
||||||
"github.com/grafana/grafana/pkg/tests/testinfra" |
|
||||||
"github.com/stretchr/testify/require" |
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors" |
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
|
||||||
"k8s.io/apimachinery/pkg/runtime" |
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema" |
|
||||||
) |
|
||||||
|
|
||||||
var gvrKeepers = schema.GroupVersionResource{ |
|
||||||
Group: secretv0alpha1.GROUP, |
|
||||||
Version: secretv0alpha1.VERSION, |
|
||||||
Resource: secretv0alpha1.KeeperResourceInfo.GetName(), |
|
||||||
} |
|
||||||
|
|
||||||
func TestIntegrationKeeper(t *testing.T) { |
|
||||||
if testing.Short() { |
|
||||||
t.Skip("skipping integration test") |
|
||||||
} |
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background()) |
|
||||||
t.Cleanup(cancel) |
|
||||||
|
|
||||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ |
|
||||||
AppModeProduction: false, // required for experimental APIs
|
|
||||||
EnableFeatureToggles: []string{ |
|
||||||
// Required to start the example service
|
|
||||||
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, |
|
||||||
featuremgmt.FlagSecretsManagementAppPlatform, |
|
||||||
}, |
|
||||||
}) |
|
||||||
|
|
||||||
permissions := map[string]ResourcePermission{ResourceKeepers: {Actions: ActionsAllKeepers}} |
|
||||||
|
|
||||||
genericUserEditor := mustCreateUsers(t, helper, permissions).Editor |
|
||||||
|
|
||||||
client := helper.GetResourceClient(apis.ResourceClientArgs{ |
|
||||||
User: genericUserEditor, |
|
||||||
GVR: gvrKeepers, |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("reading a keeper that does not exist returns a 404", func(t *testing.T) { |
|
||||||
raw, err := client.Resource.Get(ctx, "some-keeper-that-does-not-exist", metav1.GetOptions{}) |
|
||||||
require.Error(t, err) |
|
||||||
require.Nil(t, raw) |
|
||||||
|
|
||||||
var statusErr *apierrors.StatusError |
|
||||||
require.True(t, errors.As(err, &statusErr)) |
|
||||||
require.Equal(t, "keeper.secret.grafana.app \"some-keeper-that-does-not-exist\" not found", err.Error()) |
|
||||||
require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code)) |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("deleting a keeper that does not exist returns an error", func(t *testing.T) { |
|
||||||
err := client.Resource.Delete(ctx, "some-keeper-that-does-not-exist", metav1.DeleteOptions{}) |
|
||||||
require.Error(t, err) |
|
||||||
|
|
||||||
var statusErr *apierrors.StatusError |
|
||||||
require.True(t, errors.As(err, &statusErr)) |
|
||||||
require.Equal(t, "keeper.secret.grafana.app \"some-keeper-that-does-not-exist\" not found", err.Error()) |
|
||||||
require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code)) |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("creating a keeper returns it", func(t *testing.T) { |
|
||||||
raw := mustGenerateKeeper(t, helper, genericUserEditor, nil, "testdata/keeper-aws-generate.yaml") |
|
||||||
|
|
||||||
keeper := new(secretv0alpha1.Keeper) |
|
||||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, keeper) |
|
||||||
require.NoError(t, err) |
|
||||||
require.NotNil(t, keeper) |
|
||||||
|
|
||||||
require.NotEmpty(t, keeper.Spec.Description) |
|
||||||
require.NotEmpty(t, keeper.Spec.AWS) |
|
||||||
require.Empty(t, keeper.Spec.Azure) |
|
||||||
|
|
||||||
t.Run("and creating another keeper with the same name in the same namespace returns an error", func(t *testing.T) { |
|
||||||
testKeeper := helper.LoadYAMLOrJSONFile("testdata/keeper-gcp-generate.yaml") |
|
||||||
testKeeper.SetName(raw.GetName()) |
|
||||||
|
|
||||||
raw, err := client.Resource.Create(ctx, testKeeper, metav1.CreateOptions{}) |
|
||||||
require.Error(t, err) |
|
||||||
require.Nil(t, raw) |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("and reading th keeper returns it same as if when it was created", func(t *testing.T) { |
|
||||||
raw, err := client.Resource.Get(ctx, keeper.Name, metav1.GetOptions{}) |
|
||||||
require.NoError(t, err) |
|
||||||
require.NotNil(t, raw) |
|
||||||
|
|
||||||
anotherKeeper := new(secretv0alpha1.Keeper) |
|
||||||
err = runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, anotherKeeper) |
|
||||||
require.NoError(t, err) |
|
||||||
require.NotNil(t, anotherKeeper) |
|
||||||
|
|
||||||
require.EqualValues(t, keeper, anotherKeeper) |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("and listing keepers returns the created keeper", func(t *testing.T) { |
|
||||||
rawList, err := client.Resource.List(ctx, metav1.ListOptions{}) |
|
||||||
require.NoError(t, err) |
|
||||||
require.NotNil(t, rawList) |
|
||||||
require.GreaterOrEqual(t, len(rawList.Items), 1) |
|
||||||
require.Equal(t, keeper.Name, rawList.Items[0].GetName()) |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("and updating the keeper replaces the spec fields and returns them", func(t *testing.T) { |
|
||||||
newRaw := helper.LoadYAMLOrJSONFile("testdata/keeper-gcp-generate.yaml") |
|
||||||
newRaw.SetName(raw.GetName()) |
|
||||||
newRaw.Object["spec"].(map[string]any)["description"] = "New description" |
|
||||||
newRaw.Object["metadata"].(map[string]any)["annotations"] = map[string]any{"newAnnotation": "newValue"} |
|
||||||
|
|
||||||
updatedRaw, err := client.Resource.Update(ctx, newRaw, metav1.UpdateOptions{}) |
|
||||||
require.NoError(t, err) |
|
||||||
require.NotNil(t, updatedRaw) |
|
||||||
|
|
||||||
updatedKeeper := new(secretv0alpha1.Keeper) |
|
||||||
err = runtime.DefaultUnstructuredConverter.FromUnstructured(updatedRaw.Object, updatedKeeper) |
|
||||||
require.NoError(t, err) |
|
||||||
require.NotNil(t, updatedKeeper) |
|
||||||
|
|
||||||
require.NotEqualValues(t, updatedKeeper.Spec, keeper.Spec) |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("and updating the keeper to reference securevalues that does not exist returns an error", func(t *testing.T) { |
|
||||||
newRaw := helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml") |
|
||||||
newRaw.SetName(raw.GetName()) |
|
||||||
newRaw.Object["spec"].(map[string]any)["aws"] = map[string]any{ |
|
||||||
"accessKeyId": map[string]any{ |
|
||||||
"secureValueName": "securevalue-does-not-exist-1", |
|
||||||
}, |
|
||||||
"secretAccessKey": map[string]any{ |
|
||||||
"secureValueName": "securevalue-does-not-exist-2", |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
updatedRaw, err := client.Resource.Update(ctx, newRaw, metav1.UpdateOptions{}) |
|
||||||
require.Error(t, err) |
|
||||||
require.Nil(t, updatedRaw) |
|
||||||
require.Contains(t, err.Error(), "securevalue-does-not-exist-1") |
|
||||||
require.Contains(t, err.Error(), "securevalue-does-not-exist-2") |
|
||||||
}) |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("creating an invalid keeper fails validation and returns an error", func(t *testing.T) { |
|
||||||
testData := helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml") |
|
||||||
testData.Object["spec"].(map[string]any)["description"] = "" |
|
||||||
|
|
||||||
raw, err := client.Resource.Create(ctx, testData, metav1.CreateOptions{}) |
|
||||||
require.Error(t, err) |
|
||||||
require.Nil(t, raw) |
|
||||||
|
|
||||||
var statusErr *apierrors.StatusError |
|
||||||
require.True(t, errors.As(err, &statusErr)) |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("creating a keeper with a provider then changing the provider does not return an error", func(t *testing.T) { |
|
||||||
rawAWS := mustGenerateKeeper(t, helper, genericUserEditor, nil, "testdata/keeper-aws-generate.yaml") |
|
||||||
|
|
||||||
testDataKeeperGCP := rawAWS.DeepCopy() |
|
||||||
testDataKeeperGCP.Object["spec"].(map[string]any)["aws"] = nil |
|
||||||
testDataKeeperGCP.Object["spec"].(map[string]any)["gcp"] = map[string]any{ |
|
||||||
"projectId": "project-id", |
|
||||||
"credentialsFile": "/path/to/file.json", |
|
||||||
} |
|
||||||
|
|
||||||
rawGCP, err := client.Resource.Update(ctx, testDataKeeperGCP, metav1.UpdateOptions{}) |
|
||||||
require.NoError(t, err) |
|
||||||
require.NotNil(t, rawGCP) |
|
||||||
|
|
||||||
require.NotEqualValues(t, rawAWS.Object["spec"], rawGCP.Object["spec"]) |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("creating a keeper that references securevalues that does not exist returns an error", func(t *testing.T) { |
|
||||||
testDataKeeper := helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml") |
|
||||||
testDataKeeper.Object["spec"].(map[string]any)["aws"] = map[string]any{ |
|
||||||
"accessKeyId": map[string]any{ |
|
||||||
"secureValueName": "securevalue-does-not-exist-1", |
|
||||||
}, |
|
||||||
"secretAccessKey": map[string]any{ |
|
||||||
"secureValueName": "securevalue-does-not-exist-2", |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
raw, err := client.Resource.Create(ctx, testDataKeeper, metav1.CreateOptions{}) |
|
||||||
require.Error(t, err) |
|
||||||
require.Nil(t, raw) |
|
||||||
require.Contains(t, err.Error(), "securevalue-does-not-exist-1") |
|
||||||
require.Contains(t, err.Error(), "securevalue-does-not-exist-2") |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("deleting a keeper that exists does not return an error", func(t *testing.T) { |
|
||||||
generatePrefix := "generated-" |
|
||||||
|
|
||||||
testData := helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml") |
|
||||||
testData.SetGenerateName(generatePrefix) |
|
||||||
|
|
||||||
raw, err := client.Resource.Create(ctx, testData, metav1.CreateOptions{}) |
|
||||||
require.NoError(t, err) |
|
||||||
require.NotNil(t, raw) |
|
||||||
|
|
||||||
name := raw.GetName() |
|
||||||
require.True(t, strings.HasPrefix(name, generatePrefix)) |
|
||||||
|
|
||||||
err = client.Resource.Delete(ctx, name, metav1.DeleteOptions{}) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
t.Run("and then trying to read it returns a 404 error", func(t *testing.T) { |
|
||||||
raw, err := client.Resource.Get(ctx, name, metav1.GetOptions{}) |
|
||||||
require.Error(t, err) |
|
||||||
require.Nil(t, raw) |
|
||||||
|
|
||||||
var statusErr *apierrors.StatusError |
|
||||||
require.True(t, errors.As(err, &statusErr)) |
|
||||||
require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code)) |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("and listing keepers returns an empty list", func(t *testing.T) { |
|
||||||
rawList, err := client.Resource.List(ctx, metav1.ListOptions{}) |
|
||||||
require.NoError(t, err) |
|
||||||
require.NotNil(t, rawList) |
|
||||||
require.Empty(t, rawList.Items) |
|
||||||
}) |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("creating keepers in multiple namespaces", func(t *testing.T) { |
|
||||||
permissions := map[string]ResourcePermission{ |
|
||||||
ResourceKeepers: {Actions: ActionsAllKeepers}, |
|
||||||
} |
|
||||||
|
|
||||||
editorOrgA := mustCreateUsers(t, helper, permissions).Editor |
|
||||||
editorOrgB := mustCreateUsers(t, helper, permissions).Editor |
|
||||||
|
|
||||||
keeperOrgA := mustGenerateKeeper(t, helper, editorOrgA, nil, "testdata/keeper-aws-generate.yaml") |
|
||||||
keeperOrgB := mustGenerateKeeper(t, helper, editorOrgB, nil, "testdata/keeper-aws-generate.yaml") |
|
||||||
|
|
||||||
clientOrgA := helper.GetResourceClient(apis.ResourceClientArgs{User: editorOrgA, GVR: gvrKeepers}) |
|
||||||
clientOrgB := helper.GetResourceClient(apis.ResourceClientArgs{User: editorOrgB, GVR: gvrKeepers}) |
|
||||||
|
|
||||||
// Create
|
|
||||||
t.Run("creating a keeper with the same name as one from another namespace does not return an error", func(t *testing.T) { |
|
||||||
// OrgA creating a keeper with the same name from OrgB.
|
|
||||||
testData := helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml") |
|
||||||
testData.SetName(keeperOrgB.GetName()) |
|
||||||
|
|
||||||
raw, err := clientOrgA.Resource.Create(ctx, testData, metav1.CreateOptions{}) |
|
||||||
require.NoError(t, err) |
|
||||||
require.NotNil(t, raw) |
|
||||||
|
|
||||||
// OrgA creating a keeper with the same name from OrgB.
|
|
||||||
testData = helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml") |
|
||||||
testData.SetName(keeperOrgA.GetName()) |
|
||||||
|
|
||||||
raw, err = clientOrgB.Resource.Create(ctx, testData, metav1.CreateOptions{}) |
|
||||||
require.NoError(t, err) |
|
||||||
require.NotNil(t, raw) |
|
||||||
|
|
||||||
require.NoError(t, clientOrgA.Resource.Delete(ctx, keeperOrgB.GetName(), metav1.DeleteOptions{})) |
|
||||||
require.NoError(t, clientOrgB.Resource.Delete(ctx, keeperOrgA.GetName(), metav1.DeleteOptions{})) |
|
||||||
}) |
|
||||||
|
|
||||||
// Read
|
|
||||||
t.Run("fetching a keeper from another namespace returns not found", func(t *testing.T) { |
|
||||||
var statusErr *apierrors.StatusError |
|
||||||
|
|
||||||
// OrgA trying to fetch keeper from OrgB.
|
|
||||||
raw, err := clientOrgA.Resource.Get(ctx, keeperOrgB.GetName(), metav1.GetOptions{}) |
|
||||||
require.Error(t, err) |
|
||||||
require.Nil(t, raw) |
|
||||||
require.True(t, errors.As(err, &statusErr)) |
|
||||||
require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code)) |
|
||||||
|
|
||||||
// OrgB trying to fetch keeper from OrgA.
|
|
||||||
raw, err = clientOrgB.Resource.Get(ctx, keeperOrgA.GetName(), metav1.GetOptions{}) |
|
||||||
require.Error(t, err) |
|
||||||
require.Nil(t, raw) |
|
||||||
require.True(t, errors.As(err, &statusErr)) |
|
||||||
require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code)) |
|
||||||
}) |
|
||||||
|
|
||||||
// Update
|
|
||||||
t.Run("updating a keeper from another namespace returns not found", func(t *testing.T) { |
|
||||||
var statusErr *apierrors.StatusError |
|
||||||
|
|
||||||
// OrgA trying to update securevalue from OrgB.
|
|
||||||
testData := helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml") |
|
||||||
testData.SetName(keeperOrgB.GetName()) |
|
||||||
testData.Object["spec"].(map[string]any)["description"] = "New description" |
|
||||||
|
|
||||||
raw, err := clientOrgA.Resource.Update(ctx, testData, metav1.UpdateOptions{}) |
|
||||||
require.Error(t, err) |
|
||||||
require.Nil(t, raw) |
|
||||||
require.True(t, errors.As(err, &statusErr)) |
|
||||||
require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code)) |
|
||||||
|
|
||||||
// OrgB trying to update keeper from OrgA.
|
|
||||||
testData = helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml") |
|
||||||
testData.SetName(keeperOrgA.GetName()) |
|
||||||
testData.Object["spec"].(map[string]any)["description"] = "New description" |
|
||||||
|
|
||||||
raw, err = clientOrgB.Resource.Update(ctx, testData, metav1.UpdateOptions{}) |
|
||||||
require.Error(t, err) |
|
||||||
require.Nil(t, raw) |
|
||||||
require.True(t, errors.As(err, &statusErr)) |
|
||||||
require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code)) |
|
||||||
}) |
|
||||||
|
|
||||||
// Delete
|
|
||||||
t.Run("deleting a keeper from another namespace returns an error and does not delete it", func(t *testing.T) { |
|
||||||
var statusErr *apierrors.StatusError |
|
||||||
|
|
||||||
// OrgA trying to delete keeper from OrgB.
|
|
||||||
err := clientOrgA.Resource.Delete(ctx, keeperOrgB.GetName(), metav1.DeleteOptions{}) |
|
||||||
require.Error(t, err) |
|
||||||
require.True(t, errors.As(err, &statusErr)) |
|
||||||
require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code)) |
|
||||||
|
|
||||||
// Check that it still exists from the perspective of OrgB.
|
|
||||||
raw, err := clientOrgB.Resource.Get(ctx, keeperOrgB.GetName(), metav1.GetOptions{}) |
|
||||||
require.NoError(t, err) |
|
||||||
require.NotNil(t, raw) |
|
||||||
|
|
||||||
// OrgB trying to delete keeper from OrgA.
|
|
||||||
err = clientOrgB.Resource.Delete(ctx, keeperOrgA.GetName(), metav1.DeleteOptions{}) |
|
||||||
require.Error(t, err) |
|
||||||
require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code)) |
|
||||||
|
|
||||||
// Check that it still exists from the perspective of OrgA.
|
|
||||||
raw, err = clientOrgA.Resource.Get(ctx, keeperOrgA.GetName(), metav1.GetOptions{}) |
|
||||||
require.NoError(t, err) |
|
||||||
require.NotNil(t, raw) |
|
||||||
}) |
|
||||||
|
|
||||||
// List
|
|
||||||
t.Run("listing keeper from a namespace does not return the ones from another namespace", func(t *testing.T) { |
|
||||||
// OrgA listing keeper.
|
|
||||||
listOrgA, err := clientOrgA.Resource.List(ctx, metav1.ListOptions{}) |
|
||||||
require.NoError(t, err) |
|
||||||
require.NotNil(t, listOrgA) |
|
||||||
require.Len(t, listOrgA.Items, 1) |
|
||||||
require.Equal(t, *keeperOrgA, listOrgA.Items[0]) |
|
||||||
|
|
||||||
// OrgB listing keeper.
|
|
||||||
listOrgB, err := clientOrgB.Resource.List(ctx, metav1.ListOptions{}) |
|
||||||
require.NoError(t, err) |
|
||||||
require.NotNil(t, listOrgB) |
|
||||||
require.Len(t, listOrgB.Items, 1) |
|
||||||
require.Equal(t, *keeperOrgB, listOrgB.Items[0]) |
|
||||||
}) |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("keeper actions without having required permissions", func(t *testing.T) { |
|
||||||
// Create users on a random org without specifying secrets-related permissions.
|
|
||||||
editorP := mustCreateUsers(t, helper, nil).Editor |
|
||||||
|
|
||||||
clientP := helper.GetResourceClient(apis.ResourceClientArgs{ |
|
||||||
User: editorP, |
|
||||||
GVR: gvrKeepers, |
|
||||||
}) |
|
||||||
|
|
||||||
// GET
|
|
||||||
rawGet, err := clientP.Resource.Get(ctx, "some-keeper", metav1.GetOptions{}) |
|
||||||
require.Error(t, err) |
|
||||||
require.Nil(t, rawGet) |
|
||||||
|
|
||||||
var statusGetErr *apierrors.StatusError |
|
||||||
require.True(t, errors.As(err, &statusGetErr)) |
|
||||||
require.EqualValues(t, http.StatusForbidden, statusGetErr.Status().Code) |
|
||||||
|
|
||||||
// LIST
|
|
||||||
rawList, err := clientP.Resource.List(ctx, metav1.ListOptions{}) |
|
||||||
require.Error(t, err) |
|
||||||
require.Nil(t, rawList) |
|
||||||
|
|
||||||
var statusListErr *apierrors.StatusError |
|
||||||
require.True(t, errors.As(err, &statusListErr)) |
|
||||||
require.EqualValues(t, http.StatusForbidden, statusListErr.Status().Code) |
|
||||||
|
|
||||||
// CREATE
|
|
||||||
testKeeper := helper.LoadYAMLOrJSONFile("testdata/keeper-gcp-generate.yaml") // to pass validation before authz.
|
|
||||||
rawCreate, err := clientP.Resource.Create(ctx, testKeeper, metav1.CreateOptions{}) |
|
||||||
require.Error(t, err) |
|
||||||
require.Nil(t, rawCreate) |
|
||||||
|
|
||||||
var statusCreateErr *apierrors.StatusError |
|
||||||
require.True(t, errors.As(err, &statusCreateErr)) |
|
||||||
require.EqualValues(t, http.StatusForbidden, statusCreateErr.Status().Code) |
|
||||||
|
|
||||||
// UPDATE
|
|
||||||
testKeeper.SetName("test") // to pass validation before authz.
|
|
||||||
rawUpdate, err := clientP.Resource.Update(ctx, testKeeper, metav1.UpdateOptions{}) |
|
||||||
require.Error(t, err) |
|
||||||
require.Nil(t, rawUpdate) |
|
||||||
|
|
||||||
var statusUpdateErr *apierrors.StatusError |
|
||||||
require.True(t, errors.As(err, &statusUpdateErr)) |
|
||||||
require.EqualValues(t, http.StatusForbidden, statusUpdateErr.Status().Code) |
|
||||||
|
|
||||||
// DELETE
|
|
||||||
err = clientP.Resource.Delete(ctx, "some-keeper", metav1.DeleteOptions{}) |
|
||||||
require.Error(t, err) |
|
||||||
|
|
||||||
var statusDeleteErr *apierrors.StatusError |
|
||||||
require.True(t, errors.As(err, &statusDeleteErr)) |
|
||||||
require.EqualValues(t, http.StatusForbidden, statusDeleteErr.Status().Code) |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("keeper actions with permissions but with limited scope", func(t *testing.T) { |
|
||||||
suffix := strconv.FormatInt(rand.Int64(), 10) |
|
||||||
|
|
||||||
// Fix the Keeper names.
|
|
||||||
keeperName := "kp-" + suffix |
|
||||||
testKeeper := helper.LoadYAMLOrJSONFile("testdata/keeper-gcp-generate.yaml") |
|
||||||
testKeeper.SetName(keeperName) |
|
||||||
|
|
||||||
keeperNameAnother := "kp-another-" + suffix |
|
||||||
testKeeperAnother := helper.LoadYAMLOrJSONFile("testdata/keeper-gcp-generate.yaml") |
|
||||||
testKeeperAnother.SetName(keeperNameAnother) |
|
||||||
|
|
||||||
// Fix the org ID because we will create another user with scope "all" permissions on the same org, to compare.
|
|
||||||
orgID := rand.Int64() + 2 |
|
||||||
|
|
||||||
// Permissions which allow any action, but scoped actions (get, update, delete) only on `keeperName` and NO OTHER Keeper.
|
|
||||||
scopedLimitedPermissions := map[string]ResourcePermission{ |
|
||||||
ResourceKeepers: { |
|
||||||
Actions: ActionsAllKeepers, |
|
||||||
Name: keeperName, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
// Create users (+ client) with permission to manage ONLY the Keeper `keeperName`.
|
|
||||||
editorLimited := mustCreateUsersWithOrg(t, helper, orgID, scopedLimitedPermissions).Editor |
|
||||||
|
|
||||||
clientScopedLimited := helper.GetResourceClient(apis.ResourceClientArgs{ |
|
||||||
User: editorLimited, |
|
||||||
GVR: gvrKeepers, |
|
||||||
}) |
|
||||||
|
|
||||||
// Create users (+ client) with permission to manage ANY Keepers.
|
|
||||||
scopedAllPermissions := map[string]ResourcePermission{ |
|
||||||
ResourceKeepers: { |
|
||||||
Actions: ActionsAllKeepers, |
|
||||||
Name: "*", // this or not sending a `Name` have the same effect.
|
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
editorAll := mustCreateUsersWithOrg(t, helper, orgID, scopedAllPermissions).Editor |
|
||||||
|
|
||||||
clientScopedAll := helper.GetResourceClient(apis.ResourceClientArgs{ |
|
||||||
User: editorAll, |
|
||||||
GVR: gvrKeepers, |
|
||||||
}) |
|
||||||
|
|
||||||
// For create, we don't have actual granular permissions, so we can use any client that has unscoped create permissions.
|
|
||||||
// This is because when we don't know yet what the name of the resource will be, the request comes with an empty value.
|
|
||||||
// And thus the authorizer can't do granular checks.
|
|
||||||
t.Run("CREATE", func(t *testing.T) { |
|
||||||
rawCreateLimited, err := clientScopedAll.Resource.Create(ctx, testKeeper, metav1.CreateOptions{}) |
|
||||||
require.NoError(t, err) |
|
||||||
require.NotNil(t, rawCreateLimited) |
|
||||||
|
|
||||||
rawCreateLimited, err = clientScopedAll.Resource.Create(ctx, testKeeperAnother, metav1.CreateOptions{}) |
|
||||||
require.NoError(t, err) |
|
||||||
require.NotNil(t, rawCreateLimited) |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("READ", func(t *testing.T) { |
|
||||||
// Retrieve `keeperName` from the limited client.
|
|
||||||
rawGetLimited, err := clientScopedLimited.Resource.Get(ctx, keeperName, metav1.GetOptions{}) |
|
||||||
require.NoError(t, err) |
|
||||||
require.NotNil(t, rawGetLimited) |
|
||||||
require.Equal(t, rawGetLimited.GetUID(), rawGetLimited.GetUID()) |
|
||||||
|
|
||||||
// Retrieve `keeperName` from the scope-all client.
|
|
||||||
rawGetAll, err := clientScopedAll.Resource.Get(ctx, keeperName, metav1.GetOptions{}) |
|
||||||
require.NoError(t, err) |
|
||||||
require.NotNil(t, rawGetAll) |
|
||||||
require.Equal(t, rawGetAll.GetUID(), rawGetLimited.GetUID()) |
|
||||||
|
|
||||||
// Even though we can create it, we cannot retrieve `keeperNameAnother` from the limited client.
|
|
||||||
rawGetLimited, err = clientScopedLimited.Resource.Get(ctx, keeperNameAnother, metav1.GetOptions{}) |
|
||||||
require.Error(t, err) |
|
||||||
require.Nil(t, rawGetLimited) |
|
||||||
|
|
||||||
var statusGetErr *apierrors.StatusError |
|
||||||
require.True(t, errors.As(err, &statusGetErr)) |
|
||||||
require.EqualValues(t, http.StatusForbidden, statusGetErr.Status().Code) |
|
||||||
|
|
||||||
// Retrieve `keeperNameAnother` from the scope-all client.
|
|
||||||
rawGetAll, err = clientScopedAll.Resource.Get(ctx, keeperNameAnother, metav1.GetOptions{}) |
|
||||||
require.NoError(t, err) |
|
||||||
require.NotNil(t, rawGetAll) |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("LIST", func(t *testing.T) { |
|
||||||
// List Keepers from the limited client should return only 1.
|
|
||||||
rawList, err := clientScopedLimited.Resource.List(ctx, metav1.ListOptions{}) |
|
||||||
require.NoError(t, err) |
|
||||||
require.NotNil(t, rawList) |
|
||||||
require.Len(t, rawList.Items, 1) |
|
||||||
require.Equal(t, keeperName, rawList.Items[0].GetName()) |
|
||||||
|
|
||||||
// List Keepers from the scope-all client should return all of them.
|
|
||||||
rawList, err = clientScopedAll.Resource.List(ctx, metav1.ListOptions{}) |
|
||||||
require.NoError(t, err) |
|
||||||
require.NotNil(t, rawList) |
|
||||||
require.Len(t, rawList.Items, 2) |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("UPDATE", func(t *testing.T) { |
|
||||||
// Update `keeperName` from the limited client.
|
|
||||||
testKeeperUpdate := testKeeper.DeepCopy() |
|
||||||
testKeeperUpdate.Object["spec"].(map[string]any)["description"] = "keeper-description-1234" |
|
||||||
|
|
||||||
rawUpdate, err := clientScopedLimited.Resource.Update(ctx, testKeeperUpdate, metav1.UpdateOptions{}) |
|
||||||
require.NoError(t, err) |
|
||||||
require.NotNil(t, rawUpdate) |
|
||||||
|
|
||||||
// Try to update `keeperNameAnother` from the limited client.
|
|
||||||
testKeeperAnotherUpdate := testKeeperAnother.DeepCopy() |
|
||||||
testKeeperAnotherUpdate.Object["spec"].(map[string]any)["description"] = "keeper-description-5678" |
|
||||||
|
|
||||||
rawUpdate, err = clientScopedLimited.Resource.Update(ctx, testKeeperAnotherUpdate, metav1.UpdateOptions{}) |
|
||||||
require.Error(t, err) |
|
||||||
require.Nil(t, rawUpdate) |
|
||||||
|
|
||||||
var statusUpdateErr *apierrors.StatusError |
|
||||||
require.True(t, errors.As(err, &statusUpdateErr)) |
|
||||||
require.EqualValues(t, http.StatusForbidden, statusUpdateErr.Status().Code) |
|
||||||
|
|
||||||
// Update `keeperNameAnother` from the scope-all client.
|
|
||||||
rawUpdate, err = clientScopedAll.Resource.Update(ctx, testKeeperAnotherUpdate, metav1.UpdateOptions{}) |
|
||||||
require.NoError(t, err) |
|
||||||
require.NotNil(t, rawUpdate) |
|
||||||
}) |
|
||||||
|
|
||||||
// Keep this last for cleaning up the resources.
|
|
||||||
t.Run("DELETE", func(t *testing.T) { |
|
||||||
// Try to delete `keeperNameAnother` from the limited client.
|
|
||||||
err := clientScopedLimited.Resource.Delete(ctx, keeperNameAnother, metav1.DeleteOptions{}) |
|
||||||
require.Error(t, err) |
|
||||||
|
|
||||||
var statusDeleteErr *apierrors.StatusError |
|
||||||
require.True(t, errors.As(err, &statusDeleteErr)) |
|
||||||
require.EqualValues(t, http.StatusForbidden, statusDeleteErr.Status().Code) |
|
||||||
|
|
||||||
// Delete `keeperNameAnother` from the scope-all client.
|
|
||||||
err = clientScopedAll.Resource.Delete(ctx, keeperNameAnother, metav1.DeleteOptions{}) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
// Delete `keeperName` from the limited client.
|
|
||||||
err = clientScopedLimited.Resource.Delete(ctx, keeperName, metav1.DeleteOptions{}) |
|
||||||
require.NoError(t, err) |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
@ -1,150 +0,0 @@ |
|||||||
package secret |
|
||||||
|
|
||||||
import ( |
|
||||||
"cmp" |
|
||||||
"context" |
|
||||||
"encoding/json" |
|
||||||
"math/rand/v2" |
|
||||||
"strconv" |
|
||||||
"testing" |
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/secret" |
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" |
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt" |
|
||||||
"github.com/grafana/grafana/pkg/services/org" |
|
||||||
"github.com/grafana/grafana/pkg/services/team" |
|
||||||
"github.com/grafana/grafana/pkg/tests/apis" |
|
||||||
"github.com/grafana/grafana/pkg/tests/testinfra" |
|
||||||
"github.com/grafana/grafana/pkg/tests/testsuite" |
|
||||||
"github.com/stretchr/testify/require" |
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" |
|
||||||
) |
|
||||||
|
|
||||||
var ( |
|
||||||
ResourceSecureValues = "secret.securevalues" |
|
||||||
ResourceKeepers = "secret.keepers" |
|
||||||
|
|
||||||
ActionsAllKeepers = []string{ |
|
||||||
secret.ActionSecretKeepersCreate, |
|
||||||
secret.ActionSecretKeepersWrite, |
|
||||||
secret.ActionSecretKeepersRead, |
|
||||||
secret.ActionSecretKeepersDelete, |
|
||||||
} |
|
||||||
ActionsAllSecureValues = []string{ |
|
||||||
secret.ActionSecretSecureValuesCreate, |
|
||||||
secret.ActionSecretSecureValuesWrite, |
|
||||||
secret.ActionSecretSecureValuesRead, |
|
||||||
secret.ActionSecretSecureValuesDelete, |
|
||||||
} |
|
||||||
) |
|
||||||
|
|
||||||
type ResourcePermission struct { |
|
||||||
Actions []string |
|
||||||
Name string // empty or "*" for all
|
|
||||||
} |
|
||||||
|
|
||||||
func TestMain(m *testing.M) { |
|
||||||
testsuite.Run(m) |
|
||||||
} |
|
||||||
|
|
||||||
func TestIntegrationDiscoveryClient(t *testing.T) { |
|
||||||
if testing.Short() { |
|
||||||
t.Skip("skipping integration test") |
|
||||||
} |
|
||||||
|
|
||||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ |
|
||||||
AppModeProduction: false, // required for experimental APIs
|
|
||||||
EnableFeatureToggles: []string{ |
|
||||||
// Required to start the example service
|
|
||||||
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, |
|
||||||
featuremgmt.FlagSecretsManagementAppPlatform, |
|
||||||
}, |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("check discovery client", func(t *testing.T) { |
|
||||||
disco := helper.NewDiscoveryClient() |
|
||||||
|
|
||||||
resources, err := disco.ServerResourcesForGroupVersion("secret.grafana.app/v0alpha1") |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
v1Disco, err := json.MarshalIndent(resources, "", " ") |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
var apiResourceList map[string]any |
|
||||||
require.NoError(t, json.Unmarshal(v1Disco, &apiResourceList)) |
|
||||||
|
|
||||||
groupVersion, ok := apiResourceList["groupVersion"].(string) |
|
||||||
require.True(t, ok) |
|
||||||
require.Equal(t, "secret.grafana.app/v0alpha1", groupVersion) |
|
||||||
|
|
||||||
apiResources, ok := apiResourceList["resources"].([]any) |
|
||||||
require.True(t, ok) |
|
||||||
require.Len(t, apiResources, 2) // securevalue + keeper + (subresources...)
|
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
func mustCreateUsersWithOrg(t *testing.T, helper *apis.K8sTestHelper, orgID int64, permissionMap map[string]ResourcePermission) apis.OrgUsers { |
|
||||||
t.Helper() |
|
||||||
|
|
||||||
permissions := make([]resourcepermissions.SetResourcePermissionCommand, 0, len(permissionMap)) |
|
||||||
for resource, permission := range permissionMap { |
|
||||||
permissions = append(permissions, resourcepermissions.SetResourcePermissionCommand{ |
|
||||||
Actions: permission.Actions, |
|
||||||
Resource: resource, |
|
||||||
ResourceAttribute: "uid", |
|
||||||
ResourceID: cmp.Or(permission.Name, "*"), |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
orgName := "org-" + strconv.FormatInt(orgID, 10) |
|
||||||
|
|
||||||
userSuffix := strconv.FormatInt(rand.Int64(), 10) |
|
||||||
|
|
||||||
// Add here admin or viewer if necessary.
|
|
||||||
editor := helper.CreateUser("editor-"+userSuffix, orgName, org.RoleEditor, permissions) |
|
||||||
|
|
||||||
staff := helper.CreateTeam("staff-"+userSuffix, "staff-"+userSuffix+"@"+orgName, editor.Identity.GetOrgID()) |
|
||||||
|
|
||||||
// Also call this method for each new user.
|
|
||||||
helper.AddOrUpdateTeamMember(editor, staff.ID, team.PermissionTypeMember) |
|
||||||
|
|
||||||
return apis.OrgUsers{ |
|
||||||
Editor: editor, |
|
||||||
Staff: staff, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func mustCreateUsers(t *testing.T, helper *apis.K8sTestHelper, permissionMap map[string]ResourcePermission) apis.OrgUsers { |
|
||||||
orgID := rand.Int64() + 2 // if it is 0, becomes 2 and not 1.
|
|
||||||
return mustCreateUsersWithOrg(t, helper, orgID, permissionMap) |
|
||||||
} |
|
||||||
|
|
||||||
func mustGenerateKeeper(t *testing.T, helper *apis.K8sTestHelper, user apis.User, specType map[string]any, testFile string) *unstructured.Unstructured { |
|
||||||
t.Helper() |
|
||||||
|
|
||||||
require.NotEmpty(t, testFile, "testFile must not be empty") |
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background()) |
|
||||||
t.Cleanup(cancel) |
|
||||||
|
|
||||||
keeperClient := helper.GetResourceClient(apis.ResourceClientArgs{ |
|
||||||
User: user, |
|
||||||
GVR: gvrKeepers, |
|
||||||
}) |
|
||||||
|
|
||||||
testKeeper := helper.LoadYAMLOrJSONFile(testFile) |
|
||||||
if specType != nil { |
|
||||||
testKeeper.Object["spec"] = specType |
|
||||||
} |
|
||||||
|
|
||||||
raw, err := keeperClient.Resource.Create(ctx, testKeeper, metav1.CreateOptions{}) |
|
||||||
require.NoError(t, err) |
|
||||||
require.NotNil(t, raw) |
|
||||||
|
|
||||||
t.Cleanup(func() { |
|
||||||
require.NoError(t, keeperClient.Resource.Delete(ctx, raw.GetName(), metav1.DeleteOptions{})) |
|
||||||
}) |
|
||||||
|
|
||||||
return raw |
|
||||||
} |
|
@ -1,15 +0,0 @@ |
|||||||
apiVersion: secret.grafana.app/v0alpha1 |
|
||||||
kind: Keeper |
|
||||||
metadata: |
|
||||||
annotations: |
|
||||||
xx: XXX |
|
||||||
labels: |
|
||||||
aa: AAA |
|
||||||
spec: |
|
||||||
description: AWS XYZ value |
|
||||||
aws: |
|
||||||
accessKeyId: |
|
||||||
valueFromEnv: ACCESS_KEY_ID_XYZ |
|
||||||
secretAccessKey: |
|
||||||
valueFromEnv: SECRET_ACCESS_KEY_XYZ |
|
||||||
kmsKeyId: kmsKeyId-xyz |
|
@ -1,14 +0,0 @@ |
|||||||
apiVersion: secret.grafana.app/v0alpha1 |
|
||||||
kind: Keeper |
|
||||||
metadata: |
|
||||||
annotations: |
|
||||||
xx: XXX |
|
||||||
yy: YYY |
|
||||||
labels: |
|
||||||
aa: AAA |
|
||||||
bb: BBB |
|
||||||
spec: |
|
||||||
description: GCP XYZ value |
|
||||||
gcp: |
|
||||||
projectId: project-id |
|
||||||
credentialsFile: /path/to/file.json |
|
@ -1,15 +0,0 @@ |
|||||||
apiVersion: secret.grafana.app/v0alpha1 |
|
||||||
kind: SecureValue |
|
||||||
metadata: |
|
||||||
annotations: |
|
||||||
xx: XXX |
|
||||||
yy: YYY |
|
||||||
labels: |
|
||||||
aa: AAA |
|
||||||
bb: BBB |
|
||||||
spec: |
|
||||||
description: This is a secret |
|
||||||
value: this is super duper secure |
|
||||||
decrypters: |
|
||||||
- k6 |
|
||||||
- synthetic-monitoring |
|
@ -1,16 +0,0 @@ |
|||||||
apiVersion: secret.grafana.app/v0alpha1 |
|
||||||
kind: SecureValue |
|
||||||
metadata: |
|
||||||
annotations: |
|
||||||
xx: XXX |
|
||||||
yy: YYY |
|
||||||
labels: |
|
||||||
aa: AAA |
|
||||||
bb: BBB |
|
||||||
spec: |
|
||||||
description: XYZ value |
|
||||||
keeper: my-keeper-1 |
|
||||||
value: super duper secure |
|
||||||
decrypters: |
|
||||||
- k6 |
|
||||||
- synthetic-monitoring |
|
Loading…
Reference in new issue