// 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 annotation 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) }