Skip to content

Commit 7894c38

Browse files
authored
OpenAPIValidator: QueryString Support for Explode, Form, Arrays, Hashs (#2156)
* fix: OpenAPI Validator for #2155 * minor: refactor * minor: refactor * feat: object type for query parameters * minor: refactor * minor: refactor * minor: refactor * minor: refactor, tests * minor: refactor, tests * minor: refactor, tests * minor: refactor, tests * minor: refactor, tests * minor: refactor, tests * minor: missing file * minor: missing file * minor: missing file * minor: missing file * minor: missing file * minor: missing file * minor: missing file * minor: missing file * Fixed docs
1 parent 9dda259 commit 7894c38

File tree

112 files changed

+3047
-667
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

112 files changed

+3047
-667
lines changed

core/src/main/java/com/predic8/membrane/core/interceptor/authentication/xen/XenAuthenticationInterceptor.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import com.predic8.membrane.core.interceptor.*;
2323
import com.predic8.membrane.core.interceptor.authentication.session.*;
2424
import com.predic8.membrane.core.util.*;
25-
import org.jose4j.json.*;
25+
import org.jose4j.json.JsonUtil;
2626
import org.jose4j.jwk.*;
2727
import org.jose4j.jws.*;
2828
import org.jose4j.jwt.*;
@@ -84,7 +84,7 @@ public Outcome handleRequest(Exchange exc) {
8484
public Outcome handleResponse(Exchange exc) {
8585
// map session ids
8686
String sessionId = new XenSessionIdAccessor().getSessionId(exc, Flow.RESPONSE);
87-
if (sessionId == null || sessionId.length() == 0)
87+
if (sessionId == null || sessionId.isEmpty())
8888
return Outcome.CONTINUE;
8989

9090
String newSessionId = sessionManager.getExistingSessionId(sessionId);
@@ -142,7 +142,7 @@ public static class JwtSessionManager implements XenSessionManager {
142142

143143
public void init(Router router) throws Exception {
144144
String key = jwk.get(router.getResolverMap(), router.getBaseLocation());
145-
if (key == null || key.length() == 0)
145+
if (key == null || key.isEmpty())
146146
rsaJsonWebKey = generateKey();
147147
else
148148
rsaJsonWebKey = new RsaJsonWebKey(JsonUtil.parseJson(key));

core/src/main/java/com/predic8/membrane/core/interceptor/jwt/JwtSignInterceptor.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,14 @@
2020
import com.predic8.membrane.core.interceptor.*;
2121
import com.predic8.membrane.core.interceptor.session.*;
2222
import com.predic8.membrane.core.util.*;
23-
import org.jose4j.json.*;
23+
import org.jose4j.json.JsonUtil;
2424
import org.jose4j.jwk.*;
2525
import org.jose4j.jws.*;
2626
import org.jose4j.lang.*;
2727
import org.slf4j.*;
2828

2929
import java.io.*;
30-
import java.util.Map;
31-
import java.util.Objects;
30+
import java.util.*;
3231

3332
import static com.predic8.membrane.core.exceptions.ProblemDetails.*;
3433
import static com.predic8.membrane.core.interceptor.Interceptor.Flow.*;

core/src/main/java/com/predic8/membrane/core/interceptor/ratelimit/RateLimitInterceptor.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@
4646
* The X-Forwarded-For header can only be trusted when a trustworthy reverse proxy or load balancer is between the client and server. The gateway not should be
4747
* reachable directly. Only activate this feature when you know what you are doing.
4848
* </p>
49-
* <p>see: <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For">X-Forwarded-For &#64;Mozilla</a></p>
49+
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For">X-Forwarded-For &#64;Mozilla</a>
50+
* @topic 3. Security and Validation
5051
*/
5152
@MCElement(name = "rateLimiter")
5253
public class RateLimitInterceptor extends AbstractExchangeExpressionInterceptor {

core/src/main/java/com/predic8/membrane/core/openapi/OpenAPIValidator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ private ValidationErrors validateMessage(Request<? extends Body> req, Response<?
8686
}
8787
}
8888

89-
return ValidationErrors.create( ValidationContext.fromRequest(req)
89+
return ValidationErrors.error( ValidationContext.fromRequest(req)
9090
.entity(req.getPath())
9191
.entityType(PATH)
9292
.statusCode(404), format("Path %s is invalid.", req.getPath()));

core/src/main/java/com/predic8/membrane/core/openapi/model/Request.java

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@
2424
import static java.util.Collections.*;
2525
import static java.util.stream.Collectors.*;
2626

27-
public class Request<T extends Body> extends Message<T,Request<T>> {
27+
public class Request<T extends Body> extends Message<T, Request<T>> {
2828

2929
private final String method;
3030
private String path;
31-
private Map<String,String> pathParameters;
31+
private Map<String, String> pathParameters;
3232

3333
private List<SecurityScheme> securitySchemes = emptyList();
3434

@@ -48,18 +48,43 @@ public static <T extends Body> Request<T> get() {
4848
return new Request<>("GET");
4949
}
5050

51+
public static <T extends Body> Request<T> get(String path) {
52+
return new Request<>("GET", path);
53+
}
54+
5155
public static <T extends Body> Request<T> post() {
5256
return new Request<>("POST");
5357
}
5458

59+
/**
60+
* Use to simplify tests
61+
*/
62+
public static <T extends Body> Request<T> post(String path) {
63+
return new Request<>("POST", path);
64+
}
65+
5566
public static <T extends Body> Request<T> put() {
5667
return new Request<>("PUT");
5768
}
5869

70+
/**
71+
* Use to simplify tests
72+
*/
73+
public static <T extends Body> Request<T> put(String path) {
74+
return new Request<>("PUT", path);
75+
}
76+
5977
public static <T extends Body> Request<T> delete() {
6078
return new Request<>("DELETE");
6179
}
6280

81+
/**
82+
* Use to simplify tests
83+
*/
84+
public static <T extends Body> Request<T> delete(String path) {
85+
return new Request<>("DELETE", path);
86+
}
87+
6388
public static <T extends Body> Request<T> patch() {
6489
return new Request<>("PATCH");
6590
}
@@ -112,7 +137,7 @@ public void setSecuritySchemes(List<SecurityScheme> securitySchemes) {
112137
}
113138

114139
public boolean hasScheme(SecurityScheme scheme) {
115-
return securitySchemes.stream().anyMatch(s -> s.equals(scheme));
140+
return securitySchemes.stream().anyMatch(s -> s.equals(scheme));
116141
}
117142

118143
public void parsePathParameters(String uriTemplate) throws PathDoesNotMatchException {

core/src/main/java/com/predic8/membrane/core/openapi/util/OpenAPIUtil.java

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,20 @@
1919
import com.fasterxml.jackson.databind.*;
2020
import io.swagger.v3.core.util.*;
2121
import io.swagger.v3.oas.models.*;
22+
import io.swagger.v3.oas.models.media.*;
23+
import io.swagger.v3.oas.models.parameters.*;
2224
import io.swagger.v3.parser.ObjectMapperFactory;
25+
import org.jetbrains.annotations.*;
2326
import org.slf4j.*;
2427

2528
import java.io.*;
29+
import java.util.*;
2630

31+
import static com.predic8.membrane.core.http.MimeType.*;
2732
import static com.predic8.membrane.core.openapi.serviceproxy.APIProxy.*;
2833
import static com.predic8.membrane.core.openapi.util.Utils.*;
34+
import static com.predic8.membrane.core.openapi.validators.JsonSchemaValidator.*;
35+
import static io.swagger.v3.oas.models.parameters.Parameter.StyleEnum.*;
2936

3037
public class OpenAPIUtil {
3138

@@ -71,4 +78,69 @@ public static JsonNode convert2Json(OpenAPI api) throws IOException {
7178
public static boolean isOpenAPIMisplacedError(String errorMsg) {
7279
return errorMsg.matches("(?i).*invalid.+element.+http://membrane-soa.org/proxies/1/\":openapi.*'\\..*");
7380
}
81+
82+
public static PathItem getPath(OpenAPI api, String path) {
83+
if (api == null || api.getPaths() == null) return null;
84+
return api.getPaths().get(path);
85+
}
86+
87+
public static Parameter getParameter(Operation operation, String parameterName) {
88+
if (operation == null || operation.getParameters() == null) return null;
89+
return operation.getParameters().stream()
90+
.filter(Objects::nonNull)
91+
.filter(p -> parameterName.equals(p.getName()))
92+
.findFirst()
93+
.orElse(null);
94+
}
95+
96+
public static Schema<?> getProperty(Schema<?> schema, String propertyName) {
97+
if (schema == null || schema.getProperties() == null) return null;
98+
return schema.getProperties().get(propertyName);
99+
}
100+
101+
public static Schema<?> resolveSchema(OpenAPI api, Parameter p) {
102+
if (p == null) return null;
103+
Schema<?> schema = p.getSchema();
104+
if (schema == null && p.getContent() != null && !p.getContent().isEmpty()) {
105+
// Prefer application/json if present; otherwise take first
106+
var mt = p.getContent().get(APPLICATION_JSON);
107+
if (mt == null) mt = p.getContent().values().iterator().next();
108+
schema = mt != null ? mt.getSchema() : null;
109+
}
110+
if (schema == null) return null;
111+
if (schema.get$ref() != null) {
112+
if (api == null || api.getComponents() == null || api.getComponents().getSchemas() == null) return null;
113+
try {
114+
return api.getComponents().getSchemas().get(getComponentLocalNameFromRef(schema.get$ref()));
115+
} catch (RuntimeException ignore) {
116+
return null; // external or malformed ref not resolvable here
117+
}
118+
}
119+
return schema;
120+
}
121+
122+
/**
123+
* If schema has type: object or type: [..., object]
124+
*/
125+
public static boolean hasObjectType(Schema<?> schema) {
126+
if (schema == null) return false;
127+
return (schema.getTypes() != null && (schema.getTypes().contains(OBJECT)) || OBJECT.equals(schema.getType()));
128+
}
129+
130+
public static boolean isExplode(Parameter parameter) {
131+
if (parameter.getExplode() == null) {
132+
Parameter.StyleEnum style = getStyle(parameter);
133+
return style == FORM || style == DEEPOBJECT;
134+
}
135+
return parameter.getExplode();
136+
}
137+
138+
private static Parameter.@NotNull StyleEnum getStyle(Parameter parameter) {
139+
if (parameter.getStyle() != null) return parameter.getStyle();
140+
return ("query".equalsIgnoreCase(parameter.getIn()) || "cookie".equalsIgnoreCase(parameter.getIn())) ? FORM : SIMPLE;
141+
}
142+
143+
public static boolean isQueryParameter(Parameter p) {
144+
return "query".equalsIgnoreCase(p.getIn());
145+
}
74146
}

core/src/main/java/com/predic8/membrane/core/openapi/util/Utils.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,12 +237,10 @@ public static <T extends Body> Request<T> getOpenapiValidatorRequest(Exchange ex
237237
if (!exc.getRequest().isBodyEmpty()) {
238238
request.body(exc.getRequest().getBodyAsStreamDecoded());
239239
}
240-
241240
if (exc.getProperty(SECURITY_SCHEMES) != null) {
242241
//noinspection unchecked
243242
request.setSecuritySchemes((List<SecurityScheme>) exc.getProperty(SECURITY_SCHEMES));
244243
}
245-
246244
return request;
247245
}
248246

core/src/main/java/com/predic8/membrane/core/openapi/validators/AbstractBodyValidator.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,13 @@ protected ValidationErrors validateBodyAccordingToMediaType(ValidationContext ct
4646
if (mediaTypeObj.getSchema() != null && mediaTypeObj.getSchema().get$ref() != null) {
4747
ctx = ctx.schemaType(mediaTypeObj.getSchema().get$ref());
4848
}
49-
errors.add(new SchemaValidator(api, mediaTypeObj.getSchema()).validate(ctx, message.getBody()));
50-
} else if(isXML(mediaType)) {
51-
errors.add(ctx,"Validation of XML messages is not implemented yet!");
52-
} else if(isWWWFormUrlEncoded(mediaType)) {
53-
errors.add(ctx,"Validation of 'application/x-www-form-urlencoded' messages is not implemented yet!");
49+
return errors.add(new SchemaValidator(api, mediaTypeObj.getSchema()).validate(ctx, message.getBody()));
50+
}
51+
if(isXML(mediaType)) {
52+
return errors.add(ctx,"Validation of XML messages is not implemented yet!");
53+
}
54+
if(isWWWFormUrlEncoded(mediaType)) {
55+
return errors.add(ctx,"Validation of 'application/x-www-form-urlencoded' messages is not implemented yet!");
5456
}
5557
// Other types that can't be validated against OpenAPI are Ok.
5658
return errors;
@@ -94,7 +96,7 @@ protected ValidationErrors validateBodyInternal(ValidationContext ctx, Message<?
9496
try {
9597
mostSpecificMediaType = getMostSpecificMediaType(msg.getMediaType().toString(), content.keySet()).orElseThrow();
9698
} catch (Exception e) {
97-
return ValidationErrors.create(ctx.statusCode(getStatusCodeForWrongMediaType()).entityType(MEDIA_TYPE).entity(msg.getMediaType().toString()), "The media type(Content-Type header) of the %s does not match any of %s.".formatted(getMessageName(), content.keySet()));
99+
return ValidationErrors.error(ctx.statusCode(getStatusCodeForWrongMediaType()).entityType(MEDIA_TYPE).entity(msg.getMediaType().toString()), "The media type(Content-Type header) of the %s does not match any of %s.".formatted(getMessageName(), content.keySet()));
98100
}
99101

100102
return validateMediaTypeForMessageType(ctx.statusCode(getStatusCodeForWrongMediaType()), mostSpecificMediaType, content.get(mostSpecificMediaType), msg);

core/src/main/java/com/predic8/membrane/core/openapi/validators/AbstractParameterValidator.java

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,60 +16,63 @@
1616

1717
package com.predic8.membrane.core.openapi.validators;
1818

19-
import com.predic8.membrane.core.openapi.validators.ValidationContext.*;
2019
import io.swagger.v3.oas.models.*;
2120
import io.swagger.v3.oas.models.parameters.*;
21+
import org.jetbrains.annotations.*;
2222

2323
import java.util.*;
2424
import java.util.stream.*;
2525

26-
import static com.predic8.membrane.core.util.CollectionsUtil.*;
27-
import static java.lang.String.*;
26+
import static java.util.Locale.*;
27+
import static java.util.Optional.*;
2828

2929
public abstract class AbstractParameterValidator {
30+
3031
final OpenAPI api;
3132
final PathItem pathItem;
3233

33-
public AbstractParameterValidator(OpenAPI api, PathItem pathItem) {
34+
protected AbstractParameterValidator(OpenAPI api, PathItem pathItem) {
3435
this.api = api;
3536
this.pathItem = pathItem;
3637
}
3738

38-
public Stream<Parameter> getParametersOfType(Operation operation, Class<?> paramClazz) {
39-
return getAllParameterSchemas(operation).stream().filter(p -> isTypeOf(p, paramClazz));
39+
protected Stream<Parameter> getParametersOfType(Operation operation, Class<?> paramClazz) {
40+
return getAllParameter(operation).stream().filter(p -> isTypeOf(p, paramClazz));
4041
}
4142

42-
public List<Parameter> getAllParameterSchemas(Operation operation) {
43-
return concat(pathItem.getParameters(), operation.getParameters());
44-
}
43+
/**
44+
* Operation level parameters are overwriting parameters on the path level. But only
45+
* If in like query or header is the same.
46+
*
47+
* @param operation
48+
* @return
49+
*/
50+
protected List<Parameter> getAllParameter(Operation operation) {
51+
Objects.requireNonNull(operation, "operation must not be null");
4552

46-
boolean isTypeOf(Parameter p, Class<?> clazz) {
47-
return p.getClass().equals(clazz);
53+
List<Parameter> pathParams = ofNullable(pathItem.getParameters()).orElseGet(List::of);
54+
List<Parameter> opParams = ofNullable(operation.getParameters()).orElseGet(List::of);
55+
56+
// Sample key set: [number|query, string|query, bool|query, other|header]
57+
Map<String, Parameter> byKey = new LinkedHashMap<>();
58+
59+
60+
// path-level first, then operation-level to override
61+
Stream.concat(pathParams.stream(), opParams.stream())
62+
.filter(Objects::nonNull)
63+
.forEach(p -> byKey.put(getParameterKey(p), p));
64+
return new ArrayList<>(byKey.values());
4865
}
4966

50-
public ValidationErrors getValidationErrors(ValidationContext ctx, Map<String, String> parameters, Parameter param, ValidatedEntityType type) {
51-
return validateParameter(getCtx(ctx, param, type), parameters, param, type);
67+
private static @NotNull String getParameterKey(Parameter p) {
68+
return p.getName() + "|" + getInNormalized(p);
5269
}
5370

54-
private static ValidationContext getCtx(ValidationContext ctx, Parameter param, ValidatedEntityType type) {
55-
return ctx.entity(param.getName())
56-
.entityType(type)
57-
.statusCode(400);
71+
private static @NotNull String getInNormalized(Parameter p) {
72+
return p.getIn() == null ? "" : p.getIn().toLowerCase(ROOT);
5873
}
5974

60-
public ValidationErrors validateParameter(ValidationContext ctx, Map<String, String> params, Parameter param, ValidatedEntityType type) {
61-
ValidationErrors errors = new ValidationErrors();
62-
String value = params.get(param.getName());
63-
64-
if (value != null) {
65-
errors.add(new SchemaValidator(api, param.getSchema()).validate(ctx
66-
.statusCode(400)
67-
.entity(param.getName())
68-
.entityType(type)
69-
, value));
70-
} else if (param.getRequired()) {
71-
errors.add(ctx, format("Missing required %s %s.", type.name, param.getName()));
72-
}
73-
return errors;
75+
boolean isTypeOf(Parameter p, Class<?> clazz) {
76+
return clazz.isInstance(p);
7477
}
7578
}

core/src/main/java/com/predic8/membrane/core/openapi/validators/AnyOfValidator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public ValidationErrors validate(ValidationContext ctx, Object obj) {
4848
if (oneIsValid.get()) {
4949
return null;
5050
}
51-
return ValidationErrors.create(ctx,"None of the subschemas of anyOf was true.");
51+
return ValidationErrors.error(ctx,"None of the subschemas of anyOf was true.");
5252
}
5353

5454

0 commit comments

Comments
 (0)