Skip to content

Commit fb55471

Browse files
committed
Implement normalizer for security schemes
1 parent 3972c65 commit fb55471

2 files changed

Lines changed: 209 additions & 93 deletions

File tree

modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java

Lines changed: 176 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -327,16 +327,29 @@ public void processRules(Map<String, String> inputRules) {
327327
}
328328

329329
/**
330-
* Create the filter to process the FILTER normalizer.
330+
* Create the operations filter to process the FILTER normalizer.
331331
* Override this to create a custom filter normalizer.
332332
*
333333
* @param openApi Contract used in the filtering (could be used for customization).
334-
* @param filters full FILTER value
334+
* @param input full input value
335335
*
336-
* @return a Filter containing the parsed filters.
336+
* @return an OperationsFilter containing the parsed filters.
337337
*/
338-
protected Filter createFilter(OpenAPI openApi, String filters) {
339-
return new Filter(filters);
338+
protected OperationsFilter createOperationsFilter(OpenAPI openApi, String input) {
339+
return new OperationsFilter(input);
340+
}
341+
342+
/**
343+
* Create the security schemes filter to process the FILTER normalizer.
344+
* Override this to create a custom filter normalizer.
345+
*
346+
* @param openApi Contract used in the filtering (could be used for customization).
347+
* @param input full input value
348+
*
349+
* @return an SecuritySchemesFilter containing the parsed filters.
350+
*/
351+
protected SecuritySchemesFilter createSecuritySchemesFilter(OpenAPI openApi, String input) {
352+
return new SecuritySchemesFilter(input);
340353
}
341354

342355
/**
@@ -403,7 +416,7 @@ protected void normalizePaths() {
403416

404417
if (Boolean.TRUE.equals(getRule(FILTER))) {
405418
String filters = inputRules.get(FILTER);
406-
Filter filter = createFilter(this.openAPI, filters);
419+
OperationsFilter filter = createOperationsFilter(this.openAPI, filters);
407420
if (filter.parse()) {
408421
// Iterates over each HTTP method in methodMap, retrieves the corresponding Operations from the PathItem,
409422
// and marks it as internal (`x-internal=true`) if the method/operationId/tag/path is not in the filters.
@@ -586,9 +599,9 @@ protected void normalizeHeaders(Map<String, Header> headers) {
586599
* Normalizes securitySchemes in components
587600
*/
588601
protected void normalizeComponentsSecuritySchemes() {
589-
if (StringUtils.isEmpty(bearerAuthSecuritySchemeName)) {
590-
return;
591-
}
602+
if (StringUtils.isEmpty(bearerAuthSecuritySchemeName)) {
603+
return;
604+
}
592605

593606
Map<String, SecurityScheme> schemes = openAPI.getComponents().getSecuritySchemes();
594607
if (schemes == null) {
@@ -1938,20 +1951,22 @@ private void normalizeExclusiveMinMax31(Schema<?> schema) {
19381951

19391952
// ===================== end of rules =====================
19401953

1941-
protected static class Filter {
1942-
public static final String OPERATION_ID = "operationId";
1943-
public static final String METHOD = "method";
1944-
public static final String TAG = "tag";
1945-
public static final String PATH = "path";
1946-
private final String filters;
1947-
protected Set<String> operationIdFilters = Collections.emptySet();
1948-
protected Set<String> methodFilters = Collections.emptySet();
1949-
protected Set<String> tagFilters = Collections.emptySet();
1950-
protected Set<String> pathStartingWithFilters = Collections.emptySet();
1951-
private boolean hasFilter;
1954+
// Base class for filters. It provides basic parsing logic and utility functions for filters.
1955+
// All filters should have the same syntax:
1956+
// `filterName:value1|value2|value3` and multiple filters can be separated by `;`.
1957+
protected static abstract class BaseFilter {
1958+
protected boolean hasFilter;
1959+
private final String input;
1960+
// Key - filtering method, value - set of accepted values.
1961+
// For example, to filter operations by method the key would be "method" and the value is a set of {"get", "post"}.
1962+
protected Map<String, Set<String>> filteringMethodsMap;
1963+
1964+
protected BaseFilter(String input) {
1965+
this.input = input.trim();
1966+
}
19521967

1953-
protected Filter(String filters) {
1954-
this.filters = filters.trim();
1968+
public boolean hasFilter() {
1969+
return hasFilter;
19551970
}
19561971

19571972
/**
@@ -1960,23 +1975,34 @@ protected Filter(String filters) {
19601975
* @return true if filters need to be processed
19611976
*/
19621977
public boolean parse() {
1963-
if (StringUtils.isEmpty(filters)) {
1978+
if (StringUtils.isEmpty(input)) {
19641979
return false;
19651980
}
19661981
try {
19671982
doParse();
19681983
return hasFilter();
19691984
} catch (RuntimeException e) {
1970-
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",
1971-
filters, Filter.OPERATION_ID, Filter.METHOD, Filter.TAG, Filter.PATH, e.getMessage());
1985+
String usage = usageMessage();
1986+
String message = String.format(Locale.ROOT, "%s Input: `%s` Error: %s", usage, input, e.getMessage());
19721987
// throw an exception. This is a breaking change compared to pre 7.16.0
19731988
// Workaround: fix the syntax!
19741989
throw new IllegalArgumentException(message);
19751990
}
19761991
}
19771992

1993+
// Defines the filtering methods supported by the filter.
1994+
// Can be overridden by child classes to customize filtering.
1995+
public abstract Set<String> filteringMethods();
1996+
1997+
// Defines the subject being filtered, e.g. operation, security scheme, etc. This is used for logging purposes.
1998+
public abstract String filteringSubject();
1999+
2000+
// Defines the usage message for the filter. This is used for logging purposes when the filter syntax is incorrect.
2001+
public abstract String usageMessage();
2002+
19782003
private void doParse() {
1979-
for (String filter : filters.split(";")) {
2004+
Set<String> filteringMethods = filteringMethods();
2005+
for (String filter : input.split(";")) {
19802006
filter = filter.trim();
19812007
String[] filterStrs = filter.split(":");
19822008
if (filterStrs.length != 2) { // only support filter with : at the moment
@@ -1986,15 +2012,16 @@ private void doParse() {
19862012
String filterValue = filterStrs[1];
19872013
Set<String> parsedFilters = splitByPipe(filterValue);
19882014
hasFilter = true;
1989-
if (OPERATION_ID.equals(filterKey)) {
1990-
operationIdFilters = parsedFilters;
1991-
} else if (METHOD.equals(filterKey)) {
1992-
methodFilters = parsedFilters;
1993-
} else if (TAG.equals(filterKey)) {
1994-
tagFilters = parsedFilters;
1995-
} else if (PATH.equals(filterKey)) {
1996-
pathStartingWithFilters = parsedFilters;
1997-
} else {
2015+
2016+
boolean found = false;
2017+
for (String method : filteringMethods) {
2018+
if (method.equals(filterKey)) {
2019+
found = true;
2020+
filteringMethodsMap.put(filterKey, parsedFilters);
2021+
break;
2022+
}
2023+
}
2024+
if (!found) {
19982025
parse(filterKey, filterValue);
19992026
}
20002027
}
@@ -2014,7 +2041,7 @@ protected Set<String> splitByPipe(String filterValue) {
20142041
}
20152042

20162043
/**
2017-
* Parse non default filters.
2044+
* Parse non default filtering methods.
20182045
*
20192046
* Override this method to add custom parsing logic.
20202047
*
@@ -2031,6 +2058,50 @@ protected void parseFails(String filterName, String filterValue) {
20312058
throw new IllegalArgumentException("filter not supported :[" + filterName + ":" + filterValue + "]");
20322059
}
20332060

2061+
protected boolean logIfMatch(String filterName, String subjectId, boolean filterMatched) {
2062+
if (filterMatched) {
2063+
logMatch(filterName, subjectId);
2064+
}
2065+
return filterMatched;
2066+
}
2067+
2068+
protected void logMatch(String filterName, String subjectId) {
2069+
getLogger().info("{} `{}` marked as internal only (x-internal: true) by the {} filter", filteringSubject(),
2070+
subjectId, filterName);
2071+
}
2072+
2073+
protected Logger getLogger() {
2074+
return OpenAPINormalizer.LOGGER;
2075+
}
2076+
}
2077+
2078+
protected static class OperationsFilter extends BaseFilter {
2079+
public static final String OPERATION_ID = "operationId";
2080+
public static final String METHOD = "method";
2081+
public static final String TAG = "tag";
2082+
public static final String PATH = "path";
2083+
2084+
protected OperationsFilter(String filters) {
2085+
super(filters);
2086+
}
2087+
2088+
@Override
2089+
public Set<String> filteringMethods() {
2090+
return Set.of(OPERATION_ID, METHOD, TAG, PATH);
2091+
}
2092+
2093+
@Override
2094+
public String filteringSubject() {
2095+
return "Operation";
2096+
}
2097+
2098+
@Override
2099+
public String usageMessage() {
2100+
return String.format(Locale.ROOT,
2101+
"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`.",
2102+
OperationsFilter.OPERATION_ID, OperationsFilter.METHOD, OperationsFilter.TAG, OperationsFilter.PATH);
2103+
}
2104+
20342105
/**
20352106
* Test if the OpenAPI contract match an extra filter.
20362107
*
@@ -2045,58 +2116,103 @@ protected boolean hasCustomFilterMatch(String path, Operation operation) {
20452116
return false;
20462117
}
20472118

2048-
public boolean hasFilter() {
2049-
return hasFilter;
2050-
}
2051-
20522119
public void apply(String path, PathItem pathItem, Map<String, Function<PathItem, Operation>> methodMap) {
20532120
methodMap.forEach((method, getter) -> {
20542121
Operation operation = getter.apply(pathItem);
20552122
if (operation != null) {
20562123
boolean found = false;
2057-
found |= logIfMatch(PATH, operation, hasPathStarting(path));
2058-
found |= logIfMatch(TAG, operation, hasTag(operation));
2059-
found |= logIfMatch(OPERATION_ID, operation, hasOperationId(operation));
2060-
found |= logIfMatch(METHOD, operation, hasMethod(method));
2124+
String operationId = operation.getOperationId();
2125+
found |= logIfMatch(PATH, operationId, hasPathStarting(path));
2126+
found |= logIfMatch(TAG, operationId, hasTag(operation));
2127+
found |= logIfMatch(OPERATION_ID, operationId, hasOperationId(operation));
2128+
found |= logIfMatch(METHOD, operationId, hasMethod(method));
20612129
found |= hasCustomFilterMatch(path, operation);
20622130

20632131
operation.addExtension(X_INTERNAL, !found);
20642132
}
20652133
});
20662134
}
20672135

2068-
protected boolean logIfMatch(String filterName, Operation operation, boolean filterMatched) {
2069-
if (filterMatched) {
2070-
logMatch(filterName, operation);
2071-
}
2072-
return filterMatched;
2073-
}
2074-
2075-
protected void logMatch(String filterName, Operation operation) {
2076-
getLogger().info("operation `{}` marked as internal only (x-internal: true) by the {} FILTER", operation.getOperationId(), filterName);
2077-
}
2078-
2079-
protected Logger getLogger() {
2080-
return OpenAPINormalizer.LOGGER;
2081-
}
2082-
20832136
private boolean hasPathStarting(String path) {
2137+
Set<String> pathStartingWithFilters = filteringMethodsMap.getOrDefault(PATH, Collections.emptySet());
20842138
return pathStartingWithFilters.stream().anyMatch(filter -> path.startsWith(filter));
20852139
}
20862140

2087-
private boolean hasTag( Operation operation) {
2141+
private boolean hasTag(Operation operation) {
2142+
Set<String> tagFilters = filteringMethodsMap.getOrDefault(TAG, Collections.emptySet());
20882143
return operation.getTags() != null && operation.getTags().stream().anyMatch(tagFilters::contains);
20892144
}
20902145

20912146
private boolean hasOperationId(Operation operation) {
2147+
Set<String> operationIdFilters = filteringMethodsMap.getOrDefault(OPERATION_ID, Collections.emptySet());
20922148
return operationIdFilters.contains(operation.getOperationId());
20932149
}
20942150

20952151
private boolean hasMethod(String method) {
2152+
Set<String> methodFilters = filteringMethodsMap.getOrDefault(METHOD, Collections.emptySet());
20962153
return methodFilters.contains(method);
20972154
}
20982155
}
20992156

2157+
protected static class SecuritySchemesFilter extends BaseFilter {
2158+
public static final String KEY = "key";
2159+
public static final String TYPE = "type";
2160+
2161+
protected SecuritySchemesFilter(String filters) {
2162+
super(filters);
2163+
}
2164+
2165+
@Override
2166+
public Set<String> filteringMethods() {
2167+
return Set.of(KEY, TYPE);
2168+
}
2169+
2170+
@Override
2171+
public String filteringSubject() {
2172+
return "Security scheme";
2173+
}
2174+
2175+
@Override
2176+
public String usageMessage() {
2177+
return String.format(Locale.ROOT,
2178+
"SECURITY_SCHEMES_FILTER rule must be in the form of `%s:key1|key2|key3` or `%s:apiKey|http|mutualTLS|oauth2|openIdConnect`.",
2179+
KEY, TYPE);
2180+
}
2181+
2182+
/**
2183+
* Test if the OpenAPI contract match an extra filter.
2184+
*
2185+
* Override this method to add custom logic.
2186+
*
2187+
* @param schemeKey Security scheme key
2188+
* @param scheme Security scheme
2189+
*
2190+
* @return true if the security scheme matches the filter
2191+
*/
2192+
protected boolean hasCustomFilterMatch(String schemeKey, SecurityScheme scheme) {
2193+
return false;
2194+
}
2195+
2196+
public void apply(String schemeKey, SecurityScheme scheme) {
2197+
boolean found = false;
2198+
found |= logIfMatch(KEY, schemeKey, hasKey(schemeKey));
2199+
found |= logIfMatch(TYPE, schemeKey, hasType(scheme.getType()));
2200+
found |= hasCustomFilterMatch(schemeKey, scheme);
2201+
2202+
scheme.addExtension(X_INTERNAL, !found);
2203+
}
2204+
2205+
private boolean hasKey(String key) {
2206+
Set<String> keyFilters = filteringMethodsMap.getOrDefault(KEY, Collections.emptySet());
2207+
return keyFilters.contains(key);
2208+
}
2209+
2210+
private boolean hasType(String type) {
2211+
Set<String> typeFilters = filteringMethodsMap.getOrDefault(TYPE, Collections.emptySet());
2212+
return typeFilters.contains(type);
2213+
}
2214+
}
2215+
21002216
/**
21012217
* When set to true, remove "properties" attribute on schema other than "object"
21022218
* since it should be ignored and may result in odd generated code

0 commit comments

Comments
 (0)