2019-12-31 01:08:50 +00:00
package validate
import (
"errors"
"fmt"
2020-05-26 17:36:56 +00:00
"path"
2019-12-31 01:08:50 +00:00
"reflect"
"strconv"
"strings"
2020-03-17 18:05:20 +00:00
"github.com/go-logr/logr"
2019-12-31 01:08:50 +00:00
"github.com/nirmata/kyverno/pkg/engine/anchor"
"github.com/nirmata/kyverno/pkg/engine/operator"
)
2020-02-14 20:05:13 +00:00
// ValidateResourceWithPattern is a start of element-by-element validation process
2020-02-13 21:57:48 +00:00
// It assumes that validation is started from root, so "/" is passed
2020-03-17 18:05:20 +00:00
func ValidateResourceWithPattern ( log logr . Logger , resource , pattern interface { } ) ( string , error ) {
path , err := validateResourceElement ( log , resource , pattern , pattern , "/" )
2020-02-13 21:57:48 +00:00
if err != nil {
return path , err
}
return "" , nil
}
2019-12-31 01:08:50 +00:00
// validateResourceElement detects the element type (map, array, nil, string, int, bool, float)
// and calls corresponding handler
// Pattern tree and resource tree can have different structure. In this case validation fails
2020-03-17 18:05:20 +00:00
func validateResourceElement ( log logr . Logger , resourceElement , patternElement , originPattern interface { } , path string ) ( string , error ) {
2019-12-31 01:08:50 +00:00
var err error
switch typedPatternElement := patternElement . ( type ) {
// map
case map [ string ] interface { } :
typedResourceElement , ok := resourceElement . ( map [ string ] interface { } )
if ! ok {
2020-03-17 18:05:20 +00:00
log . V ( 4 ) . Info ( "Pattern and resource have different structures." , "path" , path , "expected" , fmt . Sprintf ( "%T" , patternElement ) , "current" , fmt . Sprintf ( "%T" , resourceElement ) )
2019-12-31 01:08:50 +00:00
return path , fmt . Errorf ( "Pattern and resource have different structures. Path: %s. Expected %T, found %T" , path , patternElement , resourceElement )
}
2020-03-17 18:05:20 +00:00
return validateMap ( log , typedResourceElement , typedPatternElement , originPattern , path )
2019-12-31 01:08:50 +00:00
// array
case [ ] interface { } :
typedResourceElement , ok := resourceElement . ( [ ] interface { } )
if ! ok {
2020-03-17 18:05:20 +00:00
log . V ( 4 ) . Info ( "Pattern and resource have different structures." , "path" , path , "expected" , fmt . Sprintf ( "%T" , patternElement ) , "current" , fmt . Sprintf ( "%T" , resourceElement ) )
2019-12-31 01:08:50 +00:00
return path , fmt . Errorf ( "Validation rule Failed at path %s, resource does not satisfy the expected overlay pattern" , path )
}
2020-03-17 18:05:20 +00:00
return validateArray ( log , typedResourceElement , typedPatternElement , originPattern , path )
2019-12-31 01:08:50 +00:00
// elementary values
case string , float64 , int , int64 , bool , nil :
/*Analyze pattern */
if checkedPattern := reflect . ValueOf ( patternElement ) ; checkedPattern . Kind ( ) == reflect . String {
if isStringIsReference ( checkedPattern . String ( ) ) { //check for $ anchor
2020-03-17 18:05:20 +00:00
patternElement , err = actualizePattern ( log , originPattern , checkedPattern . String ( ) , path )
2019-12-31 01:08:50 +00:00
if err != nil {
return path , err
}
}
}
2020-03-17 18:05:20 +00:00
if ! ValidateValueWithPattern ( log , resourceElement , patternElement ) {
2020-02-14 19:59:28 +00:00
return path , fmt . Errorf ( "Validation rule failed at '%s' to validate value '%v' with pattern '%v'" , path , resourceElement , patternElement )
2019-12-31 01:08:50 +00:00
}
default :
2020-03-17 18:05:20 +00:00
log . V ( 4 ) . Info ( "Pattern contains unknown type" , "path" , path , "current" , fmt . Sprintf ( "%T" , patternElement ) )
2019-12-31 01:08:50 +00:00
return path , fmt . Errorf ( "Validation rule failed at '%s', pattern contains unknown type" , path )
}
return "" , nil
}
// If validateResourceElement detects map element inside resource and pattern trees, it goes to validateMap
// For each element of the map we must detect the type again, so we pass these elements to validateResourceElement
2020-03-17 18:05:20 +00:00
func validateMap ( log logr . Logger , resourceMap , patternMap map [ string ] interface { } , origPattern interface { } , path string ) ( string , error ) {
2019-12-31 01:08:50 +00:00
// check if there is anchor in pattern
// Phase 1 : Evaluate all the anchors
// Phase 2 : Evaluate non-anchors
anchors , resources := anchor . GetAnchorsResourcesFromMap ( patternMap )
// Evaluate anchors
for key , patternElement := range anchors {
// get handler for each pattern in the pattern
// - Conditional
2020-01-24 20:05:53 +00:00
// - Existence
2019-12-31 01:08:50 +00:00
// - Equality
handler := anchor . CreateElementHandler ( key , patternElement , path )
handlerPath , err := handler . Handle ( validateResourceElement , resourceMap , origPattern )
// if there are resource values at same level, then anchor acts as conditional instead of a strict check
// but if there are non then its a if then check
if err != nil {
// If Conditional anchor fails then we dont process the resources
if anchor . IsConditionAnchor ( key ) {
2020-03-17 18:05:20 +00:00
log . Error ( err , "condition anchor did not satisfy, wont process the resource" )
2019-12-31 01:08:50 +00:00
return "" , nil
}
return handlerPath , err
}
}
// Evaluate resources
for key , resourceElement := range resources {
// get handler for resources in the pattern
handler := anchor . CreateElementHandler ( key , resourceElement , path )
handlerPath , err := handler . Handle ( validateResourceElement , resourceMap , origPattern )
if err != nil {
return handlerPath , err
}
}
return "" , nil
}
2020-03-17 18:05:20 +00:00
func validateArray ( log logr . Logger , resourceArray , patternArray [ ] interface { } , originPattern interface { } , path string ) ( string , error ) {
2019-12-31 01:08:50 +00:00
if 0 == len ( patternArray ) {
return path , fmt . Errorf ( "Pattern Array empty" )
}
switch typedPatternElement := patternArray [ 0 ] . ( type ) {
case map [ string ] interface { } :
// This is special case, because maps in arrays can have anchors that must be
// processed with the special way affecting the entire array
2020-03-17 18:05:20 +00:00
path , err := validateArrayOfMaps ( log , resourceArray , typedPatternElement , originPattern , path )
2019-12-31 01:08:50 +00:00
if err != nil {
return path , err
}
default :
// In all other cases - detect type and handle each array element with validateResourceElement
2020-07-09 18:50:05 +00:00
if len ( resourceArray ) >= len ( patternArray ) {
for i , patternElement := range patternArray {
currentPath := path + strconv . Itoa ( i ) + "/"
path , err := validateResourceElement ( log , resourceArray [ i ] , patternElement , originPattern , currentPath )
if err != nil {
return path , err
}
2019-12-31 01:08:50 +00:00
}
2020-08-06 05:16:10 +00:00
} else {
2020-07-09 18:50:05 +00:00
return "" , fmt . Errorf ( "Validate Array failed, array length mismatch, resource Array len is %d and pattern Array len is %d" , len ( resourceArray ) , len ( patternArray ) )
2019-12-31 01:08:50 +00:00
}
}
return "" , nil
}
2020-03-17 18:05:20 +00:00
func actualizePattern ( log logr . Logger , origPattern interface { } , referencePattern , absolutePath string ) ( interface { } , error ) {
2019-12-31 01:08:50 +00:00
var foundValue interface { }
referencePattern = strings . Trim ( referencePattern , "$()" )
operatorVariable := operator . GetOperatorFromStringPattern ( referencePattern )
referencePattern = referencePattern [ len ( operatorVariable ) : ]
if len ( referencePattern ) == 0 {
return nil , errors . New ( "Expected path. Found empty reference" )
}
// Check for variables
// substitute it from Context
// remove abosolute path
// {{ }}
// value :=
actualPath := formAbsolutePath ( referencePattern , absolutePath )
2020-03-17 18:05:20 +00:00
valFromReference , err := getValueFromReference ( log , origPattern , actualPath )
2019-12-31 01:08:50 +00:00
if err != nil {
return err , nil
}
//TODO validate this
if operatorVariable == operator . Equal { //if operator does not exist return raw value
return valFromReference , nil
}
foundValue , err = valFromReferenceToString ( valFromReference , string ( operatorVariable ) )
if err != nil {
return "" , err
}
return string ( operatorVariable ) + foundValue . ( string ) , nil
}
//Parse value to string
func valFromReferenceToString ( value interface { } , operator string ) ( string , error ) {
switch typed := value . ( type ) {
case string :
return typed , nil
case int , int64 :
return fmt . Sprintf ( "%d" , value ) , nil
case float64 :
return fmt . Sprintf ( "%f" , value ) , nil
default :
return "" , fmt . Errorf ( "Incorrect expression. Operator %s does not match with value: %v" , operator , value )
}
}
// returns absolute path
func formAbsolutePath ( referencePath , absolutePath string ) string {
2020-05-26 17:36:56 +00:00
if path . IsAbs ( referencePath ) {
2019-12-31 01:08:50 +00:00
return referencePath
}
2020-05-26 17:36:56 +00:00
return path . Join ( absolutePath , referencePath )
2019-12-31 01:08:50 +00:00
}
//Prepares original pattern, path to value, and call traverse function
2020-03-17 18:05:20 +00:00
func getValueFromReference ( log logr . Logger , origPattern interface { } , reference string ) ( interface { } , error ) {
2019-12-31 01:08:50 +00:00
originalPatternMap := origPattern . ( map [ string ] interface { } )
2020-02-03 21:38:24 +00:00
reference = reference [ 1 : ]
2019-12-31 01:08:50 +00:00
statements := strings . Split ( reference , "/" )
2020-03-17 18:05:20 +00:00
return getValueFromPattern ( log , originalPatternMap , statements , 0 )
2019-12-31 01:08:50 +00:00
}
2020-03-17 18:05:20 +00:00
func getValueFromPattern ( log logr . Logger , patternMap map [ string ] interface { } , keys [ ] string , currentKeyIndex int ) ( interface { } , error ) {
2019-12-31 01:08:50 +00:00
for key , pattern := range patternMap {
rawKey := getRawKeyIfWrappedWithAttributes ( key )
if rawKey == keys [ len ( keys ) - 1 ] && currentKeyIndex == len ( keys ) - 1 {
return pattern , nil
} else if rawKey != keys [ currentKeyIndex ] && currentKeyIndex != len ( keys ) - 1 {
continue
}
switch typedPattern := pattern . ( type ) {
case [ ] interface { } :
if keys [ currentKeyIndex ] == rawKey {
for i , value := range typedPattern {
resourceMap , ok := value . ( map [ string ] interface { } )
if ! ok {
2020-03-17 18:05:20 +00:00
log . V ( 4 ) . Info ( "Pattern and resource have different structures." , "expected" , fmt . Sprintf ( "%T" , pattern ) , "current" , fmt . Sprintf ( "%T" , value ) )
2019-12-31 01:08:50 +00:00
return nil , fmt . Errorf ( "Validation rule failed, resource does not have expected pattern %v" , patternMap )
}
if keys [ currentKeyIndex + 1 ] == strconv . Itoa ( i ) {
2020-03-17 18:05:20 +00:00
return getValueFromPattern ( log , resourceMap , keys , currentKeyIndex + 2 )
2019-12-31 01:08:50 +00:00
}
2020-03-23 19:05:05 +00:00
// TODO : SA4004: the surrounding loop is unconditionally terminated (staticcheck)
2019-12-31 01:08:50 +00:00
return nil , errors . New ( "Reference to non-existent place in the document" )
}
2020-03-23 19:05:05 +00:00
return nil , nil // Just a hack to fix the lint
2019-12-31 01:08:50 +00:00
}
return nil , errors . New ( "Reference to non-existent place in the document" )
case map [ string ] interface { } :
if keys [ currentKeyIndex ] == rawKey {
2020-03-17 18:05:20 +00:00
return getValueFromPattern ( log , typedPattern , keys , currentKeyIndex + 1 )
2019-12-31 01:08:50 +00:00
}
return nil , errors . New ( "Reference to non-existent place in the document" )
case string , float64 , int , int64 , bool , nil :
continue
}
}
path := ""
for _ , elem := range keys {
path = "/" + elem + path
}
return nil , fmt . Errorf ( "No value found for specified reference: %s" , path )
}
// validateArrayOfMaps gets anchors from pattern array map element, applies anchors logic
// and then validates each map due to the pattern
2020-03-17 18:05:20 +00:00
func validateArrayOfMaps ( log logr . Logger , resourceMapArray [ ] interface { } , patternMap map [ string ] interface { } , originPattern interface { } , path string ) ( string , error ) {
2019-12-31 01:08:50 +00:00
for i , resourceElement := range resourceMapArray {
// check the types of resource element
// expect it to be map, but can be anything ?:(
currentPath := path + strconv . Itoa ( i ) + "/"
2020-03-17 18:05:20 +00:00
returnpath , err := validateResourceElement ( log , resourceElement , patternMap , originPattern , currentPath )
2019-12-31 01:08:50 +00:00
if err != nil {
return returnpath , err
}
}
return "" , nil
}
2020-01-24 17:37:12 +00:00
func isStringIsReference ( str string ) bool {
if len ( str ) < len ( operator . ReferenceSign ) {
return false
}
return str [ 0 ] == '$' && str [ 1 ] == '(' && str [ len ( str ) - 1 ] == ')'
}