Skip to content

Commit a9bc3c0

Browse files
authored
Merge pull request kubernetes#79587 from yue9944882/bugfix/tighten-primitive-json-schema-type-validation
DO NOT publish openapi specs containing bad types
2 parents a7c81c6 + 1ec8746 commit a9bc3c0

File tree

4 files changed

+165
-5
lines changed

4 files changed

+165
-5
lines changed

staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import (
4747
var (
4848
printerColumnDatatypes = sets.NewString("integer", "number", "string", "boolean", "date")
4949
customResourceColumnDefinitionFormats = sets.NewString("int32", "int64", "float", "double", "byte", "date", "date-time", "password")
50+
openapiV3Types = sets.NewString("string", "number", "integer", "boolean", "array", "object")
5051
)
5152

5253
// ValidateCustomResourceDefinition statically validates
@@ -1157,3 +1158,75 @@ func validateAPIApproval(newCRD, oldCRD *apiextensions.CustomResourceDefinition,
11571158
return field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations").Key(v1beta1.KubeAPIApprovedAnnotation), newCRD.Annotations[v1beta1.KubeAPIApprovedAnnotation], reason)}
11581159
}
11591160
}
1161+
1162+
// SchemaHasInvalidTypes returns true if it contains invalid offending openapi-v3 specification.
1163+
func SchemaHasInvalidTypes(s *apiextensions.JSONSchemaProps) bool {
1164+
if s == nil {
1165+
return false
1166+
}
1167+
1168+
if len(s.Type) > 0 && !openapiV3Types.Has(s.Type) {
1169+
return true
1170+
}
1171+
1172+
if s.Items != nil {
1173+
if s.Items != nil && SchemaHasInvalidTypes(s.Items.Schema) {
1174+
return true
1175+
}
1176+
for _, s := range s.Items.JSONSchemas {
1177+
if SchemaHasInvalidTypes(&s) {
1178+
return true
1179+
}
1180+
}
1181+
}
1182+
for _, s := range s.AllOf {
1183+
if SchemaHasInvalidTypes(&s) {
1184+
return true
1185+
}
1186+
}
1187+
for _, s := range s.AnyOf {
1188+
if SchemaHasInvalidTypes(&s) {
1189+
return true
1190+
}
1191+
}
1192+
for _, s := range s.OneOf {
1193+
if SchemaHasInvalidTypes(&s) {
1194+
return true
1195+
}
1196+
}
1197+
if SchemaHasInvalidTypes(s.Not) {
1198+
return true
1199+
}
1200+
for _, s := range s.Properties {
1201+
if SchemaHasInvalidTypes(&s) {
1202+
return true
1203+
}
1204+
}
1205+
if s.AdditionalProperties != nil {
1206+
if SchemaHasInvalidTypes(s.AdditionalProperties.Schema) {
1207+
return true
1208+
}
1209+
}
1210+
for _, s := range s.PatternProperties {
1211+
if SchemaHasInvalidTypes(&s) {
1212+
return true
1213+
}
1214+
}
1215+
if s.AdditionalItems != nil {
1216+
if SchemaHasInvalidTypes(s.AdditionalItems.Schema) {
1217+
return true
1218+
}
1219+
}
1220+
for _, s := range s.Definitions {
1221+
if SchemaHasInvalidTypes(&s) {
1222+
return true
1223+
}
1224+
}
1225+
for _, d := range s.Dependencies {
1226+
if SchemaHasInvalidTypes(d.Schema) {
1227+
return true
1228+
}
1229+
}
1230+
1231+
return false
1232+
}

staging/src/k8s.io/apiextensions-apiserver/pkg/controller/openapi/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ go_library(
1414
deps = [
1515
"//staging/src/k8s.io/api/autoscaling/v1:go_default_library",
1616
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
17+
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation:go_default_library",
1718
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
1819
"//staging/src/k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion:go_default_library",
1920
"//staging/src/k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion:go_default_library",

staging/src/k8s.io/apiextensions-apiserver/pkg/controller/openapi/builder.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ import (
2222
"strings"
2323
"sync"
2424

25-
restful "github.com/emicklei/go-restful"
25+
"github.com/emicklei/go-restful"
2626
"github.com/go-openapi/spec"
2727

2828
v1 "k8s.io/api/autoscaling/v1"
29+
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation"
2930
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
3031
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3132
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
@@ -68,11 +69,12 @@ func BuildSwagger(crd *apiextensions.CustomResourceDefinition, version string) (
6869
if err != nil {
6970
return nil, err
7071
}
72+
7173
if s != nil && s.OpenAPIV3Schema != nil {
72-
ss, err := structuralschema.NewStructural(s.OpenAPIV3Schema)
73-
if err == nil && len(structuralschema.ValidateStructural(ss, nil)) == 0 {
74-
// skip non-structural schemas
75-
schema = ss.Unfold()
74+
if !validation.SchemaHasInvalidTypes(s.OpenAPIV3Schema) {
75+
if ss, err := structuralschema.NewStructural(s.OpenAPIV3Schema); err == nil {
76+
schema = ss.Unfold()
77+
}
7678
}
7779
}
7880

staging/src/k8s.io/apiextensions-apiserver/pkg/controller/openapi/builder_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,3 +540,87 @@ func schemaDiff(a, b *spec.Schema) string {
540540
}
541541
return diff.StringDiff(string(as), string(bs))
542542
}
543+
544+
func TestBuildSwagger(t *testing.T) {
545+
tests := []struct {
546+
name string
547+
schema string
548+
wantedSchema string
549+
}{
550+
{
551+
"nil",
552+
"",
553+
`{"type":"object","x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
554+
},
555+
{
556+
"with properties",
557+
`{"type":"object","properties":{"spec":{"type":"object"},"status":{"type":"object"}}}`,
558+
`{"type":"object","properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"metadata":{"$ref":"#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"},"spec":{"type":"object"},"status":{"type":"object"}},"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
559+
},
560+
{
561+
"with invalid-typed properties",
562+
`{"type":"object","properties":{"spec":{"type":"bug"},"status":{"type":"object"}}}`,
563+
`{"type":"object","x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]}`,
564+
},
565+
}
566+
for _, tt := range tests {
567+
t.Run(tt.name, func(t *testing.T) {
568+
var validation *apiextensions.CustomResourceValidation
569+
if len(tt.schema) > 0 {
570+
v1beta1Schema := &v1beta1.JSONSchemaProps{}
571+
if err := json.Unmarshal([]byte(tt.schema), &v1beta1Schema); err != nil {
572+
t.Fatal(err)
573+
}
574+
internalSchema := &apiextensions.JSONSchemaProps{}
575+
v1beta1.Convert_v1beta1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(v1beta1Schema, internalSchema, nil)
576+
validation = &apiextensions.CustomResourceValidation{
577+
OpenAPIV3Schema: internalSchema,
578+
}
579+
}
580+
581+
// TODO: mostly copied from the test above. reuse code to cleanup
582+
got, err := BuildSwagger(&apiextensions.CustomResourceDefinition{
583+
Spec: apiextensions.CustomResourceDefinitionSpec{
584+
Group: "bar.k8s.io",
585+
Version: "v1",
586+
Names: apiextensions.CustomResourceDefinitionNames{
587+
Plural: "foos",
588+
Singular: "foo",
589+
Kind: "Foo",
590+
ListKind: "FooList",
591+
},
592+
Scope: apiextensions.NamespaceScoped,
593+
Validation: validation,
594+
},
595+
}, "v1")
596+
if err != nil {
597+
t.Fatal(err)
598+
}
599+
600+
var wantedSchema spec.Schema
601+
if err := json.Unmarshal([]byte(tt.wantedSchema), &wantedSchema); err != nil {
602+
t.Fatal(err)
603+
}
604+
605+
gotSchema := got.Definitions["io.k8s.bar.v1.Foo"]
606+
gotProperties := properties(gotSchema.Properties)
607+
wantedProperties := properties(wantedSchema.Properties)
608+
if !gotProperties.Equal(wantedProperties) {
609+
t.Fatalf("unexpected properties, got: %s, expected: %s", gotProperties.List(), wantedProperties.List())
610+
}
611+
612+
// wipe out TypeMeta/ObjectMeta content, with those many lines of descriptions. We trust that they match here.
613+
for _, metaField := range []string{"kind", "apiVersion", "metadata"} {
614+
if _, found := gotSchema.Properties["kind"]; found {
615+
prop := gotSchema.Properties[metaField]
616+
prop.Description = ""
617+
gotSchema.Properties[metaField] = prop
618+
}
619+
}
620+
621+
if !reflect.DeepEqual(&wantedSchema, &gotSchema) {
622+
t.Errorf("unexpected schema: %s\nwant = %#v\ngot = %#v", schemaDiff(&wantedSchema, &gotSchema), &wantedSchema, &gotSchema)
623+
}
624+
})
625+
}
626+
}

0 commit comments

Comments
 (0)