|
12 | 12 | */ |
13 | 13 | package io.kubernetes.client.util; |
14 | 14 |
|
| 15 | +import com.google.gson.annotations.SerializedName; |
15 | 16 | import io.kubernetes.client.common.KubernetesType; |
16 | 17 | import io.kubernetes.client.custom.IntOrString; |
17 | 18 | import io.kubernetes.client.custom.Quantity; |
| 19 | +import io.kubernetes.client.openapi.models.V1JSONSchemaProps; |
| 20 | +import io.kubernetes.client.openapi.models.V1beta1JSONSchemaProps; |
18 | 21 | import java.io.File; |
19 | 22 | import java.io.FileReader; |
20 | 23 | import java.io.IOException; |
21 | 24 | import java.io.Reader; |
22 | 25 | import java.io.StringReader; |
23 | 26 | import java.io.Writer; |
| 27 | +import java.lang.reflect.Field; |
| 28 | +import java.lang.reflect.Method; |
24 | 29 | import java.time.OffsetDateTime; |
25 | 30 | import java.util.ArrayList; |
| 31 | +import java.util.Arrays; |
26 | 32 | import java.util.Collections; |
27 | 33 | import java.util.Comparator; |
28 | 34 | import java.util.Iterator; |
29 | 35 | import java.util.List; |
30 | 36 | import java.util.Map; |
| 37 | +import java.util.Optional; |
31 | 38 | import java.util.Set; |
32 | 39 | import okio.ByteString; |
33 | 40 | import org.slf4j.Logger; |
34 | 41 | import org.slf4j.LoggerFactory; |
35 | 42 | import org.yaml.snakeyaml.DumperOptions; |
| 43 | +import org.yaml.snakeyaml.TypeDescription; |
| 44 | +import org.yaml.snakeyaml.constructor.BaseConstructor; |
36 | 45 | import org.yaml.snakeyaml.constructor.Constructor; |
37 | 46 | import org.yaml.snakeyaml.constructor.SafeConstructor; |
38 | 47 | import org.yaml.snakeyaml.introspector.Property; |
|
44 | 53 | import org.yaml.snakeyaml.representer.Represent; |
45 | 54 | import org.yaml.snakeyaml.representer.Representer; |
46 | 55 |
|
| 56 | +/** The type Yaml. */ |
47 | 57 | public class Yaml { |
48 | 58 |
|
49 | 59 | static final Logger logger = LoggerFactory.getLogger(Yaml.class); |
@@ -359,17 +369,137 @@ protected NodeTuple representJavaBeanProperty( |
359 | 369 | } |
360 | 370 | } |
361 | 371 |
|
362 | | - /** @return An instantiated SnakeYaml Object. */ |
| 372 | + /** |
| 373 | + * Instantiate a snake yaml with a default {@link SafeConstructor}. |
| 374 | + * |
| 375 | + * <p>DEPRECATED: Use the parameterized "getSnakeYaml" constructing method below to get rid of |
| 376 | + * vulnerability from dynamic type serialization. |
| 377 | + * |
| 378 | + * @return the snake yaml |
| 379 | + */ |
363 | 380 | @Deprecated |
364 | 381 | public static org.yaml.snakeyaml.Yaml getSnakeYaml() { |
365 | 382 | return getSnakeYaml(null); |
366 | 383 | } |
367 | 384 |
|
368 | | - private static org.yaml.snakeyaml.Yaml getSnakeYaml(Class<?> type) { |
| 385 | + /** |
| 386 | + * Instantiate a snake yaml with the target model type specified.. |
| 387 | + * |
| 388 | + * @param type the target model type |
| 389 | + * @param typeDescriptions additional type descriptions for customizing the serializer |
| 390 | + * @return the new snake yaml instance |
| 391 | + */ |
| 392 | + public static org.yaml.snakeyaml.Yaml getSnakeYaml( |
| 393 | + Class<?> type, TypeDescription... typeDescriptions) { |
| 394 | + BaseConstructor constructor = new SafeConstructor(); |
| 395 | + Representer representer = new CustomRepresenter(); |
369 | 396 | if (type != null) { |
370 | | - return new org.yaml.snakeyaml.Yaml(new CustomConstructor(type), new CustomRepresenter()); |
| 397 | + constructor = new CustomConstructor(type); |
| 398 | + } |
| 399 | + // register builtin type descriptions |
| 400 | + registerBuiltinGsonCompatibles(constructor, representer); |
| 401 | + for (TypeDescription desc : typeDescriptions) { |
| 402 | + registerCustomTypeDescriptions(constructor, representer, desc); |
| 403 | + } |
| 404 | + return new org.yaml.snakeyaml.Yaml(constructor, representer); |
| 405 | + } |
| 406 | + |
| 407 | + /** |
| 408 | + * Instantiate a new {@link TypeDescription} which will load the {@link SerializedName} via |
| 409 | + * reflection so that yaml serialization can work for the custom gson serialized name. |
| 410 | + * |
| 411 | + * @param modelClass the kubenretes api model class |
| 412 | + * @param gsonTaggedFields the custom serialized names tagged by gson |
| 413 | + * @return the type description |
| 414 | + */ |
| 415 | + public static TypeDescription newGsonCompatibleTypeDescription( |
| 416 | + Class modelClass, String... gsonTaggedFields) { |
| 417 | + TypeDescription desc = new TypeDescription(modelClass); |
| 418 | + List<String> excluding = new ArrayList<>(); |
| 419 | + for (String targetGsonAnnotation : gsonTaggedFields) { |
| 420 | + Field field = |
| 421 | + Arrays.stream(modelClass.getDeclaredFields()) |
| 422 | + .filter(f -> f.getAnnotation(SerializedName.class) != null) |
| 423 | + .filter( |
| 424 | + f -> targetGsonAnnotation.equals(f.getAnnotation(SerializedName.class).value())) |
| 425 | + .findAny() |
| 426 | + .orElseThrow( |
| 427 | + () -> |
| 428 | + new IllegalArgumentException( |
| 429 | + "Api model class " |
| 430 | + + modelClass.getSimpleName() |
| 431 | + + " doesn't have field with Gson @SerializedName with value " |
| 432 | + + targetGsonAnnotation)); |
| 433 | + Method getterMethod = |
| 434 | + tryFindGetterMethod(modelClass, field) |
| 435 | + .orElseThrow( |
| 436 | + () -> |
| 437 | + new IllegalArgumentException( |
| 438 | + "Cannot find getter method for " |
| 439 | + + targetGsonAnnotation |
| 440 | + + " on api model class " |
| 441 | + + modelClass.getSimpleName())); |
| 442 | + Method setterMethod = |
| 443 | + tryFindSetterMethod(modelClass, field) |
| 444 | + .orElseThrow( |
| 445 | + () -> |
| 446 | + new IllegalArgumentException( |
| 447 | + "Cannot find setter method for " |
| 448 | + + targetGsonAnnotation |
| 449 | + + " on api model class " |
| 450 | + + modelClass.getSimpleName())); |
| 451 | + |
| 452 | + desc.substituteProperty( |
| 453 | + targetGsonAnnotation, field.getType(), getterMethod.getName(), setterMethod.getName()); |
| 454 | + excluding.add(field.getName()); |
371 | 455 | } |
372 | | - return new org.yaml.snakeyaml.Yaml(new SafeConstructor(), new CustomRepresenter()); |
| 456 | + desc.setExcludes(excluding.toArray(new String[0])); |
| 457 | + return desc; |
| 458 | + } |
| 459 | + |
| 460 | + private static void registerBuiltinGsonCompatibles( |
| 461 | + BaseConstructor constructor, Representer representer) { |
| 462 | + // TODO: Are there more builtin api classes need these explicit substitution below |
| 463 | + String[] crdOpenApiExtensions = |
| 464 | + new String[] { |
| 465 | + "x-kubernetes-embedded-resource", |
| 466 | + "x-kubernetes-int-or-string", |
| 467 | + "x-kubernetes-list-map-keys", |
| 468 | + "x-kubernetes-list-type", |
| 469 | + "x-kubernetes-map-type", |
| 470 | + "x-kubernetes-preserve-unknown-fields", |
| 471 | + }; |
| 472 | + registerCustomTypeDescriptions( |
| 473 | + constructor, |
| 474 | + representer, |
| 475 | + newGsonCompatibleTypeDescription(V1JSONSchemaProps.class, crdOpenApiExtensions), |
| 476 | + newGsonCompatibleTypeDescription(V1beta1JSONSchemaProps.class, crdOpenApiExtensions)); |
| 477 | + } |
| 478 | + |
| 479 | + private static void registerCustomTypeDescriptions( |
| 480 | + BaseConstructor constructor, |
| 481 | + Representer representer, |
| 482 | + TypeDescription... addtionalTypeDescriptions) { |
| 483 | + Arrays.stream(addtionalTypeDescriptions) |
| 484 | + .forEach( |
| 485 | + desc -> { |
| 486 | + constructor.addTypeDescription(desc); |
| 487 | + representer.addTypeDescription(desc); |
| 488 | + }); |
| 489 | + } |
| 490 | + |
| 491 | + private static Optional<Method> tryFindGetterMethod(Class modelClass, Field targetField) { |
| 492 | + return Arrays.stream(modelClass.getDeclaredMethods()) |
| 493 | + .filter(f -> f.getName().startsWith("get")) |
| 494 | + .filter(f -> f.getName().equalsIgnoreCase("get" + targetField.getName())) |
| 495 | + .findAny(); |
| 496 | + } |
| 497 | + |
| 498 | + private static Optional<Method> tryFindSetterMethod(Class modelClass, Field targetField) { |
| 499 | + return Arrays.stream(modelClass.getDeclaredMethods()) |
| 500 | + .filter(f -> f.getName().startsWith("set")) |
| 501 | + .filter(f -> f.getName().equalsIgnoreCase("set" + targetField.getName())) |
| 502 | + .findAny(); |
373 | 503 | } |
374 | 504 |
|
375 | 505 | /** |
|
0 commit comments