Skip to content

Commit

Permalink
UPSTREAM: <carry>: add metadataAdmission plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
sanchezl committed Jul 22, 2024
1 parent 1bead22 commit 7e2c1da
Show file tree
Hide file tree
Showing 3 changed files with 322 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"k8s.io/apiserver/pkg/admission/plugin/resourcequota"
mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
"k8s.io/kubernetes/openshift-kube-apiserver/admission/autoscaling/mixedcpus"
"k8s.io/kubernetes/openshift-kube-apiserver/admission/metadata"

"github.com/openshift/apiserver-library-go/pkg/admission/imagepolicy"
imagepolicyapiv1 "github.com/openshift/apiserver-library-go/pkg/admission/imagepolicy/apis/imagepolicy/v1"
Expand Down Expand Up @@ -44,6 +45,7 @@ func RegisterOpenshiftKubeAdmissionPlugins(plugins *admission.Plugins) {
externalipranger.RegisterExternalIP(plugins)
restrictedendpoints.RegisterRestrictedEndpoints(plugins)
csiinlinevolumesecurity.Register(plugins)
metadata.Register(plugins)
}

var (
Expand Down Expand Up @@ -77,6 +79,7 @@ var (
csiinlinevolumesecurity.PluginName, // "storage.openshift.io/CSIInlineVolumeSecurity"
managednode.PluginName, // "autoscaling.openshift.io/ManagedNode"
mixedcpus.PluginName, // "autoscaling.openshift.io/MixedCPUs"
metadata.PluginName,
}

// openshiftAdmissionPluginsForKubeAfterResourceQuota are the plugins to add after ResourceQuota plugin
Expand Down
84 changes: 84 additions & 0 deletions openshift-kube-apiserver/admission/metadata/admission.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package metadata

import (
"context"
"fmt"
"io"

"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/admission"
"k8s.io/klog/v2"
)

const PluginName = "operator.openshift.io/MetadataAdmission"

func Register(plugins *admission.Plugins) {
klog.InfoS("Registering OpenShift Metadata Admission Plugin")
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
return newMetadataAdmission(), nil
})
}

func newMetadataAdmission() metadataAdmission {
return metadataAdmission{
Handler: admission.NewHandler(admission.Update),
badActors: sets.New(
"system:serviceaccount:openshift-operators:strimzi-cluster-operator",
"system:serviceaccount:openshift-operators-redhat:elasticsearch-operator",
),
}
}

// metadataAdmission plugin prevents a list of known "bad actors"
// from overriding annotations they do not own.
type metadataAdmission struct {
*admission.Handler
badActors sets.Set[string]
}

const protectedAnnotationKey = "openshift.io/internal-registry-pull-secret-ref"

func (m metadataAdmission) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error {
if a.GetResource().Resource != "serviceaccounts" {
return nil
}
if a.GetUserInfo() == nil || !m.badActors.Has(a.GetUserInfo().GetName()) {
// no user or not a known bad actor
if klog.V(4).Enabled() {
// log user if a protected annotation has changed
if changed, err := annotationChanged(a, protectedAnnotationKey); err == nil && changed {
klog.InfoS("Protected annotation changed", "ns", a.GetNamespace(), "sa", a.GetName(), "annotation", protectedAnnotationKey, "user", a.GetUserInfo().GetName())
} else if err != nil {
klog.ErrorS(err, "Error checking annotation")
}
}
return nil
}
changed, err := annotationChanged(a, protectedAnnotationKey)
if err != nil {
return err
}
if !changed {
return nil
}
return admission.NewForbidden(a, fmt.Errorf("user '%s' is forbidden from altering the '%s' annotation on serviceaccounts", a.GetUserInfo().GetName(), protectedAnnotationKey))
}

func annotationChanged(a admission.Attributes, key string) (bool, error) {
before, err := meta.Accessor(a.GetOldObject())
if err != nil {
return false, err
}
after, err := meta.Accessor(a.GetObject())
if err != nil {
return false, err
}
valueBefore, keyExistedBefore := before.GetAnnotations()[key]
valueAfter, keyExistsAfter := after.GetAnnotations()[key]
if (keyExistedBefore == keyExistsAfter) && (valueBefore == valueAfter) {
// annotation was not changed
return false, nil
}
return true, nil
}
235 changes: 235 additions & 0 deletions openshift-kube-apiserver/admission/metadata/admission_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
package metadata

import (
"context"
"testing"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/api/meta/testrestmapper"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/admission"
auditinternal "k8s.io/apiserver/pkg/apis/audit"
"k8s.io/apiserver/pkg/authentication/user"
)

func TestMetadataAdmissionValidate(t *testing.T) {

_ = sa()
testCases := []struct {
name string
forbidden sets.Set[string]
attributes admission.Attributes
wantError bool
}{
{
name: "forbidden user edit",
forbidden: sets.New[string]("operator1", "operator2"),
attributes: attributes(
withOldObject(sa(withProtectedAnnotationValue("before"))),
withObject(sa(withProtectedAnnotationValue("after"))),
withUser("operator1"),
),
wantError: true,
},
{
name: "forbidden user delete",
forbidden: sets.New[string]("operator1", "operator2"),
attributes: attributes(
withOldObject(sa(withProtectedAnnotationValue("before"))),
withObject(sa()),
withUser("operator1"),
),
wantError: true,
},
{
name: "forbidden user add",
forbidden: sets.New[string]("operator1", "operator2"),
attributes: attributes(
withObject(sa(withProtectedAnnotationValue("after"))),
withOldObject(sa()),
withUser("operator1"),
),
wantError: true,
},
{
name: "forbidden user no change",
forbidden: sets.New[string]("operator1", "operator2"),
attributes: attributes(
withOldObject(sa(withProtectedAnnotationValue("before"))),
withObject(sa(withProtectedAnnotationValue("before"), withAnnotation("foo", "bar"))),
withUser("operator1"),
),
wantError: false,
},
{
name: "not forbidden user",
forbidden: sets.New[string]("operator1", "operator2"),
attributes: attributes(
withOldObject(sa(withProtectedAnnotationValue("before"))),
withObject(sa(withProtectedAnnotationValue("after"))),
withOldObject(sa()),
withUser("operator3"),
),
wantError: false,
},
{
name: "no forbidden users",
forbidden: sets.New[string](),
attributes: attributes(
withOldObject(sa(withProtectedAnnotationValue("before"))),
withObject(sa(withProtectedAnnotationValue("after"))),
withOldObject(sa()),
withUser("operator1"),
),
wantError: false,
},
{
name: "forbidden user, not service account",
forbidden: sets.New[string]("operator1", "operator2"),
attributes: attributes(
withOldObject(pod(withProtectedAnnotationValue("before"))),
withObject(pod(withProtectedAnnotationValue("after"))),
withOldObject(sa()),
withUser("operator1"),
),
wantError: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := metadataAdmission{badActors: tc.forbidden}.Validate(context.TODO(), tc.attributes, nil)
if (err != nil) != tc.wantError {
if tc.wantError {
t.Fatal("expected error")
}
t.Fatalf("unexpected error: %v", err)
}
})
}

}

type attributesRecord struct {
kind schema.GroupVersionKind
namespace string
name string
resource schema.GroupVersionResource
subresource string
operation admission.Operation
options runtime.Object
dryRun bool
object runtime.Object
oldObject runtime.Object
userInfo user.Info
}

func (a attributesRecord) GetName() string { return a.name }
func (a attributesRecord) GetNamespace() string { return a.namespace }
func (a attributesRecord) GetResource() schema.GroupVersionResource { return a.resource }
func (a attributesRecord) GetSubresource() string { return a.subresource }
func (a attributesRecord) GetOperation() admission.Operation { return a.operation }
func (a attributesRecord) GetOperationOptions() runtime.Object { return a.options }
func (a attributesRecord) IsDryRun() bool { return a.dryRun }
func (a attributesRecord) GetObject() runtime.Object { return a.object }
func (a attributesRecord) GetOldObject() runtime.Object { return a.oldObject }
func (a attributesRecord) GetKind() schema.GroupVersionKind { return a.kind }
func (a attributesRecord) GetUserInfo() user.Info { return a.userInfo }
func (a attributesRecord) AddAnnotation(key, value string) error { panic("implement me") }
func (a attributesRecord) AddAnnotationWithLevel(key, value string, level auditinternal.Level) error {
panic("implement me")
}
func (a attributesRecord) GetReinvocationContext() admission.ReinvocationContext {
panic("implement me")
}

func attributes(opts ...func(*attributesRecord)) *attributesRecord {
a := &attributesRecord{
namespace: "test-ns",
operation: admission.Update,
options: &metav1.UpdateOptions{},
}
for _, opt := range opts {
opt(a)
}
return a
}

func withObject(o runtime.Object) func(*attributesRecord) {
return func(record *attributesRecord) {
record.object = o
record.kind = o.GetObjectKind().GroupVersionKind()
record.resource = gvk2gvr(record.kind)
}
}

func gvk2gvr(gvk schema.GroupVersionKind) schema.GroupVersionResource {
scheme := runtime.NewScheme()
corev1.AddToScheme(scheme)
mapper := testrestmapper.TestOnlyStaticRESTMapper(scheme)
mapping, err := mapper.RESTMapping(gvk.GroupKind())
if err != nil {
panic(err)
}
return mapping.Resource
}

func withOldObject(o runtime.Object) func(*attributesRecord) {
return func(record *attributesRecord) {
record.oldObject = o
}
}

func withUser(u string) func(*attributesRecord) {
return func(record *attributesRecord) {
record.userInfo = &user.DefaultInfo{Name: u}
}
}

func pod(opts ...func(runtime.Object)) *corev1.Pod {
pod := &corev1.Pod{
TypeMeta: metav1.TypeMeta{
Kind: "Pod",
APIVersion: "v1",
},
}
pod.Name = "test-pod"
for _, opt := range opts {
opt(pod)
}
return pod
}

func sa(opts ...func(runtime.Object)) *corev1.ServiceAccount {
sa := &corev1.ServiceAccount{
TypeMeta: metav1.TypeMeta{
Kind: "ServiceAccount",
APIVersion: "v1",
},
}
sa.Name = "test-sa"
for _, opt := range opts {
opt(sa)
}
return sa
}

func withProtectedAnnotationValue(v string) func(object runtime.Object) {
return withAnnotation(protectedAnnotationKey, v)
}

func withAnnotation(k, v string) func(object runtime.Object) {
return func(o runtime.Object) {
m, err := meta.Accessor(o)
if err != nil {
panic(err)
}
if m.GetAnnotations() == nil {
m.SetAnnotations(make(map[string]string))
}
m.GetAnnotations()[k] = v
}
}

0 comments on commit 7e2c1da

Please sign in to comment.