k8s.io/kube-openapi@v0.0.0-20240228011516-70dd3763d340/pkg/validation/spec/fuzz_test.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package spec
    18  
    19  import (
    20  	"github.com/go-openapi/jsonreference"
    21  	"github.com/google/go-cmp/cmp"
    22  	fuzz "github.com/google/gofuzz"
    23  )
    24  
    25  var SwaggerFuzzFuncs []interface{} = []interface{}{
    26  	func(v *Responses, c fuzz.Continue) {
    27  		c.FuzzNoCustom(v)
    28  		if v.Default != nil {
    29  			// Check if we hit maxDepth and left an incomplete value
    30  			if v.Default.Description == "" {
    31  				v.Default = nil
    32  				v.StatusCodeResponses = nil
    33  			}
    34  		}
    35  
    36  		// conversion has no way to discern empty statusCodeResponses from
    37  		// nil, since "default" is always included in the map.
    38  		// So avoid empty responses list
    39  		if len(v.StatusCodeResponses) == 0 {
    40  			v.StatusCodeResponses = nil
    41  		}
    42  	},
    43  	func(v *Operation, c fuzz.Continue) {
    44  		c.FuzzNoCustom(v)
    45  
    46  		if v != nil {
    47  			// force non-nil
    48  			v.Responses = &Responses{}
    49  			c.Fuzz(v.Responses)
    50  
    51  			v.Schemes = nil
    52  			if c.RandBool() {
    53  				v.Schemes = append(v.Schemes, "http")
    54  			}
    55  
    56  			if c.RandBool() {
    57  				v.Schemes = append(v.Schemes, "https")
    58  			}
    59  
    60  			if c.RandBool() {
    61  				v.Schemes = append(v.Schemes, "ws")
    62  			}
    63  
    64  			if c.RandBool() {
    65  				v.Schemes = append(v.Schemes, "wss")
    66  			}
    67  
    68  			// Gnostic unconditionally makes security values non-null
    69  			// So do not fuzz null values into the array.
    70  			for i, val := range v.Security {
    71  				if val == nil {
    72  					v.Security[i] = make(map[string][]string)
    73  				}
    74  
    75  				for k, v := range val {
    76  					if v == nil {
    77  						val[k] = make([]string, 0)
    78  					}
    79  				}
    80  			}
    81  		}
    82  	},
    83  	func(v map[int]Response, c fuzz.Continue) {
    84  		n := 0
    85  		c.Fuzz(&n)
    86  		if n == 0 {
    87  			// Test that fuzzer is not at maxDepth so we do not
    88  			// end up with empty elements
    89  			return
    90  		}
    91  
    92  		// Prevent negative numbers
    93  		num := c.Intn(4)
    94  		for i := 0; i < num+2; i++ {
    95  			val := Response{}
    96  			c.Fuzz(&val)
    97  
    98  			val.Description = c.RandString() + "x"
    99  			v[100*(i+1)+c.Intn(100)] = val
   100  		}
   101  	},
   102  	func(v map[string]PathItem, c fuzz.Continue) {
   103  		n := 0
   104  		c.Fuzz(&n)
   105  		if n == 0 {
   106  			// Test that fuzzer is not at maxDepth so we do not
   107  			// end up with empty elements
   108  			return
   109  		}
   110  
   111  		num := c.Intn(5)
   112  		for i := 0; i < num+2; i++ {
   113  			val := PathItem{}
   114  			c.Fuzz(&val)
   115  
   116  			// Ref params are only allowed in certain locations, so
   117  			// possibly add a few to PathItems
   118  			numRefsToAdd := c.Intn(5)
   119  			for i := 0; i < numRefsToAdd; i++ {
   120  				theRef := Parameter{}
   121  				c.Fuzz(&theRef.Refable)
   122  
   123  				val.Parameters = append(val.Parameters, theRef)
   124  			}
   125  
   126  			v["/"+c.RandString()] = val
   127  		}
   128  	},
   129  	func(v *SchemaOrArray, c fuzz.Continue) {
   130  		*v = SchemaOrArray{}
   131  		// gnostic parser just doesn't support more
   132  		// than one Schema here
   133  		v.Schema = &Schema{}
   134  		c.Fuzz(&v.Schema)
   135  
   136  	},
   137  	func(v *SchemaOrBool, c fuzz.Continue) {
   138  		*v = SchemaOrBool{}
   139  
   140  		if c.RandBool() {
   141  			v.Allows = c.RandBool()
   142  		} else {
   143  			v.Schema = &Schema{}
   144  			v.Allows = true
   145  			c.Fuzz(&v.Schema)
   146  		}
   147  	},
   148  	func(v map[string]Response, c fuzz.Continue) {
   149  		n := 0
   150  		c.Fuzz(&n)
   151  		if n == 0 {
   152  			// Test that fuzzer is not at maxDepth so we do not
   153  			// end up with empty elements
   154  			return
   155  		}
   156  
   157  		// Response definitions are not allowed to
   158  		// be refs
   159  		for i := 0; i < c.Intn(5)+1; i++ {
   160  			resp := &Response{}
   161  
   162  			c.Fuzz(resp)
   163  			resp.Ref = Ref{}
   164  			resp.Description = c.RandString() + "x"
   165  
   166  			// Response refs are not vendor extensible by gnostic
   167  			resp.VendorExtensible.Extensions = nil
   168  			v[c.RandString()+"x"] = *resp
   169  		}
   170  	},
   171  	func(v *Header, c fuzz.Continue) {
   172  		if v != nil {
   173  			c.FuzzNoCustom(v)
   174  
   175  			// descendant Items of Header may not be refs
   176  			cur := v.Items
   177  			for cur != nil {
   178  				cur.Ref = Ref{}
   179  				cur = cur.Items
   180  			}
   181  		}
   182  	},
   183  	func(v *Ref, c fuzz.Continue) {
   184  		*v = Ref{}
   185  		v.Ref, _ = jsonreference.New("http://asd.com/" + c.RandString())
   186  	},
   187  	func(v *Response, c fuzz.Continue) {
   188  		*v = Response{}
   189  		if c.RandBool() {
   190  			v.Ref = Ref{}
   191  			v.Ref.Ref, _ = jsonreference.New("http://asd.com/" + c.RandString())
   192  		} else {
   193  			c.Fuzz(&v.VendorExtensible)
   194  			c.Fuzz(&v.Schema)
   195  			c.Fuzz(&v.ResponseProps)
   196  
   197  			v.Headers = nil
   198  			v.Ref = Ref{}
   199  
   200  			n := 0
   201  			c.Fuzz(&n)
   202  			if n != 0 {
   203  				// Test that fuzzer is not at maxDepth so we do not
   204  				// end up with empty elements
   205  				num := c.Intn(4)
   206  				for i := 0; i < num; i++ {
   207  					if v.Headers == nil {
   208  						v.Headers = make(map[string]Header)
   209  					}
   210  					hdr := Header{}
   211  					c.Fuzz(&hdr)
   212  					if hdr.Type == "" {
   213  						// hit maxDepth, just abort trying to make haders
   214  						v.Headers = nil
   215  						break
   216  					}
   217  					v.Headers[c.RandString()+"x"] = hdr
   218  				}
   219  			} else {
   220  				v.Headers = nil
   221  			}
   222  		}
   223  
   224  		v.Description = c.RandString() + "x"
   225  
   226  		// Gnostic parses empty as nil, so to keep avoid putting empty
   227  		if len(v.Headers) == 0 {
   228  			v.Headers = nil
   229  		}
   230  	},
   231  	func(v **Info, c fuzz.Continue) {
   232  		// Info is never nil
   233  		*v = &Info{}
   234  		c.FuzzNoCustom(*v)
   235  
   236  		(*v).Title = c.RandString() + "x"
   237  	},
   238  	func(v *Extensions, c fuzz.Continue) {
   239  		// gnostic parser only picks up x- vendor extensions
   240  		numChildren := c.Intn(5)
   241  		for i := 0; i < numChildren; i++ {
   242  			if *v == nil {
   243  				*v = Extensions{}
   244  			}
   245  			(*v)["x-"+c.RandString()] = c.RandString()
   246  		}
   247  	},
   248  	func(v *Swagger, c fuzz.Continue) {
   249  		c.FuzzNoCustom(v)
   250  
   251  		if v.Paths == nil {
   252  			// Force paths non-nil since it does not have omitempty in json tag.
   253  			// This means a perfect roundtrip (via json) is impossible,
   254  			// since we can't tell the difference between empty/unspecified paths
   255  			v.Paths = &Paths{}
   256  			c.Fuzz(v.Paths)
   257  		}
   258  
   259  		v.Swagger = "2.0"
   260  
   261  		// Gnostic support serializing ID at all
   262  		// unavoidable data loss
   263  		v.ID = ""
   264  
   265  		v.Schemes = nil
   266  		if c.RandUint64()%2 == 1 {
   267  			v.Schemes = append(v.Schemes, "http")
   268  		}
   269  
   270  		if c.RandUint64()%2 == 1 {
   271  			v.Schemes = append(v.Schemes, "https")
   272  		}
   273  
   274  		if c.RandUint64()%2 == 1 {
   275  			v.Schemes = append(v.Schemes, "ws")
   276  		}
   277  
   278  		if c.RandUint64()%2 == 1 {
   279  			v.Schemes = append(v.Schemes, "wss")
   280  		}
   281  
   282  		// Gnostic unconditionally makes security values non-null
   283  		// So do not fuzz null values into the array.
   284  		for i, val := range v.Security {
   285  			if val == nil {
   286  				v.Security[i] = make(map[string][]string)
   287  			}
   288  
   289  			for k, v := range val {
   290  				if v == nil {
   291  					val[k] = make([]string, 0)
   292  				}
   293  			}
   294  		}
   295  	},
   296  	func(v *SecurityScheme, c fuzz.Continue) {
   297  		v.Description = c.RandString() + "x"
   298  		c.Fuzz(&v.VendorExtensible)
   299  
   300  		switch c.Intn(3) {
   301  		case 0:
   302  			v.Type = "basic"
   303  		case 1:
   304  			v.Type = "apiKey"
   305  			switch c.Intn(2) {
   306  			case 0:
   307  				v.In = "header"
   308  			case 1:
   309  				v.In = "query"
   310  			default:
   311  				panic("unreachable")
   312  			}
   313  			v.Name = "x" + c.RandString()
   314  		case 2:
   315  			v.Type = "oauth2"
   316  
   317  			switch c.Intn(4) {
   318  			case 0:
   319  				v.Flow = "accessCode"
   320  				v.TokenURL = "https://" + c.RandString()
   321  				v.AuthorizationURL = "https://" + c.RandString()
   322  			case 1:
   323  				v.Flow = "application"
   324  				v.TokenURL = "https://" + c.RandString()
   325  			case 2:
   326  				v.Flow = "implicit"
   327  				v.AuthorizationURL = "https://" + c.RandString()
   328  			case 3:
   329  				v.Flow = "password"
   330  				v.TokenURL = "https://" + c.RandString()
   331  			default:
   332  				panic("unreachable")
   333  			}
   334  			c.Fuzz(&v.Scopes)
   335  		default:
   336  			panic("unreachable")
   337  		}
   338  	},
   339  	func(v *interface{}, c fuzz.Continue) {
   340  		*v = c.RandString() + "x"
   341  	},
   342  	func(v *string, c fuzz.Continue) {
   343  		*v = c.RandString() + "x"
   344  	},
   345  	func(v *ExternalDocumentation, c fuzz.Continue) {
   346  		v.Description = c.RandString() + "x"
   347  		v.URL = c.RandString() + "x"
   348  	},
   349  	func(v *SimpleSchema, c fuzz.Continue) {
   350  		c.FuzzNoCustom(v)
   351  
   352  		switch c.Intn(5) {
   353  		case 0:
   354  			v.Type = "string"
   355  		case 1:
   356  			v.Type = "number"
   357  		case 2:
   358  			v.Type = "boolean"
   359  		case 3:
   360  			v.Type = "integer"
   361  		case 4:
   362  			v.Type = "array"
   363  		default:
   364  			panic("unreachable")
   365  		}
   366  
   367  		switch c.Intn(5) {
   368  		case 0:
   369  			v.CollectionFormat = "csv"
   370  		case 1:
   371  			v.CollectionFormat = "ssv"
   372  		case 2:
   373  			v.CollectionFormat = "tsv"
   374  		case 3:
   375  			v.CollectionFormat = "pipes"
   376  		case 4:
   377  			v.CollectionFormat = ""
   378  		default:
   379  			panic("unreachable")
   380  		}
   381  
   382  		// None of the types which include SimpleSchema in our definitions
   383  		// actually support "example" in the official spec
   384  		v.Example = nil
   385  
   386  		// unsupported by openapi
   387  		v.Nullable = false
   388  	},
   389  	func(v *int64, c fuzz.Continue) {
   390  		c.Fuzz(v)
   391  
   392  		// Gnostic does not differentiate between 0 and non-specified
   393  		// so avoid using 0 for fuzzer
   394  		if *v == 0 {
   395  			*v = 1
   396  		}
   397  	},
   398  	func(v *float64, c fuzz.Continue) {
   399  		c.Fuzz(v)
   400  
   401  		// Gnostic does not differentiate between 0 and non-specified
   402  		// so avoid using 0 for fuzzer
   403  		if *v == 0.0 {
   404  			*v = 1.0
   405  		}
   406  	},
   407  	func(v *Parameter, c fuzz.Continue) {
   408  		if v == nil {
   409  			return
   410  		}
   411  		c.Fuzz(&v.VendorExtensible)
   412  		if c.RandBool() {
   413  			// body param
   414  			v.Description = c.RandString() + "x"
   415  			v.Name = c.RandString() + "x"
   416  			v.In = "body"
   417  			c.Fuzz(&v.Description)
   418  			c.Fuzz(&v.Required)
   419  
   420  			v.Schema = &Schema{}
   421  			c.Fuzz(&v.Schema)
   422  
   423  		} else {
   424  			c.Fuzz(&v.SimpleSchema)
   425  			c.Fuzz(&v.CommonValidations)
   426  			v.AllowEmptyValue = false
   427  			v.Description = c.RandString() + "x"
   428  			v.Name = c.RandString() + "x"
   429  
   430  			switch c.Intn(4) {
   431  			case 0:
   432  				// Header param
   433  				v.In = "header"
   434  			case 1:
   435  				// Form data param
   436  				v.In = "formData"
   437  				v.AllowEmptyValue = c.RandBool()
   438  			case 2:
   439  				// Query param
   440  				v.In = "query"
   441  				v.AllowEmptyValue = c.RandBool()
   442  			case 3:
   443  				// Path param
   444  				v.In = "path"
   445  				v.Required = true
   446  			default:
   447  				panic("unreachable")
   448  			}
   449  
   450  			// descendant Items of Parameter may not be refs
   451  			cur := v.Items
   452  			for cur != nil {
   453  				cur.Ref = Ref{}
   454  				cur = cur.Items
   455  			}
   456  		}
   457  	},
   458  	func(v *Schema, c fuzz.Continue) {
   459  		if c.RandBool() {
   460  			// file schema
   461  			c.Fuzz(&v.Default)
   462  			c.Fuzz(&v.Description)
   463  			c.Fuzz(&v.Example)
   464  			c.Fuzz(&v.ExternalDocs)
   465  
   466  			c.Fuzz(&v.Format)
   467  			c.Fuzz(&v.ReadOnly)
   468  			c.Fuzz(&v.Required)
   469  			c.Fuzz(&v.Title)
   470  			v.Type = StringOrArray{"file"}
   471  
   472  		} else {
   473  			// normal schema
   474  			c.Fuzz(&v.SchemaProps)
   475  			c.Fuzz(&v.SwaggerSchemaProps)
   476  			c.Fuzz(&v.VendorExtensible)
   477  			// c.Fuzz(&v.ExtraProps)
   478  			// ExtraProps will not roundtrip - gnostic throws out
   479  			// unrecognized keys
   480  		}
   481  
   482  		// Not supported by official openapi v2 spec
   483  		// and stripped by k8s apiserver
   484  		v.ID = ""
   485  		v.AnyOf = nil
   486  		v.OneOf = nil
   487  		v.Not = nil
   488  		v.Nullable = false
   489  		v.AdditionalItems = nil
   490  		v.Schema = ""
   491  		v.PatternProperties = nil
   492  		v.Definitions = nil
   493  		v.Dependencies = nil
   494  	},
   495  }
   496  
   497  var SwaggerDiffOptions = []cmp.Option{
   498  	// cmp.Diff panics on Ref since jsonreference.Ref uses unexported fields
   499  	cmp.Comparer(func(a Ref, b Ref) bool {
   500  		return a.String() == b.String()
   501  	}),
   502  }