mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-22 22:16:46 +00:00
* 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>
271 lines
5.6 KiB
Go
271 lines
5.6 KiB
Go
// Copyright 2020-2026 Project Capsule Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
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 := h.controlledMetadataEntries(obj, gvk, enforceBodies)
|
|
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,
|
|
},
|
|
}
|
|
},
|
|
|
|
Rules: func(enforce *apirules.NamespaceRuleEnforceBody) []runtime.ExpressionMatch {
|
|
if enforce == nil || len(enforce.Metadata) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var out []runtime.ExpressionMatch
|
|
|
|
for i := range enforce.Metadata {
|
|
rule := enforce.Metadata[i]
|
|
if !rule.MatchesGroupVersionKind(gvk) {
|
|
continue
|
|
}
|
|
|
|
switch entry.Field {
|
|
case metadataFieldLabel:
|
|
policy, ok := rule.Labels[entry.Key]
|
|
if ok {
|
|
out = append(out, policy.Values...)
|
|
}
|
|
case metadataFieldAnnotation:
|
|
policy, ok := rule.Annotations[entry.Key]
|
|
if ok {
|
|
out = append(out, policy.Values...)
|
|
}
|
|
}
|
|
}
|
|
|
|
return out
|
|
},
|
|
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",
|
|
}
|
|
}
|
|
|
|
func (h *genericRules) controlledMetadataEntries(
|
|
obj genericObject,
|
|
gvk schema.GroupVersionKind,
|
|
enforceBodies []*apirules.NamespaceRuleEnforceBody,
|
|
) []metadataEntry {
|
|
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 key, policy := range rule.Labels {
|
|
if h.managedMetadata.HasLabel(key) {
|
|
continue
|
|
}
|
|
|
|
value, exists := labels[key]
|
|
required := action == apirules.ActionTypeAllow && policy.Required
|
|
|
|
if !exists && !required {
|
|
continue
|
|
}
|
|
|
|
path := metadataLabelPath(key)
|
|
|
|
entry := seen[path]
|
|
entry.Field = metadataFieldLabel
|
|
entry.Key = key
|
|
entry.Path = path
|
|
entry.Present = exists
|
|
entry.Value = value
|
|
entry.Required = entry.Required || required
|
|
|
|
seen[path] = entry
|
|
}
|
|
|
|
for key, policy := range rule.Annotations {
|
|
if h.managedMetadata.HasAnnotation(key) {
|
|
continue
|
|
}
|
|
|
|
value, exists := annotations[key]
|
|
required := action == apirules.ActionTypeAllow && policy.Required
|
|
|
|
if !exists && !required {
|
|
continue
|
|
}
|
|
|
|
path := metadataAnnotationPath(key)
|
|
|
|
entry := seen[path]
|
|
entry.Field = metadataFieldAnnotation
|
|
entry.Key = key
|
|
entry.Path = path
|
|
entry.Present = exists
|
|
entry.Value = value
|
|
entry.Required = entry.Required || required
|
|
|
|
seen[path] = entry
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(seen) == 0 {
|
|
return 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
|
|
}
|
|
|
|
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)
|
|
}
|