mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-25 16:07:24 +00:00
feat: add metadata enforcement (#1990)
* 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: add metadata enforcement Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add metadata enforcement Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: add resourcepoolclaim validation Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: add resourcepoolclaim validation Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: add resourcepoolclaim validation Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: add resourcepoolclaim validation 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> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI
parent
6fbd472f27
commit
77d1810bb9
@@ -903,6 +903,234 @@ func TestEvaluation_Append(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestEvaluateEnforce_SkipsEmptyExtractedValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
action api.ActionType
|
||||
values []Value
|
||||
rules []string
|
||||
wantMatcherCalls int
|
||||
wantBlocking bool
|
||||
wantFinal bool
|
||||
wantAudits int
|
||||
wantBlockingPath string
|
||||
}
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
name: "empty value is skipped before deny evaluation",
|
||||
action: api.ActionTypeDeny,
|
||||
values: []Value{
|
||||
{
|
||||
Value: "",
|
||||
Path: "spec.value",
|
||||
},
|
||||
},
|
||||
rules: []string{
|
||||
"",
|
||||
},
|
||||
wantMatcherCalls: 0,
|
||||
wantBlocking: false,
|
||||
wantFinal: false,
|
||||
wantAudits: 0,
|
||||
},
|
||||
{
|
||||
name: "empty value is skipped before allow miss",
|
||||
action: api.ActionTypeAllow,
|
||||
values: []Value{
|
||||
{
|
||||
Value: "",
|
||||
Path: "spec.value",
|
||||
},
|
||||
},
|
||||
rules: []string{
|
||||
"allowed",
|
||||
},
|
||||
wantMatcherCalls: 0,
|
||||
wantBlocking: false,
|
||||
wantFinal: false,
|
||||
wantAudits: 0,
|
||||
},
|
||||
{
|
||||
name: "empty value is skipped before audit evaluation",
|
||||
action: api.ActionTypeAudit,
|
||||
values: []Value{
|
||||
{
|
||||
Value: "",
|
||||
Path: "spec.value",
|
||||
},
|
||||
},
|
||||
rules: []string{
|
||||
"",
|
||||
},
|
||||
wantMatcherCalls: 0,
|
||||
wantBlocking: false,
|
||||
wantFinal: false,
|
||||
wantAudits: 0,
|
||||
},
|
||||
{
|
||||
name: "empty value is skipped but later non empty value is evaluated",
|
||||
action: api.ActionTypeDeny,
|
||||
values: []Value{
|
||||
{
|
||||
Value: "",
|
||||
Path: "spec.empty",
|
||||
},
|
||||
{
|
||||
Value: "deny",
|
||||
Path: "spec.nonEmpty",
|
||||
},
|
||||
},
|
||||
rules: []string{
|
||||
"deny",
|
||||
},
|
||||
wantMatcherCalls: 1,
|
||||
wantBlocking: true,
|
||||
wantFinal: true,
|
||||
wantAudits: 0,
|
||||
wantBlockingPath: "spec.nonEmpty",
|
||||
},
|
||||
{
|
||||
name: "whitespace value is not skipped",
|
||||
action: api.ActionTypeDeny,
|
||||
values: []Value{
|
||||
{
|
||||
Value: " ",
|
||||
Path: "spec.value",
|
||||
},
|
||||
},
|
||||
rules: []string{
|
||||
" ",
|
||||
},
|
||||
wantMatcherCalls: 1,
|
||||
wantBlocking: true,
|
||||
wantFinal: true,
|
||||
wantAudits: 0,
|
||||
wantBlockingPath: "spec.value",
|
||||
},
|
||||
{
|
||||
name: "empty values are skipped before non matching allow value triggers allow miss",
|
||||
action: api.ActionTypeAllow,
|
||||
values: []Value{
|
||||
{
|
||||
Value: "",
|
||||
Path: "spec.empty",
|
||||
},
|
||||
{
|
||||
Value: "actual",
|
||||
Path: "spec.actual",
|
||||
},
|
||||
},
|
||||
rules: []string{
|
||||
"allowed",
|
||||
},
|
||||
wantMatcherCalls: 1,
|
||||
wantBlocking: true,
|
||||
wantFinal: false,
|
||||
wantAudits: 0,
|
||||
wantBlockingPath: "spec.actual",
|
||||
},
|
||||
{
|
||||
name: "all empty values are skipped",
|
||||
action: api.ActionTypeDeny,
|
||||
values: []Value{
|
||||
{
|
||||
Value: "",
|
||||
Path: "spec.first",
|
||||
},
|
||||
{
|
||||
Value: "",
|
||||
Path: "spec.second",
|
||||
},
|
||||
},
|
||||
rules: []string{
|
||||
"",
|
||||
},
|
||||
wantMatcherCalls: 0,
|
||||
wantBlocking: false,
|
||||
wantFinal: false,
|
||||
wantAudits: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
matcherCalls := 0
|
||||
|
||||
evaluation, err := EvaluateEnforce(
|
||||
struct{}{},
|
||||
[]*api.NamespaceRuleEnforceBody{
|
||||
{
|
||||
Action: tt.action,
|
||||
},
|
||||
},
|
||||
Set[string, struct{}]{
|
||||
Name: "registry",
|
||||
EventReason: "NamespaceRuleViolation",
|
||||
|
||||
Values: func(struct{}) []Value {
|
||||
return tt.values
|
||||
},
|
||||
|
||||
Rules: func(*api.NamespaceRuleEnforceBody) []string {
|
||||
return tt.rules
|
||||
},
|
||||
|
||||
Matches: func(rule string, value Value) (Match, error) {
|
||||
matcherCalls++
|
||||
|
||||
return Match{
|
||||
Matched: value.Value == rule,
|
||||
MatchedValue: rule,
|
||||
}, nil
|
||||
},
|
||||
|
||||
RuleDescription: func(rule string) string {
|
||||
return rule
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if evaluation == nil {
|
||||
t.Fatalf("expected evaluation")
|
||||
}
|
||||
|
||||
if matcherCalls != tt.wantMatcherCalls {
|
||||
t.Fatalf("expected %d matcher calls, got %d", tt.wantMatcherCalls, matcherCalls)
|
||||
}
|
||||
|
||||
if got := evaluation.Blocking != nil; got != tt.wantBlocking {
|
||||
t.Fatalf("expected blocking=%t, got %t: %#v", tt.wantBlocking, got, evaluation.Blocking)
|
||||
}
|
||||
|
||||
if got := evaluation.Final != nil; got != tt.wantFinal {
|
||||
t.Fatalf("expected final=%t, got %t: %#v", tt.wantFinal, got, evaluation.Final)
|
||||
}
|
||||
|
||||
if len(evaluation.Audits) != tt.wantAudits {
|
||||
t.Fatalf("expected %d audits, got %d", tt.wantAudits, len(evaluation.Audits))
|
||||
}
|
||||
|
||||
if tt.wantBlockingPath != "" {
|
||||
if evaluation.Blocking == nil {
|
||||
t.Fatalf("expected blocking decision")
|
||||
}
|
||||
|
||||
if evaluation.Blocking.Value.Path != tt.wantBlockingPath {
|
||||
t.Fatalf("expected blocking path %q, got %q", tt.wantBlockingPath, evaluation.Blocking.Value.Path)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageHelpers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
+110
-3
@@ -3,16 +3,23 @@
|
||||
package ruleengine
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
k8smeta "k8s.io/apimachinery/pkg/api/meta"
|
||||
k8svalidation "k8s.io/apimachinery/pkg/util/validation"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/api/rules"
|
||||
"github.com/projectcapsule/capsule/pkg/api/runtime"
|
||||
)
|
||||
|
||||
func ValidateRuleStatusBody(bodies []*rules.NamespaceRuleBodyNamespace) error {
|
||||
func ValidateRuleStatusBody(
|
||||
mapper k8smeta.RESTMapper,
|
||||
bodies []*rules.NamespaceRuleBodyNamespace,
|
||||
) error {
|
||||
for i, rule := range bodies {
|
||||
if rule == nil || rule.Enforce == nil {
|
||||
continue
|
||||
@@ -25,6 +32,10 @@ func ValidateRuleStatusBody(bodies []*rules.NamespaceRuleBodyNamespace) error {
|
||||
if err := validateServiceRules(i, rule.Enforce.Services); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateMetadataRules(i, rule.Enforce.Metadata, mapper); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -112,7 +123,76 @@ func validateServiceRules(
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateExpressionMatch(match api.ExpressionMatch, fieldPath string) error {
|
||||
func validateMetadataRules(
|
||||
ruleIndex int,
|
||||
metadata []rules.MetadataRule,
|
||||
mapper k8smeta.RESTMapper,
|
||||
) error {
|
||||
for j, rule := range metadata {
|
||||
fieldPath := fmt.Sprintf("rules[%d].enforce.metadata[%d]", ruleIndex, j)
|
||||
|
||||
if err := validateMetadataTargets(fieldPath, rule, mapper); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for key, policy := range rule.Labels {
|
||||
if err := validateMetadataKey(key); err != nil {
|
||||
return fmt.Errorf(
|
||||
"%s.labels[%q] is invalid: %w",
|
||||
fieldPath,
|
||||
key,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
for k, matcher := range policy.Values {
|
||||
if err := validateExpressionMatch(
|
||||
matcher,
|
||||
fmt.Sprintf("%s.labels[%q].values[%d]", fieldPath, key, k),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for key, policy := range rule.Annotations {
|
||||
if err := validateMetadataKey(key); err != nil {
|
||||
return fmt.Errorf(
|
||||
"%s.annotations[%q] is invalid: %w",
|
||||
fieldPath,
|
||||
key,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
for k, matcher := range policy.Values {
|
||||
if err := validateExpressionMatch(
|
||||
matcher,
|
||||
fmt.Sprintf("%s.annotations[%q].values[%d]", fieldPath, key, k),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateMetadataKey(key string) error {
|
||||
key = strings.TrimSpace(key)
|
||||
if key == "" {
|
||||
return errors.New("key is empty")
|
||||
}
|
||||
|
||||
if errs := k8svalidation.IsQualifiedName(key); len(errs) > 0 {
|
||||
return errors.New(strings.Join(errs, ", "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateExpressionMatch(match runtime.ExpressionMatch, fieldPath string) error {
|
||||
if err := validateExpression(match.Expression, fieldPath+".exp"); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -181,3 +261,30 @@ func validateNodePortRange(portRange rules.ServiceNodePortRange) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateMetadataTargets(
|
||||
fieldPath string,
|
||||
rule rules.MetadataRule,
|
||||
mapper k8smeta.RESTMapper,
|
||||
) error {
|
||||
if len(rule.Kinds) == 0 {
|
||||
return fmt.Errorf("%s.kinds is invalid: at least one kind must be configured", fieldPath)
|
||||
}
|
||||
|
||||
for i, kind := range rule.Kinds {
|
||||
kind = strings.TrimSpace(kind)
|
||||
if kind == "" {
|
||||
return fmt.Errorf("%s.kinds[%d] is invalid: kind is empty", fieldPath, i)
|
||||
}
|
||||
}
|
||||
|
||||
if mapper == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := rule.ValidateKnownKinds(mapper, fieldPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+867
-32
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user