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
|
|
@ -24,6 +24,7 @@ import (
|
|||
"k8s.io/klog/v2"
|
||||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
ing_errors "k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
|
|
@ -31,6 +32,118 @@ import (
|
|||
"k8s.io/ingress-nginx/pkg/util/sets"
|
||||
)
|
||||
|
||||
const (
|
||||
authReqURLAnnotation = "auth-url"
|
||||
authReqMethodAnnotation = "auth-method"
|
||||
authReqSigninAnnotation = "auth-signin"
|
||||
authReqSigninRedirParamAnnotation = "auth-signin-redirect-param"
|
||||
authReqSnippetAnnotation = "auth-snippet"
|
||||
authReqCacheKeyAnnotation = "auth-cache-key"
|
||||
authReqKeepaliveAnnotation = "auth-keepalive"
|
||||
authReqKeepaliveRequestsAnnotation = "auth-keepalive-requests"
|
||||
authReqKeepaliveTimeout = "auth-keepalive-timeout"
|
||||
authReqCacheDuration = "auth-cache-duration"
|
||||
authReqResponseHeadersAnnotation = "auth-response-headers"
|
||||
authReqProxySetHeadersAnnotation = "auth-proxy-set-headers"
|
||||
authReqRequestRedirectAnnotation = "auth-request-redirect"
|
||||
authReqAlwaysSetCookieAnnotation = "auth-always-set-cookie"
|
||||
|
||||
// This should be exported as it is imported by other packages
|
||||
AuthSecretAnnotation = "auth-secret"
|
||||
)
|
||||
|
||||
var authReqAnnotations = parser.Annotation{
|
||||
Group: "authentication",
|
||||
Annotations: parser.AnnotationFields{
|
||||
authReqURLAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.URLWithNginxVariableRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskHigh,
|
||||
Documentation: `This annotation allows to indicate the URL where the HTTP request should be sent`,
|
||||
},
|
||||
authReqMethodAnnotation: {
|
||||
Validator: parser.ValidateRegex(*methodsRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation allows to specify the HTTP method to use`,
|
||||
},
|
||||
authReqSigninAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.URLWithNginxVariableRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskHigh,
|
||||
Documentation: `This annotation allows to specify the location of the error page`,
|
||||
},
|
||||
authReqSigninRedirParamAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation allows to specify the URL parameter in the error page which should contain the original URL for a failed signin request`,
|
||||
},
|
||||
authReqSnippetAnnotation: {
|
||||
Validator: parser.ValidateNull,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskCritical,
|
||||
Documentation: `This annotation allows to specify a custom snippet to use with external authentication`,
|
||||
},
|
||||
authReqCacheKeyAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.NGINXVariable, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation enables caching for auth requests.`,
|
||||
},
|
||||
authReqKeepaliveAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation specifies the maximum number of keepalive connections to auth-url. Only takes effect when no variables are used in the host part of the URL`,
|
||||
},
|
||||
authReqKeepaliveRequestsAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines the maximum number of requests that can be served through one keepalive connection`,
|
||||
},
|
||||
authReqKeepaliveTimeout: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation specifies a duration in seconds which an idle keepalive connection to an upstream server will stay open`,
|
||||
},
|
||||
authReqCacheDuration: {
|
||||
Validator: parser.ValidateRegex(*parser.ExtendedCharsRegex, false),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation allows to specify a caching time for auth responses based on their response codes, e.g. 200 202 30m`,
|
||||
},
|
||||
authReqResponseHeadersAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.HeadersVariable, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation sets the headers to pass to backend once authentication request completes. They should be separated by comma.`,
|
||||
},
|
||||
authReqProxySetHeadersAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation sets the name of a ConfigMap that specifies headers to pass to the authentication service.
|
||||
Only ConfigMaps on the same namespace are allowed`,
|
||||
},
|
||||
authReqRequestRedirectAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation allows to specify the X-Auth-Request-Redirect header value`,
|
||||
},
|
||||
authReqAlwaysSetCookieAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables setting a cookie returned by auth request.
|
||||
By default, the cookie will be set only if an upstream reports with the code 200, 201, 204, 206, 301, 302, 303, 304, 307, or 308`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Config returns external authentication configuration for an Ingress rule
|
||||
type Config struct {
|
||||
URL string `json:"url"`
|
||||
|
|
@ -121,7 +234,7 @@ func (e1 *Config) Equal(e2 *Config) bool {
|
|||
}
|
||||
|
||||
var (
|
||||
methods = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "CONNECT", "OPTIONS", "TRACE"}
|
||||
methodsRegex = regexp.MustCompile("(GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|OPTIONS|TRACE)")
|
||||
headerRegexp = regexp.MustCompile(`^[a-zA-Z\d\-_]+$`)
|
||||
statusCodeRegex = regexp.MustCompile(`^[\d]{3}$`)
|
||||
durationRegex = regexp.MustCompile(`^[\d]+(ms|s|m|h|d|w|M|y)$`) // see http://nginx.org/en/docs/syntax.html
|
||||
|
|
@ -129,16 +242,7 @@ var (
|
|||
|
||||
// ValidMethod checks is the provided string a valid HTTP method
|
||||
func ValidMethod(method string) bool {
|
||||
if len(method) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, m := range methods {
|
||||
if method == m {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return methodsRegex.MatchString(method)
|
||||
}
|
||||
|
||||
// ValidHeader checks is the provided string satisfies the header's name regex
|
||||
|
|
@ -173,19 +277,23 @@ func ValidCacheDuration(duration string) bool {
|
|||
}
|
||||
|
||||
type authReq struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new authentication request annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return authReq{r}
|
||||
return authReq{
|
||||
r: r,
|
||||
annotationConfig: authReqAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
// rule used to use an Config URL as source for authentication
|
||||
func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
// Required Parameters
|
||||
urlString, err := parser.GetStringAnnotation("auth-url", ing)
|
||||
urlString, err := parser.GetStringAnnotation(authReqURLAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -195,33 +303,44 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
return nil, ing_errors.LocationDenied{Reason: fmt.Errorf("could not parse auth-url annotation: %v", err)}
|
||||
}
|
||||
|
||||
authMethod, _ := parser.GetStringAnnotation("auth-method", ing)
|
||||
if len(authMethod) != 0 && !ValidMethod(authMethod) {
|
||||
return nil, ing_errors.NewLocationDenied("invalid HTTP method")
|
||||
authMethod, err := parser.GetStringAnnotation(authReqMethodAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if ing_errors.IsValidationError(err) {
|
||||
return nil, ing_errors.NewLocationDenied("invalid HTTP method")
|
||||
}
|
||||
}
|
||||
|
||||
// Optional Parameters
|
||||
signIn, err := parser.GetStringAnnotation("auth-signin", ing)
|
||||
signIn, err := parser.GetStringAnnotation(authReqSigninAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if ing_errors.IsValidationError(err) {
|
||||
klog.Warningf("%s value is invalid: %s", authReqSigninAnnotation, err)
|
||||
}
|
||||
klog.V(3).InfoS("auth-signin annotation is undefined and will not be set")
|
||||
}
|
||||
|
||||
signInRedirectParam, err := parser.GetStringAnnotation("auth-signin-redirect-param", ing)
|
||||
signInRedirectParam, err := parser.GetStringAnnotation(authReqSigninRedirParamAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if ing_errors.IsValidationError(err) {
|
||||
klog.Warningf("%s value is invalid: %s", authReqSigninRedirParamAnnotation, err)
|
||||
}
|
||||
klog.V(3).Infof("auth-signin-redirect-param annotation is undefined and will not be set")
|
||||
}
|
||||
|
||||
authSnippet, err := parser.GetStringAnnotation("auth-snippet", ing)
|
||||
authSnippet, err := parser.GetStringAnnotation(authReqSnippetAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("auth-snippet annotation is undefined and will not be set")
|
||||
}
|
||||
|
||||
authCacheKey, err := parser.GetStringAnnotation("auth-cache-key", ing)
|
||||
authCacheKey, err := parser.GetStringAnnotation(authReqCacheKeyAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if ing_errors.IsValidationError(err) {
|
||||
klog.Warningf("%s value is invalid: %s", authReqCacheKeyAnnotation, err)
|
||||
}
|
||||
klog.V(3).InfoS("auth-cache-key annotation is undefined and will not be set")
|
||||
}
|
||||
|
||||
keepaliveConnections, err := parser.GetIntAnnotation("auth-keepalive", ing)
|
||||
keepaliveConnections, err := parser.GetIntAnnotation(authReqKeepaliveAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("auth-keepalive annotation is undefined and will be set to its default value")
|
||||
keepaliveConnections = defaultKeepaliveConnections
|
||||
|
|
@ -238,9 +357,9 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
}
|
||||
}
|
||||
|
||||
keepaliveRequests, err := parser.GetIntAnnotation("auth-keepalive-requests", ing)
|
||||
keepaliveRequests, err := parser.GetIntAnnotation(authReqKeepaliveRequestsAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("auth-keepalive-requests annotation is undefined and will be set to its default value")
|
||||
klog.V(3).InfoS("auth-keepalive-requests annotation is undefined or invalid and will be set to its default value")
|
||||
keepaliveRequests = defaultKeepaliveRequests
|
||||
}
|
||||
if keepaliveRequests <= 0 {
|
||||
|
|
@ -248,7 +367,7 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
keepaliveConnections = 0
|
||||
}
|
||||
|
||||
keepaliveTimeout, err := parser.GetIntAnnotation("auth-keepalive-timeout", ing)
|
||||
keepaliveTimeout, err := parser.GetIntAnnotation(authReqKeepaliveTimeout, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("auth-keepalive-timeout annotation is undefined and will be set to its default value")
|
||||
keepaliveTimeout = defaultKeepaliveTimeout
|
||||
|
|
@ -258,14 +377,20 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
keepaliveConnections = 0
|
||||
}
|
||||
|
||||
durstr, _ := parser.GetStringAnnotation("auth-cache-duration", ing)
|
||||
durstr, err := parser.GetStringAnnotation(authReqCacheDuration, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && ing_errors.IsValidationError(err) {
|
||||
return nil, fmt.Errorf("%s contains invalid value", authReqCacheDuration)
|
||||
}
|
||||
authCacheDuration, err := ParseStringToCacheDurations(durstr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responseHeaders := []string{}
|
||||
hstr, _ := parser.GetStringAnnotation("auth-response-headers", ing)
|
||||
hstr, err := parser.GetStringAnnotation(authReqResponseHeadersAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && ing_errors.IsValidationError(err) {
|
||||
return nil, ing_errors.NewLocationDenied("validation error")
|
||||
}
|
||||
if len(hstr) != 0 {
|
||||
harr := strings.Split(hstr, ",")
|
||||
for _, header := range harr {
|
||||
|
|
@ -279,9 +404,28 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
}
|
||||
}
|
||||
|
||||
proxySetHeaderMap, err := parser.GetStringAnnotation("auth-proxy-set-headers", ing)
|
||||
proxySetHeaderMap, err := parser.GetStringAnnotation(authReqProxySetHeadersAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("auth-set-proxy-headers annotation is undefined and will not be set")
|
||||
klog.V(3).InfoS("auth-set-proxy-headers annotation is undefined and will not be set", "err", err)
|
||||
}
|
||||
|
||||
cns, _, err := cache.SplitMetaNamespaceKey(proxySetHeaderMap)
|
||||
if err != nil {
|
||||
return nil, ing_errors.LocationDenied{
|
||||
Reason: fmt.Errorf("error reading configmap name %s from annotation: %w", proxySetHeaderMap, err),
|
||||
}
|
||||
}
|
||||
|
||||
if cns == "" {
|
||||
cns = ing.Namespace
|
||||
}
|
||||
|
||||
secCfg := a.r.GetSecurityConfiguration()
|
||||
// We don't accept different namespaces for secrets.
|
||||
if !secCfg.AllowCrossNamespaceResources && cns != ing.Namespace {
|
||||
return nil, ing_errors.LocationDenied{
|
||||
Reason: fmt.Errorf("cross namespace usage of secrets is not allowed"),
|
||||
}
|
||||
}
|
||||
|
||||
var proxySetHeaders map[string]string
|
||||
|
|
@ -301,9 +445,15 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
proxySetHeaders = proxySetHeadersMapContents.Data
|
||||
}
|
||||
|
||||
requestRedirect, _ := parser.GetStringAnnotation("auth-request-redirect", ing)
|
||||
requestRedirect, err := parser.GetStringAnnotation(authReqRequestRedirectAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && ing_errors.IsValidationError(err) {
|
||||
return nil, fmt.Errorf("%s is invalid: %w", authReqRequestRedirectAnnotation, err)
|
||||
}
|
||||
|
||||
alwaysSetCookie, _ := parser.GetBoolAnnotation("auth-always-set-cookie", ing)
|
||||
alwaysSetCookie, err := parser.GetBoolAnnotation(authReqAlwaysSetCookieAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && ing_errors.IsValidationError(err) {
|
||||
return nil, fmt.Errorf("%s is invalid: %w", authReqAlwaysSetCookieAnnotation, err)
|
||||
}
|
||||
|
||||
return &Config{
|
||||
URL: urlString,
|
||||
|
|
@ -348,3 +498,12 @@ func ParseStringToCacheDurations(input string) ([]string, error) {
|
|||
}
|
||||
return authCacheDuration, nil
|
||||
}
|
||||
|
||||
func (a authReq) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a authReq) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, authReqAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -192,11 +192,13 @@ func TestHeaderAnnotations(t *testing.T) {
|
|||
i, err := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
if test.expErr {
|
||||
if err == nil {
|
||||
t.Error("expected error but retuned nil")
|
||||
t.Errorf("%v expected error but retuned nil", test.title)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("no error was expected but %v happened in %s", err, test.title)
|
||||
}
|
||||
u, ok := i.(*Config)
|
||||
if !ok {
|
||||
t.Errorf("%v: expected an External type", test.title)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue