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
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue