diff --git a/docs/customization.md b/docs/customization.md index ba113c6705fc..6c17341d2853 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -724,6 +724,28 @@ Into this securityScheme: type: http ``` +- `SECURITY_SCHEMES_FILTER` + +The `SECURITY_SCHEMES_FILTER` parameter allows selective inclusion of API security schemes based on specific criteria. It applies the `x-internal: true` property to operations that do **not** match the specified values, preventing them from being generated. Multiple filters can be separated by a semicolon. + +### Available Filters + +- **`key`** + When set to `key:api_key|http_bearer`, security schemes **not** matching `api_key` or `http_bearer` will be marked as internal (`x-internal: true`), and excluded from generation. Matching operations will have `x-internal: false`. + +- **`type`** + When set to `type:apiKey|http`, security schemes **not** using `apiKey` or `http` types will be marked as internal (`x-internal: true`), preventing their generation. + +### Example Usage + +```sh +java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate \ + -g java \ + -i modules/openapi-generator/src/test/resources/3_0/petstore.yaml \ + -o /tmp/java-okhttp/ \ + --openapi-normalizer SECURITY_SCHEMES_FILTER="key:api_key|http_bearer ; type:oauth2" +``` + - `SORT_MODEL_PROPERTIES`: When set to true, model properties will be sorted alphabetically by name. This ensures deterministic code generation output regardless of property ordering in the source spec. Example: diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index 95f793c195da..65506b899280 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -27,6 +27,7 @@ import io.swagger.v3.oas.models.responses.ApiResponse; import io.swagger.v3.oas.models.responses.ApiResponses; import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.security.SecurityScheme.Type; import org.apache.commons.lang3.StringUtils; import org.openapitools.codegen.utils.ModelUtils; import org.slf4j.Logger; @@ -137,6 +138,9 @@ public class OpenAPINormalizer { // when set (e.g. operationId:getPetById|addPet), filter out (or remove) everything else final String FILTER = "FILTER"; + // when set (e.g. type:http|oauth2), filter out (or remove) everything else + final String SECURITY_SCHEMES_FILTER = "SECURITY_SCHEMES_FILTER"; + // when set (e.g. operationId:getPetById|addPet), filter out (or remove) everything else final String SET_CONTAINER_TO_NULLABLE = "SET_CONTAINER_TO_NULLABLE"; HashSet setContainerToNullable = new HashSet<>(); @@ -209,6 +213,7 @@ public OpenAPINormalizer(OpenAPI openAPI, Map inputRules) { ruleNames.add(NORMALIZE_31SPEC); ruleNames.add(REMOVE_X_INTERNAL); ruleNames.add(FILTER); + ruleNames.add(SECURITY_SCHEMES_FILTER); ruleNames.add(SET_CONTAINER_TO_NULLABLE); ruleNames.add(SET_PRIMITIVE_TYPES_TO_NULLABLE); ruleNames.add(SIMPLIFY_ONEOF_ANYOF_ENUM); @@ -283,6 +288,11 @@ public void processRules(Map inputRules) { // actual parsing is delayed to allow customization of the Filter processing } + if (inputRules.get(SECURITY_SCHEMES_FILTER) != null) { + rules.put(SECURITY_SCHEMES_FILTER, true); + // actual parsing is delayed to allow customization of the Filter processing + } + if (inputRules.get(SET_CONTAINER_TO_NULLABLE) != null) { rules.put(SET_CONTAINER_TO_NULLABLE, true); setContainerToNullable = new HashSet<>(Arrays.asList(inputRules.get(SET_CONTAINER_TO_NULLABLE).split("[|]"))); @@ -327,16 +337,29 @@ public void processRules(Map inputRules) { } /** - * Create the filter to process the FILTER normalizer. + * Create the operations filter to process the FILTER normalizer. + * Override this to create a custom filter normalizer. + * + * @param openApi Contract used in the filtering (could be used for customization). + * @param input full input value + * + * @return an OperationsFilter containing the parsed filters. + */ + protected OperationsFilter createOperationsFilter(OpenAPI openApi, String input) { + return new OperationsFilter(input); + } + + /** + * Create the security schemes filter to process the FILTER normalizer. * Override this to create a custom filter normalizer. * * @param openApi Contract used in the filtering (could be used for customization). - * @param filters full FILTER value + * @param input full input value * - * @return a Filter containing the parsed filters. + * @return an SecuritySchemesFilter containing the parsed filters. */ - protected Filter createFilter(OpenAPI openApi, String filters) { - return new Filter(filters); + protected SecuritySchemesFilter createSecuritySchemesFilter(OpenAPI openApi, String input) { + return new SecuritySchemesFilter(input); } /** @@ -386,6 +409,15 @@ protected void normalizePaths() { return; } + OperationsFilter filter = null; + if (Boolean.TRUE.equals(getRule(FILTER))) { + String filters = inputRules.get(FILTER); + filter = createOperationsFilter(this.openAPI, filters); + if (!filter.parse()) { + filter = null; + } + } + for (Map.Entry pathsEntry : paths.entrySet()) { PathItem path = pathsEntry.getValue(); List operations = new ArrayList<>(path.readOperations()); @@ -401,14 +433,10 @@ protected void normalizePaths() { "trace", PathItem::getTrace ); - if (Boolean.TRUE.equals(getRule(FILTER))) { - String filters = inputRules.get(FILTER); - Filter filter = createFilter(this.openAPI, filters); - if (filter.parse()) { - // Iterates over each HTTP method in methodMap, retrieves the corresponding Operations from the PathItem, - // and marks it as internal (`x-internal=true`) if the method/operationId/tag/path is not in the filters. - filter.apply(pathsEntry.getKey(), path, methodMap); - } + if (filter != null && filter.hasFilter()) { + // Iterates over each HTTP method in methodMap, retrieves the corresponding Operations from the PathItem, + // and marks it as internal (`x-internal=true`) if the method/operationId/tag/path is not in the filters. + filter.apply(pathsEntry.getKey(), path, methodMap); } // Include callback operation as well @@ -586,15 +614,19 @@ protected void normalizeHeaders(Map headers) { * Normalizes securitySchemes in components */ protected void normalizeComponentsSecuritySchemes() { - if (StringUtils.isEmpty(bearerAuthSecuritySchemeName)) { - return; - } - Map schemes = openAPI.getComponents().getSecuritySchemes(); if (schemes == null) { return; } + SecuritySchemesFilter filter = null; + if (Boolean.TRUE.equals(getRule(SECURITY_SCHEMES_FILTER))) { + filter = createSecuritySchemesFilter(openAPI, inputRules.get(SECURITY_SCHEMES_FILTER)); + if (!filter.parse()) { + filter = null; + } + } + for (String schemeKey : schemes.keySet()) { if (schemeKey.equals(bearerAuthSecuritySchemeName)) { SecurityScheme scheme = schemes.get(schemeKey); @@ -609,6 +641,14 @@ protected void normalizeComponentsSecuritySchemes() { scheme.set$ref(null); schemes.put(schemeKey, scheme); } + + // At first we transform a scheme to HTTP bearer and then apply the filter. + // It may happen that bearer scheme will be filtered out on this step. + // To keep the scheme - change filter input. + if (filter != null && filter.hasFilter()) { + SecurityScheme scheme = schemes.get(schemeKey); + filter.apply(schemeKey, scheme); + } } } @@ -1938,20 +1978,22 @@ private void normalizeExclusiveMinMax31(Schema schema) { // ===================== end of rules ===================== - protected static class Filter { - public static final String OPERATION_ID = "operationId"; - public static final String METHOD = "method"; - public static final String TAG = "tag"; - public static final String PATH = "path"; - private final String filters; - protected Set operationIdFilters = Collections.emptySet(); - protected Set methodFilters = Collections.emptySet(); - protected Set tagFilters = Collections.emptySet(); - protected Set pathStartingWithFilters = Collections.emptySet(); - private boolean hasFilter; + // Base class for filters. It provides parsing logic and utility functions for filters. + // All filters should have the same syntax: + // `filterName:value1|value2|value3` and multiple filters can be separated by `;`. + protected static abstract class BaseFilter { + protected boolean hasFilter; + private final String input; + // Key - filtering method, value - set of accepted values. + // For example, to filter operations by method the key would be "method" and the value is a set of {"get", "post"}. + protected Map> filteringMethodsMap = new HashMap<>(); - protected Filter(String filters) { - this.filters = filters.trim(); + protected BaseFilter(String input) { + this.input = input.trim(); + } + + public boolean hasFilter() { + return hasFilter; } /** @@ -1960,23 +2002,34 @@ protected Filter(String filters) { * @return true if filters need to be processed */ public boolean parse() { - if (StringUtils.isEmpty(filters)) { + if (StringUtils.isEmpty(input)) { return false; } try { doParse(); return hasFilter(); } catch (RuntimeException e) { - String message = String.format(Locale.ROOT, "FILTER rule [%s] must be in the form of `%s:name1|name2|name3` or `%s:get|post|put` or `%s:tag1|tag2|tag3` or `%s:/v1|/v2`. Error: %s", - filters, Filter.OPERATION_ID, Filter.METHOD, Filter.TAG, Filter.PATH, e.getMessage()); + String usage = usageMessage(); + String message = String.format(Locale.ROOT, "%s Input: `%s`. Error: %s", usage, input, e.getMessage()); // throw an exception. This is a breaking change compared to pre 7.16.0 // Workaround: fix the syntax! throw new IllegalArgumentException(message); } } + // Defines the filtering methods supported by the filter. + // Can be overridden by child classes to customize filtering. + public abstract Set filteringMethods(); + + // Defines the subject being filtered, e.g. operation, security scheme, etc. This is used for logging purposes. + public abstract String filteringSubject(); + + // Defines the usage message for the filter. This is used for logging purposes when the filter syntax is incorrect. + public abstract String usageMessage(); + private void doParse() { - for (String filter : filters.split(";")) { + Set filteringMethods = filteringMethods(); + for (String filter : input.split(";")) { filter = filter.trim(); String[] filterStrs = filter.split(":"); if (filterStrs.length != 2) { // only support filter with : at the moment @@ -1986,15 +2039,16 @@ private void doParse() { String filterValue = filterStrs[1]; Set parsedFilters = splitByPipe(filterValue); hasFilter = true; - if (OPERATION_ID.equals(filterKey)) { - operationIdFilters = parsedFilters; - } else if (METHOD.equals(filterKey)) { - methodFilters = parsedFilters; - } else if (TAG.equals(filterKey)) { - tagFilters = parsedFilters; - } else if (PATH.equals(filterKey)) { - pathStartingWithFilters = parsedFilters; - } else { + + boolean found = false; + for (String method : filteringMethods) { + if (method.equals(filterKey)) { + found = true; + filteringMethodsMap.put(filterKey, parsedFilters); + break; + } + } + if (!found) { parse(filterKey, filterValue); } } @@ -2014,7 +2068,7 @@ protected Set splitByPipe(String filterValue) { } /** - * Parse non default filters. + * Parse non default filtering methods. * * Override this method to add custom parsing logic. * @@ -2031,6 +2085,50 @@ protected void parseFails(String filterName, String filterValue) { throw new IllegalArgumentException("filter not supported :[" + filterName + ":" + filterValue + "]"); } + protected boolean logIfMatch(String filterName, String subjectId, boolean filterMatched) { + if (filterMatched) { + logMatch(filterName, subjectId); + } + return filterMatched; + } + + protected void logMatch(String filterName, String subjectId) { + getLogger().info("{} `{}` marked as internal only (x-internal: true) by the {} filter", filteringSubject(), + subjectId, filterName); + } + + protected Logger getLogger() { + return OpenAPINormalizer.LOGGER; + } + } + + protected static class OperationsFilter extends BaseFilter { + public static final String OPERATION_ID = "operationId"; + public static final String METHOD = "method"; + public static final String TAG = "tag"; + public static final String PATH = "path"; + + protected OperationsFilter(String filters) { + super(filters); + } + + @Override + public Set filteringMethods() { + return Set.of(OPERATION_ID, METHOD, TAG, PATH); + } + + @Override + public String filteringSubject() { + return "Operation"; + } + + @Override + public String usageMessage() { + return String.format(Locale.ROOT, + "FILTER rule must be in the form of `%s:name1|name2|name3` or `%s:get|post|put` or `%s:tag1|tag2|tag3` or `%s:/v1|/v2`.", + OperationsFilter.OPERATION_ID, OperationsFilter.METHOD, OperationsFilter.TAG, OperationsFilter.PATH); + } + /** * Test if the OpenAPI contract match an extra filter. * @@ -2045,19 +2143,16 @@ protected boolean hasCustomFilterMatch(String path, Operation operation) { return false; } - public boolean hasFilter() { - return hasFilter; - } - public void apply(String path, PathItem pathItem, Map> methodMap) { methodMap.forEach((method, getter) -> { Operation operation = getter.apply(pathItem); if (operation != null) { boolean found = false; - found |= logIfMatch(PATH, operation, hasPathStarting(path)); - found |= logIfMatch(TAG, operation, hasTag(operation)); - found |= logIfMatch(OPERATION_ID, operation, hasOperationId(operation)); - found |= logIfMatch(METHOD, operation, hasMethod(method)); + String operationId = operation.getOperationId(); + found |= logIfMatch(PATH, operationId, hasPathStarting(path)); + found |= logIfMatch(TAG, operationId, hasTag(operation)); + found |= logIfMatch(OPERATION_ID, operationId, hasOperationId(operation)); + found |= logIfMatch(METHOD, operationId, hasMethod(method)); found |= hasCustomFilterMatch(path, operation); operation.addExtension(X_INTERNAL, !found); @@ -2065,38 +2160,86 @@ public void apply(String path, PathItem pathItem, Map pathStartingWithFilters = filteringMethodsMap.getOrDefault(PATH, Collections.emptySet()); return pathStartingWithFilters.stream().anyMatch(filter -> path.startsWith(filter)); } - private boolean hasTag( Operation operation) { + private boolean hasTag(Operation operation) { + Set tagFilters = filteringMethodsMap.getOrDefault(TAG, Collections.emptySet()); return operation.getTags() != null && operation.getTags().stream().anyMatch(tagFilters::contains); } private boolean hasOperationId(Operation operation) { + Set operationIdFilters = filteringMethodsMap.getOrDefault(OPERATION_ID, Collections.emptySet()); return operationIdFilters.contains(operation.getOperationId()); } private boolean hasMethod(String method) { + Set methodFilters = filteringMethodsMap.getOrDefault(METHOD, Collections.emptySet()); return methodFilters.contains(method); } } + protected static class SecuritySchemesFilter extends BaseFilter { + public static final String KEY = "key"; + public static final String TYPE = "type"; + + protected SecuritySchemesFilter(String filters) { + super(filters); + } + + @Override + public Set filteringMethods() { + return Set.of(KEY, TYPE); + } + + @Override + public String filteringSubject() { + return "Security scheme"; + } + + @Override + public String usageMessage() { + return String.format(Locale.ROOT, + "SECURITY_SCHEMES_FILTER rule must be in the form of `%s:key1|key2|key3` or `%s:apiKey|http|mutualTLS|oauth2|openIdConnect`.", + KEY, TYPE); + } + + /** + * Test if the OpenAPI contract match an extra filter. + * + * Override this method to add custom logic. + * + * @param schemeKey Security scheme key + * @param scheme Security scheme + * + * @return true if the security scheme matches the filter + */ + protected boolean hasCustomFilterMatch(String schemeKey, SecurityScheme scheme) { + return false; + } + + public void apply(String schemeKey, SecurityScheme scheme) { + boolean found = false; + found |= logIfMatch(KEY, schemeKey, hasKey(schemeKey)); + found |= logIfMatch(TYPE, schemeKey, hasType(scheme.getType().toString())); + found |= hasCustomFilterMatch(schemeKey, scheme); + + scheme.addExtension(X_INTERNAL, !found); + } + + private boolean hasKey(String key) { + Set keyFilters = filteringMethodsMap.getOrDefault(KEY, Collections.emptySet()); + return keyFilters.contains(key); + } + + private boolean hasType(String type) { + Set typeFilters = filteringMethodsMap.getOrDefault(TYPE, Collections.emptySet()); + return typeFilters.contains(type); + } + } + /** * When set to true, remove "properties" attribute on schema other than "object" * since it should be ignored and may result in odd generated code diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java index 28829fb748d8..bd5df9738bd1 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java @@ -796,57 +796,57 @@ public void testFilterWithMethod() { assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions().get(X_INTERNAL), true); } - static OpenAPINormalizer.Filter parseFilter(String filters) { - OpenAPINormalizer.Filter filter = new OpenAPINormalizer.Filter(filters); + static OpenAPINormalizer.OperationsFilter parseOperationsFilter(String filters) { + OpenAPINormalizer.OperationsFilter filter = new OpenAPINormalizer.OperationsFilter(filters); filter.parse(); return filter; } @Test - public void testFilterParsing() { - OpenAPINormalizer.Filter filter; + public void testOperationsFilterParsing() { + OpenAPINormalizer.OperationsFilter filter; // no filter - filter = parseFilter(" "); + filter = parseOperationsFilter(" "); assertFalse(filter.hasFilter()); // invalid filter assertThrows(IllegalArgumentException.class, () -> - parseFilter("operationId:")); + parseOperationsFilter("operationId:")); assertThrows(IllegalArgumentException.class, () -> - parseFilter("invalid:invalid:")); + parseOperationsFilter("invalid:invalid:")); // extra spaces are trimmed - filter = parseFilter("method:\n\t\t\t\tget"); + filter = parseOperationsFilter("method:\n\t\t\t\tget"); assertTrue(filter.hasFilter()); - assertEquals(filter.methodFilters, Set.of("get")); - assertTrue(filter.operationIdFilters.isEmpty()); - assertTrue(filter.tagFilters.isEmpty()); - assertTrue(filter.pathStartingWithFilters.isEmpty()); + assertEquals(filter.filteringMethodsMap.get(OpenAPINormalizer.OperationsFilter.METHOD), Set.of("get")); + assertFalse(filter.filteringMethodsMap.containsKey(OpenAPINormalizer.OperationsFilter.OPERATION_ID)); + assertFalse(filter.filteringMethodsMap.containsKey(OpenAPINormalizer.OperationsFilter.TAG)); + assertFalse(filter.filteringMethodsMap.containsKey(OpenAPINormalizer.OperationsFilter.PATH)); // multiple values separated by pipe - filter = parseFilter("operationId:\n\t\t\t\tdelete|\n\t\tlist\t"); + filter = parseOperationsFilter("operationId:\n\t\t\t\tdelete|\n\t\tlist\t"); assertTrue(filter.hasFilter()); - assertTrue(filter.methodFilters.isEmpty()); - assertEquals(filter.operationIdFilters, Set.of("delete", "list")); - assertTrue(filter.tagFilters.isEmpty()); - assertTrue(filter.pathStartingWithFilters.isEmpty()); + assertFalse(filter.filteringMethodsMap.containsKey(OpenAPINormalizer.OperationsFilter.METHOD)); + assertEquals(filter.filteringMethodsMap.get(OpenAPINormalizer.OperationsFilter.OPERATION_ID), Set.of("delete", "list")); + assertFalse(filter.filteringMethodsMap.containsKey(OpenAPINormalizer.OperationsFilter.TAG)); + assertFalse(filter.filteringMethodsMap.containsKey(OpenAPINormalizer.OperationsFilter.PATH)); // multiple filters - filter = parseFilter("operationId:delete|list;path:/v1"); + filter = parseOperationsFilter("operationId:delete|list;path:/v1"); assertTrue(filter.hasFilter()); - assertTrue(filter.methodFilters.isEmpty()); - assertEquals(filter.operationIdFilters, Set.of("delete", "list")); - assertTrue(filter.tagFilters.isEmpty()); - assertEquals(filter.pathStartingWithFilters, Set.of("/v1")); + assertFalse(filter.filteringMethodsMap.containsKey(OpenAPINormalizer.OperationsFilter.METHOD)); + assertEquals(filter.filteringMethodsMap.get(OpenAPINormalizer.OperationsFilter.OPERATION_ID), Set.of("delete", "list")); + assertFalse(filter.filteringMethodsMap.containsKey(OpenAPINormalizer.OperationsFilter.TAG)); + assertEquals(filter.filteringMethodsMap.get(OpenAPINormalizer.OperationsFilter.PATH), Set.of("/v1")); } @Test public void testMultiFilterParsing() { - OpenAPINormalizer.Filter filter = parseFilter("operationId: delete| list ; tag : testA |testB "); - assertEquals(filter.operationIdFilters, Set.of("delete", "list")); - assertEquals(filter.tagFilters, Set.of("testA", "testB")); + OpenAPINormalizer.OperationsFilter filter = parseOperationsFilter("operationId: delete| list ; tag : testA |testB "); + assertEquals(filter.filteringMethodsMap.get(OpenAPINormalizer.OperationsFilter.OPERATION_ID), Set.of("delete", "list")); + assertEquals(filter.filteringMethodsMap.get(OpenAPINormalizer.OperationsFilter.TAG), Set.of("testA", "testB")); } @Test @@ -872,7 +872,7 @@ public void testCustomRoleFilter() { Map options = Map.of("FILTER", "role:admin"); OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options) { @Override - protected Filter createFilter(OpenAPI openApi, String filters) { + protected OperationsFilter createOperationsFilter(OpenAPI openApi, String filters) { return new CustomRoleFilter(filters); } }; @@ -883,7 +883,7 @@ protected Filter createFilter(OpenAPI openApi, String filters) { assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions().get(X_INTERNAL), false); } - private class CustomRoleFilter extends OpenAPINormalizer.Filter { + private class CustomRoleFilter extends OpenAPINormalizer.OperationsFilter { private Set filteredRoles; public CustomRoleFilter(String filters) { @@ -914,7 +914,7 @@ public void testFilterInvalidSyntaxDoesThrow() { new OpenAPINormalizer(openAPI, options).normalize(); fail("Expected IllegalArgumentException"); } catch (IllegalArgumentException e) { - assertEquals(e.getMessage(), "FILTER rule [tag ; invalid] must be in the form of `operationId:name1|name2|name3` or `method:get|post|put` or `tag:tag1|tag2|tag3` or `path:/v1|/v2`. Error: filter with no value not supported :[tag]"); + assertEquals(e.getMessage(), "FILTER rule must be in the form of `operationId:name1|name2|name3` or `method:get|post|put` or `tag:tag1|tag2|tag3` or `path:/v1|/v2`. Input: `tag ; invalid`. Error: filter with no value not supported :[tag]"); } } @@ -927,10 +927,61 @@ public void testFilterInvalidFilterDoesThrow() { new OpenAPINormalizer(openAPI, options).normalize(); fail("Expected IllegalArgumentException"); } catch (IllegalArgumentException e) { - assertEquals(e.getMessage(), "FILTER rule [method:get ; unknown:test] must be in the form of `operationId:name1|name2|name3` or `method:get|post|put` or `tag:tag1|tag2|tag3` or `path:/v1|/v2`. Error: filter not supported :[unknown:test]"); + assertEquals(e.getMessage(), "FILTER rule must be in the form of `operationId:name1|name2|name3` or `method:get|post|put` or `tag:tag1|tag2|tag3` or `path:/v1|/v2`. Input: `method:get ; unknown:test`. Error: filter not supported :[unknown:test]"); } } + @Test + public void testSecuritySchemesFilter() { + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/all_security_schemes.yaml"); + Map options = Map.of("SECURITY_SCHEMES_FILTER", "key:api_key1 ; type:oauth2"); + + new OpenAPINormalizer(openAPI, options).normalize(); + + assertEquals(openAPI.getComponents().getSecuritySchemes().get("api_key1").getExtensions().get(X_INTERNAL), + false); + assertEquals(openAPI.getComponents().getSecuritySchemes().get("api_key2").getExtensions().get(X_INTERNAL), + true); + assertEquals(openAPI.getComponents().getSecuritySchemes().get("http1").getExtensions().get(X_INTERNAL), true); + assertEquals(openAPI.getComponents().getSecuritySchemes().get("http2").getExtensions().get(X_INTERNAL), true); + assertEquals(openAPI.getComponents().getSecuritySchemes().get("mutualTLS1").getExtensions().get(X_INTERNAL), true); + assertEquals(openAPI.getComponents().getSecuritySchemes().get("mutualTLS2").getExtensions().get(X_INTERNAL), true); + assertEquals(openAPI.getComponents().getSecuritySchemes().get("oauth2_1").getExtensions().get(X_INTERNAL), + false); + assertEquals(openAPI.getComponents().getSecuritySchemes().get("oauth2_2").getExtensions().get(X_INTERNAL), + false); + assertEquals(openAPI.getComponents().getSecuritySchemes().get("openIdConnect1").getExtensions().get(X_INTERNAL), true); + assertEquals(openAPI.getComponents().getSecuritySchemes().get("openIdConnect2").getExtensions().get(X_INTERNAL), true); + } + + @Test + public void testSecuritySchemesFilterAndBearerAuthName() { + // We expect that api_key1 scheme will converted to bearer auth at first and then the filter will be applied + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/all_security_schemes.yaml"); + Map options = Map.of("SECURITY_SCHEMES_FILTER", "key:api_key1", + "SET_BEARER_AUTH_FOR_NAME", "api_key1" + ); + + new OpenAPINormalizer(openAPI, options).normalize(); + + assertEquals(openAPI.getComponents().getSecuritySchemes().get("api_key1").getExtensions().get(X_INTERNAL), + false); + SecurityScheme scheme = openAPI.getComponents().getSecuritySchemes().get("api_key1"); + assertEquals(scheme.getType(), SecurityScheme.Type.HTTP); + assertEquals(scheme.getScheme(), "bearer"); + assertEquals(openAPI.getComponents().getSecuritySchemes().get("api_key2").getExtensions().get(X_INTERNAL), + true); + assertEquals(openAPI.getComponents().getSecuritySchemes().get("http1").getExtensions().get(X_INTERNAL), true); + assertEquals(openAPI.getComponents().getSecuritySchemes().get("http2").getExtensions().get(X_INTERNAL), true); + assertEquals(openAPI.getComponents().getSecuritySchemes().get("mutualTLS1").getExtensions().get(X_INTERNAL), true); + assertEquals(openAPI.getComponents().getSecuritySchemes().get("mutualTLS2").getExtensions().get(X_INTERNAL), true); + assertEquals(openAPI.getComponents().getSecuritySchemes().get("oauth2_1").getExtensions().get(X_INTERNAL), + true); + assertEquals(openAPI.getComponents().getSecuritySchemes().get("oauth2_2").getExtensions().get(X_INTERNAL), + true); + assertEquals(openAPI.getComponents().getSecuritySchemes().get("openIdConnect1").getExtensions().get(X_INTERNAL), true); + assertEquals(openAPI.getComponents().getSecuritySchemes().get("openIdConnect2").getExtensions().get(X_INTERNAL), true); + } @Test public void testComposedSchemaDoesNotThrow() { @@ -1403,30 +1454,30 @@ public void testRemoveXInternalFromInlineProperties() { OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/inline_x_internal_test.yaml"); Schema parentSchema = openAPI.getComponents().getSchemas().get("ParentSchema"); Schema inlineProperty = (Schema) parentSchema.getProperties().get("inlineXInternalProperty"); - + // Before normalization: x-internal should be present on inline property assertNotNull(inlineProperty.getExtensions()); assertEquals(inlineProperty.getExtensions().get("x-internal"), true); - + // Run normalizer with REMOVE_X_INTERNAL=true Map options = new HashMap<>(); options.put("REMOVE_X_INTERNAL", "true"); OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options); openAPINormalizer.normalize(); - + // After normalization: x-internal should be removed from inline property Schema parentSchemaAfter = openAPI.getComponents().getSchemas().get("ParentSchema"); Schema inlinePropertyAfter = (Schema) parentSchemaAfter.getProperties().get("inlineXInternalProperty"); - + // x-internal extension should be removed (null or not present in map) if (inlinePropertyAfter.getExtensions() != null) { assertNull(inlinePropertyAfter.getExtensions().get("x-internal")); } - + // The property itself should still exist (we're removing the flag, not the property) assertNotNull(inlinePropertyAfter); assertEquals(inlinePropertyAfter.getType(), "object"); - + // Nested properties should still exist assertNotNull(inlinePropertyAfter.getProperties()); assertNotNull(inlinePropertyAfter.getProperties().get("nestedField")); diff --git a/modules/openapi-generator/src/test/resources/3_1/all_security_schemes.yaml b/modules/openapi-generator/src/test/resources/3_1/all_security_schemes.yaml new file mode 100644 index 000000000000..4693d40e00b3 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/all_security_schemes.yaml @@ -0,0 +1,255 @@ +openapi: 3.1.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is OpenAPI spec with all possible security schemes. + version: 1.0.0 + title: OpenAPI All Security Schemes + license: + name: Apache-2.0 + url: 'https://www.apache.org/licenses/LICENSE-2.0.html' +paths: + '/pet/{petId}': + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + get: + tags: + - pet + summary: Find pet by ID + description: Returns a single pet + operationId: getPetById + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - api_key1: [] + - api_key2: [] + +components: + securitySchemes: + api_key1: + type: apiKey + description: API Key Authentication sample 1 + name: api_key + in: header + api_key2: + type: apiKey + description: API Key Authentication sample 2 + name: api_key + in: query + http1: + type: http + description: HTTP Authentication sample 1 + scheme: bearer + bearerFormat: JWT + http2: + type: http + description: HTTP Authentication sample 2 + scheme: basic + mutualTLS1: + type: mutualTLS + description: Mutual TLS Authentication sample 1 + mutualTLS2: + type: mutualTLS + description: Mutual TLS Authentication sample 2 + oauth2_1: + type: oauth2 + description: OAuth2 Authentication sample 1 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/api/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + oauth2_2: + type: oauth2 + description: OAuth2 Authentication sample 2 + flows: + password: + tokenUrl: 'http://petstore.swagger.io/api/oauth/token' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + openIdConnect1: + type: openIdConnect + description: OpenID Connect Authentication sample 1 + openIdConnectUrl: 'http://petstore.swagger.io/.well-known/openid-configuration' + openIdConnect2: + type: openIdConnect + description: OpenID Connect Authentication sample 2 + openIdConnectUrl: 'http://petstore.swagger.io/.well-known/openid-configuration2' + + responses: + JustAnotherResponse: + description: JustAnotherResponse + content: + application/json: + schema: + type: object + properties: + uuid: + type: integer + label: + type: string + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + schemas: + Order: + title: Pet Order + description: An order for a pets from the pet store + type: object + properties: + id: + type: integer + format: int64 + petId: + type: integer + format: int64 + quantity: + type: integer + format: int32 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + type: boolean + default: false + xml: + name: Order + Category: + title: Pet category + description: A category for a pet + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + pattern: '^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$' + xml: + name: Category + User: + title: a User + description: A User who is purchasing from the pet store + type: object + properties: + id: + type: integer + format: int64 + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + type: integer + format: int32 + description: User Status + xml: + name: User + Tag: + title: Pet Tag + description: A tag for a pet + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + title: a Pet + description: A pet for sale in the pet store + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + deprecated: true + enum: + - available + - pending + - sold + xml: + name: Pet + ApiResponse: + title: An uploaded response + description: Describes the result of uploading an image resource + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string