Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
wip: perform route validation on external v1 api
to allow for reuse in ocp kas on variants serving route via CRD
- Loading branch information
Showing
4 changed files
with
463 additions
and
395 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,344 +1,65 @@ | ||
package validation | ||
|
||
import ( | ||
"crypto/ecdsa" | ||
"crypto/rsa" | ||
"crypto/x509" | ||
"encoding/pem" | ||
"fmt" | ||
"strings" | ||
|
||
routev1 "github.com/openshift/api/route/v1" | ||
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" | ||
"k8s.io/apimachinery/pkg/util/intstr" | ||
"k8s.io/apimachinery/pkg/util/sets" | ||
kvalidation "k8s.io/apimachinery/pkg/util/validation" | ||
"k8s.io/apimachinery/pkg/util/validation/field" | ||
"k8s.io/kubernetes/pkg/apis/core/validation" | ||
|
||
routeapi "github.com/openshift/openshift-apiserver/pkg/route/apis/route" | ||
routev1 "github.com/openshift/api/route/v1" | ||
routeinternal "github.com/openshift/openshift-apiserver/pkg/route/apis/route" | ||
routev1conversion "github.com/openshift/openshift-apiserver/pkg/route/apis/route/v1" | ||
) | ||
|
||
var ValidateRouteName = apimachineryvalidation.NameIsDNSSubdomain | ||
|
||
// ValidateRoute tests if required fields in the route are set. | ||
func ValidateRoute(route *routeapi.Route) field.ErrorList { | ||
return validateRoute(route, true) | ||
} | ||
|
||
// validLabels - used in the ValidateRouteUpdate function to check if "older" routes conform to DNS1123Labels or not | ||
func validLabels(host string) bool { | ||
if len(host) == 0 { | ||
return true | ||
} | ||
return checkLabelSegments(host) | ||
} | ||
|
||
// checkLabelSegments - function that checks if hostname labels conform to DNS1123Labels | ||
func checkLabelSegments(host string) bool { | ||
segments := strings.Split(host, ".") | ||
for _, s := range segments { | ||
errs := kvalidation.IsDNS1123Label(s) | ||
if len(errs) > 0 { | ||
return false | ||
} | ||
func toRouteV1(internal *routeinternal.Route) (*routev1.Route, field.ErrorList) { | ||
var external routev1.Route | ||
if err := routev1conversion.Convert_route_Route_To_v1_Route(internal, &external, nil); err != nil { | ||
return nil, field.ErrorList{field.InternalError(field.NewPath(""), err)} | ||
} | ||
return true | ||
return &external, nil | ||
} | ||
|
||
// validateRoute - private function to validate route | ||
func validateRoute(route *routeapi.Route, checkHostname bool) field.ErrorList { | ||
//ensure meta is set properly | ||
result := validation.ValidateObjectMeta(&route.ObjectMeta, true, ValidateRouteName, field.NewPath("metadata")) | ||
|
||
specPath := field.NewPath("spec") | ||
|
||
//host is not required but if it is set ensure it meets DNS requirements | ||
if len(route.Spec.Host) > 0 { | ||
if len(kvalidation.IsDNS1123Subdomain(route.Spec.Host)) != 0 { | ||
result = append(result, field.Invalid(specPath.Child("host"), route.Spec.Host, "host must conform to DNS 952 subdomain conventions")) | ||
} | ||
|
||
// Check the hostname only if the old route did not have an invalid DNS1123Label | ||
// and the new route cares about DNS compliant labels. | ||
if checkHostname && route.Annotations[routev1.AllowNonDNSCompliantHostAnnotation] != "true" { | ||
segments := strings.Split(route.Spec.Host, ".") | ||
for _, s := range segments { | ||
errs := kvalidation.IsDNS1123Label(s) | ||
for _, e := range errs { | ||
result = append(result, field.Invalid(specPath.Child("host"), route.Spec.Host, e)) | ||
} | ||
} | ||
} | ||
} | ||
|
||
if len(route.Spec.Subdomain) > 0 { | ||
// Subdomain is not lenient because it was never used outside of | ||
// routes. | ||
// | ||
// TODO: Use ValidateSubdomain from library-go. | ||
if len(route.Spec.Subdomain) > kvalidation.DNS1123SubdomainMaxLength { | ||
result = append(result, field.Invalid(field.NewPath("spec.subdomain"), route.Spec.Subdomain, kvalidation.MaxLenError(kvalidation.DNS1123SubdomainMaxLength))) | ||
} | ||
for _, label := range strings.Split(route.Spec.Subdomain, ".") { | ||
if errs := kvalidation.IsDNS1123Label(label); len(errs) > 0 { | ||
result = append(result, field.Invalid(field.NewPath("spec.subdomain"), label, strings.Join(errs, ", "))) | ||
} | ||
} | ||
} | ||
|
||
if err := validateWildcardPolicy(route.Spec.Host, route.Spec.WildcardPolicy, specPath.Child("wildcardPolicy")); err != nil { | ||
result = append(result, err) | ||
} | ||
|
||
if len(route.Spec.Path) > 0 && !strings.HasPrefix(route.Spec.Path, "/") { | ||
result = append(result, field.Invalid(specPath.Child("path"), route.Spec.Path, "path must begin with /")) | ||
func ValidateRoute(route *routeinternal.Route) field.ErrorList { | ||
external, errs := toRouteV1(route) | ||
if len(errs) > 0 { | ||
return errs | ||
} | ||
|
||
if len(route.Spec.Path) > 0 && route.Spec.TLS != nil && | ||
route.Spec.TLS.Termination == routeapi.TLSTerminationPassthrough { | ||
result = append(result, field.Invalid(specPath.Child("path"), route.Spec.Path, "passthrough termination does not support paths")) | ||
} | ||
|
||
if len(route.Spec.To.Name) == 0 { | ||
result = append(result, field.Required(specPath.Child("to", "name"), "")) | ||
} | ||
if route.Spec.To.Kind != "Service" { | ||
result = append(result, field.Invalid(specPath.Child("to", "kind"), route.Spec.To.Kind, "must reference a Service")) | ||
} | ||
if route.Spec.To.Weight != nil && (*route.Spec.To.Weight < 0 || *route.Spec.To.Weight > 256) { | ||
result = append(result, field.Invalid(specPath.Child("to", "weight"), route.Spec.To.Weight, "weight must be an integer between 0 and 256")) | ||
} | ||
|
||
backendPath := specPath.Child("alternateBackends") | ||
if len(route.Spec.AlternateBackends) > 3 { | ||
result = append(result, field.Required(backendPath, "cannot specify more than 3 alternate backends")) | ||
} | ||
for i, svc := range route.Spec.AlternateBackends { | ||
if len(svc.Name) == 0 { | ||
result = append(result, field.Required(backendPath.Index(i).Child("name"), "")) | ||
} | ||
if svc.Kind != "Service" { | ||
result = append(result, field.Invalid(backendPath.Index(i).Child("kind"), svc.Kind, "must reference a Service")) | ||
} | ||
if svc.Weight != nil && (*svc.Weight < 0 || *svc.Weight > 256) { | ||
result = append(result, field.Invalid(backendPath.Index(i).Child("weight"), svc.Weight, "weight must be an integer between 0 and 256")) | ||
} | ||
} | ||
return validateRouteV1(external) | ||
} | ||
|
||
if route.Spec.Port != nil { | ||
switch target := route.Spec.Port.TargetPort; { | ||
case target.Type == intstr.Int && target.IntVal == 0, | ||
target.Type == intstr.String && len(target.StrVal) == 0: | ||
result = append(result, field.Required(specPath.Child("port", "targetPort"), "")) | ||
} | ||
func ValidateRouteUpdate(route *routeinternal.Route, oldRoute *routeinternal.Route) field.ErrorList { | ||
external, errs := toRouteV1(route) | ||
if len(errs) > 0 { | ||
return errs | ||
} | ||
|
||
if errs := validateTLS(route, specPath.Child("tls")); len(errs) != 0 { | ||
result = append(result, errs...) | ||
oldExternal, errs := toRouteV1(oldRoute) | ||
if len(errs) > 0 { | ||
return errs | ||
} | ||
|
||
return result | ||
return validateRouteUpdateV1(external, oldExternal) | ||
} | ||
|
||
func ValidateRouteUpdate(route *routeapi.Route, older *routeapi.Route) field.ErrorList { | ||
allErrs := validation.ValidateObjectMetaUpdate(&route.ObjectMeta, &older.ObjectMeta, field.NewPath("metadata")) | ||
allErrs = append(allErrs, validation.ValidateImmutableField(route.Spec.WildcardPolicy, older.Spec.WildcardPolicy, field.NewPath("spec", "wildcardPolicy"))...) | ||
hostnameUpdated := route.Spec.Host != older.Spec.Host | ||
allErrs = append(allErrs, validateRoute(route, hostnameUpdated && validLabels(older.Spec.Host))...) | ||
return allErrs | ||
} | ||
|
||
// ValidateRouteStatusUpdate validates status updates for routes. | ||
// | ||
// Note that this function shouldn't call ValidateRouteUpdate, otherwise | ||
// we are risking to break existing routes. | ||
func ValidateRouteStatusUpdate(route *routeapi.Route, older *routeapi.Route) field.ErrorList { | ||
allErrs := validation.ValidateObjectMetaUpdate(&route.ObjectMeta, &older.ObjectMeta, field.NewPath("metadata")) | ||
|
||
// TODO: validate route status | ||
return allErrs | ||
} | ||
|
||
type blockVerifierFunc func(block *pem.Block) (*pem.Block, error) | ||
|
||
func publicKeyBlockVerifier(block *pem.Block) (*pem.Block, error) { | ||
key, err := x509.ParsePKIXPublicKey(block.Bytes) | ||
if err != nil { | ||
return nil, err | ||
} | ||
block = &pem.Block{ | ||
Type: "PUBLIC KEY", | ||
} | ||
if block.Bytes, err = x509.MarshalPKIXPublicKey(key); err != nil { | ||
return nil, err | ||
} | ||
return block, nil | ||
} | ||
|
||
func certificateBlockVerifier(block *pem.Block) (*pem.Block, error) { | ||
cert, err := x509.ParseCertificate(block.Bytes) | ||
if err != nil { | ||
return nil, err | ||
} | ||
block = &pem.Block{ | ||
Type: "CERTIFICATE", | ||
Bytes: cert.Raw, | ||
} | ||
return block, nil | ||
} | ||
|
||
func privateKeyBlockVerifier(block *pem.Block) (*pem.Block, error) { | ||
key, err := x509.ParsePKCS8PrivateKey(block.Bytes) | ||
if err != nil { | ||
key, err = x509.ParsePKCS1PrivateKey(block.Bytes) | ||
if err != nil { | ||
key, err = x509.ParseECPrivateKey(block.Bytes) | ||
if err != nil { | ||
return nil, fmt.Errorf("block %s is not valid", block.Type) | ||
} | ||
} | ||
} | ||
switch t := key.(type) { | ||
case *rsa.PrivateKey: | ||
block = &pem.Block{ | ||
Type: "RSA PRIVATE KEY", | ||
Bytes: x509.MarshalPKCS1PrivateKey(t), | ||
} | ||
case *ecdsa.PrivateKey: | ||
block = &pem.Block{ | ||
Type: "ECDSA PRIVATE KEY", | ||
} | ||
if block.Bytes, err = x509.MarshalECPrivateKey(t); err != nil { | ||
return nil, err | ||
} | ||
default: | ||
return nil, fmt.Errorf("block private key %T is not valid", key) | ||
} | ||
return block, nil | ||
} | ||
|
||
func ignoreBlockVerifier(block *pem.Block) (*pem.Block, error) { | ||
return nil, nil | ||
} | ||
|
||
var knownBlockDecoders = map[string]blockVerifierFunc{ | ||
"RSA PRIVATE KEY": privateKeyBlockVerifier, | ||
"ECDSA PRIVATE KEY": privateKeyBlockVerifier, | ||
"PRIVATE KEY": privateKeyBlockVerifier, | ||
"PUBLIC KEY": publicKeyBlockVerifier, | ||
// Potential "in the wild" PEM encoded blocks that can be normalized | ||
"RSA PUBLIC KEY": publicKeyBlockVerifier, | ||
"DSA PUBLIC KEY": publicKeyBlockVerifier, | ||
"ECDSA PUBLIC KEY": publicKeyBlockVerifier, | ||
"CERTIFICATE": certificateBlockVerifier, | ||
// Blocks that should be dropped | ||
"EC PARAMETERS": ignoreBlockVerifier, | ||
} | ||
|
||
// validateTLS tests fields for different types of TLS combinations are set. Called | ||
// by ValidateRoute. | ||
func validateTLS(route *routeapi.Route, fldPath *field.Path) field.ErrorList { | ||
result := field.ErrorList{} | ||
tls := route.Spec.TLS | ||
|
||
// no tls config present, no need for validation | ||
if tls == nil { | ||
return nil | ||
} | ||
|
||
switch tls.Termination { | ||
// reencrypt may specify destination ca cert | ||
// cert, key, cacert may not be specified because the route may be a wildcard | ||
case routeapi.TLSTerminationReencrypt: | ||
//passthrough term should not specify any cert | ||
case routeapi.TLSTerminationPassthrough: | ||
if len(tls.Certificate) > 0 { | ||
result = append(result, field.Invalid(fldPath.Child("certificate"), "redacted certificate data", "passthrough termination does not support certificates")) | ||
} | ||
|
||
if len(tls.Key) > 0 { | ||
result = append(result, field.Invalid(fldPath.Child("key"), "redacted key data", "passthrough termination does not support certificates")) | ||
} | ||
|
||
if len(tls.CACertificate) > 0 { | ||
result = append(result, field.Invalid(fldPath.Child("caCertificate"), "redacted ca certificate data", "passthrough termination does not support certificates")) | ||
} | ||
|
||
if len(tls.DestinationCACertificate) > 0 { | ||
result = append(result, field.Invalid(fldPath.Child("destinationCACertificate"), "redacted destination ca certificate data", "passthrough termination does not support certificates")) | ||
} | ||
// edge cert should only specify cert, key, and cacert but those certs | ||
// may not be specified if the route is a wildcard route | ||
case routeapi.TLSTerminationEdge: | ||
if len(tls.DestinationCACertificate) > 0 { | ||
result = append(result, field.Invalid(fldPath.Child("destinationCACertificate"), "redacted destination ca certificate data", "edge termination does not support destination certificates")) | ||
} | ||
default: | ||
validValues := []string{string(routeapi.TLSTerminationEdge), string(routeapi.TLSTerminationPassthrough), string(routeapi.TLSTerminationReencrypt)} | ||
result = append(result, field.NotSupported(fldPath.Child("termination"), tls.Termination, validValues)) | ||
} | ||
|
||
if err := validateInsecureEdgeTerminationPolicy(tls, fldPath.Child("insecureEdgeTerminationPolicy")); err != nil { | ||
result = append(result, err) | ||
} | ||
|
||
return result | ||
} | ||
|
||
// validateInsecureEdgeTerminationPolicy tests fields for different types of | ||
// insecure options. Called by validateTLS. | ||
func validateInsecureEdgeTerminationPolicy(tls *routeapi.TLSConfig, fldPath *field.Path) *field.Error { | ||
// Check insecure option value if specified (empty is ok). | ||
if len(tls.InsecureEdgeTerminationPolicy) == 0 { | ||
return nil | ||
} | ||
|
||
// It is an edge-terminated or reencrypt route, check insecure option value is | ||
// one of None(for disable), Allow or Redirect. | ||
allowedValues := map[routeapi.InsecureEdgeTerminationPolicyType]struct{}{ | ||
routeapi.InsecureEdgeTerminationPolicyNone: {}, | ||
routeapi.InsecureEdgeTerminationPolicyAllow: {}, | ||
routeapi.InsecureEdgeTerminationPolicyRedirect: {}, | ||
func ValidateRouteStatusUpdate(route *routeinternal.Route, oldRoute *routeinternal.Route) field.ErrorList { | ||
external, errs := toRouteV1(route) | ||
if len(errs) > 0 { | ||
return errs | ||
} | ||
|
||
switch tls.Termination { | ||
case routeapi.TLSTerminationReencrypt: | ||
fallthrough | ||
case routeapi.TLSTerminationEdge: | ||
if _, ok := allowedValues[tls.InsecureEdgeTerminationPolicy]; !ok { | ||
msg := fmt.Sprintf("invalid value for InsecureEdgeTerminationPolicy option, acceptable values are %s, %s, %s, or empty", routeapi.InsecureEdgeTerminationPolicyNone, routeapi.InsecureEdgeTerminationPolicyAllow, routeapi.InsecureEdgeTerminationPolicyRedirect) | ||
return field.Invalid(fldPath, tls.InsecureEdgeTerminationPolicy, msg) | ||
} | ||
case routeapi.TLSTerminationPassthrough: | ||
if routeapi.InsecureEdgeTerminationPolicyNone != tls.InsecureEdgeTerminationPolicy && routeapi.InsecureEdgeTerminationPolicyRedirect != tls.InsecureEdgeTerminationPolicy { | ||
msg := fmt.Sprintf("invalid value for InsecureEdgeTerminationPolicy option, acceptable values are %s, %s, or empty", routeapi.InsecureEdgeTerminationPolicyNone, routeapi.InsecureEdgeTerminationPolicyRedirect) | ||
return field.Invalid(fldPath, tls.InsecureEdgeTerminationPolicy, msg) | ||
} | ||
oldExternal, errs := toRouteV1(oldRoute) | ||
if len(errs) > 0 { | ||
return errs | ||
} | ||
|
||
return nil | ||
return validateRouteStatusUpdateV1(external, oldExternal) | ||
} | ||
|
||
var ( | ||
allowedWildcardPolicies = []string{string(routeapi.WildcardPolicyNone), string(routeapi.WildcardPolicySubdomain)} | ||
allowedWildcardPoliciesSet = sets.NewString(allowedWildcardPolicies...) | ||
) | ||
|
||
// validateWildcardPolicy tests that the wildcard policy is either empty or one of the supported types. | ||
func validateWildcardPolicy(host string, policy routeapi.WildcardPolicyType, fldPath *field.Path) *field.Error { | ||
if len(policy) == 0 { | ||
func Warnings(route *routeinternal.Route) []string { | ||
external, errs := toRouteV1(route) | ||
if errs != nil { | ||
return nil | ||
} | ||
|
||
// Check if policy is one of None or Subdomain. | ||
if !allowedWildcardPoliciesSet.Has(string(policy)) { | ||
return field.NotSupported(fldPath, policy, allowedWildcardPolicies) | ||
} | ||
|
||
if policy == routeapi.WildcardPolicySubdomain && len(host) == 0 { | ||
return field.Invalid(fldPath, policy, "host name not specified for wildcard policy") | ||
} | ||
|
||
return nil | ||
return warningsV1(external) | ||
} |
Oops, something went wrong.