diff --git a/pkg/registry/apis/folders/register.go b/pkg/registry/apis/folders/register.go index 7ce913883fe..69fa72bf479 100644 --- a/pkg/registry/apis/folders/register.go +++ b/pkg/registry/apis/folders/register.go @@ -8,6 +8,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" @@ -28,6 +29,7 @@ import ( ) var _ builder.APIGroupBuilder = (*FolderAPIBuilder)(nil) +var _ builder.APIGroupValidation = (*FolderAPIBuilder)(nil) var resourceInfo = v0alpha1.FolderResourceInfo @@ -181,6 +183,10 @@ func (b *FolderAPIBuilder) GetAuthorizer() authorizer.Authorizer { }) } +func (b *FolderAPIBuilder) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error { + return nil +} + func authorizerFunc(ctx context.Context, attr authorizer.Attributes) (*authorizerParams, error) { verb := attr.GetVerb() name := attr.GetName() diff --git a/pkg/services/apiserver/builder/admission.go b/pkg/services/apiserver/builder/admission.go new file mode 100644 index 00000000000..4b2aa86ea2e --- /dev/null +++ b/pkg/services/apiserver/builder/admission.go @@ -0,0 +1,43 @@ +package builder + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/admission" +) + +const PluginName = "GrafanaAdmission" + +type builderAdmission struct { + validators map[schema.GroupVersion]APIGroupValidation +} + +var _ admission.ValidationInterface = (*builderAdmission)(nil) + +func (b *builderAdmission) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) { + if v, ok := b.validators[a.GetResource().GroupVersion()]; ok { + return v.Validate(ctx, a, o) + } + return nil +} + +func (b *builderAdmission) Handles(operation admission.Operation) bool { + return true +} + +func NewAdmissionFromBuilders(builders []APIGroupBuilder) *builderAdmission { + validators := make(map[schema.GroupVersion]APIGroupValidation) + for _, builder := range builders { + if v, ok := builder.(APIGroupValidation); ok { + validators[builder.GetGroupVersion()] = v + } + } + return NewAdmission(validators) +} + +func NewAdmission(validators map[schema.GroupVersion]APIGroupValidation) *builderAdmission { + return &builderAdmission{ + validators: validators, + } +} diff --git a/pkg/services/apiserver/builder/admission_test.go b/pkg/services/apiserver/builder/admission_test.go new file mode 100644 index 00000000000..ac1c22ff911 --- /dev/null +++ b/pkg/services/apiserver/builder/admission_test.go @@ -0,0 +1,85 @@ +package builder_test + +import ( + "context" + "errors" + "testing" + + "github.com/grafana/grafana/pkg/services/apiserver/builder" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/admission" +) + +func TestBuilderAdmission_Validate(t *testing.T) { + gvk := schema.GroupVersionKind{ + Group: "testGroup", + Version: "v1", + Kind: "testKind", + } + gvr := gvk.GroupVersion().WithResource("testkinds") + defaultAttributes := admission.NewAttributesRecord(nil, nil, gvk, "", "", gvr, "", admission.Create, nil, false, nil) + tests := []struct { + name string + validators map[schema.GroupVersion]builder.APIGroupValidation + attributes admission.Attributes + wantErr bool + }{ + { + name: "validator exists - forbidden", + validators: map[schema.GroupVersion]builder.APIGroupValidation{ + {Group: "testGroup", Version: "v1"}: &mockValidator{ + validateFunc: func(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error { + return admission.NewForbidden(a, errors.New("test error")) + }, + }, + }, + attributes: defaultAttributes, + wantErr: true, + }, + { + name: "validator exists - allowed", + validators: map[schema.GroupVersion]builder.APIGroupValidation{ + {Group: "testGroup", Version: "v1"}: &mockValidator{ + validateFunc: func(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error { + return nil + }, + }, + }, + attributes: defaultAttributes, + wantErr: false, + }, + { + name: "validator does not exist", + validators: map[schema.GroupVersion]builder.APIGroupValidation{ + {Group: "testGroup", Version: "v1"}: &mockValidator{ + validateFunc: func(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error { + return nil + }, + }, + }, + attributes: defaultAttributes, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := builder.NewAdmission(tt.validators) + err := b.Validate(context.Background(), tt.attributes, nil) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +type mockValidator struct { + validateFunc func(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error +} + +func (m *mockValidator) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error { + return m.validateFunc(ctx, a, o) +} diff --git a/pkg/services/apiserver/builder/common.go b/pkg/services/apiserver/builder/common.go index 5e2f9b14ba9..965ef403a66 100644 --- a/pkg/services/apiserver/builder/common.go +++ b/pkg/services/apiserver/builder/common.go @@ -1,18 +1,19 @@ package builder import ( + "context" "net/http" + "github.com/prometheus/client_golang/prometheus" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/registry/generic" genericapiserver "k8s.io/apiserver/pkg/server" "k8s.io/kube-openapi/pkg/common" "k8s.io/kube-openapi/pkg/spec3" - "github.com/prometheus/client_golang/prometheus" - grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" ) @@ -46,6 +47,10 @@ type APIGroupBuilder interface { GetAuthorizer() authorizer.Authorizer } +type APIGroupValidation interface { + Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) +} + type APIGroupOptions struct { Scheme *runtime.Scheme OptsGetter generic.RESTOptionsGetter diff --git a/pkg/services/apiserver/builder/helper.go b/pkg/services/apiserver/builder/helper.go index d22a7cc9b2a..251d62912f0 100644 --- a/pkg/services/apiserver/builder/helper.go +++ b/pkg/services/apiserver/builder/helper.go @@ -103,6 +103,7 @@ func SetupConfig( buildBranch string, buildHandlerChainFunc func(delegateHandler http.Handler, c *genericapiserver.Config) http.Handler, ) error { + serverConfig.AdmissionControl = NewAdmissionFromBuilders(builders) defsGetter := GetOpenAPIDefinitions(builders) serverConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig( openapi.GetOpenAPIDefinitionsWithoutDisabledFeatures(defsGetter),