diff --git a/openapi2kong/oas3_testfiles/38-ignore-security-errors-oidc.expected.json b/openapi2kong/oas3_testfiles/38-ignore-security-errors-oidc.expected.json new file mode 100644 index 0000000..d5b9868 --- /dev/null +++ b/openapi2kong/oas3_testfiles/38-ignore-security-errors-oidc.expected.json @@ -0,0 +1,75 @@ +{ + "_format_version": "3.0", + "services": [ + { + "host": "example.com", + "id": "730d612d-914b-5fe8-8ead-e6aa654318ef", + "name": "example", + "path": "/", + "plugins": [], + "port": 443, + "protocol": "https", + "routes": [ + { + "id": "52dbb730-8d56-578c-bc69-0543a34bff4a", + "methods": [ + "GET" + ], + "name": "example_getoidc", + "paths": [ + "~/oidc$" + ], + "plugins": [ + { + "config": { + "issuer": "https://example.com/.well-known/openid-configuration", + "scopes_required": [] + }, + "name": "openid-connect" + } + ], + "regex_priority": 200, + "strip_path": false, + "tags": [ + "OAS3_import", + "OAS3file_38-ignore-security-errors-oidc.yaml" + ] + }, + { + "id": "9e0937d1-93db-53ba-8ec8-4034f983557e", + "methods": [ + "GET" + ], + "name": "example_getoidcscopes", + "paths": [ + "~/oidc-with-scopes$" + ], + "plugins": [ + { + "config": { + "issuer": "https://example.com/.well-known/openid-configuration", + "scopes_required": [ + "read", + "write" + ] + }, + "name": "openid-connect" + } + ], + "regex_priority": 200, + "strip_path": false, + "tags": [ + "OAS3_import", + "OAS3file_38-ignore-security-errors-oidc.yaml" + ] + } + ], + "tags": [ + "OAS3_import", + "OAS3file_38-ignore-security-errors-oidc.yaml" + ] + } + ], + "upstreams": [] +} + diff --git a/openapi2kong/oas3_testfiles/38-ignore-security-errors-oidc.yaml b/openapi2kong/oas3_testfiles/38-ignore-security-errors-oidc.yaml new file mode 100644 index 0000000..20d40f9 --- /dev/null +++ b/openapi2kong/oas3_testfiles/38-ignore-security-errors-oidc.yaml @@ -0,0 +1,33 @@ +# Regression test for https://github.com/Kong/deck/issues/1829 +# When --ignore-security-errors is used with a valid single openIdConnect scheme, +# the openid-connect plugin should still be generated. + +openapi: 3.0.4 +info: + title: example + version: 1.2.0 +servers: + - url: https://example.com +paths: + /oidc: + get: + operationId: getOidc + security: + - OpenIDConnect: [] + responses: + '204': + description: No content + /oidc-with-scopes: + get: + operationId: getOidcScopes + security: + - OpenIDConnect: ["read", "write"] + responses: + '200': + description: OK +components: + securitySchemes: + OpenIDConnect: + type: openIdConnect + openIdConnectUrl: https://example.com/.well-known/openid-configuration + diff --git a/openapi2kong/openapi2kong.go b/openapi2kong/openapi2kong.go index 798fd5f..14b3959 100644 --- a/openapi2kong/openapi2kong.go +++ b/openapi2kong/openapi2kong.go @@ -162,24 +162,31 @@ func getOIDCdefaults( scheme *v3.SecurityScheme // the security-scheme object ) { - if len(requirements) == 0 || ignoreSecurityErrors { - // no security requirements or nothing is defined + if len(requirements) == 0 { + // no security requirements defined // so return inherited (can be nil) return inherited, nil } - if len(requirements) > 1 && !ignoreSecurityErrors { - return nil, fmt.Errorf("only a single security-requirement is supported") + if len(requirements) > 1 { + // multiple requirements represent OR logic, which is not supported + if !ignoreSecurityErrors { + return nil, fmt.Errorf("only a single security-requirement is supported") + } + return inherited, nil } requirement := requirements[0].Requirements - if requirement.Len() == 0 || ignoreSecurityErrors { + if requirement.Len() == 0 { return inherited, nil // there is nothing defined, so return inherited (can be nil) } - if requirement.Len() > 1 && !ignoreSecurityErrors { + if requirement.Len() > 1 { // multiple schemes are a logical AND, which is not supported - return nil, fmt.Errorf("within a security-requirement only a single security-scheme is supported") + if !ignoreSecurityErrors { + return nil, fmt.Errorf("within a security-requirement only a single security-scheme is supported") + } + return inherited, nil } // requirement has only 1 entry diff --git a/openapi2kong/openapi2kong_test.go b/openapi2kong/openapi2kong_test.go index 3175a30..9df85d4 100644 --- a/openapi2kong/openapi2kong_test.go +++ b/openapi2kong/openapi2kong_test.go @@ -237,3 +237,189 @@ func Test_Openapi2kong_SkipRouteByHeader(t *testing.T) { } } } + +// Test_Openapi2kong_IgnoreSecurityErrors verifies that --ignore-security-errors +// does not suppress OIDC plugin generation for valid single openIdConnect schemes. +// Regression test for https://github.com/Kong/deck/issues/1829 +func Test_Openapi2kong_IgnoreSecurityErrors(t *testing.T) { + suffix := ".expected.json" + testFiles := []string{ + "38-ignore-security-errors-oidc", + } + + for _, fileNameBase := range testFiles { + t.Run(fileNameBase, func(t *testing.T) { + fileNameIn := fileNameBase + ".yaml" + fileNameExpected := fileNameBase + suffix + fileNameOut := fileNameBase + ".generated.json" + + dataIn, _ := os.ReadFile(fixturePath + fileNameIn) + dataOut, err := Convert(dataIn, O2kOptions{ + Tags: []string{"OAS3_import", "OAS3file_" + fileNameIn}, + OIDC: true, + IgnoreSecurityErrors: true, + }) + + if err != nil { + t.Errorf("'%s' didn't expect error: %v", fixturePath+fileNameIn, err) + } else { + JSONOut, _ := json.MarshalIndent(dataOut, "", " ") + os.WriteFile(fixturePath+fileNameOut, JSONOut, 0o600) + JSONExpected, _ := os.ReadFile(fixturePath + fileNameExpected) + assert.JSONEq(t, string(JSONExpected), string(JSONOut), + "'%s': the JSON blobs should be equal", fixturePath+fileNameIn) + } + }) + } +} + +// Test_Openapi2kong_IgnoreSecurityErrors_SkipsInvalid verifies that --ignore-security-errors +// suppresses errors for unsupported security configurations (multiple requirements, multiple schemes, +// non-OIDC types) but still processes valid ones in the same document. +func Test_Openapi2kong_IgnoreSecurityErrors_SkipsInvalid(t *testing.T) { + tests := []struct { + name string + spec string + wantOIDC bool + }{ + { + name: "multiple requirements at doc level are ignored", + spec: ` +openapi: "3.0.0" +info: + title: test + version: v1 +servers: + - url: https://example.com +security: + - oidc1: [] + - oidc2: [] +paths: + /test: + get: + operationId: getTest + responses: + "200": + description: OK +components: + securitySchemes: + oidc1: + type: openIdConnect + openIdConnectUrl: https://example.com/.well-known/openid-configuration + oidc2: + type: openIdConnect + openIdConnectUrl: https://example.com/.well-known/openid-configuration +`, + wantOIDC: false, + }, + { + name: "non-OIDC type at operation level is ignored, no plugin generated", + spec: ` +openapi: "3.0.0" +info: + title: test + version: v1 +servers: + - url: https://example.com +paths: + /test: + get: + operationId: getTest + security: + - apiKey: [] + responses: + "200": + description: OK +components: + securitySchemes: + apiKey: + type: apiKey + in: header + name: X-API-Key +`, + wantOIDC: false, + }, + { + name: "valid single OIDC at operation level generates plugin", + spec: ` +openapi: "3.0.0" +info: + title: test + version: v1 +servers: + - url: https://example.com +paths: + /test: + get: + operationId: getTest + security: + - oidc: [] + responses: + "200": + description: OK +components: + securitySchemes: + oidc: + type: openIdConnect + openIdConnectUrl: https://example.com/.well-known/openid-configuration +`, + wantOIDC: true, + }, + { + name: "mixed supported OIDC and unsupported apiKey in one spec", + spec: ` +openapi: "3.0.4" +info: + title: mixed-security + version: "1.0.0" +servers: + - url: https://example.com +components: + securitySchemes: + OpenIDConnect: + type: openIdConnect + openIdConnectUrl: https://auth.example.com/.well-known/openid-configuration + ApiKeyAuth: + type: apiKey + in: header + name: X-API-Key +paths: + /secure-endpoint: + get: + operationId: secureEndpoint + security: + - OpenIDConnect: [] + responses: + "204": + description: No content + /legacy-endpoint: + get: + operationId: legacyEndpoint + security: + - ApiKeyAuth: [] + responses: + "204": + description: No content +`, + wantOIDC: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result, err := Convert([]byte(tc.spec), O2kOptions{ + OIDC: true, + IgnoreSecurityErrors: true, + }) + assert.NoError(t, err) + + jsonData, _ := json.Marshal(result) + hasOIDC := strings.Contains(string(jsonData), `"name":"openid-connect"`) + if tc.wantOIDC { + assert.True(t, hasOIDC, "expected openid-connect plugin to be generated") + } else { + assert.False(t, hasOIDC, "expected no openid-connect plugin to be generated") + } + }) + } +}