Skip to content
This repository has been archived by the owner on Jul 9, 2021. It is now read-only.

Commit

Permalink
Operation validator: add request validation from URL only (#63)
Browse files Browse the repository at this point in the history
  • Loading branch information
llfbandit committed Mar 7, 2020
1 parent 656b570 commit 64fadf2
Show file tree
Hide file tree
Showing 9 changed files with 338 additions and 102 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -28,6 +29,8 @@
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;

import static org.openapi4j.operation.validator.util.PathResolver.Anchor.END_STRING;

public class OpenApi3RouterFactoryImpl implements OpenApi3RouterFactory {
private static final String OP_ID_NOT_FOUND_ERR_MSG = "Operation with id '%s' not found.";

Expand Down Expand Up @@ -114,7 +117,7 @@ public Router getRouter() throws ResolutionException {
private Route createRoute(Router router, OperationSpec operationSpec) {
Pattern pattern = PathResolver
.instance()
.solve(operationSpec.path, true);
.solve(operationSpec.path, EnumSet.of(END_STRING));

// If this optional is empty, this route doesn't need regex
return pattern != null
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
package org.openapi4j.operation.validator.util;

import org.openapi4j.core.model.OAIContext;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.EnumSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class PathResolver {
public enum Anchor {
NONE,
START_STRING,
END_STRING
}

private static final Pattern OAS_PATH_PARAMETERS_PATTERN = Pattern.compile("\\{[.;?*+]*([^{}.;?*+]+)[^}]*}");
private static final Pattern ABSOLUTE_URL_PATTERN = Pattern.compile("\\A[a-z0-9.+-]+://.*", Pattern.CASE_INSENSITIVE);

private static final PathResolver INSTANCE = new PathResolver();

Expand All @@ -17,23 +29,23 @@ public static PathResolver instance() {

/**
* This method returns a pattern only if a pattern is needed, otherwise it returns {@code null}.
* This will not add begin or end of string anchors to the regular expression.
* This will not add any anchor to the regular expression.
*
* @param oasPath The OAS path to build.
* @return a pattern only if a pattern is needed.
*/
public Pattern solve(String oasPath) {
return solve(oasPath, false);
return solve(oasPath, EnumSet.of(Anchor.NONE));
}

/**
* This method returns a pattern only if a pattern is needed, otherwise it returns {@code null}.
*
* @param oasPath The OAS path to build.
* @param addEndString Add end of string anchor to the regular expression.
* @param oasPath The OAS path to build.
* @param anchors Anchor options to add to the regular expression.
* @return a pattern only if a pattern is needed.
*/
public Pattern solve(String oasPath, boolean addEndString) {
public Pattern solve(String oasPath, EnumSet<Anchor> anchors) {
final StringBuilder regex = new StringBuilder();
int lastMatchEnd = 0;
boolean foundParameter = false;
Expand All @@ -51,7 +63,10 @@ public Pattern solve(String oasPath, boolean addEndString) {
if (foundParameter) {
addConstantFragment(regex, oasPath, lastMatchEnd, oasPath.length());

if (addEndString) {
if (anchors.contains(Anchor.START_STRING)) {
regex.insert(0, "^");
}
if (anchors.contains(Anchor.END_STRING)) {
regex.append("$");
}

Expand All @@ -65,12 +80,60 @@ public Pattern solve(String oasPath, boolean addEndString) {
* This method returns a pattern for a non templated path.
*
* @param oasPath The OAS path to build.
* @param anchors Anchor options to add to the regular expression.
* @return a pattern only if a pattern is needed.
*/
public Pattern solveFixedPath(String oasPath, boolean addEndString) {
return addEndString
? Pattern.compile(Pattern.quote(oasPath))
: Pattern.compile(Pattern.quote(oasPath) + "$");
public Pattern solveFixedPath(String oasPath, EnumSet<Anchor> anchors) {
final StringBuilder regex = new StringBuilder(Pattern.quote(oasPath));

if (anchors.contains(Anchor.START_STRING)) {
regex.insert(0, "^");
}
if (anchors.contains(Anchor.END_STRING)) {
regex.append("$");
}

return Pattern.compile(regex.toString());
}

public String getResolvedPath(OAIContext context, String url) {
// server URL may be relative to the location where the OpenAPI document is being served.
// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#server-object
try {
if (isAbsoluteUrl(url)) {
return new URL(url).getPath();
} else {
// Check if there's a defined file name in URL
URL resource = context.getBaseUri().toURL();
// trim query & anchor
String basePath = resource.toString().split("\\?")[0].split("#")[0];
// handle scheme://api.com
String host = resource.getHost();
if (host.length() > 0 && basePath.endsWith(host)) {
return "/";
}

// Get last path fragment (maybe file name)
String lastFragment = basePath.substring(basePath.lastIndexOf('/') + 1);

// remove filename from URL
if (lastFragment.contains(".")) {
basePath = basePath.substring(0, basePath.indexOf(lastFragment));
}

return new URL(new URL(basePath), url).getPath();
}
} catch (MalformedURLException e) {
return "/";
}
}

/**
* Decides if a URL is absolute based on whether it contains a valid scheme name, as
* defined in RFC 1738.
*/
private boolean isAbsoluteUrl(String url) {
return ABSOLUTE_URL_PATTERN.matcher(url).matches();
}

private void addVariableFragment(StringBuilder regex, String paramName) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,25 @@
package org.openapi4j.operation.validator.validation;

import com.fasterxml.jackson.databind.JsonNode;

import org.openapi4j.core.model.v3.OAI3;
import org.openapi4j.core.validation.ValidationResults;
import org.openapi4j.operation.validator.model.Request;
import org.openapi4j.operation.validator.model.impl.Body;
import org.openapi4j.operation.validator.model.impl.MediaTypeContainer;
import org.openapi4j.operation.validator.util.PathResolver;
import org.openapi4j.operation.validator.util.parameter.ParameterConverter;
import org.openapi4j.parser.model.v3.AbsParameter;
import org.openapi4j.parser.model.v3.Header;
import org.openapi4j.parser.model.v3.MediaType;
import org.openapi4j.parser.model.v3.OpenApi3;
import org.openapi4j.parser.model.v3.Operation;
import org.openapi4j.parser.model.v3.Parameter;
import org.openapi4j.parser.model.v3.Path;
import org.openapi4j.parser.model.v3.Response;
import org.openapi4j.parser.model.v3.*;
import org.openapi4j.schema.validator.ValidationContext;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.Objects.requireNonNull;
import static org.openapi4j.operation.validator.util.PathResolver.Anchor.END_STRING;
import static org.openapi4j.operation.validator.util.PathResolver.Anchor.START_STRING;

/**
* Validator for OpenAPI Operation.
Expand All @@ -44,7 +34,8 @@ public class OperationValidator {
private static final String BODY_CONTENT_TYPE_ERR_MSG = "Body content type cannot be determined. No 'Content-Type' header available.";
private static final String BODY_WRONG_CONTENT_TYPE_ERR_MSG = "Content type '%s' is not allowed in body.";
private static final String RESPONSE_STATUS_NOT_FOUND_ERR_MSG = "Response status '%s', ranged or default has not been found.";
private static final String PATH_NOT_FOUND_ERR_MSG = "Path template '%s' has not been found in value '%s'.";
private static final String PATH_NOT_FOUND_ERR_MSG = "Path template '%s' has not been found from value '%s'.";

// Parameter specifics
private static final String IN_PATH = "path";
private static final String IN_QUERY = "query";
Expand All @@ -66,7 +57,7 @@ public class OperationValidator {
private final OpenApi3 openApi;
private final Operation operation;
private final String templatePath;
private final Pattern pathPattern;
private final List<Pattern> pathPatterns;

/**
* Creates a validator for the given operation.
Expand All @@ -82,16 +73,33 @@ public OperationValidator(final OpenApi3 openApi, final Path path, final Operati
/**
* Creates a validator for the given operation.
*
* @param context The validation context for additional or changing behaviours.
* @param openApi The full Document Description where the Operation is located.
* @param path The Path of the Operation.
* @param operation The Operation to validate.
* @param context The validation context for additional or changing behaviours.
* @param openApi The full Document Description where the Operation is located.
* @param path The Path of the Operation.
* @param operation The Operation to validate.
*/
@SuppressWarnings("WeakerAccess")
public OperationValidator(final ValidationContext<OAI3> context,
final OpenApi3 openApi,
final Path path,
final Operation operation) {
this(context, null, openApi, path, operation);
}

/**
* Creates a validator for the given operation.
*
* @param context The validation context for additional or changing behaviours.
* @param pathPatterns Pattern for the current path related to servers or OAI Document origin.
* @param openApi The full Document Description where the Operation is located.
* @param path The Path of the Operation.
* @param operation The Operation to validate.
*/
OperationValidator(final ValidationContext<OAI3> context,
final List<Pattern> pathPatterns,
final OpenApi3 openApi,
final Path path,
final Operation operation) {

this.context = requireNonNull(context, VALIDATION_CTX_REQUIRED_ERR_MSG);
this.openApi = requireNonNull(openApi, OAI_REQUIRED_ERR_MSG);
Expand All @@ -107,7 +115,11 @@ public OperationValidator(final ValidationContext<OAI3> context,

// Request path parameters
specRequestPathValidator = createParameterValidator(IN_PATH);
pathPattern = initPathPattern(templatePath);
this.pathPatterns
= pathPatterns == null
? buildPathPatterns(openApi.getServers(), templatePath)
: pathPatterns;

// Request query parameters
specRequestQueryValidator = createParameterValidator(IN_QUERY);
// Request header parameters
Expand All @@ -134,20 +146,20 @@ public Operation getOperation() {
* @return The mapped parameters with their values.
*/
public Map<String, JsonNode> validatePath(final Request request, final ValidationResults results) {
if (specRequestPathValidator == null) return null;

// Check paths are matching before trying to map values
// This also aligns the result to the relative path template
Matcher matcher = pathPattern.matcher(request.getPath());
if (!matcher.find()) {
Pattern pathPattern = findPathPattern(request);
if (pathPattern == null) {
results.addError(String.format(PATH_NOT_FOUND_ERR_MSG, templatePath, request.getPath()), IN_PATH);
return new HashMap<>();
return null;
}

if (specRequestPathValidator == null) return null;

Map<String, JsonNode> mappedValues = ParameterConverter.pathToNode(
specRequestPathValidator.getParameters(),
pathPattern,
matcher.group());
request.getPath());

specRequestPathValidator.validate(mappedValues, results);

Expand Down Expand Up @@ -402,19 +414,6 @@ private <T> T getResponseValidator(final Map<String, T> validators,
return validator;
}

private Pattern initPathPattern(String specPath) {
if (specRequestPathValidator == null) {
return null;
}

Pattern pattern = PathResolver.instance().solve(specPath, true);
// No parameter found in template, build full fixed path
if (pattern == null) {
pattern = PathResolver.instance().solveFixedPath(specPath, true);
}
return pattern;
}

private void mergePathToOperationParameters(final Path path) {
if (path.getParameters() == null) {
return; // Nothing to do
Expand Down Expand Up @@ -445,4 +444,46 @@ private void mergePathToOperationParameters(final Path path) {
operation.setParameters(result);
}
}

private List<Pattern> buildPathPatterns(List<Server> servers, String templatePath) {
List<Pattern> patterns = new ArrayList<>();

if (servers == null) {
patterns.add(buildPathPattern("", templatePath));
} else {
for (Server server : servers) {
patterns.add(
buildPathPattern(
PathResolver.instance().getResolvedPath(context.getContext(), server.getUrl()),
templatePath));
}
}

return patterns;
}

private Pattern buildPathPattern(String basePath, String templatePath) {
Pattern pattern = PathResolver.instance().solve(basePath + templatePath, EnumSet.of(START_STRING, END_STRING));

return pattern != null
? pattern
: PathResolver.instance().solveFixedPath(basePath + templatePath, EnumSet.of(START_STRING, END_STRING));
}

private Pattern findPathPattern(Request request) {
String requestPath = request.getPath();
if (requestPath.isEmpty()) {
requestPath = "/";
}

// Match path pattern
for (Pattern pathPattern : pathPatterns) {
Matcher matcher = pathPattern.matcher(requestPath);
if (matcher.matches()) {
return pathPattern;
}
}

return null;
}
}
Loading

0 comments on commit 64fadf2

Please sign in to comment.