Implement annotation validation (#9673)
* Add validation to all annotations * Add annotation validation for fcgi * Fix reviews and fcgi e2e * Add flag to disable cross namespace validation * Add risk, flag for validation, tests * Add missing formating * Enable validation by default on tests * Test validation flag * remove ajp from list * Finalize validation changes * Add validations to CI * Update helm docs * Fix code review * Use a better name for annotation risk
This commit is contained in:
parent
86c00a2310
commit
c5f348ea2e
109 changed files with 4320 additions and 586 deletions
|
|
@ -29,20 +29,79 @@ import (
|
|||
)
|
||||
|
||||
// DefaultAnnotationsPrefix defines the common prefix used in the nginx ingress controller
|
||||
const DefaultAnnotationsPrefix = "nginx.ingress.kubernetes.io"
|
||||
const (
|
||||
DefaultAnnotationsPrefix = "nginx.ingress.kubernetes.io"
|
||||
DefaultEnableAnnotationValidation = true
|
||||
)
|
||||
|
||||
var (
|
||||
// AnnotationsPrefix is the mutable attribute that the controller explicitly refers to
|
||||
AnnotationsPrefix = DefaultAnnotationsPrefix
|
||||
// Enable is the mutable attribute for enabling or disabling the validation functions
|
||||
EnableAnnotationValidation = DefaultEnableAnnotationValidation
|
||||
)
|
||||
|
||||
// AnnotationGroup defines the group that this annotation may belong
|
||||
// eg.: Security, Snippets, Rewrite, etc
|
||||
type AnnotationGroup string
|
||||
|
||||
// AnnotationScope defines which scope this annotation applies. May be to the whole
|
||||
// ingress, per location, etc
|
||||
type AnnotationScope string
|
||||
|
||||
var (
|
||||
AnnotationScopeLocation AnnotationScope = "location"
|
||||
AnnotationScopeIngress AnnotationScope = "ingress"
|
||||
)
|
||||
|
||||
// AnnotationRisk is a subset of risk that an annotation may represent.
|
||||
// Based on the Risk, the admin will be able to allow or disallow users to set it
|
||||
// on their ingress objects
|
||||
type AnnotationRisk int
|
||||
|
||||
type AnnotationFields map[string]AnnotationConfig
|
||||
|
||||
// AnnotationConfig defines the configuration that a single annotation field
|
||||
// has, with the Validator and the documentation of this field.
|
||||
type AnnotationConfig struct {
|
||||
// Validator defines a function to validate the annotation value
|
||||
Validator AnnotationValidator
|
||||
// Documentation defines a user facing documentation for this annotation. This
|
||||
// field will be used to auto generate documentations
|
||||
Documentation string
|
||||
// Risk defines a risk of this annotation being exposed to the user. Annotations
|
||||
// with bool fields, or to set timeout are usually low risk. Annotations that allows
|
||||
// string input without a limited set of options may represent a high risk
|
||||
Risk AnnotationRisk
|
||||
|
||||
// Scope defines which scope this annotation applies, may be to location, to an Ingress object, etc
|
||||
Scope AnnotationScope
|
||||
|
||||
// AnnotationAliases defines other names this annotation may have.
|
||||
AnnotationAliases []string
|
||||
}
|
||||
|
||||
// Annotation defines an annotation feature an Ingress may have.
|
||||
// It should contain the internal resolver, and all the annotations
|
||||
// with configs and Validators that should be used for each Annotation
|
||||
type Annotation struct {
|
||||
// Annotations contains all the annotations that belong to this feature
|
||||
Annotations AnnotationFields
|
||||
// Group defines which annotation group this feature belongs to
|
||||
Group AnnotationGroup
|
||||
}
|
||||
|
||||
// IngressAnnotation has a method to parse annotations located in Ingress
|
||||
type IngressAnnotation interface {
|
||||
Parse(ing *networking.Ingress) (interface{}, error)
|
||||
GetDocumentation() AnnotationFields
|
||||
Validate(anns map[string]string) error
|
||||
}
|
||||
|
||||
type ingAnnotations map[string]string
|
||||
|
||||
// TODO: We already parse all of this on checkAnnotation and can just do a parse over the
|
||||
// value
|
||||
func (a ingAnnotations) parseBool(name string) (bool, error) {
|
||||
val, ok := a[name]
|
||||
if ok {
|
||||
|
|
@ -92,21 +151,9 @@ func (a ingAnnotations) parseFloat32(name string) (float32, error) {
|
|||
return 0, errors.ErrMissingAnnotations
|
||||
}
|
||||
|
||||
func checkAnnotation(name string, ing *networking.Ingress) error {
|
||||
if ing == nil || len(ing.GetAnnotations()) == 0 {
|
||||
return errors.ErrMissingAnnotations
|
||||
}
|
||||
if name == "" {
|
||||
return errors.ErrInvalidAnnotationName
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetBoolAnnotation extracts a boolean from an Ingress annotation
|
||||
func GetBoolAnnotation(name string, ing *networking.Ingress) (bool, error) {
|
||||
v := GetAnnotationWithPrefix(name)
|
||||
err := checkAnnotation(v, ing)
|
||||
func GetBoolAnnotation(name string, ing *networking.Ingress, fields AnnotationFields) (bool, error) {
|
||||
v, err := checkAnnotation(name, ing, fields)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
|
@ -114,9 +161,8 @@ func GetBoolAnnotation(name string, ing *networking.Ingress) (bool, error) {
|
|||
}
|
||||
|
||||
// GetStringAnnotation extracts a string from an Ingress annotation
|
||||
func GetStringAnnotation(name string, ing *networking.Ingress) (string, error) {
|
||||
v := GetAnnotationWithPrefix(name)
|
||||
err := checkAnnotation(v, ing)
|
||||
func GetStringAnnotation(name string, ing *networking.Ingress, fields AnnotationFields) (string, error) {
|
||||
v, err := checkAnnotation(name, ing, fields)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -125,9 +171,8 @@ func GetStringAnnotation(name string, ing *networking.Ingress) (string, error) {
|
|||
}
|
||||
|
||||
// GetIntAnnotation extracts an int from an Ingress annotation
|
||||
func GetIntAnnotation(name string, ing *networking.Ingress) (int, error) {
|
||||
v := GetAnnotationWithPrefix(name)
|
||||
err := checkAnnotation(v, ing)
|
||||
func GetIntAnnotation(name string, ing *networking.Ingress, fields AnnotationFields) (int, error) {
|
||||
v, err := checkAnnotation(name, ing, fields)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
|
@ -135,9 +180,8 @@ func GetIntAnnotation(name string, ing *networking.Ingress) (int, error) {
|
|||
}
|
||||
|
||||
// GetFloatAnnotation extracts a float32 from an Ingress annotation
|
||||
func GetFloatAnnotation(name string, ing *networking.Ingress) (float32, error) {
|
||||
v := GetAnnotationWithPrefix(name)
|
||||
err := checkAnnotation(v, ing)
|
||||
func GetFloatAnnotation(name string, ing *networking.Ingress, fields AnnotationFields) (float32, error) {
|
||||
v, err := checkAnnotation(name, ing, fields)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
|
@ -149,6 +193,23 @@ func GetAnnotationWithPrefix(suffix string) string {
|
|||
return fmt.Sprintf("%v/%v", AnnotationsPrefix, suffix)
|
||||
}
|
||||
|
||||
func TrimAnnotationPrefix(annotation string) string {
|
||||
return strings.TrimPrefix(annotation, AnnotationsPrefix+"/")
|
||||
}
|
||||
|
||||
func StringRiskToRisk(risk string) AnnotationRisk {
|
||||
switch strings.ToLower(risk) {
|
||||
case "critical":
|
||||
return AnnotationRiskCritical
|
||||
case "high":
|
||||
return AnnotationRiskHigh
|
||||
case "medium":
|
||||
return AnnotationRiskMedium
|
||||
default:
|
||||
return AnnotationRiskLow
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeString(input string) string {
|
||||
trimmedContent := []string{}
|
||||
for _, line := range strings.Split(input, "\n") {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ func buildIngress() *networking.Ingress {
|
|||
func TestGetBoolAnnotation(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
_, err := GetBoolAnnotation("", nil)
|
||||
_, err := GetBoolAnnotation("", nil, nil)
|
||||
if err == nil {
|
||||
t.Errorf("expected error but retuned nil")
|
||||
}
|
||||
|
|
@ -59,8 +59,8 @@ func TestGetBoolAnnotation(t *testing.T) {
|
|||
|
||||
for _, test := range tests {
|
||||
data[GetAnnotationWithPrefix(test.field)] = test.value
|
||||
|
||||
u, err := GetBoolAnnotation(test.field, ing)
|
||||
ing.SetAnnotations(data)
|
||||
u, err := GetBoolAnnotation(test.field, ing, nil)
|
||||
if test.expErr {
|
||||
if err == nil {
|
||||
t.Errorf("%v: expected error but retuned nil", test.name)
|
||||
|
|
@ -68,7 +68,7 @@ func TestGetBoolAnnotation(t *testing.T) {
|
|||
continue
|
||||
}
|
||||
if u != test.exp {
|
||||
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.name, test.exp, u)
|
||||
t.Errorf("%v: expected \"%v\" but \"%v\" was returned, %+v", test.name, test.exp, u, ing)
|
||||
}
|
||||
|
||||
delete(data, test.field)
|
||||
|
|
@ -78,7 +78,7 @@ func TestGetBoolAnnotation(t *testing.T) {
|
|||
func TestGetStringAnnotation(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
_, err := GetStringAnnotation("", nil)
|
||||
_, err := GetStringAnnotation("", nil, nil)
|
||||
if err == nil {
|
||||
t.Errorf("expected error but none returned")
|
||||
}
|
||||
|
|
@ -109,7 +109,7 @@ rewrite (?i)/arcgis/services/Utilities/Geometry/GeometryServer(.*)$ /arcgis/serv
|
|||
for _, test := range tests {
|
||||
data[GetAnnotationWithPrefix(test.field)] = test.value
|
||||
|
||||
s, err := GetStringAnnotation(test.field, ing)
|
||||
s, err := GetStringAnnotation(test.field, ing, nil)
|
||||
if test.expErr {
|
||||
if err == nil {
|
||||
t.Errorf("%v: expected error but none returned", test.name)
|
||||
|
|
@ -133,7 +133,7 @@ rewrite (?i)/arcgis/services/Utilities/Geometry/GeometryServer(.*)$ /arcgis/serv
|
|||
func TestGetFloatAnnotation(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
_, err := GetFloatAnnotation("", nil)
|
||||
_, err := GetFloatAnnotation("", nil, nil)
|
||||
if err == nil {
|
||||
t.Errorf("expected error but retuned nil")
|
||||
}
|
||||
|
|
@ -156,7 +156,7 @@ func TestGetFloatAnnotation(t *testing.T) {
|
|||
for _, test := range tests {
|
||||
data[GetAnnotationWithPrefix(test.field)] = test.value
|
||||
|
||||
s, err := GetFloatAnnotation(test.field, ing)
|
||||
s, err := GetFloatAnnotation(test.field, ing, nil)
|
||||
if test.expErr {
|
||||
if err == nil {
|
||||
t.Errorf("%v: expected error but retuned nil", test.name)
|
||||
|
|
@ -174,7 +174,7 @@ func TestGetFloatAnnotation(t *testing.T) {
|
|||
func TestGetIntAnnotation(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
_, err := GetIntAnnotation("", nil)
|
||||
_, err := GetIntAnnotation("", nil, nil)
|
||||
if err == nil {
|
||||
t.Errorf("expected error but retuned nil")
|
||||
}
|
||||
|
|
@ -196,7 +196,7 @@ func TestGetIntAnnotation(t *testing.T) {
|
|||
for _, test := range tests {
|
||||
data[GetAnnotationWithPrefix(test.field)] = test.value
|
||||
|
||||
s, err := GetIntAnnotation(test.field, ing)
|
||||
s, err := GetIntAnnotation(test.field, ing, nil)
|
||||
if test.expErr {
|
||||
if err == nil {
|
||||
t.Errorf("%v: expected error but retuned nil", test.name)
|
||||
|
|
@ -224,6 +224,7 @@ func TestStringToURL(t *testing.T) {
|
|||
}{
|
||||
{"empty", "", "url scheme is empty", nil, true},
|
||||
{"no scheme", "bar", "url scheme is empty", nil, true},
|
||||
{"invalid parse", "://lala.com", "://lala.com is not a valid URL: parse \"://lala.com\": missing protocol scheme", nil, true},
|
||||
{"invalid host", "http://", "url host is empty", nil, true},
|
||||
{"invalid host (multiple dots)", "http://foo..bar.com", "invalid url host", nil, true},
|
||||
{"valid URL", validURL, "", validParsedURL, false},
|
||||
|
|
|
|||
239
internal/ingress/annotations/parser/validators.go
Normal file
239
internal/ingress/annotations/parser/validators.go
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
/*
|
||||
Copyright 2023 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
machineryvalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||
ing_errors "k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/net"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
type AnnotationValidator func(string) error
|
||||
|
||||
const (
|
||||
AnnotationRiskLow AnnotationRisk = iota
|
||||
AnnotationRiskMedium
|
||||
AnnotationRiskHigh
|
||||
AnnotationRiskCritical
|
||||
)
|
||||
|
||||
var (
|
||||
alphaNumericChars = `\-\.\_\~a-zA-Z0-9\/:`
|
||||
extendedAlphaNumeric = alphaNumericChars + ", "
|
||||
regexEnabledChars = regexp.QuoteMeta(`^$[](){}*+?|&=\`)
|
||||
urlEnabledChars = regexp.QuoteMeta(`:?&=`)
|
||||
)
|
||||
|
||||
// IsValidRegex checks if the tested string can be used as a regex, but without any weird character.
|
||||
// It includes regex characters for paths that may contain regexes
|
||||
var IsValidRegex = regexp.MustCompile("^[/" + alphaNumericChars + regexEnabledChars + "]*$")
|
||||
|
||||
// SizeRegex validates sizes understood by NGINX, like 1000, 100k, 1000M
|
||||
var SizeRegex = regexp.MustCompile("^(?i)[0-9]+[bkmg]?$")
|
||||
|
||||
// URLRegex is used to validate a URL but with only a specific set of characters:
|
||||
// It is alphanumericChar + ":", "?", "&"
|
||||
// A valid URL would be proto://something.com:port/something?arg=param
|
||||
var (
|
||||
// URLIsValidRegex is used on full URLs, containing query strings (:, ? and &)
|
||||
URLIsValidRegex = regexp.MustCompile("^[" + alphaNumericChars + urlEnabledChars + "]*$")
|
||||
// BasicChars is alphanumeric and ".", "-", "_", "~" and ":", usually used on simple host:port/path composition.
|
||||
// This combination can also be used on fields that may contain characters like / (as ns/name)
|
||||
BasicCharsRegex = regexp.MustCompile("^[/" + alphaNumericChars + "]*$")
|
||||
// ExtendedChars is alphanumeric and ".", "-", "_", "~" and ":" plus "," and spaces, usually used on simple host:port/path composition
|
||||
ExtendedCharsRegex = regexp.MustCompile("^[/" + extendedAlphaNumeric + "]*$")
|
||||
// CharsWithSpace is like basic chars, but includes the space character
|
||||
CharsWithSpace = regexp.MustCompile("^[/" + alphaNumericChars + " ]*$")
|
||||
// NGINXVariable allows entries with alphanumeric characters, -, _ and the special "$"
|
||||
NGINXVariable = regexp.MustCompile(`^[A-Za-z0-9\-\_\$\{\}]*$`)
|
||||
// RegexPathWithCapture allows entries that SHOULD start with "/" and may contain alphanumeric + capture
|
||||
// character for regex based paths, like /something/$1/anything/$2
|
||||
RegexPathWithCapture = regexp.MustCompile(`^/[` + alphaNumericChars + `\/\$]*$`)
|
||||
// HeadersVariable defines a regex that allows headers separated by comma
|
||||
HeadersVariable = regexp.MustCompile(`^[A-Za-z0-9-_, ]*$`)
|
||||
// URLWithNginxVariableRegex defines a url that can contain nginx variables.
|
||||
// It is a risky operation
|
||||
URLWithNginxVariableRegex = regexp.MustCompile("^[" + alphaNumericChars + urlEnabledChars + "$]*$")
|
||||
)
|
||||
|
||||
// ValidateArrayOfServerName validates if all fields on a Server name annotation are
|
||||
// regexes. They can be *.something*, ~^www\d+\.example\.com$ but not fancy character
|
||||
func ValidateArrayOfServerName(value string) error {
|
||||
for _, fqdn := range strings.Split(value, ",") {
|
||||
if err := ValidateServerName(fqdn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateServerName validates if the passed value is an acceptable server name. The server name
|
||||
// can contain regex characters, as those are accepted values on nginx configuration
|
||||
func ValidateServerName(value string) error {
|
||||
value = strings.TrimSpace(value)
|
||||
if !IsValidRegex.MatchString(value) {
|
||||
return fmt.Errorf("value %s is invalid server name", value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateRegex receives a regex as an argument and uses it to validate
|
||||
// the value of the field.
|
||||
// Annotation can define if the spaces should be trimmed before validating the value
|
||||
func ValidateRegex(regex regexp.Regexp, removeSpace bool) AnnotationValidator {
|
||||
return func(s string) error {
|
||||
if removeSpace {
|
||||
s = strings.ReplaceAll(s, " ", "")
|
||||
}
|
||||
if !regex.MatchString(s) {
|
||||
return fmt.Errorf("value %s is invalid", s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateOptions receives an array of valid options that can be the value of annotation.
|
||||
// If no valid option is found, it will return an error
|
||||
func ValidateOptions(options []string, caseSensitive bool, trimSpace bool) AnnotationValidator {
|
||||
return func(s string) error {
|
||||
if trimSpace {
|
||||
s = strings.TrimSpace(s)
|
||||
}
|
||||
if !caseSensitive {
|
||||
s = strings.ToLower(s)
|
||||
}
|
||||
for _, option := range options {
|
||||
if s == option {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("value does not match any valid option")
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateBool validates if the specified value is a bool
|
||||
func ValidateBool(value string) error {
|
||||
_, err := strconv.ParseBool(value)
|
||||
return err
|
||||
}
|
||||
|
||||
// ValidateInt validates if the specified value is an integer
|
||||
func ValidateInt(value string) error {
|
||||
_, err := strconv.Atoi(value)
|
||||
return err
|
||||
}
|
||||
|
||||
// ValidateCIDRs validates if the specified value is an array of IPs and CIDRs
|
||||
func ValidateCIDRs(value string) error {
|
||||
_, err := net.ParseCIDRs(value)
|
||||
return err
|
||||
}
|
||||
|
||||
// ValidateDuration validates if the specified value is a valid time
|
||||
func ValidateDuration(value string) error {
|
||||
_, err := time.ParseDuration(value)
|
||||
return err
|
||||
}
|
||||
|
||||
// ValidateNull always return null values and should not be widely used.
|
||||
// It is used on the "snippet" annotations, as it is up to the admin to allow its
|
||||
// usage, knowing it can be critical!
|
||||
func ValidateNull(value string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateServiceName validates if a provided service name is a valid string
|
||||
func ValidateServiceName(value string) error {
|
||||
errs := machineryvalidation.NameIsDNS1035Label(value, false)
|
||||
if len(errs) != 0 {
|
||||
return fmt.Errorf("annotation does not contain a valid service name: %+v", errs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkAnnotations will check each annotation for:
|
||||
// 1 - Does it contain the internal validation and docs config?
|
||||
// 2 - Does the ingress contains annotations? (validate null pointers)
|
||||
// 3 - Does it contains a validator? Should it contain a validator (not containing is a bug!)
|
||||
// 4 - Does the annotation contain aliases? So we should use if the alias is defined an the annotation not.
|
||||
// 4 - Runs the validator on the value
|
||||
// It will return the full annotation name if all is fine
|
||||
func checkAnnotation(name string, ing *networking.Ingress, fields AnnotationFields) (string, error) {
|
||||
var validateFunc AnnotationValidator
|
||||
if fields != nil {
|
||||
config, ok := fields[name]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("annotation does not contain a valid internal configuration, this is an Ingress Controller issue! Please raise an issue on github.com/kubernetes/ingress-nginx")
|
||||
}
|
||||
validateFunc = config.Validator
|
||||
}
|
||||
|
||||
if ing == nil || len(ing.GetAnnotations()) == 0 {
|
||||
return "", ing_errors.ErrMissingAnnotations
|
||||
}
|
||||
|
||||
annotationFullName := GetAnnotationWithPrefix(name)
|
||||
if annotationFullName == "" {
|
||||
return "", ing_errors.ErrInvalidAnnotationName
|
||||
}
|
||||
|
||||
annotationValue := ing.GetAnnotations()[annotationFullName]
|
||||
if fields != nil {
|
||||
if validateFunc == nil {
|
||||
return "", fmt.Errorf("annotation does not contain a validator. This is an ingress-controller bug. Please open an issue")
|
||||
}
|
||||
if annotationValue == "" {
|
||||
for _, annotationAlias := range fields[name].AnnotationAliases {
|
||||
tempAnnotationFullName := GetAnnotationWithPrefix(annotationAlias)
|
||||
if aliasVal := ing.GetAnnotations()[tempAnnotationFullName]; aliasVal != "" {
|
||||
annotationValue = aliasVal
|
||||
annotationFullName = tempAnnotationFullName
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// We don't run validation against empty values
|
||||
if EnableAnnotationValidation && annotationValue != "" {
|
||||
if err := validateFunc(annotationValue); err != nil {
|
||||
klog.Warningf("validation error on ingress %s/%s: annotation %s contains invalid value %s", ing.GetNamespace(), ing.GetName(), name, annotationValue)
|
||||
return "", ing_errors.NewValidationError(annotationFullName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return annotationFullName, nil
|
||||
}
|
||||
|
||||
func CheckAnnotationRisk(annotations map[string]string, maxrisk AnnotationRisk, config AnnotationFields) error {
|
||||
var err error
|
||||
for annotation := range annotations {
|
||||
annPure := TrimAnnotationPrefix(annotation)
|
||||
if cfg, ok := config[annPure]; ok && cfg.Risk > maxrisk {
|
||||
err = errors.Join(err, fmt.Errorf("annotation %s is too risky for environment", annotation))
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
310
internal/ingress/annotations/parser/validators_test.go
Normal file
310
internal/ingress/annotations/parser/validators_test.go
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
/*
|
||||
Copyright 2023 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestValidateArrayOfServerName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "should accept common name",
|
||||
value: "something.com,anything.com",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should accept wildcard name",
|
||||
value: "*.something.com,otherthing.com",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should allow names with spaces between array and some regexes",
|
||||
value: `~^www\d+\.example\.com$,something.com`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should allow names with regexes",
|
||||
value: `http://some.test.env.com:2121/$someparam=1&$someotherparam=2`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should allow names with wildcard in middle common name",
|
||||
value: "*.so*mething.com,bla.com",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should deny names with weird characters",
|
||||
value: "something.com,lolo;xpto.com,nothing.com",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := ValidateArrayOfServerName(tt.value); (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateArrayOfServerName() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_checkAnnotation(t *testing.T) {
|
||||
type args struct {
|
||||
name string
|
||||
ing *networking.Ingress
|
||||
fields AnnotationFields
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "null ingress should error",
|
||||
want: "",
|
||||
args: args{
|
||||
name: "some-random-annotation",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "not having a validator for a specific annotation is a bug",
|
||||
want: "",
|
||||
args: args{
|
||||
name: "some-new-invalid-annotation",
|
||||
ing: &networking.Ingress{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
GetAnnotationWithPrefix("some-new-invalid-annotation"): "xpto",
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: AnnotationFields{
|
||||
"otherannotation": AnnotationConfig{
|
||||
Validator: func(value string) error { return nil },
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "annotationconfig found and no validation func defined on annotation is a bug",
|
||||
want: "",
|
||||
args: args{
|
||||
name: "some-new-invalid-annotation",
|
||||
ing: &networking.Ingress{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
GetAnnotationWithPrefix("some-new-invalid-annotation"): "xpto",
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: AnnotationFields{
|
||||
"some-new-invalid-annotation": AnnotationConfig{},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no annotation can turn into a null pointer and should fail",
|
||||
want: "",
|
||||
args: args{
|
||||
name: "some-new-invalid-annotation",
|
||||
ing: &networking.Ingress{
|
||||
ObjectMeta: v1.ObjectMeta{},
|
||||
},
|
||||
fields: AnnotationFields{
|
||||
"some-new-invalid-annotation": AnnotationConfig{},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no AnnotationField config should bypass validations",
|
||||
want: GetAnnotationWithPrefix("some-valid-annotation"),
|
||||
args: args{
|
||||
name: "some-valid-annotation",
|
||||
ing: &networking.Ingress{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
GetAnnotationWithPrefix("some-valid-annotation"): "xpto",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "annotation with invalid value should fail",
|
||||
want: "",
|
||||
args: args{
|
||||
name: "some-new-annotation",
|
||||
ing: &networking.Ingress{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
GetAnnotationWithPrefix("some-new-annotation"): "xpto1",
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: AnnotationFields{
|
||||
"some-new-annotation": AnnotationConfig{
|
||||
Validator: func(value string) error {
|
||||
if value != "xpto" {
|
||||
return fmt.Errorf("this is an error")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "annotation with valid value should pass",
|
||||
want: GetAnnotationWithPrefix("some-other-annotation"),
|
||||
args: args{
|
||||
name: "some-other-annotation",
|
||||
ing: &networking.Ingress{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
GetAnnotationWithPrefix("some-other-annotation"): "xpto",
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: AnnotationFields{
|
||||
"some-other-annotation": AnnotationConfig{
|
||||
Validator: func(value string) error {
|
||||
if value != "xpto" {
|
||||
return fmt.Errorf("this is an error")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := checkAnnotation(tt.args.name, tt.args.ing, tt.args.fields)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("checkAnnotation() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("checkAnnotation() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckAnnotationRisk(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
annotations map[string]string
|
||||
maxrisk AnnotationRisk
|
||||
config AnnotationFields
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "high risk should not be accepted with maximum medium",
|
||||
maxrisk: AnnotationRiskMedium,
|
||||
annotations: map[string]string{
|
||||
"nginx.ingress.kubernetes.io/bla": "blo",
|
||||
"nginx.ingress.kubernetes.io/bli": "bl3",
|
||||
},
|
||||
config: AnnotationFields{
|
||||
"bla": {
|
||||
Risk: AnnotationRiskHigh,
|
||||
},
|
||||
"bli": {
|
||||
Risk: AnnotationRiskMedium,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "high risk should be accepted with maximum critical",
|
||||
maxrisk: AnnotationRiskCritical,
|
||||
annotations: map[string]string{
|
||||
"nginx.ingress.kubernetes.io/bla": "blo",
|
||||
"nginx.ingress.kubernetes.io/bli": "bl3",
|
||||
},
|
||||
config: AnnotationFields{
|
||||
"bla": {
|
||||
Risk: AnnotationRiskHigh,
|
||||
},
|
||||
"bli": {
|
||||
Risk: AnnotationRiskMedium,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "low risk should be accepted with maximum low",
|
||||
maxrisk: AnnotationRiskLow,
|
||||
annotations: map[string]string{
|
||||
"nginx.ingress.kubernetes.io/bla": "blo",
|
||||
"nginx.ingress.kubernetes.io/bli": "bl3",
|
||||
},
|
||||
config: AnnotationFields{
|
||||
"bla": {
|
||||
Risk: AnnotationRiskLow,
|
||||
},
|
||||
"bli": {
|
||||
Risk: AnnotationRiskLow,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "critical risk should be accepted with maximum critical",
|
||||
maxrisk: AnnotationRiskCritical,
|
||||
annotations: map[string]string{
|
||||
"nginx.ingress.kubernetes.io/bla": "blo",
|
||||
"nginx.ingress.kubernetes.io/bli": "bl3",
|
||||
},
|
||||
config: AnnotationFields{
|
||||
"bla": {
|
||||
Risk: AnnotationRiskCritical,
|
||||
},
|
||||
"bli": {
|
||||
Risk: AnnotationRiskCritical,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := CheckAnnotationRisk(tt.annotations, tt.maxrisk, tt.config); (err != nil) != tt.wantErr {
|
||||
t.Errorf("CheckAnnotationRisk() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue