mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-25 16:07:24 +00:00
feat(manager): add forbidden annotations, forbidden labels to service options
Signed-off-by: Siarhei Rasiukevich <s.rasiukevich@gmail.com>
This commit is contained in:
committed by
Dario Tranchitella
parent
8695dfb7a2
commit
b27780d74c
@@ -4,13 +4,21 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// +kubebuilder:object:generate=true
|
||||
const (
|
||||
// ForbiddenLabelReason used as reason string to deny forbidden labels.
|
||||
ForbiddenLabelReason = "ForbiddenLabel"
|
||||
// ForbiddenAnnotationReason used as reason string to deny forbidden annotations.
|
||||
ForbiddenAnnotationReason = "ForbiddenAnnotation"
|
||||
)
|
||||
|
||||
// +kubebuilder:object:generate=true
|
||||
type ForbiddenListSpec struct {
|
||||
Exact []string `json:"denied,omitempty"`
|
||||
Regex string `json:"deniedRegex,omitempty"`
|
||||
@@ -37,3 +45,57 @@ func (in ForbiddenListSpec) RegexMatch(value string) (ok bool) {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type ForbiddenError struct {
|
||||
key string
|
||||
spec ForbiddenListSpec
|
||||
}
|
||||
|
||||
func NewForbiddenError(key string, forbiddenSpec ForbiddenListSpec) error {
|
||||
return &ForbiddenError{
|
||||
key: key,
|
||||
spec: forbiddenSpec,
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:predeclared
|
||||
func (f *ForbiddenError) appendForbiddenError() (append string) {
|
||||
append += "Forbidden are "
|
||||
if len(f.spec.Exact) > 0 {
|
||||
append += fmt.Sprintf("one of the following (%s)", strings.Join(f.spec.Exact, ", "))
|
||||
if len(f.spec.Regex) > 0 {
|
||||
append += " or "
|
||||
}
|
||||
}
|
||||
|
||||
if len(f.spec.Regex) > 0 {
|
||||
append += fmt.Sprintf("matching the regex %s", f.spec.Regex)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (f ForbiddenError) Error() string {
|
||||
return fmt.Sprintf("%s is forbidden for the current Tenant. %s", f.key, f.appendForbiddenError())
|
||||
}
|
||||
|
||||
func ValidateForbidden(metadata map[string]string, forbiddenList ForbiddenListSpec) error {
|
||||
if reflect.DeepEqual(ForbiddenListSpec{}, forbiddenList) {
|
||||
return nil
|
||||
}
|
||||
|
||||
for key := range metadata {
|
||||
var forbidden, matched bool
|
||||
forbidden = forbiddenList.ExactMatch(key)
|
||||
matched = forbiddenList.RegexMatch(key)
|
||||
|
||||
if forbidden || matched {
|
||||
return NewForbiddenError(
|
||||
key,
|
||||
forbiddenList,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -72,3 +72,50 @@ func TestForbiddenListSpec_RegexMatch(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateForbidden(t *testing.T) {
|
||||
type tc struct {
|
||||
Keys map[string]string
|
||||
ForbiddenSpec ForbiddenListSpec
|
||||
HasError bool
|
||||
}
|
||||
|
||||
for _, tc := range []tc{
|
||||
{
|
||||
Keys: map[string]string{"foobar": "", "thesecondkey": "", "anotherkey": ""},
|
||||
ForbiddenSpec: ForbiddenListSpec{
|
||||
Exact: []string{"foobar", "somelabelkey1"},
|
||||
},
|
||||
HasError: true,
|
||||
},
|
||||
{
|
||||
Keys: map[string]string{"foobar": ""},
|
||||
ForbiddenSpec: ForbiddenListSpec{
|
||||
Exact: []string{"foobar.io", "somelabelkey1", "test-exact"},
|
||||
},
|
||||
HasError: false,
|
||||
},
|
||||
{
|
||||
Keys: map[string]string{"foobar": "", "barbaz": ""},
|
||||
ForbiddenSpec: ForbiddenListSpec{
|
||||
Regex: "foo.*",
|
||||
},
|
||||
HasError: true,
|
||||
},
|
||||
{
|
||||
Keys: map[string]string{"foobar": "", "another-annotation-key": ""},
|
||||
ForbiddenSpec: ForbiddenListSpec{
|
||||
Regex: "foo1111",
|
||||
},
|
||||
HasError: false,
|
||||
},
|
||||
} {
|
||||
if tc.HasError {
|
||||
assert.Error(t, ValidateForbidden(tc.Keys, tc.ForbiddenSpec))
|
||||
}
|
||||
|
||||
if !tc.HasError {
|
||||
assert.NoError(t, ValidateForbidden(tc.Keys, tc.ForbiddenSpec))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,4 +12,8 @@ type ServiceOptions struct {
|
||||
AllowedServices *AllowedServices `json:"allowedServices,omitempty"`
|
||||
// Specifies the external IPs that can be used in Services with type ClusterIP. An empty list means no IPs are allowed. Optional.
|
||||
ExternalServiceIPs *ExternalServiceIPsSpec `json:"externalIPs,omitempty"`
|
||||
// Define the labels that a Tenant Owner cannot set for their Service resources.
|
||||
ForbiddenLabels ForbiddenListSpec `json:"forbiddenLabels,omitempty"`
|
||||
// Define the annotations that a Tenant Owner cannot set for their Service resources.
|
||||
ForbiddenAnnotations ForbiddenListSpec `json:"forbiddenAnnotations,omitempty"`
|
||||
}
|
||||
|
||||
@@ -290,6 +290,8 @@ func (in *ServiceOptions) DeepCopyInto(out *ServiceOptions) {
|
||||
*out = new(ExternalServiceIPsSpec)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
in.ForbiddenLabels.DeepCopyInto(&out.ForbiddenLabels)
|
||||
in.ForbiddenAnnotations.DeepCopyInto(&out.ForbiddenAnnotations)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceOptions.
|
||||
|
||||
@@ -3,30 +3,6 @@
|
||||
|
||||
package namespace
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
capsuleapi "github.com/projectcapsule/capsule/pkg/api"
|
||||
)
|
||||
|
||||
//nolint:predeclared
|
||||
func appendForbiddenError(spec *capsuleapi.ForbiddenListSpec) (append string) {
|
||||
append += "Forbidden are "
|
||||
if len(spec.Exact) > 0 {
|
||||
append += fmt.Sprintf("one of the following (%s)", strings.Join(spec.Exact, ", "))
|
||||
if len(spec.Regex) > 0 {
|
||||
append += " or "
|
||||
}
|
||||
}
|
||||
|
||||
if len(spec.Regex) > 0 {
|
||||
append += fmt.Sprintf("matching the regex %s", spec.Regex)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type namespaceQuotaExceededError struct{}
|
||||
|
||||
func NewNamespaceQuotaExceededError() error {
|
||||
@@ -36,35 +12,3 @@ func NewNamespaceQuotaExceededError() error {
|
||||
func (namespaceQuotaExceededError) Error() string {
|
||||
return "Cannot exceed Namespace quota: please, reach out to the system administrators"
|
||||
}
|
||||
|
||||
type namespaceLabelForbiddenError struct {
|
||||
label string
|
||||
spec *capsuleapi.ForbiddenListSpec
|
||||
}
|
||||
|
||||
func NewNamespaceLabelForbiddenError(label string, forbiddenSpec *capsuleapi.ForbiddenListSpec) error {
|
||||
return &namespaceLabelForbiddenError{
|
||||
label: label,
|
||||
spec: forbiddenSpec,
|
||||
}
|
||||
}
|
||||
|
||||
func (f namespaceLabelForbiddenError) Error() string {
|
||||
return fmt.Sprintf("Label %s is forbidden for namespaces in the current Tenant. %s", f.label, appendForbiddenError(f.spec))
|
||||
}
|
||||
|
||||
type namespaceAnnotationForbiddenError struct {
|
||||
annotation string
|
||||
spec *capsuleapi.ForbiddenListSpec
|
||||
}
|
||||
|
||||
func NewNamespaceAnnotationForbiddenError(annotation string, forbiddenSpec *capsuleapi.ForbiddenListSpec) error {
|
||||
return &namespaceAnnotationForbiddenError{
|
||||
annotation: annotation,
|
||||
spec: forbiddenSpec,
|
||||
}
|
||||
}
|
||||
|
||||
func (f namespaceAnnotationForbiddenError) Error() string {
|
||||
return fmt.Sprintf("Annotation %s is forbidden for namespaces in the current Tenant. %s", f.annotation, appendForbiddenError(f.spec))
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ package namespace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/record"
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
capsulewebhook "github.com/projectcapsule/capsule/pkg/webhook"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/utils"
|
||||
)
|
||||
@@ -24,48 +25,6 @@ func UserMetadataHandler() capsulewebhook.Handler {
|
||||
return &userMetadataHandler{}
|
||||
}
|
||||
|
||||
func (r *userMetadataHandler) validateUserMetadata(tnt *capsulev1beta2.Tenant, recorder record.EventRecorder, labels map[string]string, annotations map[string]string) *admission.Response {
|
||||
if tnt.Spec.NamespaceOptions != nil {
|
||||
forbiddenLabels := tnt.Spec.NamespaceOptions.ForbiddenLabels
|
||||
|
||||
for label := range labels {
|
||||
var forbidden, matched bool
|
||||
forbidden = forbiddenLabels.ExactMatch(label)
|
||||
matched = forbiddenLabels.RegexMatch(label)
|
||||
|
||||
if forbidden || matched {
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, "ForbiddenNamespaceLabel", fmt.Sprintf("Label %s is forbidden for a namespaces of the current Tenant ", label))
|
||||
|
||||
response := admission.Denied(NewNamespaceLabelForbiddenError(label, &forbiddenLabels).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tnt.Spec.NamespaceOptions == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
forbiddenAnnotations := tnt.Spec.NamespaceOptions.ForbiddenLabels
|
||||
|
||||
for annotation := range annotations {
|
||||
var forbidden, matched bool
|
||||
forbidden = forbiddenAnnotations.ExactMatch(annotation)
|
||||
matched = forbiddenAnnotations.RegexMatch(annotation)
|
||||
|
||||
if forbidden || matched {
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, "ForbiddenNamespaceAnnotation", fmt.Sprintf("Annotation %s is forbidden for a namespaces of the current Tenant ", annotation))
|
||||
|
||||
response := admission.Denied(NewNamespaceAnnotationForbiddenError(annotation, &forbiddenAnnotations).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *userMetadataHandler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
ns := &corev1.Namespace{}
|
||||
@@ -81,10 +40,27 @@ func (r *userMetadataHandler) OnCreate(client client.Client, decoder *admission.
|
||||
}
|
||||
}
|
||||
|
||||
labels := ns.GetLabels()
|
||||
annotations := ns.GetAnnotations()
|
||||
if tnt.Spec.NamespaceOptions != nil {
|
||||
err := api.ValidateForbidden(ns.ObjectMeta.Annotations, tnt.Spec.NamespaceOptions.ForbiddenAnnotations)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "namespace annotations validation failed")
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, api.ForbiddenAnnotationReason, err.Error())
|
||||
response := admission.Denied(err.Error())
|
||||
|
||||
return r.validateUserMetadata(tnt, recorder, labels, annotations)
|
||||
return &response
|
||||
}
|
||||
|
||||
err = api.ValidateForbidden(ns.ObjectMeta.Labels, tnt.Spec.NamespaceOptions.ForbiddenLabels)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "namespace labels validation failed")
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, api.ForbiddenLabelReason, err.Error())
|
||||
response := admission.Denied(err.Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,6 +149,26 @@ func (r *userMetadataHandler) OnUpdate(client client.Client, decoder *admission.
|
||||
delete(annotations, key)
|
||||
}
|
||||
|
||||
return r.validateUserMetadata(tnt, recorder, labels, annotations)
|
||||
if tnt.Spec.NamespaceOptions != nil {
|
||||
err := api.ValidateForbidden(annotations, tnt.Spec.NamespaceOptions.ForbiddenAnnotations)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "namespace annotations validation failed")
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, api.ForbiddenAnnotationReason, err.Error())
|
||||
response := admission.Denied(err.Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
err = api.ValidateForbidden(labels, tnt.Spec.NamespaceOptions.ForbiddenLabels)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "namespace labels validation failed")
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, api.ForbiddenLabelReason, err.Error())
|
||||
response := admission.Denied(err.Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/client-go/tools/record"
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
capsulewebhook "github.com/projectcapsule/capsule/pkg/webhook"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/utils"
|
||||
)
|
||||
@@ -68,6 +70,26 @@ func (r *handler) handleService(ctx context.Context, clt client.Client, decoder
|
||||
return &response
|
||||
}
|
||||
|
||||
if tnt.Spec.ServiceOptions != nil {
|
||||
err := api.ValidateForbidden(svc.Annotations, tnt.Spec.ServiceOptions.ForbiddenAnnotations)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "service annotations validation failed")
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, api.ForbiddenAnnotationReason, err.Error())
|
||||
response := admission.Denied(err.Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
err = api.ValidateForbidden(svc.Labels, tnt.Spec.ServiceOptions.ForbiddenLabels)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "service labels validation failed")
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, api.ForbiddenLabelReason, err.Error())
|
||||
response := admission.Denied(err.Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
|
||||
if svc.Spec.ExternalIPs == nil || (tnt.Spec.ServiceOptions == nil || tnt.Spec.ServiceOptions.ExternalServiceIPs == nil) {
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user