Skip to content

Commit

Permalink
Open api param validation (#745)
Browse files Browse the repository at this point in the history
* added referenced query param

* added tests for referenced param

* New list concat, tests

* added header params to yaml

* added header to openapi request

* refactoring

* working on HeaderParam Validation  WIP

* added header param validation + test

* updated OAS3

* added test header

* Refactoring

* optimized code

* added status code test

* Renamed getErrorsAndRemoveParam

---------

Co-authored-by: Christian Gördes <[email protected]>
Co-authored-by: T. Burch <[email protected]>
  • Loading branch information
3 people authored Sep 25, 2023
1 parent 09bbf76 commit 261f18e
Show file tree
Hide file tree
Showing 24 changed files with 1,545 additions and 105 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,8 @@ private ValidationErrors validateMethods(ValidationContext ctx, Request req, Res
.entityType(METHOD), format("Method %s is not allowed", req.getMethod()));
}
}

public OpenAPI getApi() {
return api;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.slf4j.*;

import java.io.*;
import java.util.Map;

import static com.predic8.membrane.core.http.MimeType.APPLICATION_JSON_CONTENT_TYPE;

Expand All @@ -30,13 +31,21 @@ public abstract class Message<T> {

protected Body body = new NoBody();
protected ContentType mediaType;
private Map<String,String> headers;

protected Message() {
}

protected Message(String mediaType) throws ParseException {
this.mediaType = new ContentType(mediaType);
public Map<String, String> getHeaders() {
return headers;
}

protected Message() {
public void setHeaders(Map<String, String> headers) {
this.headers = headers;
}

protected Message(String mediaType) throws ParseException {
this.mediaType = new ContentType(mediaType);
}

public Body getBody() {
Expand Down Expand Up @@ -101,6 +110,4 @@ public T json() {
public ContentType getMediaType() {
return mediaType;
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public class Request extends Message<Request> {
private final UriTemplateMatcher uriTemplateMatcher = new UriTemplateMatcher();
private Map<String,String> pathParameters;


public Request(String method) {
this.method = method;
}
Expand Down Expand Up @@ -85,5 +86,4 @@ public String toString() {
", pathParameters=" + pathParameters +
'}';
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@

import java.util.regex.*;

import static com.predic8.membrane.core.openapi.serviceproxy.APIProxy.X_MEMBRANE_ID;
import static com.predic8.membrane.core.openapi.util.Utils.normalizeForId;
import static com.predic8.membrane.core.openapi.serviceproxy.APIProxy.*;
import static com.predic8.membrane.core.openapi.util.Utils.*;

public class OpenAPIUtil {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.predic8.membrane.core.openapi.util;

import com.predic8.membrane.core.exchange.*;
import com.predic8.membrane.core.http.HeaderField;
import com.predic8.membrane.core.openapi.model.*;
import com.predic8.membrane.core.openapi.validators.*;
import jakarta.mail.internet.*;
Expand Down Expand Up @@ -136,6 +137,11 @@ public static boolean areThereErrors(ValidationErrors ve) {

public static Request getOpenapiValidatorRequest(Exchange exc) throws IOException, ParseException {
Request request = new Request(exc.getRequest().getMethod()).path(exc.getRequestURI());
Map<String, String> headers = new HashMap<>();
for (HeaderField header : exc.getRequest().getHeader().getAllHeaderFields()) {
headers.put(header.getHeaderName().toString(), header.getValue());
}
request.setHeaders(headers);
if (exc.getRequest().getHeader().getContentType() != null) {
request.mediaType(exc.getRequest().getHeader().getContentType());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright 2022 predic8 GmbH, www.predic8.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

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

import com.predic8.membrane.core.openapi.validators.ValidationContext.ValidatedEntityType;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.parameters.Parameter;

import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import static com.predic8.membrane.core.openapi.util.Utils.getComponentLocalNameFromRef;
import static com.predic8.membrane.core.util.CollectionsUtil.concat;
import static java.lang.String.format;

public abstract class AbstractParameterValidator {
OpenAPI api;
PathItem pathItem;

public AbstractParameterValidator(OpenAPI api, PathItem pathItem) {
this.api = api;
this.pathItem = pathItem;
}

public Stream<Parameter> getParametersOfType(Operation operation, Class<?> paramClazz) {
return getAllParameterSchemas(operation).stream().filter(p -> isTypeOf(p, paramClazz));
}

public List<Parameter> getAllParameterSchemas(Operation operation) {
return concat(resolveRefs(pathItem.getParameters()), resolveRefs(operation.getParameters()));
}

boolean isTypeOf(Parameter p, Class<?> clazz) {
return p.getClass().equals(clazz);
}

private List<Parameter> resolveRefs(List<Parameter> parameters) {
if (parameters == null)
return null;

return parameters.stream().map(this::resolveParamIfNeeded).toList();
}

private Parameter resolveParamIfNeeded(Parameter p ) {
if (p.get$ref() != null)
return resolveReferencedParameter(p);
return p;
}

public ValidationErrors getValidationErrors(ValidationContext ctx, Map<String, String> parameters, Parameter param, ValidatedEntityType type) {
return validateParameter(getCtx(ctx, param, type), parameters, param, type);
}

private static ValidationContext getCtx(ValidationContext ctx, Parameter param, ValidatedEntityType type) {
return ctx.entity(param.getName())
.entityType(type)
.statusCode(400);
}

public Parameter resolveReferencedParameter(Parameter p) {
return api.getComponents().getParameters().get(getComponentLocalNameFromRef(p.get$ref()));
}

public ValidationErrors validateParameter(ValidationContext ctx, Map<String, String> params, Parameter param, ValidatedEntityType type) {
ValidationErrors errors = new ValidationErrors();
String value = params.get(param.getName());

if (value != null) {
errors.add(new SchemaValidator(api, param.getSchema()).validate(ctx
.statusCode(400)
.entity(param.getName())
.entityType(type)
, value));
} else if (param.getRequired()) {
errors.add(ctx, format("Missing required %s %s.", type.name, param.getName()));
}
return errors;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2022 predic8 GmbH, www.predic8.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

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

import com.predic8.membrane.core.openapi.model.Request;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.parameters.HeaderParameter;

import static com.predic8.membrane.core.openapi.validators.ValidationContext.ValidatedEntityType.HEADER_PARAMETER;

public class HeaderParameterValidator extends AbstractParameterValidator{

public HeaderParameterValidator(OpenAPI api, PathItem pathItem) {
super(api, pathItem);
}

ValidationErrors validateHeaderParameters(ValidationContext ctx, Request request, Operation operation) {
return getParametersOfType(operation, HeaderParameter.class)
.map(param -> getValidationErrors(ctx, request.getHeaders(), param, HEADER_PARAMETER))
.reduce(ValidationErrors::add)
.orElse(new ValidationErrors());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public ValidationErrors validateOperation(ValidationContext ctx, Request req, Re
validatePathParameters(ctx, req, operation.getParameters());

errors.add(new QueryParameterValidator(api,pathItem).validateQueryParameters(ctx, req, operation));
errors.add(new HeaderParameterValidator(api,pathItem).validateHeaderParameters(ctx, req, operation));

return errors.add(new RequestBodyValidator(api).validateRequestBody(ctx.entityType(BODY).entity("REQUEST"), operation, req));
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,50 +16,44 @@

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

import com.predic8.membrane.core.openapi.model.*;
import com.predic8.membrane.core.util.*;
import io.swagger.v3.oas.models.*;
import io.swagger.v3.oas.models.parameters.*;

import java.util.*;
import com.predic8.membrane.core.openapi.model.Request;
import com.predic8.membrane.core.util.URIFactory;
import com.predic8.membrane.core.util.URLParamUtil;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.parameters.QueryParameter;

import java.util.HashMap;
import java.util.Map;

import static com.predic8.membrane.core.openapi.validators.ValidationContext.ValidatedEntityType.QUERY_PARAMETER;
import static com.predic8.membrane.core.util.URLParamUtil.DuplicateKeyOrInvalidFormStrategy.ERROR;
import static java.lang.String.format;
import static java.util.Objects.requireNonNullElseGet;

public class QueryParameterValidator {

OpenAPI api;
PathItem pathItem;
public class QueryParameterValidator extends AbstractParameterValidator{

public QueryParameterValidator(OpenAPI api, PathItem pathItem) {
this.api = api;
this.pathItem = pathItem;
super(api, pathItem);
}

ValidationErrors validateQueryParameters(ValidationContext ctx, Request request, Operation operation) {
Map<String, String> queryParams = getQueryParams(getQueryString(request));
return getParametersOfType(operation, QueryParameter.class)
.map(param -> getErrors(ctx, queryParams, param))
.reduce(ValidationErrors::add)
.orElse(new ValidationErrors())
.add(checkForAdditionalQueryParameters(ctx, queryParams));
}

ValidationErrors errors = new ValidationErrors();

// Map<String, String> qparams = request.getQueryParams();

// TODO
// Router?
String query = (new URIFactory().createWithoutException(request.getPath())).getQuery();
Map<String, String> qparams = getQueryParams(query);

getAllParameterSchemas(operation).forEach(param -> {
if (!(param instanceof QueryParameter)) {
return;
}
errors.add(validateQueryParameter(ctx.entity(param.getName()).entityType(QUERY_PARAMETER), qparams, param));
qparams.remove(param.getName()); // Delete param so there should't be any parameter left
});

errors.add(checkForAdditionalQueryParameters(ctx, qparams));
private ValidationErrors getErrors(ValidationContext ctx, Map<String, String> queryParams, Parameter param) {
ValidationErrors err = getValidationErrors(ctx, queryParams, param, QUERY_PARAMETER);
queryParams.remove(param.getName());
return err;
}

return errors;
private static String getQueryString(Request request) {
return (new URIFactory().createWithoutException(request.getPath())).getQuery();
}

private Map<String, String> getQueryParams(String query) {
Expand All @@ -68,37 +62,8 @@ private Map<String, String> getQueryParams(String query) {
return new HashMap<>();
}

private List<Parameter> getAllParameterSchemas(Operation operation) {
return concat(pathItem.getParameters(), operation.getParameters());
}

private static List<Parameter> concat(List<Parameter> l1, List<Parameter> l2) {
if (l1 == null) {
return requireNonNullElseGet(l2, ArrayList::new);
}
if (l2!=null)
l1.addAll(l2);
return l1;
}

private ValidationErrors validateQueryParameter(ValidationContext ctx, Map<String, String> qparams, Parameter param) {
ValidationErrors errors = new ValidationErrors();
String value = qparams.get(param.getName());

if (value != null) {
errors.add(new SchemaValidator(api, param.getSchema()).validate(ctx
.statusCode(400)
.entity(param.getName())
.entityType(QUERY_PARAMETER)
, value));
} else if (param.getRequired()) {
errors.add(ctx, format("Missing required query parameter %s.", param.getName()));
}
return errors;
}

private ValidationError checkForAdditionalQueryParameters(ValidationContext ctx, Map<String, String> qparams) {
if (qparams.size() > 0) {
if (!qparams.isEmpty()) {
return new ValidationError(ctx.entityType(QUERY_PARAMETER), "There are query parameters that are not supported by the API: " + qparams.keySet());
}
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ private String getLocation(String message) {
sb.append("HEADER/Content-Type");
} else {
sb.append(validatedEntityType.name());
if (jsonPointer.length() > 0) {
if (!jsonPointer.isEmpty()) {
sb.append("#");
sb.append(getJSONpointer());
}
Expand Down Expand Up @@ -192,7 +192,12 @@ public ValidationContext addJSONpointerSegment(String segment) {
}

public enum ValidatedEntityType {
PATH, METHOD, PATH_PARAMETER, QUERY_PARAMETER, BODY, FIELD, PROPERTY, MEDIA_TYPE
PATH("path"), METHOD("method"), PATH_PARAMETER("path parameter"), QUERY_PARAMETER("query parameter"), HEADER_PARAMETER("header parameter"), BODY("body"), FIELD("field"), PROPERTY("property"), MEDIA_TYPE("media type");

public final String name;
ValidatedEntityType(String s) {
this.name = s;
}
}

@Override
Expand Down
Loading

0 comments on commit 261f18e

Please sign in to comment.