k8s.io/kube-openapi@v0.0.0-20240228011516-70dd3763d340/pkg/schemamutation/walker_test.go (about) 1 /* 2 Copyright 2019 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 schemamutation 18 19 import ( 20 "encoding/json" 21 "fmt" 22 "math" 23 "math/rand" 24 "reflect" 25 "regexp" 26 "strings" 27 "testing" 28 "time" 29 30 fuzz "github.com/google/gofuzz" 31 "k8s.io/kube-openapi/pkg/util/jsontesting" 32 "k8s.io/kube-openapi/pkg/util/sets" 33 "k8s.io/kube-openapi/pkg/validation/spec" 34 ) 35 36 func fuzzFuncs(f *fuzz.Fuzzer, refFunc func(ref *spec.Ref, c fuzz.Continue, visible bool)) { 37 invisible := 0 // == 0 means visible, > 0 means invisible 38 depth := 0 39 maxDepth := 3 40 nilChance := func(depth int) float64 { 41 return math.Pow(0.9, math.Max(0.0, float64(maxDepth-depth))) 42 } 43 updateFuzzer := func(depth int) { 44 f.NilChance(nilChance(depth)) 45 f.NumElements(0, max(0, maxDepth-depth)) 46 } 47 updateFuzzer(depth) 48 enter := func(o interface{}, recursive bool, c fuzz.Continue) { 49 if recursive { 50 depth++ 51 updateFuzzer(depth) 52 } 53 54 invisible++ 55 c.FuzzNoCustom(o) 56 invisible-- 57 } 58 leave := func(recursive bool) { 59 if recursive { 60 depth-- 61 updateFuzzer(depth) 62 } 63 } 64 f.Funcs( 65 func(ref *spec.Ref, c fuzz.Continue) { 66 refFunc(ref, c, invisible == 0) 67 }, 68 func(sa *spec.SchemaOrStringArray, c fuzz.Continue) { 69 *sa = spec.SchemaOrStringArray{} 70 if c.RandBool() { 71 c.Fuzz(&sa.Schema) 72 } else { 73 c.Fuzz(&sa.Property) 74 } 75 if sa.Schema == nil && len(sa.Property) == 0 { 76 *sa = spec.SchemaOrStringArray{Schema: &spec.Schema{}} 77 } 78 }, 79 func(url *spec.SchemaURL, c fuzz.Continue) { 80 *url = spec.SchemaURL("http://url") 81 }, 82 func(s *spec.Swagger, c fuzz.Continue) { 83 enter(s, false, c) 84 defer leave(false) 85 86 // only fuzz those fields we walk into with invisible==false 87 c.Fuzz(&s.Parameters) 88 c.Fuzz(&s.Responses) 89 c.Fuzz(&s.Definitions) 90 c.Fuzz(&s.Paths) 91 }, 92 func(p *spec.PathItem, c fuzz.Continue) { 93 enter(p, false, c) 94 defer leave(false) 95 96 // only fuzz those fields we walk into with invisible==false 97 c.Fuzz(&p.Parameters) 98 c.Fuzz(&p.Delete) 99 c.Fuzz(&p.Get) 100 c.Fuzz(&p.Head) 101 c.Fuzz(&p.Options) 102 c.Fuzz(&p.Patch) 103 c.Fuzz(&p.Post) 104 c.Fuzz(&p.Put) 105 }, 106 func(p *spec.Parameter, c fuzz.Continue) { 107 enter(p, false, c) 108 defer leave(false) 109 110 // only fuzz those fields we walk into with invisible==false 111 c.Fuzz(&p.Ref) 112 c.Fuzz(&p.Schema) 113 if c.RandBool() { 114 p.Items = &spec.Items{} 115 c.Fuzz(&p.Items.Ref) 116 } else { 117 p.Items = nil 118 } 119 }, 120 func(s *spec.Response, c fuzz.Continue) { 121 enter(s, false, c) 122 defer leave(false) 123 124 // only fuzz those fields we walk into with invisible==false 125 c.Fuzz(&s.Ref) 126 c.Fuzz(&s.Description) 127 c.Fuzz(&s.Schema) 128 c.Fuzz(&s.Examples) 129 }, 130 func(s *spec.Dependencies, c fuzz.Continue) { 131 enter(s, false, c) 132 defer leave(false) 133 134 // and nothing with invisible==false 135 }, 136 func(p *spec.SimpleSchema, c fuzz.Continue) { 137 // gofuzz is broken and calls this even for *SimpleSchema fields, ignoring NilChance, leading to infinite recursion 138 if c.Float64() > nilChance(depth) { 139 return 140 } 141 142 enter(p, true, c) 143 defer leave(true) 144 145 c.FuzzNoCustom(p) 146 }, 147 func(s *spec.SchemaProps, c fuzz.Continue) { 148 // gofuzz is broken and calls this even for *SchemaProps fields, ignoring NilChance, leading to infinite recursion 149 if c.Float64() > nilChance(depth) { 150 return 151 } 152 153 enter(s, true, c) 154 defer leave(true) 155 156 c.FuzzNoCustom(s) 157 }, 158 func(i *interface{}, c fuzz.Continue) { 159 // do nothing for examples and defaults. These are free form JSON fields. 160 }, 161 ) 162 } 163 164 func TestReplaceReferences(t *testing.T) { 165 visibleRE, err := regexp.Compile("\"\\$ref\":\"(http://ref-[^\"]*)\"") 166 if err != nil { 167 t.Fatalf("failed to compile ref regex: %v", err) 168 } 169 invisibleRE, err := regexp.Compile("\"\\$ref\":\"(http://invisible-[^\"]*)\"") 170 if err != nil { 171 t.Fatalf("failed to compile ref regex: %v", err) 172 } 173 174 for i := 0; i < 1000; i++ { 175 var visibleRefs, invisibleRefs sets.String 176 var seed int64 177 var randSource rand.Source 178 var s *spec.Swagger 179 for { 180 visibleRefs = sets.NewString() 181 invisibleRefs = sets.NewString() 182 183 f := fuzz.New() 184 seed = time.Now().UnixNano() 185 //seed = int64(1549012506261785182) 186 randSource = rand.New(rand.NewSource(seed)) 187 f.RandSource(randSource) 188 189 visibleRefsNum := 0 190 invisibleRefsNum := 0 191 fuzzFuncs(f, 192 func(ref *spec.Ref, c fuzz.Continue, visible bool) { 193 var url string 194 if visible { 195 // this is a ref that is seen by the walker (we have some exceptions where we don't walk into) 196 url = fmt.Sprintf("http://ref-%d", visibleRefsNum) 197 visibleRefsNum++ 198 } else { 199 // this is a ref that is not seen by the walker (we have some exceptions where we don't walk into) 200 url = fmt.Sprintf("http://invisible-%d", invisibleRefsNum) 201 invisibleRefsNum++ 202 } 203 204 r, err := spec.NewRef(url) 205 if err != nil { 206 t.Fatalf("failed to fuzz ref: %v", err) 207 } 208 *ref = r 209 }, 210 ) 211 212 // create random swagger spec with random URL references, but at least one ref 213 s = &spec.Swagger{} 214 f.Fuzz(s) 215 216 // clone spec to normalize (fuzz might generate objects which do not roundtrip json marshalling 217 var err error 218 s, err = cloneSwagger(s) 219 if err != nil { 220 t.Fatalf("failed to normalize swagger after fuzzing: %v", err) 221 } 222 223 // find refs 224 bs, err := s.MarshalJSON() 225 if err != nil { 226 t.Fatalf("failed to marshal swagger: %v", err) 227 } 228 for _, m := range invisibleRE.FindAllStringSubmatch(string(bs), -1) { 229 invisibleRefs.Insert(m[1]) 230 } 231 if res := visibleRE.FindAllStringSubmatch(string(bs), -1); len(res) > 0 { 232 for _, m := range res { 233 visibleRefs.Insert(m[1]) 234 } 235 break 236 } 237 } 238 239 t.Run(fmt.Sprintf("iteration %d", i), func(t *testing.T) { 240 mutatedRefs := sets.NewString() 241 mutationProbability := rand.New(randSource).Float64() 242 for _, vr := range visibleRefs.List() { 243 if rand.New(randSource).Float64() > mutationProbability { 244 mutatedRefs.Insert(vr) 245 } 246 } 247 248 origString, err := s.MarshalJSON() 249 if err != nil { 250 t.Fatalf("failed to marshal swagger: %v", err) 251 } 252 t.Logf("created schema with %d walked refs, %d invisible refs, mutating %v, seed %d: %s", visibleRefs.Len(), invisibleRefs.Len(), mutatedRefs.List(), seed, string(origString)) 253 254 // convert to json string, replace one of the refs, and unmarshal back 255 mutatedString := string(origString) 256 for _, r := range mutatedRefs.List() { 257 mr := strings.Replace(r, "ref", "mutated", -1) 258 mutatedString = strings.Replace(mutatedString, "\""+r+"\"", "\""+mr+"\"", -1) 259 } 260 mutatedViaJSON := &spec.Swagger{} 261 if err := json.Unmarshal([]byte(mutatedString), mutatedViaJSON); err != nil { 262 t.Fatalf("failed to unmarshal mutated spec: %v", err) 263 } 264 265 // replay the same mutation using the mutating walker 266 seenRefs := sets.NewString() 267 walker := Walker{ 268 RefCallback: func(ref *spec.Ref) *spec.Ref { 269 seenRefs.Insert(ref.String()) 270 if mutatedRefs.Has(ref.String()) { 271 r, err := spec.NewRef(strings.Replace(ref.String(), "ref", "mutated", -1)) 272 if err != nil { 273 t.Fatalf("failed to create ref: %v", err) 274 } 275 return &r 276 } 277 return ref 278 }, 279 SchemaCallback: SchemaCallBackNoop, 280 } 281 mutatedViaWalker := walker.WalkRoot(s) 282 283 // compare that we got the same 284 if !reflect.DeepEqual(mutatedViaJSON, mutatedViaWalker) { 285 t.Errorf("mutation via walker differ from JSON text replacement (got A, expected B): %s", objectDiff(mutatedViaWalker, mutatedViaJSON)) 286 } 287 if !seenRefs.HasAll(visibleRefs.List()...) { 288 t.Errorf("expected to see the same refs in the walker as during fuzzing. Not seen: %v", visibleRefs.Difference(seenRefs).List()) 289 } 290 if shouldNotSee := seenRefs.Intersection(invisibleRefs); shouldNotSee.Len() > 0 { 291 t.Errorf("refs seen that the walker is not expected to see: %v", shouldNotSee.List()) 292 } 293 }) 294 } 295 } 296 297 func TestReplaceSchema(t *testing.T) { 298 for i := 0; i < 1000; i++ { 299 t.Run(fmt.Sprintf("iteration-%d", i), func(t *testing.T) { 300 seed := time.Now().UnixNano() 301 f := fuzz.NewWithSeed(seed).NilChance(0).MaxDepth(5) 302 rootSchema := &spec.Schema{} 303 f.Funcs(func(s *spec.Schema, c fuzz.Continue) { 304 c.Fuzz(&s.Description) 305 s.Description += " original" 306 if c.RandBool() { 307 // append enums 308 var enums []string 309 c.Fuzz(&enums) 310 for _, enum := range enums { 311 s.Enum = append(s.Enum, enum) 312 } 313 } 314 if c.RandBool() { 315 c.Fuzz(&s.Properties) 316 } 317 if c.RandBool() { 318 c.Fuzz(&s.AdditionalProperties) 319 } 320 if c.RandBool() { 321 c.Fuzz(&s.PatternProperties) 322 } 323 if c.RandBool() { 324 c.Fuzz(&s.AdditionalItems) 325 } 326 if c.RandBool() { 327 c.Fuzz(&s.AnyOf) 328 } 329 if c.RandBool() { 330 c.Fuzz(&s.AllOf) 331 } 332 if c.RandBool() { 333 c.Fuzz(&s.OneOf) 334 } 335 if c.RandBool() { 336 c.Fuzz(&s.Not) 337 } 338 if c.RandBool() { 339 c.Fuzz(&s.Definitions) 340 } 341 if c.RandBool() { 342 items := new(spec.SchemaOrArray) 343 if c.RandBool() { 344 c.Fuzz(&items.Schema) 345 } else { 346 c.Fuzz(&items.Schemas) 347 } 348 s.Items = items 349 } 350 }) 351 f.Fuzz(rootSchema) 352 w := &Walker{SchemaCallback: func(schema *spec.Schema) *spec.Schema { 353 s := *schema 354 s.Description = strings.Replace(s.Description, "original", "modified", -1) 355 return &s 356 }, RefCallback: RefCallbackNoop} 357 newSchema := w.WalkSchema(rootSchema) 358 origBytes, err := json.Marshal(rootSchema) 359 if err != nil { 360 t.Fatalf("cannot marshal original schema: %v", err) 361 } 362 origJSON := string(origBytes) 363 mutatedWithString := strings.Replace(origJSON, "original", "modified", -1) 364 newBytes, err := json.Marshal(newSchema) 365 if err != nil { 366 t.Fatalf("cannot marshal mutated schema: %v", err) 367 } 368 if err := jsontesting.JsonCompare(newBytes, []byte(mutatedWithString)); err != nil { 369 t.Error(err) 370 } 371 if !strings.Contains(origJSON, `"enum":[`) { 372 t.Logf("did not contain enum, skipping enum checks") 373 return 374 } 375 // test enum removal 376 w = &Walker{SchemaCallback: func(schema *spec.Schema) *spec.Schema { 377 s := *schema 378 s.Enum = nil 379 return &s 380 }, RefCallback: RefCallbackNoop} 381 newSchema = w.WalkSchema(rootSchema) 382 newBytes, err = json.Marshal(newSchema) 383 if err != nil { 384 t.Fatalf("cannot marshal mutated schema: %v", err) 385 } 386 if strings.Contains(string(newBytes), `"enum":[`) { 387 t.Errorf("enum still exists in %q", newBytes) 388 } 389 }) 390 } 391 } 392 393 func cloneSwagger(orig *spec.Swagger) (*spec.Swagger, error) { 394 bs, err := orig.MarshalJSON() 395 if err != nil { 396 return nil, fmt.Errorf("error marshaling: %v", err) 397 } 398 s := &spec.Swagger{} 399 if err := json.Unmarshal(bs, s); err != nil { 400 return nil, fmt.Errorf("error unmarshaling: %v", err) 401 } 402 return s, nil 403 } 404 405 // stringDiff diffs a and b and returns a human readable diff. 406 func stringDiff(a, b string) string { 407 ba := []byte(a) 408 bb := []byte(b) 409 out := []byte{} 410 i := 0 411 for ; i < len(ba) && i < len(bb); i++ { 412 if ba[i] != bb[i] { 413 break 414 } 415 out = append(out, ba[i]) 416 } 417 out = append(out, []byte("\n\nA: ")...) 418 out = append(out, ba[i:]...) 419 out = append(out, []byte("\n\nB: ")...) 420 out = append(out, bb[i:]...) 421 out = append(out, []byte("\n\n")...) 422 return string(out) 423 } 424 425 // objectDiff writes the two objects out as JSON and prints out the identical part of 426 // the objects followed by the remaining part of 'a' and finally the remaining part of 'b'. 427 // For debugging tests. 428 func objectDiff(a, b interface{}) string { 429 ab, err := json.Marshal(a) 430 if err != nil { 431 panic(fmt.Sprintf("a: %v", err)) 432 } 433 bb, err := json.Marshal(b) 434 if err != nil { 435 panic(fmt.Sprintf("b: %v", err)) 436 } 437 return stringDiff(string(ab), string(bb)) 438 } 439 440 func max(i, j int) int { 441 if i > j { 442 return i 443 } 444 return j 445 }