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:
Ricardo Katz 2023-07-22 00:32:07 -03:00 committed by GitHub
parent 86c00a2310
commit c5f348ea2e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
109 changed files with 4320 additions and 586 deletions

View file

@ -22,8 +22,10 @@ import (
"time"
networking "k8s.io/api/networking/v1"
"k8s.io/klog/v2"
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
"k8s.io/ingress-nginx/internal/ingress/errors"
ing_errors "k8s.io/ingress-nginx/internal/ingress/errors"
"k8s.io/ingress-nginx/internal/ingress/resolver"
"k8s.io/ingress-nginx/internal/net"
@ -32,6 +34,46 @@ import (
const defaultKey = "$remote_addr"
const (
globalRateLimitAnnotation = "global-rate-limit"
globalRateLimitWindowAnnotation = "global-rate-limit-window"
globalRateLimitKeyAnnotation = "global-rate-limit-key"
globalRateLimitIgnoredCidrsAnnotation = "global-rate-limit-ignored-cidrs"
)
var globalRateLimitAnnotationConfig = parser.Annotation{
Group: "ratelimit",
Annotations: parser.AnnotationFields{
globalRateLimitAnnotation: {
Validator: parser.ValidateInt,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation configures maximum allowed number of requests per window`,
},
globalRateLimitWindowAnnotation: {
Validator: parser.ValidateDuration,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `Configures a time window (i.e 1m) that the limit is applied`,
},
globalRateLimitKeyAnnotation: {
Validator: parser.ValidateRegex(*parser.NGINXVariable, true),
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskHigh,
Documentation: `This annotation Configures a key for counting the samples. Defaults to $remote_addr.
You can also combine multiple NGINX variables here, like ${remote_addr}-${http_x_api_client} which would mean the limit will be applied to
requests coming from the same API client (indicated by X-API-Client HTTP request header) with the same source IP address`,
},
globalRateLimitIgnoredCidrsAnnotation: {
Validator: parser.ValidateCIDRs,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation defines a comma separated list of IPs and CIDRs to match client IP against.
When there's a match request is not considered for rate limiting.`,
},
},
}
// Config encapsulates all global rate limit attributes
type Config struct {
Namespace string `json:"namespace"`
@ -63,12 +105,16 @@ func (l *Config) Equal(r *Config) bool {
}
type globalratelimit struct {
r resolver.Resolver
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new globalratelimit annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return globalratelimit{r}
return globalratelimit{
r: r,
annotationConfig: globalRateLimitAnnotationConfig,
}
}
// Parse extracts globalratelimit annotations from the given ingress
@ -76,8 +122,16 @@ func NewParser(r resolver.Resolver) parser.IngressAnnotation {
func (a globalratelimit) Parse(ing *networking.Ingress) (interface{}, error) {
config := &Config{}
limit, _ := parser.GetIntAnnotation("global-rate-limit", ing)
rawWindowSize, _ := parser.GetStringAnnotation("global-rate-limit-window", ing)
limit, err := parser.GetIntAnnotation(globalRateLimitAnnotation, ing, a.annotationConfig.Annotations)
if err != nil && errors.IsInvalidContent(err) {
return nil, err
}
rawWindowSize, err := parser.GetStringAnnotation(globalRateLimitWindowAnnotation, ing, a.annotationConfig.Annotations)
if err != nil && errors.IsValidationError(err) {
return config, ing_errors.LocationDenied{
Reason: fmt.Errorf("failed to parse 'global-rate-limit-window' value: %w", err),
}
}
if limit == 0 || len(rawWindowSize) == 0 {
return config, nil
@ -90,12 +144,18 @@ func (a globalratelimit) Parse(ing *networking.Ingress) (interface{}, error) {
}
}
key, _ := parser.GetStringAnnotation("global-rate-limit-key", ing)
key, err := parser.GetStringAnnotation(globalRateLimitKeyAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
klog.Warningf("invalid %s, defaulting to %s", globalRateLimitKeyAnnotation, defaultKey)
}
if len(key) == 0 {
key = defaultKey
}
rawIgnoredCIDRs, _ := parser.GetStringAnnotation("global-rate-limit-ignored-cidrs", ing)
rawIgnoredCIDRs, err := parser.GetStringAnnotation(globalRateLimitIgnoredCidrsAnnotation, ing, a.annotationConfig.Annotations)
if err != nil && errors.IsInvalidContent(err) {
return nil, err
}
ignoredCIDRs, err := net.ParseCIDRs(rawIgnoredCIDRs)
if err != nil {
return nil, err
@ -109,3 +169,12 @@ func (a globalratelimit) Parse(ing *networking.Ingress) (interface{}, error) {
return config, nil
}
func (a globalratelimit) GetDocumentation() parser.AnnotationFields {
return a.annotationConfig.Annotations
}
func (a globalratelimit) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, globalRateLimitAnnotationConfig.Annotations)
}

View file

@ -149,6 +149,22 @@ func TestGlobalRateLimiting(t *testing.T) {
},
nil,
},
{
"global-rate-limit-complex-key",
map[string]string{
annRateLimit: "100",
annRateLimitWindow: "2m",
annRateLimitKey: "${http_x_api_user}${otherinfo}",
},
&Config{
Namespace: expectedUID,
Limit: 100,
WindowSize: 120,
Key: "${http_x_api_user}${otherinfo}",
IgnoredCIDRs: make([]string, 0),
},
nil,
},
{
"incorrect duration for window",
map[string]string{
@ -157,8 +173,8 @@ func TestGlobalRateLimiting(t *testing.T) {
annRateLimitKey: "$http_x_api_user",
},
&Config{},
ing_errors.LocationDenied{
Reason: fmt.Errorf("failed to parse 'global-rate-limit-window' value: time: unknown unit \"mb\" in duration \"2mb\""),
ing_errors.ValidationError{
Reason: fmt.Errorf("failed to parse 'global-rate-limit-window' value: annotation nginx.ingress.kubernetes.io/global-rate-limit-window contains invalid value"),
},
},
}
@ -168,7 +184,7 @@ func TestGlobalRateLimiting(t *testing.T) {
i, actualErr := NewParser(mockBackend{}).Parse(ing)
if (testCase.expectedErr == nil || actualErr == nil) && testCase.expectedErr != actualErr {
t.Errorf("expected error 'nil' but got '%v'", actualErr)
t.Errorf("%s expected error '%v' but got '%v'", testCase.title, testCase.expectedErr, actualErr)
} else if testCase.expectedErr != nil && actualErr != nil &&
testCase.expectedErr.Error() != actualErr.Error() {
t.Errorf("expected error '%v' but got '%v'", testCase.expectedErr, actualErr)