mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-19 04:26:45 +00:00
feat(rules): add service enforcement rules (#1982)
* fix(controller): decode old object for delete requests Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: modernize golang Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: modernize golang Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: modernize golang Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * fix: preserve ca-bundles injected from external providers Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat(rules): add service enforcement rules Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat(rules): add service enforcement rules Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat(rules): add service enforcement rules Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat(rules): add service enforcement rules Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat(rules): add service enforcement rules Signed-off-by: Oliver Baehler <oliver@sudo-i.net> --------- Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
This commit is contained in:
@@ -7,7 +7,7 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -17,18 +17,8 @@ type ForbiddenListSpec struct {
|
||||
Regex string `json:"deniedRegex,omitempty"`
|
||||
}
|
||||
|
||||
func (in ForbiddenListSpec) ExactMatch(value string) (ok bool) {
|
||||
if len(in.Exact) > 0 {
|
||||
sort.SliceStable(in.Exact, func(i, j int) bool {
|
||||
return strings.ToLower(in.Exact[i]) < strings.ToLower(in.Exact[j])
|
||||
})
|
||||
|
||||
i := sort.SearchStrings(in.Exact, value)
|
||||
|
||||
ok = i < len(in.Exact) && in.Exact[i] == value
|
||||
}
|
||||
|
||||
return ok
|
||||
func (in ForbiddenListSpec) ExactMatch(value string) bool {
|
||||
return slices.Contains(in.Exact, value)
|
||||
}
|
||||
|
||||
func (in ForbiddenListSpec) RegexMatch(value string) (ok bool) {
|
||||
|
||||
@@ -11,6 +11,16 @@ import (
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
)
|
||||
|
||||
func denied() api.ForbiddenListSpec {
|
||||
return api.ForbiddenListSpec{
|
||||
Exact: []string{
|
||||
"kubernetes.io/metadata.name",
|
||||
"pod-security.kubernetes.io/enforce",
|
||||
"NetworkPolicy",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestForbiddenListSpec_ExactMatch(t *testing.T) {
|
||||
type tc struct {
|
||||
In []string
|
||||
@@ -120,3 +130,38 @@ func TestValidateForbidden(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestForbiddenKeysBypassed(t *testing.T) {
|
||||
for _, k := range []string{"NetworkPolicy", "kubernetes.io/metadata.name"} {
|
||||
if err := api.ValidateForbidden(map[string]string{k: "owned"}, denied()); err == nil {
|
||||
t.Errorf("BYPASS CONFIRMED: ValidateForbidden ALLOWED denied key %q (list=%v)", k, denied().Exact)
|
||||
} else {
|
||||
t.Logf("(no bypass) correctly denied %q: %v", k, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Positive control: a third denied key in the SAME list is still correctly
|
||||
// blocked — proving the policy genuinely forbids these keys and the harness is
|
||||
// wired right (i.e. the bypass above is selective, not a dead enforcement path).
|
||||
func TestPositiveControl_StillBlocked(t *testing.T) {
|
||||
if err := api.ValidateForbidden(map[string]string{"pod-security.kubernetes.io/enforce": "privileged"}, denied()); err == nil {
|
||||
t.Errorf("control failure: denied key 'pod-security.kubernetes.io/enforce' was NOT blocked")
|
||||
}
|
||||
}
|
||||
|
||||
// Negative control: a key the admin did NOT deny is correctly allowed,
|
||||
// proving the webhook is not simply denying everything.
|
||||
func TestPoC_NegativeControl_BenignAllowed(t *testing.T) {
|
||||
if err := api.ValidateForbidden(map[string]string{"app.kubernetes.io/name": "frontend"}, denied()); err != nil {
|
||||
t.Errorf("control failure: benign key was wrongly denied: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Direct primitive check, minimal repro of the root cause.
|
||||
func TestExactMatch_RootCause(t *testing.T) {
|
||||
spec := api.ForbiddenListSpec{Exact: []string{"B", "a"}} // mixed case
|
||||
if !spec.ExactMatch("B") {
|
||||
t.Errorf("ROOT CAUSE: ExactMatch(%q) returned false though %q is in %v", "B", "B", spec.Exact)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2020-2026 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package rules
|
||||
|
||||
import "github.com/projectcapsule/capsule/pkg/api"
|
||||
|
||||
// +kubebuilder:object:generate=true
|
||||
type NamespaceRuleEnforceServicesBody struct {
|
||||
// Types defines the Service types matched by this rule.
|
||||
//
|
||||
// Supported values:
|
||||
// - ClusterIP
|
||||
// - NodePort
|
||||
// - LoadBalancer
|
||||
// - ExternalName
|
||||
//
|
||||
// +optional
|
||||
// +kubebuilder:validation:items:Enum=ClusterIP;NodePort;LoadBalancer;ExternalName
|
||||
Types []ServiceType `json:"types,omitempty"`
|
||||
|
||||
// LoadBalancers defines additional constraints for Services of type LoadBalancer.
|
||||
// +optional
|
||||
LoadBalancers *ServiceLoadBalancerRule `json:"loadBalancers,omitempty"`
|
||||
|
||||
// ExternalNames defines additional constraints for Services of type ExternalName.
|
||||
// +optional
|
||||
ExternalNames *ServiceExternalNameRule `json:"externalNames,omitempty"`
|
||||
|
||||
// NodePorts defines additional constraints for nodePort values.
|
||||
// +optional
|
||||
NodePorts *ServiceNodePortRule `json:"nodePorts,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:validation:Enum=ClusterIP;NodePort;LoadBalancer;ExternalName
|
||||
type ServiceType string
|
||||
|
||||
const (
|
||||
ServiceTypeClusterIP ServiceType = "ClusterIP"
|
||||
ServiceTypeNodePort ServiceType = "NodePort"
|
||||
ServiceTypeLoadBalancer ServiceType = "LoadBalancer"
|
||||
ServiceTypeExternalName ServiceType = "ExternalName"
|
||||
)
|
||||
|
||||
// +kubebuilder:object:generate=true
|
||||
type ServiceLoadBalancerRule struct {
|
||||
// CIDRs restricts spec.loadBalancerIP and spec.loadBalancerSourceRanges.
|
||||
// Empty means no additional CIDR restriction once LoadBalancer is allowed by types.
|
||||
// +optional
|
||||
CIDRs []string `json:"cidrs,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:generate=true
|
||||
type ServiceExternalNameRule struct {
|
||||
// Hostnames restricts spec.externalName.
|
||||
// Empty means no additional hostname restriction once ExternalName is allowed by types.
|
||||
// +optional
|
||||
Hostnames []api.ExpressionMatch `json:"hostnames,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:generate=true
|
||||
type ServiceNodePortRule struct {
|
||||
// Ports restricts explicitly requested nodePort values.
|
||||
// Empty means no additional port restriction once NodePort is allowed by types.
|
||||
// +optional
|
||||
Ports []ServiceNodePortRange `json:"ports,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:generate=true
|
||||
type ServiceNodePortRange struct {
|
||||
// +kubebuilder:validation:Minimum=1
|
||||
// +kubebuilder:validation:Maximum=65535
|
||||
From int32 `json:"from"`
|
||||
|
||||
// +kubebuilder:validation:Minimum=1
|
||||
// +kubebuilder:validation:Maximum=65535
|
||||
To int32 `json:"to"`
|
||||
}
|
||||
@@ -14,4 +14,8 @@ type NamespaceRuleEnforceBody struct {
|
||||
|
||||
// Enforcement for Workloads (Pods)
|
||||
Workloads NamespaceRuleEnforceWorkloadsBody `json:"workloads,omitempty"`
|
||||
|
||||
// Enforcement for Services.
|
||||
// +optional
|
||||
Services NamespaceRuleEnforceServicesBody `json:"services,omitempty"`
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ func (in *NamespaceRuleBodyTenant) DeepCopy() *NamespaceRuleBodyTenant {
|
||||
func (in *NamespaceRuleEnforceBody) DeepCopyInto(out *NamespaceRuleEnforceBody) {
|
||||
*out = *in
|
||||
in.Workloads.DeepCopyInto(&out.Workloads)
|
||||
in.Services.DeepCopyInto(&out.Services)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceRuleEnforceBody.
|
||||
@@ -75,6 +76,41 @@ func (in *NamespaceRuleEnforceBody) DeepCopy() *NamespaceRuleEnforceBody {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *NamespaceRuleEnforceServicesBody) DeepCopyInto(out *NamespaceRuleEnforceServicesBody) {
|
||||
*out = *in
|
||||
if in.Types != nil {
|
||||
in, out := &in.Types, &out.Types
|
||||
*out = make([]ServiceType, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.LoadBalancers != nil {
|
||||
in, out := &in.LoadBalancers, &out.LoadBalancers
|
||||
*out = new(ServiceLoadBalancerRule)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.ExternalNames != nil {
|
||||
in, out := &in.ExternalNames, &out.ExternalNames
|
||||
*out = new(ServiceExternalNameRule)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.NodePorts != nil {
|
||||
in, out := &in.NodePorts, &out.NodePorts
|
||||
*out = new(ServiceNodePortRule)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceRuleEnforceServicesBody.
|
||||
func (in *NamespaceRuleEnforceServicesBody) DeepCopy() *NamespaceRuleEnforceServicesBody {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(NamespaceRuleEnforceServicesBody)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *NamespaceRuleEnforceWorkloadsBody) DeepCopyInto(out *NamespaceRuleEnforceWorkloadsBody) {
|
||||
*out = *in
|
||||
@@ -185,3 +221,80 @@ func (in *OCIRegistry) DeepCopy() *OCIRegistry {
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ServiceExternalNameRule) DeepCopyInto(out *ServiceExternalNameRule) {
|
||||
*out = *in
|
||||
if in.Hostnames != nil {
|
||||
in, out := &in.Hostnames, &out.Hostnames
|
||||
*out = make([]api.ExpressionMatch, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceExternalNameRule.
|
||||
func (in *ServiceExternalNameRule) DeepCopy() *ServiceExternalNameRule {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ServiceExternalNameRule)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ServiceLoadBalancerRule) DeepCopyInto(out *ServiceLoadBalancerRule) {
|
||||
*out = *in
|
||||
if in.CIDRs != nil {
|
||||
in, out := &in.CIDRs, &out.CIDRs
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceLoadBalancerRule.
|
||||
func (in *ServiceLoadBalancerRule) DeepCopy() *ServiceLoadBalancerRule {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ServiceLoadBalancerRule)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ServiceNodePortRange) DeepCopyInto(out *ServiceNodePortRange) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceNodePortRange.
|
||||
func (in *ServiceNodePortRange) DeepCopy() *ServiceNodePortRange {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ServiceNodePortRange)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ServiceNodePortRule) DeepCopyInto(out *ServiceNodePortRule) {
|
||||
*out = *in
|
||||
if in.Ports != nil {
|
||||
in, out := &in.Ports, &out.Ports
|
||||
*out = make([]ServiceNodePortRange, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceNodePortRule.
|
||||
func (in *ServiceNodePortRule) DeepCopy() *ServiceNodePortRule {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ServiceNodePortRule)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
// Copyright 2020-2026 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ruleengine
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
api "github.com/projectcapsule/capsule/pkg/api/rules"
|
||||
)
|
||||
|
||||
func TestEnforceBodiesFromNamespaceRules(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []*api.NamespaceRuleBodyNamespace
|
||||
assert func(t *testing.T, got []*api.NamespaceRuleEnforceBody)
|
||||
}{
|
||||
{
|
||||
name: "nil input returns nil",
|
||||
input: nil,
|
||||
assert: func(t *testing.T, got []*api.NamespaceRuleEnforceBody) {
|
||||
t.Helper()
|
||||
|
||||
if got != nil {
|
||||
t.Fatalf("expected nil, got %#v", got)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty input returns nil",
|
||||
input: []*api.NamespaceRuleBodyNamespace{},
|
||||
assert: func(t *testing.T, got []*api.NamespaceRuleEnforceBody) {
|
||||
t.Helper()
|
||||
|
||||
if got != nil {
|
||||
t.Fatalf("expected nil, got %#v", got)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only nil bodies returns empty slice",
|
||||
input: []*api.NamespaceRuleBodyNamespace{
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
assert: func(t *testing.T, got []*api.NamespaceRuleEnforceBody) {
|
||||
t.Helper()
|
||||
|
||||
if got == nil {
|
||||
t.Fatalf("expected non-nil empty slice, got nil")
|
||||
}
|
||||
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("expected empty slice, got len=%d", len(got))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bodies without enforce are skipped",
|
||||
input: []*api.NamespaceRuleBodyNamespace{
|
||||
{},
|
||||
{
|
||||
Enforce: nil,
|
||||
},
|
||||
},
|
||||
assert: func(t *testing.T, got []*api.NamespaceRuleEnforceBody) {
|
||||
t.Helper()
|
||||
|
||||
if got == nil {
|
||||
t.Fatalf("expected non-nil empty slice, got nil")
|
||||
}
|
||||
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("expected empty slice, got len=%d", len(got))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "returns enforce bodies in original order",
|
||||
input: func() []*api.NamespaceRuleBodyNamespace {
|
||||
first := &api.NamespaceRuleEnforceBody{
|
||||
Action: api.ActionTypeAllow,
|
||||
}
|
||||
second := &api.NamespaceRuleEnforceBody{
|
||||
Action: api.ActionTypeDeny,
|
||||
}
|
||||
third := &api.NamespaceRuleEnforceBody{
|
||||
Action: api.ActionTypeAudit,
|
||||
}
|
||||
|
||||
return []*api.NamespaceRuleBodyNamespace{
|
||||
{
|
||||
Enforce: first,
|
||||
},
|
||||
nil,
|
||||
{
|
||||
Enforce: second,
|
||||
},
|
||||
{},
|
||||
{
|
||||
Enforce: third,
|
||||
},
|
||||
}
|
||||
}(),
|
||||
assert: func(t *testing.T, got []*api.NamespaceRuleEnforceBody) {
|
||||
t.Helper()
|
||||
|
||||
if len(got) != 3 {
|
||||
t.Fatalf("expected 3 enforce bodies, got %d", len(got))
|
||||
}
|
||||
|
||||
if got[0].Action != api.ActionTypeAllow {
|
||||
t.Fatalf("expected first action %q, got %q", api.ActionTypeAllow, got[0].Action)
|
||||
}
|
||||
|
||||
if got[1].Action != api.ActionTypeDeny {
|
||||
t.Fatalf("expected second action %q, got %q", api.ActionTypeDeny, got[1].Action)
|
||||
}
|
||||
|
||||
if got[2].Action != api.ActionTypeAudit {
|
||||
t.Fatalf("expected third action %q, got %q", api.ActionTypeAudit, got[2].Action)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "returns original enforce pointers without deep copy",
|
||||
input: func() []*api.NamespaceRuleBodyNamespace {
|
||||
enforce := &api.NamespaceRuleEnforceBody{
|
||||
Action: api.ActionTypeAllow,
|
||||
}
|
||||
|
||||
return []*api.NamespaceRuleBodyNamespace{
|
||||
{
|
||||
Enforce: enforce,
|
||||
},
|
||||
}
|
||||
}(),
|
||||
assert: func(t *testing.T, got []*api.NamespaceRuleEnforceBody) {
|
||||
t.Helper()
|
||||
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("expected one enforce body, got %d", len(got))
|
||||
}
|
||||
|
||||
if got[0].Action != api.ActionTypeAllow {
|
||||
t.Fatalf("expected action %q, got %q", api.ActionTypeAllow, got[0].Action)
|
||||
}
|
||||
|
||||
got[0].Action = api.ActionTypeDeny
|
||||
|
||||
if got[0].Action != api.ActionTypeDeny {
|
||||
t.Fatalf("expected returned pointer to be mutable")
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := EnforceBodiesFromNamespaceRules(tt.input)
|
||||
tt.assert(t, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnforceBodiesFromNamespaceRulesReturnsOriginalPointers(t *testing.T) {
|
||||
first := &api.NamespaceRuleEnforceBody{
|
||||
Action: api.ActionTypeAllow,
|
||||
}
|
||||
second := &api.NamespaceRuleEnforceBody{
|
||||
Action: api.ActionTypeDeny,
|
||||
}
|
||||
|
||||
got := EnforceBodiesFromNamespaceRules([]*api.NamespaceRuleBodyNamespace{
|
||||
{
|
||||
Enforce: first,
|
||||
},
|
||||
{
|
||||
Enforce: second,
|
||||
},
|
||||
})
|
||||
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("expected 2 enforce bodies, got %d", len(got))
|
||||
}
|
||||
|
||||
if got[0] != first {
|
||||
t.Fatalf("expected first returned enforce body to be the original pointer")
|
||||
}
|
||||
|
||||
if got[1] != second {
|
||||
t.Fatalf("expected second returned enforce body to be the original pointer")
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ package ruleengine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
api "github.com/projectcapsule/capsule/pkg/api/rules"
|
||||
)
|
||||
@@ -17,15 +18,27 @@ type Value struct {
|
||||
type Match struct {
|
||||
Matched bool
|
||||
MatchedValue any
|
||||
|
||||
// Detail is optional human-readable matcher context.
|
||||
// Example: "10.0.171.239 is contained in 10.0.0.0/16".
|
||||
Detail string
|
||||
}
|
||||
|
||||
type Decision struct {
|
||||
SetName string
|
||||
EventReason string
|
||||
Action api.ActionType
|
||||
Value Value
|
||||
SetName string
|
||||
EventReason string
|
||||
Action api.ActionType
|
||||
Value Value
|
||||
|
||||
MatchedValue any
|
||||
Message string
|
||||
|
||||
// MatchedRule is the human-readable rule description returned by Set.RuleDescription.
|
||||
MatchedRule string
|
||||
|
||||
// MatchDetail is the human-readable detail returned by Match.Detail.
|
||||
MatchDetail string
|
||||
|
||||
Message string
|
||||
}
|
||||
|
||||
type DecisionError struct {
|
||||
@@ -77,18 +90,25 @@ func (e *Evaluation) Append(other *Evaluation) {
|
||||
}
|
||||
}
|
||||
|
||||
type Set[R any, T any] struct {
|
||||
Name string
|
||||
|
||||
type Set[R any, O any] struct {
|
||||
Name string
|
||||
EventReason string
|
||||
|
||||
Values func(T) []Value
|
||||
|
||||
Rules func(*api.NamespaceRuleEnforceBody) []R
|
||||
|
||||
Values func(O) []Value
|
||||
Rules func(*api.NamespaceRuleEnforceBody) []R
|
||||
Matches func(R, Value) (Match, error)
|
||||
|
||||
Message func(action api.ActionType, value Value, matchedValue any) string
|
||||
// Message can fully override the default message.
|
||||
// Prefer leaving this nil unless a rule requires very specific wording.
|
||||
Message func(api.ActionType, Value, any) string
|
||||
|
||||
// RuleDescription returns a human-readable representation of one rule.
|
||||
// It is used only for admission/audit messages.
|
||||
RuleDescription func(R) string
|
||||
|
||||
// AllowedDescription optionally overrides the "Allowed values" label.
|
||||
// Example: "Allowed CIDRs", "Allowed ranges", "Allowed hostnames".
|
||||
AllowedDescription string
|
||||
}
|
||||
|
||||
func EvaluateEnforce[R any, T any](
|
||||
@@ -125,6 +145,7 @@ func EvaluateEnforce[R any, T any](
|
||||
}
|
||||
|
||||
hasAllowRule := false
|
||||
allowRules := make([]R, 0)
|
||||
|
||||
var lastDecision *Decision
|
||||
|
||||
@@ -144,6 +165,8 @@ func EvaluateEnforce[R any, T any](
|
||||
case api.ActionTypeAllow:
|
||||
hasAllowRule = true
|
||||
|
||||
allowRules = append(allowRules, items...)
|
||||
|
||||
case api.ActionTypeDeny, api.ActionTypeAudit:
|
||||
// Supported actions.
|
||||
|
||||
@@ -165,13 +188,24 @@ func EvaluateEnforce[R any, T any](
|
||||
continue
|
||||
}
|
||||
|
||||
matchedRule := describeRule(set, item)
|
||||
|
||||
decision := &Decision{
|
||||
SetName: set.Name,
|
||||
EventReason: set.EventReason,
|
||||
Action: action,
|
||||
Value: value,
|
||||
MatchedValue: match.MatchedValue,
|
||||
Message: decisionMessage(set, action, value, match.MatchedValue),
|
||||
MatchedRule: matchedRule,
|
||||
MatchDetail: strings.TrimSpace(match.Detail),
|
||||
Message: decisionMessage(
|
||||
set,
|
||||
action,
|
||||
value,
|
||||
match.MatchedValue,
|
||||
matchedRule,
|
||||
match.Detail,
|
||||
),
|
||||
}
|
||||
|
||||
switch action {
|
||||
@@ -205,12 +239,7 @@ func EvaluateEnforce[R any, T any](
|
||||
EventReason: set.EventReason,
|
||||
Action: api.ActionTypeDeny,
|
||||
Value: value,
|
||||
Message: fmt.Sprintf(
|
||||
"%s %q at %s is not allowed by namespace rule",
|
||||
set.Name,
|
||||
value.Value,
|
||||
value.Path,
|
||||
),
|
||||
Message: allowMissMessage(set, value, allowRules),
|
||||
}
|
||||
|
||||
return evaluation, nil
|
||||
@@ -220,41 +249,123 @@ func EvaluateEnforce[R any, T any](
|
||||
return evaluation, nil
|
||||
}
|
||||
|
||||
const maxRuleDescriptions = 10
|
||||
|
||||
func describeRule[R any, O any](set Set[R, O], rule R) string {
|
||||
if set.RuleDescription == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.TrimSpace(set.RuleDescription(rule))
|
||||
}
|
||||
|
||||
func describeRules[R any, O any](set Set[R, O], rules []R) string {
|
||||
if len(rules) == 0 || set.RuleDescription == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
limit := min(len(rules), maxRuleDescriptions)
|
||||
|
||||
parts := make([]string, 0, limit)
|
||||
|
||||
for i := range limit {
|
||||
description := describeRule(set, rules[i])
|
||||
if description == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts = append(parts, description)
|
||||
}
|
||||
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(rules) > maxRuleDescriptions {
|
||||
parts = append(parts, fmt.Sprintf("and %d more", len(rules)-maxRuleDescriptions))
|
||||
}
|
||||
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
func allowedLabel[R any, O any](set Set[R, O]) string {
|
||||
if set.AllowedDescription != "" {
|
||||
return set.AllowedDescription
|
||||
}
|
||||
|
||||
return "Allowed values"
|
||||
}
|
||||
|
||||
func allowMissMessage[R any, T any](
|
||||
set Set[R, T],
|
||||
value Value,
|
||||
allowRules []R,
|
||||
) string {
|
||||
message := fmt.Sprintf(
|
||||
"%s %q at %s is not allowed by namespace rule",
|
||||
set.Name,
|
||||
value.Value,
|
||||
value.Path,
|
||||
)
|
||||
|
||||
descriptions := describeRules(set, allowRules)
|
||||
if descriptions == "" {
|
||||
return message
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"%s: value did not match any allowed rule. %s: %s",
|
||||
message,
|
||||
allowedLabel(set),
|
||||
descriptions,
|
||||
)
|
||||
}
|
||||
|
||||
func decisionMessage[R any, T any](
|
||||
set Set[R, T],
|
||||
action api.ActionType,
|
||||
value Value,
|
||||
matchedValue any,
|
||||
matchedRule string,
|
||||
matchDetail string,
|
||||
) string {
|
||||
if set.Message != nil {
|
||||
return set.Message(action, value, matchedValue)
|
||||
}
|
||||
|
||||
matchDetail = strings.TrimSpace(matchDetail)
|
||||
|
||||
switch action {
|
||||
case api.ActionTypeAudit:
|
||||
return fmt.Sprintf(
|
||||
message := fmt.Sprintf(
|
||||
"%s %q at %s matched audit namespace rule",
|
||||
set.Name,
|
||||
value.Value,
|
||||
value.Path,
|
||||
)
|
||||
|
||||
return appendMatchContext(message, matchedRule, matchDetail, "matched audit rule")
|
||||
|
||||
case api.ActionTypeDeny:
|
||||
return fmt.Sprintf(
|
||||
message := fmt.Sprintf(
|
||||
"%s %q at %s is denied by namespace rule",
|
||||
set.Name,
|
||||
value.Value,
|
||||
value.Path,
|
||||
)
|
||||
|
||||
return appendMatchContext(message, matchedRule, matchDetail, "matched denied rule")
|
||||
|
||||
case api.ActionTypeAllow:
|
||||
return fmt.Sprintf(
|
||||
message := fmt.Sprintf(
|
||||
"%s %q at %s is allowed by namespace rule",
|
||||
set.Name,
|
||||
value.Value,
|
||||
value.Path,
|
||||
)
|
||||
|
||||
return appendMatchContext(message, matchedRule, matchDetail, "matched allowed rule")
|
||||
|
||||
default:
|
||||
return fmt.Sprintf(
|
||||
"%s %q at %s matched namespace rule action %q",
|
||||
@@ -265,3 +376,20 @@ func decisionMessage[R any, T any](
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func appendMatchContext(
|
||||
message string,
|
||||
matchedRule string,
|
||||
matchDetail string,
|
||||
rulePrefix string,
|
||||
) string {
|
||||
if matchDetail != "" {
|
||||
return fmt.Sprintf("%s: %s", message, matchDetail)
|
||||
}
|
||||
|
||||
if matchedRule != "" {
|
||||
return fmt.Sprintf("%s: %s %s", message, rulePrefix, matchedRule)
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,183 @@
|
||||
// Copyright 2020-2026 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
package ruleengine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
"github.com/projectcapsule/capsule/pkg/api/rules"
|
||||
)
|
||||
|
||||
func ValidateRuleStatusBody(bodies []*rules.NamespaceRuleBodyNamespace) error {
|
||||
for i, rule := range bodies {
|
||||
if rule == nil || rule.Enforce == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := validateWorkloadRules(i, rule.Enforce.Workloads); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateServiceRules(i, rule.Enforce.Services); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateWorkloadRules(
|
||||
ruleIndex int,
|
||||
workloads rules.NamespaceRuleEnforceWorkloadsBody,
|
||||
) error {
|
||||
for j, registry := range workloads.Registries {
|
||||
if err := validateExpression(
|
||||
registry.Expression,
|
||||
fmt.Sprintf("rules[%d].enforce.workloads.registries[%d].exp", ruleIndex, j),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for j, scheduler := range workloads.Schedulers {
|
||||
if err := validateExpression(
|
||||
scheduler.Expression,
|
||||
fmt.Sprintf("rules[%d].enforce.workloads.schedulers[%d].exp", ruleIndex, j),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateServiceRules(
|
||||
ruleIndex int,
|
||||
services rules.NamespaceRuleEnforceServicesBody,
|
||||
) error {
|
||||
for j, serviceType := range services.Types {
|
||||
if err := validateServiceType(serviceType); err != nil {
|
||||
return fmt.Errorf(
|
||||
"rules[%d].enforce.services.types[%d] %q is invalid: %w",
|
||||
ruleIndex,
|
||||
j,
|
||||
serviceType,
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if services.LoadBalancers != nil {
|
||||
for j, cidr := range services.LoadBalancers.CIDRs {
|
||||
if err := validateCIDR(cidr); err != nil {
|
||||
return fmt.Errorf(
|
||||
"rules[%d].enforce.services.loadBalancers.cidrs[%d] %q is invalid: %w",
|
||||
ruleIndex,
|
||||
j,
|
||||
cidr,
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if services.ExternalNames != nil {
|
||||
for j, hostname := range services.ExternalNames.Hostnames {
|
||||
if err := validateExpressionMatch(
|
||||
hostname,
|
||||
fmt.Sprintf("rules[%d].enforce.services.externalNames.hostnames[%d]", ruleIndex, j),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if services.NodePorts != nil {
|
||||
for j, portRange := range services.NodePorts.Ports {
|
||||
if err := validateNodePortRange(portRange); err != nil {
|
||||
return fmt.Errorf(
|
||||
"rules[%d].enforce.services.nodePorts.ports[%d] is invalid: %w",
|
||||
ruleIndex,
|
||||
j,
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateExpressionMatch(match api.ExpressionMatch, fieldPath string) error {
|
||||
if err := validateExpression(match.Expression, fieldPath+".exp"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateExpression(expression string, fieldPath string) error {
|
||||
if strings.TrimSpace(expression) == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := regexp.Compile(expression); err != nil {
|
||||
return fmt.Errorf("%s %q is invalid: %w", fieldPath, expression, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateServiceType(serviceType rules.ServiceType) error {
|
||||
switch serviceType {
|
||||
case rules.ServiceTypeClusterIP,
|
||||
rules.ServiceTypeNodePort,
|
||||
rules.ServiceTypeLoadBalancer,
|
||||
rules.ServiceTypeExternalName:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported service type")
|
||||
}
|
||||
}
|
||||
|
||||
func validateCIDR(raw string) error {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return fmt.Errorf("CIDR is empty")
|
||||
}
|
||||
|
||||
if !strings.Contains(raw, "/") {
|
||||
ip := net.ParseIP(raw)
|
||||
if ip == nil {
|
||||
return fmt.Errorf("must be a valid IP or CIDR")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, _, err := net.ParseCIDR(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateNodePortRange(portRange rules.ServiceNodePortRange) error {
|
||||
if portRange.From < 1 || portRange.From > 65535 {
|
||||
return fmt.Errorf("from %d must be between 1 and 65535", portRange.From)
|
||||
}
|
||||
|
||||
if portRange.To < 1 || portRange.To > 65535 {
|
||||
return fmt.Errorf("to %d must be between 1 and 65535", portRange.To)
|
||||
}
|
||||
|
||||
if portRange.From > portRange.To {
|
||||
return fmt.Errorf("from %d must be lower than or equal to %d", portRange.From, portRange.To)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
// Copyright 2020-2026 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
package ruleengine
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
"github.com/projectcapsule/capsule/pkg/api/rules"
|
||||
)
|
||||
|
||||
func TestValidateRuleStatusBody(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
bodies []*rules.NamespaceRuleBodyNamespace
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "nil bodies are valid",
|
||||
bodies: []*rules.NamespaceRuleBodyNamespace{
|
||||
nil,
|
||||
{},
|
||||
{
|
||||
Enforce: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid workload and service rules",
|
||||
bodies: []*rules.NamespaceRuleBodyNamespace{
|
||||
{
|
||||
Enforce: &rules.NamespaceRuleEnforceBody{
|
||||
Action: rules.ActionTypeAllow,
|
||||
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
||||
Registries: []rules.OCIRegistry{
|
||||
{
|
||||
ExpressionMatch: api.ExpressionMatch{
|
||||
ExpressionRegex: api.ExpressionRegex{
|
||||
Expression: "harbor/.*",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ExpressionMatch: api.ExpressionMatch{
|
||||
Exact: []string{
|
||||
"harbor/platform/debian:latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Schedulers: []api.ExpressionMatch{
|
||||
{
|
||||
ExpressionRegex: api.ExpressionRegex{
|
||||
Expression: "tenant-[a-z0-9-]+",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Services: rules.NamespaceRuleEnforceServicesBody{
|
||||
Types: []rules.ServiceType{
|
||||
rules.ServiceTypeClusterIP,
|
||||
rules.ServiceTypeNodePort,
|
||||
rules.ServiceTypeLoadBalancer,
|
||||
rules.ServiceTypeExternalName,
|
||||
},
|
||||
LoadBalancers: &rules.ServiceLoadBalancerRule{
|
||||
CIDRs: []string{
|
||||
"10.0.0.2/32",
|
||||
"10.0.1.0/24",
|
||||
"2001:db8::/32",
|
||||
"10.0.0.3",
|
||||
},
|
||||
},
|
||||
ExternalNames: &rules.ServiceExternalNameRule{
|
||||
Hostnames: []api.ExpressionMatch{
|
||||
{
|
||||
Exact: []string{
|
||||
"internal.git.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
ExpressionRegex: api.ExpressionRegex{
|
||||
Expression: ".*\\.example\\.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
ExpressionRegex: api.ExpressionRegex{
|
||||
Expression: "trusted\\..*",
|
||||
Negate: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
NodePorts: &rules.ServiceNodePortRule{
|
||||
Ports: []rules.ServiceNodePortRange{
|
||||
{
|
||||
From: 30000,
|
||||
To: 32767,
|
||||
},
|
||||
{
|
||||
From: 30500,
|
||||
To: 30500,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid workload registry regex",
|
||||
bodies: []*rules.NamespaceRuleBodyNamespace{
|
||||
{
|
||||
Enforce: &rules.NamespaceRuleEnforceBody{
|
||||
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
||||
Registries: []rules.OCIRegistry{
|
||||
{
|
||||
ExpressionMatch: api.ExpressionMatch{
|
||||
ExpressionRegex: api.ExpressionRegex{
|
||||
Expression: "[",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: `rules[0].enforce.workloads.registries[0].exp "[" is invalid`,
|
||||
},
|
||||
{
|
||||
name: "invalid workload scheduler regex",
|
||||
bodies: []*rules.NamespaceRuleBodyNamespace{
|
||||
{
|
||||
Enforce: &rules.NamespaceRuleEnforceBody{
|
||||
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
||||
Schedulers: []api.ExpressionMatch{
|
||||
{
|
||||
ExpressionRegex: api.ExpressionRegex{
|
||||
Expression: "[",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: `rules[0].enforce.workloads.schedulers[0].exp "[" is invalid`,
|
||||
},
|
||||
{
|
||||
name: "invalid service type",
|
||||
bodies: []*rules.NamespaceRuleBodyNamespace{
|
||||
{
|
||||
Enforce: &rules.NamespaceRuleEnforceBody{
|
||||
Services: rules.NamespaceRuleEnforceServicesBody{
|
||||
Types: []rules.ServiceType{
|
||||
rules.ServiceTypeClusterIP,
|
||||
rules.ServiceType("InvalidType"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: `rules[0].enforce.services.types[1] "InvalidType" is invalid`,
|
||||
},
|
||||
{
|
||||
name: "invalid loadBalancer CIDR",
|
||||
bodies: []*rules.NamespaceRuleBodyNamespace{
|
||||
{
|
||||
Enforce: &rules.NamespaceRuleEnforceBody{
|
||||
Services: rules.NamespaceRuleEnforceServicesBody{
|
||||
LoadBalancers: &rules.ServiceLoadBalancerRule{
|
||||
CIDRs: []string{
|
||||
"10.0.0.0/33",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: `rules[0].enforce.services.loadBalancers.cidrs[0] "10.0.0.0/33" is invalid`,
|
||||
},
|
||||
{
|
||||
name: "empty loadBalancer CIDR",
|
||||
bodies: []*rules.NamespaceRuleBodyNamespace{
|
||||
{
|
||||
Enforce: &rules.NamespaceRuleEnforceBody{
|
||||
Services: rules.NamespaceRuleEnforceServicesBody{
|
||||
LoadBalancers: &rules.ServiceLoadBalancerRule{
|
||||
CIDRs: []string{
|
||||
"",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: `rules[0].enforce.services.loadBalancers.cidrs[0] "" is invalid: CIDR is empty`,
|
||||
},
|
||||
{
|
||||
name: "invalid externalName hostname regex",
|
||||
bodies: []*rules.NamespaceRuleBodyNamespace{
|
||||
{
|
||||
Enforce: &rules.NamespaceRuleEnforceBody{
|
||||
Services: rules.NamespaceRuleEnforceServicesBody{
|
||||
ExternalNames: &rules.ServiceExternalNameRule{
|
||||
Hostnames: []api.ExpressionMatch{
|
||||
{
|
||||
ExpressionRegex: api.ExpressionRegex{
|
||||
Expression: "[",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: `rules[0].enforce.services.externalNames.hostnames[0].exp "[" is invalid`,
|
||||
},
|
||||
{
|
||||
name: "nodePort from greater than to",
|
||||
bodies: []*rules.NamespaceRuleBodyNamespace{
|
||||
{
|
||||
Enforce: &rules.NamespaceRuleEnforceBody{
|
||||
Services: rules.NamespaceRuleEnforceServicesBody{
|
||||
NodePorts: &rules.ServiceNodePortRule{
|
||||
Ports: []rules.ServiceNodePortRange{
|
||||
{
|
||||
From: 32767,
|
||||
To: 30000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: `rules[0].enforce.services.nodePorts.ports[0] is invalid: from 32767 must be lower than or equal to 30000`,
|
||||
},
|
||||
{
|
||||
name: "nodePort from below valid port range",
|
||||
bodies: []*rules.NamespaceRuleBodyNamespace{
|
||||
{
|
||||
Enforce: &rules.NamespaceRuleEnforceBody{
|
||||
Services: rules.NamespaceRuleEnforceServicesBody{
|
||||
NodePorts: &rules.ServiceNodePortRule{
|
||||
Ports: []rules.ServiceNodePortRange{
|
||||
{
|
||||
From: 0,
|
||||
To: 30000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: `rules[0].enforce.services.nodePorts.ports[0] is invalid: from 0 must be between 1 and 65535`,
|
||||
},
|
||||
{
|
||||
name: "nodePort to above valid port range",
|
||||
bodies: []*rules.NamespaceRuleBodyNamespace{
|
||||
{
|
||||
Enforce: &rules.NamespaceRuleEnforceBody{
|
||||
Services: rules.NamespaceRuleEnforceServicesBody{
|
||||
NodePorts: &rules.ServiceNodePortRule{
|
||||
Ports: []rules.ServiceNodePortRange{
|
||||
{
|
||||
From: 30000,
|
||||
To: 70000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: `rules[0].enforce.services.nodePorts.ports[0] is invalid: to 70000 must be between 1 and 65535`,
|
||||
},
|
||||
{
|
||||
name: "single nodePort range is valid",
|
||||
bodies: []*rules.NamespaceRuleBodyNamespace{
|
||||
{
|
||||
Enforce: &rules.NamespaceRuleEnforceBody{
|
||||
Services: rules.NamespaceRuleEnforceServicesBody{
|
||||
NodePorts: &rules.ServiceNodePortRule{
|
||||
Ports: []rules.ServiceNodePortRange{
|
||||
{
|
||||
From: 30500,
|
||||
To: 30500,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reports correct indexes across multiple rules",
|
||||
bodies: []*rules.NamespaceRuleBodyNamespace{
|
||||
{
|
||||
Enforce: &rules.NamespaceRuleEnforceBody{
|
||||
Services: rules.NamespaceRuleEnforceServicesBody{
|
||||
Types: []rules.ServiceType{
|
||||
rules.ServiceTypeClusterIP,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Enforce: &rules.NamespaceRuleEnforceBody{
|
||||
Services: rules.NamespaceRuleEnforceServicesBody{
|
||||
ExternalNames: &rules.ServiceExternalNameRule{
|
||||
Hostnames: []api.ExpressionMatch{
|
||||
{
|
||||
ExpressionRegex: api.ExpressionRegex{
|
||||
Expression: "valid\\..*",
|
||||
},
|
||||
},
|
||||
{
|
||||
ExpressionRegex: api.ExpressionRegex{
|
||||
Expression: "[",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: `rules[1].enforce.services.externalNames.hostnames[1].exp "[" is invalid`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateRuleStatusBody(tt.bodies)
|
||||
|
||||
if tt.wantErr == "" {
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("expected error containing %q, got nil", tt.wantErr)
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Fatalf("expected error containing %q, got %q", tt.wantErr, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ const (
|
||||
|
||||
// RuleStatus.
|
||||
ReasonNamespaceRuleAudit string = "NamespaceRuleAudit"
|
||||
|
||||
// Namespace.
|
||||
ReasonNamespaceHijack string = "ReasonNamespacePatch"
|
||||
|
||||
@@ -59,6 +58,8 @@ const (
|
||||
ReasonForbiddenLoadBalancer string = "ForbiddenLoadBalancer"
|
||||
ReasonForbiddenExternalName string = "ForbiddenExternalName"
|
||||
ReasonForbiddenNodePort string = "ForbiddenNodePort"
|
||||
ReasonForbiddenServiceType string = "ForbiddenServiceType"
|
||||
ReasonForbiddenLoadBalancerCIDR string = "ForbiddenLoadBalancerCIDR"
|
||||
|
||||
// Storage.
|
||||
ReasonCrossTenantReference string = "CrossTenantReference"
|
||||
|
||||
Reference in New Issue
Block a user