mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-18 20:17:04 +00:00
* feat: implement namespace metadata enforcement Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add ingress enforcment Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add ingress enforcment Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add ingress enforcment Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add ingress enforcment Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add ingress enforcment Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add ingress enforcment Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add ingress enforcment Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add ingress enforcment Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add ingress enforcment Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add ingress enforcment Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add ingress enforcment Signed-off-by: Oliver Baehler <oliver@sudo-i.net> --------- Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
335 lines
7.2 KiB
Go
335 lines
7.2 KiB
Go
// Copyright 2020-2026 Project Capsule Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
//nolint:dupl
|
|
package validation
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
|
|
apirules "github.com/projectcapsule/capsule/pkg/api/rules"
|
|
"github.com/projectcapsule/capsule/pkg/api/runtime"
|
|
"github.com/projectcapsule/capsule/pkg/ruleengine"
|
|
"github.com/projectcapsule/capsule/pkg/runtime/events"
|
|
)
|
|
|
|
type metadataField string
|
|
|
|
const (
|
|
metadataFieldLabel metadataField = "label"
|
|
metadataFieldAnnotation metadataField = "annotation"
|
|
)
|
|
|
|
type metadataEntry struct {
|
|
Field metadataField
|
|
Key string
|
|
Value string
|
|
Path string
|
|
Present bool
|
|
Required bool
|
|
}
|
|
|
|
func (h *genericRules) validateMetadata(
|
|
obj genericObject,
|
|
gvk schema.GroupVersionKind,
|
|
enforceBodies []*apirules.NamespaceRuleEnforceBody,
|
|
) (*ruleengine.Evaluation, error) {
|
|
if obj == nil || len(enforceBodies) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
entries, err := h.controlledMetadataEntries(obj, gvk, enforceBodies)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(entries) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
out := &ruleengine.Evaluation{}
|
|
|
|
for i := range entries {
|
|
entry := entries[i]
|
|
|
|
if !entry.Present {
|
|
if entry.Required {
|
|
out.Blocking = metadataRequiredDecision(entry)
|
|
|
|
return out, nil
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
evaluation, err := evaluateGenericRules(
|
|
obj,
|
|
enforceBodies,
|
|
h.metadataSet(gvk, entry),
|
|
)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
|
|
out.Append(evaluation)
|
|
|
|
//nolint:nilerr
|
|
if evaluation != nil && evaluation.BlockingError() != nil {
|
|
return out, nil
|
|
}
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func metadataRequiredDecision(entry metadataEntry) *ruleengine.Decision {
|
|
return &ruleengine.Decision{
|
|
SetName: metadataSetName(entry.Field),
|
|
EventReason: events.ReasonForbiddenMetadata,
|
|
Action: apirules.ActionTypeDeny,
|
|
Value: ruleengine.Value{
|
|
Value: "",
|
|
Path: entry.Path,
|
|
},
|
|
Message: fmt.Sprintf(
|
|
"metadata %s %q is required at %s",
|
|
entry.Field,
|
|
entry.Key,
|
|
entry.Path,
|
|
),
|
|
}
|
|
}
|
|
|
|
func (h *genericRules) metadataSet(
|
|
gvk schema.GroupVersionKind,
|
|
entry metadataEntry,
|
|
) genericRuleSet[runtime.ExpressionMatch] {
|
|
return genericRuleSet[runtime.ExpressionMatch]{
|
|
Name: metadataSetName(entry.Field),
|
|
EventReason: events.ReasonForbiddenMetadata,
|
|
|
|
Values: func(_ genericObject) []ruleengine.Value {
|
|
return []ruleengine.Value{
|
|
{
|
|
Value: entry.Value,
|
|
Path: entry.Path,
|
|
},
|
|
}
|
|
},
|
|
|
|
RulesWithError: func(enforce *apirules.NamespaceRuleEnforceBody) ([]runtime.ExpressionMatch, error) {
|
|
if enforce == nil || len(enforce.Metadata) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
var out []runtime.ExpressionMatch
|
|
|
|
for i := range enforce.Metadata {
|
|
rule := enforce.Metadata[i]
|
|
if !rule.MatchesGroupVersionKind(gvk) {
|
|
continue
|
|
}
|
|
|
|
switch entry.Field {
|
|
case metadataFieldLabel:
|
|
for selector, policy := range rule.Labels {
|
|
matched, err := h.matchesMetadataKey(selector, entry.Key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid label selector %q: %w", selector, err)
|
|
}
|
|
|
|
if matched {
|
|
out = append(out, policy.Values...)
|
|
}
|
|
}
|
|
case metadataFieldAnnotation:
|
|
for selector, policy := range rule.Annotations {
|
|
matched, err := h.matchesMetadataKey(selector, entry.Key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid annotation selector %q: %w", selector, err)
|
|
}
|
|
|
|
if matched {
|
|
out = append(out, policy.Values...)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return out, nil
|
|
},
|
|
Matches: func(match runtime.ExpressionMatch, value ruleengine.Value) (ruleengine.Match, error) {
|
|
matched, err := match.MatchesWithExpressionMatcher(h.regexCache, value.Value)
|
|
if err != nil {
|
|
return ruleengine.Match{}, err
|
|
}
|
|
|
|
return ruleengine.Match{
|
|
Matched: matched,
|
|
}, nil
|
|
},
|
|
RuleDescription: runtime.DescribeExpressionMatch,
|
|
AllowedDescription: "Allowed metadata values",
|
|
}
|
|
}
|
|
|
|
//nolint:gocognit
|
|
func (h *genericRules) controlledMetadataEntries(
|
|
obj genericObject,
|
|
gvk schema.GroupVersionKind,
|
|
enforceBodies []*apirules.NamespaceRuleEnforceBody,
|
|
) ([]metadataEntry, error) {
|
|
labels := obj.GetLabels()
|
|
annotations := obj.GetAnnotations()
|
|
|
|
seen := make(map[string]metadataEntry)
|
|
|
|
for _, enforce := range enforceBodies {
|
|
if enforce == nil || len(enforce.Metadata) == 0 {
|
|
continue
|
|
}
|
|
|
|
action := enforce.Action.OrDefault()
|
|
|
|
for i := range enforce.Metadata {
|
|
rule := enforce.Metadata[i]
|
|
if !rule.MatchesGroupVersionKind(gvk) {
|
|
continue
|
|
}
|
|
|
|
for selector, policy := range rule.Labels {
|
|
if h.managedMetadata.HasLabel(selector) {
|
|
continue
|
|
}
|
|
|
|
required := action == apirules.ActionTypeAllow && policy.Required
|
|
matchedAny := false
|
|
|
|
for key, value := range labels {
|
|
matched, err := h.matchesMetadataKey(selector, key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid label selector %q: %w", selector, err)
|
|
}
|
|
|
|
if !matched {
|
|
continue
|
|
}
|
|
|
|
if h.managedMetadata.HasLabel(key) {
|
|
continue
|
|
}
|
|
|
|
matchedAny = true
|
|
|
|
h.addMetadataEntry(seen, metadataFieldLabel, key, value, true, required)
|
|
}
|
|
|
|
if !matchedAny && required {
|
|
h.addMetadataEntry(seen, metadataFieldLabel, selector, "", false, true)
|
|
}
|
|
}
|
|
|
|
for selector, policy := range rule.Annotations {
|
|
if h.managedMetadata.HasAnnotation(selector) {
|
|
continue
|
|
}
|
|
|
|
required := action == apirules.ActionTypeAllow && policy.Required
|
|
matchedAny := false
|
|
|
|
for key, value := range annotations {
|
|
matched, err := h.matchesMetadataKey(selector, key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid label selector %q: %w", selector, err)
|
|
}
|
|
|
|
if !matched {
|
|
continue
|
|
}
|
|
|
|
if h.managedMetadata.HasAnnotation(key) {
|
|
continue
|
|
}
|
|
|
|
matchedAny = true
|
|
|
|
h.addMetadataEntry(seen, metadataFieldAnnotation, key, value, true, required)
|
|
}
|
|
|
|
if !matchedAny && required {
|
|
h.addMetadataEntry(seen, metadataFieldAnnotation, selector, "", false, true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(seen) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
out := make([]metadataEntry, 0, len(seen))
|
|
for _, entry := range seen {
|
|
out = append(out, entry)
|
|
}
|
|
|
|
sort.Slice(out, func(i, j int) bool {
|
|
return out[i].Path < out[j].Path
|
|
})
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func (h *genericRules) matchesMetadataKey(selector, key string) (bool, error) {
|
|
return h.regexCache.MatchRegex(apirules.MetadataKeyExpression(selector), key)
|
|
}
|
|
|
|
func (h *genericRules) addMetadataEntry(
|
|
seen map[string]metadataEntry,
|
|
field metadataField,
|
|
key string,
|
|
value string,
|
|
present bool,
|
|
required bool,
|
|
) {
|
|
path := metadataLabelPath(key)
|
|
if field == metadataFieldAnnotation {
|
|
path = metadataAnnotationPath(key)
|
|
}
|
|
|
|
entry := seen[path]
|
|
entry.Field = field
|
|
entry.Key = key
|
|
entry.Path = path
|
|
|
|
entry.Present = entry.Present || present
|
|
|
|
if present {
|
|
entry.Value = value
|
|
}
|
|
|
|
entry.Required = entry.Required || required
|
|
seen[path] = entry
|
|
}
|
|
|
|
func metadataSetName(field metadataField) string {
|
|
switch field {
|
|
case metadataFieldLabel:
|
|
return "metadata label"
|
|
case metadataFieldAnnotation:
|
|
return "metadata annotation"
|
|
default:
|
|
return "metadata"
|
|
}
|
|
}
|
|
|
|
func metadataLabelPath(key string) string {
|
|
return fmt.Sprintf("metadata.labels[%q]", key)
|
|
}
|
|
|
|
func metadataAnnotationPath(key string) string {
|
|
return fmt.Sprintf("metadata.annotations[%q]", key)
|
|
}
|