package io.apiman.plugins.urlwhitelist; import io.apiman.gateway.engine.beans.ApiRequest; import io.apiman.gateway.engine.beans.PolicyFailure; import io.apiman.gateway.engine.beans.PolicyFailureType; import io.apiman.gateway.engine.beans.exceptions.ConfigurationParseException; import io.apiman.gateway.engine.policies.AbstractMappedPolicy; import io.apiman.gateway.engine.policy.IPolicyChain; import io.apiman.gateway.engine.policy.IPolicyContext; import io.apiman.plugins.urlwhitelist.beans.UrlWhitelistBean; import io.apiman.plugins.urlwhitelist.beans.WhitelistEntryBean; import io.apiman.plugins.urlwhitelist.util.Messages; import java.net.HttpURLConnection; import java.net.URI; import java.net.URISyntaxException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; /** * A policy that only permits requests matching a whitelist. * * @author Pete Cornish {@literal <outofcoffee@gmail.com>} */ public class UrlWhitelistPolicy extends AbstractMappedPolicy<UrlWhitelistBean> { private static final Messages MESSAGES = new Messages("io.apiman.plugins.urlwhitelist", "UrlWhitelistPolicy"); //$NON-NLS-1$ //$NON-NLS-2$ private static final String APIMAN_GATEWAY = "/apiman-gateway"; //$NON-NLS-1$ /** * Cache of precompiled URL patterns. Note that {@link Pattern} is thread-safe. */ private static final Map<String, Pattern> PATTERN_MAP = new ConcurrentHashMap<>(); /** * @see io.apiman.gateway.engine.policies.AbstractMappedPolicy#getConfigurationClass() */ @Override protected Class<UrlWhitelistBean> getConfigurationClass() { return UrlWhitelistBean.class; } /** * @see io.apiman.gateway.engine.policies.AbstractMappedPolicy#parseConfiguration(String) */ @Override public UrlWhitelistBean parseConfiguration(String jsonConfiguration) throws ConfigurationParseException { final UrlWhitelistBean config = super.parseConfiguration(jsonConfiguration); // precompile patterns for performance for (WhitelistEntryBean whitelistEntry : config.getWhitelist()) { try { PATTERN_MAP.put(whitelistEntry.getRegex(), Pattern.compile(whitelistEntry.getRegex())); } catch (Exception e) { throw new ConfigurationParseException(MESSAGES.format("Error.CompilingPattern", whitelistEntry.getRegex()), e); //$NON-NLS-1$ } } return config; } /** * @see io.apiman.gateway.engine.policies.AbstractMappedPolicy#doApply(io.apiman.gateway.engine.beans.ApiRequest, io.apiman.gateway.engine.policy.IPolicyContext, java.lang.Object, io.apiman.gateway.engine.policy.IPolicyChain) */ @Override protected void doApply(ApiRequest request, IPolicyContext context, UrlWhitelistBean config, IPolicyChain<ApiRequest> chain) { // normalise, for safety final String normalisedPath; try { normalisedPath = getNormalisedPath(config, request); } catch (Exception e) { chain.throwError(new RuntimeException(MESSAGES.format("Error.NormalisingPath", request.getUrl()), e)); //$NON-NLS-1$ return; } final boolean requestPermitted; try { requestPermitted = isRequestPermitted(config, normalisedPath, request.getType()); } catch (Exception e) { chain.throwError(new RuntimeException(MESSAGES.format( "Error.CheckingRequest", request.getType(), normalisedPath), e)); //$NON-NLS-1$ return; } if (requestPermitted) { chain.doApply(request); } else { chain.doFailure(new PolicyFailure(PolicyFailureType.Authorization, HttpURLConnection.HTTP_FORBIDDEN, MESSAGES.format("Failure.UrlNotPermitted", normalisedPath))); //$NON-NLS-1$ } } /** * Normalise the request URL before evaluating any access control rules, for safety. Returns the path * component of the normalised URL. * * @param config the policy configuration * @param request the incoming request * @return the normalised path * @throws URISyntaxException */ private String getNormalisedPath(UrlWhitelistBean config, ApiRequest request) throws URISyntaxException { // normalise, for safety final URI normalisedUrl = new URI(request.getUrl()).normalize(); String path = normalisedUrl.getPath(); if (config.isRemovePathPrefix()) { if (path.startsWith(APIMAN_GATEWAY)) { path = path.substring(APIMAN_GATEWAY.length()); } // remove org/API/version prefix, e.g. '/myorg/myapi/1.0' final String apiPrefix = String.format("/%s/%s/%s", //$NON-NLS-1$ request.getApiOrgId(), request.getApiId(), request.getApiVersion()); path = path.substring(apiPrefix.length()); } return path; } /** * Evaluates whether the request for the {@code normalisedPath} and {@code method} is permitted by * the rules in the {@code config}. * * @param config the policy configuration * @param normalisedPath the normalised request path * @param method the HTTP method * @return {@code true} if the request is permitted, otherwise {@code false} */ private boolean isRequestPermitted(UrlWhitelistBean config, String normalisedPath, String method) { for (WhitelistEntryBean whitelistEntry : config.getWhitelist()) { final Pattern pattern = PATTERN_MAP.get(whitelistEntry.getRegex()); if (null != pattern && pattern.matcher(normalisedPath).matches()) { return isMethodPermitted(whitelistEntry, method); } } return false; } /** * Evaluates whether the request for the {@code method} is permitted by the configuration of * the {@code whitelistEntry}. * * @param whitelistEntry the whitelist entry matching the request URL * @param method the HTTP method * @return {@code true} if the method is permitted, otherwise {@code false} */ private boolean isMethodPermitted(WhitelistEntryBean whitelistEntry, String method) { switch (method.toUpperCase()) { case "GET": //$NON-NLS-1$ return whitelistEntry.isMethodGet(); case "POST": //$NON-NLS-1$ return whitelistEntry.isMethodPost(); case "PUT": //$NON-NLS-1$ return whitelistEntry.isMethodPut(); case "PATCH": //$NON-NLS-1$ return whitelistEntry.isMethodPatch(); case "DELETE": //$NON-NLS-1$ return whitelistEntry.isMethodDelete(); case "HEAD": //$NON-NLS-1$ return whitelistEntry.isMethodHead(); case "OPTIONS": //$NON-NLS-1$ return whitelistEntry.isMethodOptions(); case "TRACE": //$NON-NLS-1$ return whitelistEntry.isMethodTrace(); default: throw new UnsupportedOperationException(MESSAGES.format("Error.MethodNotSupported", method)); //$NON-NLS-1$ } } }