diff --git a/docs/release-notes.md b/docs/release-notes.md
index 2771d1571e..b7dd50df97 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -13,7 +13,7 @@
-----------------------
New Features
-*
+* [1172618](https://bugzilla.redhat.com/show_bug.cgi?id=1172618) - Allow anonymous pull from Zanata
----
diff --git a/zanata-war/src/main/java/org/zanata/limits/RateLimitManager.java b/zanata-war/src/main/java/org/zanata/limits/RateLimitManager.java
index e6feae77a4..2c546c4be8 100644
--- a/zanata-war/src/main/java/org/zanata/limits/RateLimitManager.java
+++ b/zanata-war/src/main/java/org/zanata/limits/RateLimitManager.java
@@ -37,7 +37,7 @@ public class RateLimitManager implements Introspectable {
public static final String INTROSPECTABLE_FIELD_RATE_LIMITERS =
"RateLimiters";
- private final Cache activeCallers = CacheBuilder
+ private final Cache activeCallers = CacheBuilder
.newBuilder().maximumSize(100).build();
@Getter(AccessLevel.PROTECTED)
@@ -111,13 +111,13 @@ public String getFieldValueAsString(String fieldName) {
}
private Iterable peekCurrentBuckets() {
- ConcurrentMap map = activeCallers.asMap();
+ ConcurrentMap map = activeCallers.asMap();
return Iterables.transform(map.entrySet(),
- new Function, String>() {
+ new Function, String>() {
@Override
public String
- apply(Map.Entry input) {
+ apply(Map.Entry input) {
RestCallLimiter rateLimiter = input.getValue();
return input.getKey() + ":" + rateLimiter;
@@ -125,7 +125,10 @@ private Iterable peekCurrentBuckets() {
});
}
- public RestCallLimiter getLimiter(final String apiKey) {
+ /**
+ * @param key - {@link RateLimiterToken.TYPE )
+ */
+ public RestCallLimiter getLimiter(final RateLimiterToken key) {
if (getMaxConcurrent() == 0 && getMaxActive() == 0) {
if (activeCallers.size() > 0) {
@@ -135,10 +138,10 @@ public RestCallLimiter getLimiter(final String apiKey) {
return NoLimitLimiter.INSTANCE;
}
try {
- return activeCallers.get(apiKey, new Callable() {
+ return activeCallers.get(key, new Callable() {
@Override
public RestCallLimiter call() throws Exception {
- log.debug("creating rate limiter for api key: {}", apiKey);
+ log.debug("creating rate limiter for key: {}", key);
return new RestCallLimiter(getMaxConcurrent(),
getMaxActive());
}
diff --git a/zanata-war/src/main/java/org/zanata/limits/RateLimiterToken.java b/zanata-war/src/main/java/org/zanata/limits/RateLimiterToken.java
new file mode 100644
index 0000000000..001d2060e2
--- /dev/null
+++ b/zanata-war/src/main/java/org/zanata/limits/RateLimiterToken.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2015, Red Hat, Inc. and individual contributors as indicated by the
+ * @author tags. See the copyright.txt file in the distribution for a full
+ * listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this software; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF
+ * site: http://www.fsf.org.
+ */
+
+package org.zanata.limits;
+
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+
+/**
+ * Token used for rate limiter queue.
+ *
+ * @author Alex Eng aeng@redhat.com
+ */
+@EqualsAndHashCode
+@ToString
+public class RateLimiterToken {
+
+ @Getter
+ private final String value;
+
+ @Getter
+ private final TYPE type;
+
+ public static enum TYPE {
+ USERNAME, API_KEY, IP_ADDRESS;
+ }
+
+ public RateLimiterToken(TYPE type, String value) {
+ this.type = type;
+ this.value = value;
+ }
+
+ /**
+ * Generate key from username
+ */
+ public static RateLimiterToken fromUsername(String username) {
+ return new RateLimiterToken(TYPE.USERNAME, username);
+ }
+
+ /**
+ * Generate key from api key
+ */
+ public static RateLimiterToken fromApiKey(String apiKey) {
+ return new RateLimiterToken(TYPE.API_KEY, apiKey);
+ }
+
+ /**
+ * Generate key from ip address
+ */
+ public static RateLimiterToken fromIPAddress(String ipAddress) {
+ return new RateLimiterToken(TYPE.IP_ADDRESS, ipAddress);
+ }
+}
diff --git a/zanata-war/src/main/java/org/zanata/limits/RateLimitingProcessor.java b/zanata-war/src/main/java/org/zanata/limits/RateLimitingProcessor.java
index 88d72ac6a4..f199ff7ac0 100644
--- a/zanata-war/src/main/java/org/zanata/limits/RateLimitingProcessor.java
+++ b/zanata-war/src/main/java/org/zanata/limits/RateLimitingProcessor.java
@@ -31,13 +31,23 @@ public RateLimitingProcessor() {
private final LeakyBucket logLimiter = new LeakyBucket(1, 5,
TimeUnit.MINUTES);
- public void processApiKey(String apiKey, HttpResponse response,
- Runnable taskToRun) throws Exception {
- process(apiKey, response, taskToRun);
+ public void processForApiKey(String apiKey, HttpResponse response,
+ Runnable taskToRun) throws Exception {
+ process(RateLimiterToken.fromApiKey(apiKey), response, taskToRun);
}
- private void process(String key, HttpResponse response, Runnable taskToRun)
- throws IOException {
+ public void processForUser(String username, HttpResponse response,
+ Runnable taskToRun) throws IOException {
+ process(RateLimiterToken.fromUsername(username), response, taskToRun);
+ }
+
+ public void processForAnonymousIP(String ip, HttpResponse response,
+ Runnable taskToRun) throws IOException {
+ process(RateLimiterToken.fromIPAddress(ip), response, taskToRun);
+ }
+
+ private void process(RateLimiterToken key, HttpResponse response,
+ Runnable taskToRun) throws IOException {
RestCallLimiter rateLimiter = rateLimitManager.getLimiter(key);
log.debug("check semaphore for {}", this);
@@ -48,16 +58,20 @@ private void process(String key, HttpResponse response, Runnable taskToRun)
"{} has too many concurrent requests. Returning status 429",
key);
}
- String errorMessage =
+
+ String errorMessage;
+ if(key.getType().equals(RateLimiterToken.TYPE.API_KEY)) {
+ errorMessage =
String.format(
- "Too many concurrent requests for this user (maximum is %d)",
- rateLimiter.getMaxConcurrentPermits());
+ "Too many concurrent requests for client API key (maximum is %d)",
+ rateLimiter.getMaxConcurrentPermits());
+ } else {
+ errorMessage =
+ String.format(
+ "Too many concurrent requests for client '%s' (maximum is %d)",
+ key.getValue(), rateLimiter.getMaxConcurrentPermits());
+ }
response.sendError(TOO_MANY_REQUEST, errorMessage);
}
}
-
- public void processUsername(String username, HttpResponse response,
- Runnable taskToRun) throws IOException {
- process(username, response, taskToRun);
- }
}
diff --git a/zanata-war/src/main/java/org/zanata/rest/HeaderHelper.java b/zanata-war/src/main/java/org/zanata/rest/HeaderHelper.java
deleted file mode 100644
index b11dd43489..0000000000
--- a/zanata-war/src/main/java/org/zanata/rest/HeaderHelper.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package org.zanata.rest;
-
-import org.jboss.resteasy.spi.HttpRequest;
-
-/**
- * @author Patrick Huang
- * pahuang@redhat.com
- */
-public final class HeaderHelper {
- private HeaderHelper() {
- }
-
- public static final String X_AUTH_TOKEN_HEADER = "X-Auth-Token";
- public static final String X_AUTH_USER_HEADER = "X-Auth-User";
-
- protected static String getApiKey(HttpRequest request) {
- return request.getHttpHeaders().getRequestHeaders()
- .getFirst(X_AUTH_TOKEN_HEADER);
- }
-
- protected static String getUserName(HttpRequest request) {
- return request.getHttpHeaders().getRequestHeaders()
- .getFirst(X_AUTH_USER_HEADER);
- }
-}
diff --git a/zanata-war/src/main/java/org/zanata/rest/InvalidApiKeyUtil.java b/zanata-war/src/main/java/org/zanata/rest/InvalidApiKeyUtil.java
new file mode 100644
index 0000000000..db60fea90d
--- /dev/null
+++ b/zanata-war/src/main/java/org/zanata/rest/InvalidApiKeyUtil.java
@@ -0,0 +1,31 @@
+package org.zanata.rest;
+
+/**
+ * Utility for invalid API key exception.
+ *
+ * @author Alex Eng aeng@redhat.com
+ */
+public class InvalidApiKeyUtil {
+ public static final String message = "Invalid API key";
+
+ public static String getMessage(String username, String apiKey,
+ String additionalMessage) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(getMessage(username, apiKey))
+ .append(" ").append(additionalMessage);
+ return sb.toString();
+ }
+
+ public static String getMessage(String username, String apiKey) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(message).append(" for user: [").append(username).append("]")
+ .append(" apiKey: [").append(apiKey).append("].");
+ return sb.toString();
+ }
+
+ public static String getMessage(String additionalMessage) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(message).append(". ").append(additionalMessage);
+ return sb.toString();
+ }
+}
diff --git a/zanata-war/src/main/java/org/zanata/rest/RestLimitingSynchronousDispatcher.java b/zanata-war/src/main/java/org/zanata/rest/RestLimitingSynchronousDispatcher.java
index ccf515d457..3624f11c9a 100644
--- a/zanata-war/src/main/java/org/zanata/rest/RestLimitingSynchronousDispatcher.java
+++ b/zanata-war/src/main/java/org/zanata/rest/RestLimitingSynchronousDispatcher.java
@@ -1,8 +1,31 @@
+/*
+ * Copyright 2015, Red Hat, Inc. and individual contributors as indicated by the
+ * @author tags. See the copyright.txt file in the distribution for a full
+ * listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this software; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF
+ * site: http://www.fsf.org.
+ */
package org.zanata.rest;
import java.io.IOException;
+import javax.annotation.Nonnull;
+import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.Response;
+import org.apache.commons.lang.StringUtils;
import org.jboss.resteasy.core.SynchronousDispatcher;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.HttpResponse;
@@ -10,12 +33,17 @@
import org.jboss.resteasy.spi.UnhandledException;
import org.jboss.seam.resteasy.SeamResteasyProviderFactory;
import org.jboss.seam.security.management.JpaIdentityStore;
+import org.jboss.seam.web.ServletContexts;
+import org.zanata.dao.AccountDAO;
import org.zanata.limits.RateLimitingProcessor;
import org.zanata.model.HAccount;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import lombok.extern.slf4j.Slf4j;
+
+import org.zanata.security.SecurityFunctions;
+import org.zanata.util.HttpUtil;
import org.zanata.util.ServiceLocator;
/**
@@ -45,18 +73,43 @@ public RestLimitingSynchronousDispatcher(
this.processor = processor;
}
+ HttpServletRequest getServletRequest() {
+ return ServletContexts.instance().getRequest();
+ }
+
@Override
public void invoke(final HttpRequest request, final HttpResponse response) {
+ /**
+ * This is only non-null if request came from same browser which
+ * user used to logged into Zanata.
+ */
HAccount authenticatedUser = getAuthenticatedUser();
- String apiKey = HeaderHelper.getApiKey(request);
+
+ /**
+ * If apiKey is empty, request is from anonymous user,
+ * If apiKey is not empty, it must be an authenticated
+ * user from pre-process in ZanataRestSecurityInterceptor.
+ */
+ String apiKey = HttpUtil.getApiKey(request);
try {
- // we are not validating api key but will rate limit any api key
- if (authenticatedUser == null && Strings.isNullOrEmpty(apiKey)) {
- response.sendError(
- Response.Status.UNAUTHORIZED.getStatusCode(),
- API_KEY_ABSENCE_WARNING);
+ // Get user account with apiKey if request is from client
+ if(authenticatedUser == null && StringUtils.isNotEmpty(apiKey)) {
+ authenticatedUser = getUser(apiKey);
+ }
+
+ if(!SecurityFunctions.canAccessRestPath(authenticatedUser,
+ request.getHttpMethod(), request.getPreprocessedPath())) {
+
+ /**
+ * Not using response.sendError because the app server will generate
+ * an HTML page which includes the message. We want to return
+ * the message string as is.
+ */
+ response.setStatus(Response.Status.UNAUTHORIZED.getStatusCode());
+ response.getOutputStream().write(InvalidApiKeyUtil.getMessage(
+ API_KEY_ABSENCE_WARNING).getBytes());
return;
}
@@ -69,16 +122,25 @@ public void run() {
}
};
- if (authenticatedUser == null) {
- processor.processApiKey(apiKey, response, taskToRun);
- } else if (!Strings.isNullOrEmpty(authenticatedUser.getApiKey())) {
- processor.processApiKey(authenticatedUser.getApiKey(),
- response, taskToRun);
+ //authenticatedUser can be from browser or client request
+ if(authenticatedUser == null) {
+ /**
+ * Process anonymous request for rate limiting
+ * Note: clientIP might be a proxy server IP address, due to
+ * different implementation of each proxy server. This will put
+ * all the requests from same proxy server into a single queue.
+ */
+ String clientIP = HttpUtil.getClientIp(getServletRequest());
+ processor.processForAnonymousIP(clientIP, response, taskToRun);
} else {
- processor.processUsername(authenticatedUser.getUsername(),
+ if (!Strings.isNullOrEmpty(authenticatedUser.getApiKey())) {
+ processor.processForApiKey(authenticatedUser.getApiKey(),
+ response, taskToRun);
+ } else {
+ processor.processForUser(authenticatedUser.getUsername(),
response, taskToRun);
+ }
}
-
} catch (UnhandledException e) {
Throwable cause = e.getCause();
log.error("Failed to process REST request", cause);
@@ -108,4 +170,9 @@ protected HAccount getAuthenticatedUser() {
return ServiceLocator.instance().getInstance(
JpaIdentityStore.AUTHENTICATED_USER, HAccount.class);
}
+
+ protected HAccount getUser(@Nonnull String apiKey) {
+ return ServiceLocator.instance().getInstance(AccountDAO.class)
+ .getByApiKey(apiKey);
+ }
}
diff --git a/zanata-war/src/main/java/org/zanata/rest/ZanataRestSecurityInterceptor.java b/zanata-war/src/main/java/org/zanata/rest/ZanataRestSecurityInterceptor.java
index 99b92bc365..65d7575457 100644
--- a/zanata-war/src/main/java/org/zanata/rest/ZanataRestSecurityInterceptor.java
+++ b/zanata-war/src/main/java/org/zanata/rest/ZanataRestSecurityInterceptor.java
@@ -6,6 +6,7 @@
import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang.StringUtils;
import org.jboss.resteasy.annotations.interception.SecurityPrecedence;
import org.jboss.resteasy.annotations.interception.ServerInterceptor;
import org.jboss.resteasy.core.ResourceMethod;
@@ -13,7 +14,9 @@
import org.jboss.resteasy.spi.Failure;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.interception.PreProcessInterceptor;
+import org.zanata.security.SecurityFunctions;
import org.zanata.security.ZanataIdentity;
+import org.zanata.util.HttpUtil;
@SecurityPrecedence
@ServerInterceptor
@@ -25,21 +28,19 @@ public class ZanataRestSecurityInterceptor implements PreProcessInterceptor {
preProcess(HttpRequest request, ResourceMethod method)
throws Failure, WebApplicationException {
- String username =
- HeaderHelper.getUserName(request);
- String apiKey =
- HeaderHelper.getApiKey(request);
-
- if (username != null && apiKey != null) {
+ String username = HttpUtil.getUsername(request);
+ String apiKey = HttpUtil.getApiKey(request);
+ if (StringUtils.isNotEmpty(username)|| StringUtils.isNotEmpty(apiKey)) {
ZanataIdentity.instance().getCredentials().setUsername(username);
ZanataIdentity.instance().setApiKey(apiKey);
ZanataIdentity.instance().tryLogin();
- if (!ZanataIdentity.instance().isLoggedIn()) {
- log.info(
- "Failed attempt to authenticate REST request for user {}",
- username);
+ if (!SecurityFunctions.canAccessRestPath(ZanataIdentity.instance(),
+ request.getHttpMethod(), request.getPreprocessedPath())) {
+ log.info(InvalidApiKeyUtil.getMessage(username, apiKey));
return ServerResponse.copyIfNotServerResponse(Response.status(
- Status.UNAUTHORIZED).build());
+ Status.UNAUTHORIZED).entity(
+ InvalidApiKeyUtil.getMessage(username, apiKey))
+ .build());
}
}
return null;
diff --git a/zanata-war/src/main/java/org/zanata/rest/service/TranslationMemoryResourceService.java b/zanata-war/src/main/java/org/zanata/rest/service/TranslationMemoryResourceService.java
index f3352a25c1..f973916d94 100644
--- a/zanata-war/src/main/java/org/zanata/rest/service/TranslationMemoryResourceService.java
+++ b/zanata-war/src/main/java/org/zanata/rest/service/TranslationMemoryResourceService.java
@@ -100,6 +100,7 @@ public Response getAllTranslationMemory(@Nullable LocaleId locale) {
}
@Override
+ @Restrict("#{s:hasPermission('', 'download-tmx')}")
public Response getProjectTranslationMemory(@Nonnull String projectSlug,
@Nullable LocaleId locale) {
log.debug("exporting TMX for project {}, locale {}", projectSlug,
@@ -118,6 +119,7 @@ public Response getProjectTranslationMemory(@Nonnull String projectSlug,
}
@Override
+ @Restrict("#{s:hasPermission('', 'download-tmx')}")
public Response getProjectIterationTranslationMemory(
@Nonnull String projectSlug, @Nonnull String iterationSlug,
@Nullable LocaleId locale) {
diff --git a/zanata-war/src/main/java/org/zanata/security/SecurityFunctions.java b/zanata-war/src/main/java/org/zanata/security/SecurityFunctions.java
index e6568cf2fb..1ed7181b42 100644
--- a/zanata-war/src/main/java/org/zanata/security/SecurityFunctions.java
+++ b/zanata-war/src/main/java/org/zanata/security/SecurityFunctions.java
@@ -29,18 +29,24 @@
import org.zanata.model.HIterationGroup;
import org.zanata.model.HLocale;
import org.zanata.model.HLocaleMember;
-import org.zanata.model.HPerson;
import org.zanata.model.HProject;
import org.zanata.model.HProjectIteration;
import org.zanata.security.permission.GrantsPermission;
+import org.zanata.util.HttpUtil;
import org.zanata.util.ServiceLocator;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import lombok.extern.slf4j.Slf4j;
+
/**
* Contains static helper functions used inside the rules files.
*
* @author Carlos Munoz camunoz@redhat.com
*/
+@Slf4j
public class SecurityFunctions {
protected SecurityFunctions() {
}
@@ -389,4 +395,75 @@ private static final T extractTarget(Object[] array, Class type) {
}
return null;
}
+
+ /*****************************************************************************************
+ * TMX rules
+ ******************************************************************************************/
+
+ @GrantsPermission(actions = "download-tmx")
+ public static boolean canDownloadTMX() {
+ Optional account = getAuthenticatedAccount();
+ return account.isPresent();
+ }
+
+
+ /*****************************************************************************************
+ * HTTP request rules
+ ******************************************************************************************/
+
+ /**
+ * Check if user can access to REST URL with httpMethod.
+ * 1) Check if request can communicate to with rest service path,
+ * 2) then check if request can perform the specific API action.
+ *
+ * If request is from anonymous user(account == null),
+ * only 'Read' action are allowed. Additionally, role-based check will be
+ * performed in the REST service class.
+ *
+ * This rule apply to all REST endpoint.
+ *
+ * @param account - Authenticated account
+ * @param httpMethod - {@link javax.ws.rs.HttpMethod}
+ * @param restServicePath - service path of rest request.
+ * See annotation @Path in REST service class.
+ */
+ public static final boolean canAccessRestPath(@Nullable HAccount account,
+ String httpMethod, String restServicePath) {
+ //This is to allow data injection for function-test/rest-test
+ if(isTestServicePath(restServicePath)) {
+ log.debug("Allow rest access for Zanata test");
+ return true;
+ }
+ if (account != null) {
+ return true;
+ }
+ if (HttpUtil.isReadMethod(httpMethod)) {
+ return true;
+ }
+ return false;
+ }
+
+ public static final boolean canAccessRestPath(
+ @Nonnull ZanataIdentity identity,
+ String httpMethod, String restServicePath) {
+ // This is to allow data injection for function-test/rest-test
+ if (isTestServicePath(restServicePath)) {
+ log.debug("Allow rest access for Zanata test");
+ return true;
+ }
+ if (identity.isLoggedIn()) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Check if request path are from functional test or RestTest
+ *
+ * @param servicePath - service path of rest request.
+ * See annotation @Path in REST service class.
+ */
+ private static boolean isTestServicePath(String servicePath) {
+ return servicePath != null && servicePath.startsWith("/test");
+ }
}
diff --git a/zanata-war/src/main/java/org/zanata/util/HttpUtil.java b/zanata-war/src/main/java/org/zanata/util/HttpUtil.java
new file mode 100644
index 0000000000..63a3608291
--- /dev/null
+++ b/zanata-war/src/main/java/org/zanata/util/HttpUtil.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2015, Red Hat, Inc. and individual contributors as indicated by the
+ * @author tags. See the copyright.txt file in the distribution for a full
+ * listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this software; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF
+ * site: http://www.fsf.org.
+ */
+package org.zanata.util;
+
+import java.util.List;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.HttpMethod;
+
+import org.apache.commons.lang.StringUtils;
+import org.jboss.resteasy.spi.HttpRequest;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+
+/**
+ * Utility class for HTTP related methods.
+ *
+ * @author Patrick Huang
+ * pahuang@redhat.com
+ */
+public final class HttpUtil {
+
+ private final static List HTTP_REQUEST_READ_METHODS = Lists.newArrayList(
+ HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS);
+
+ public static final String X_AUTH_TOKEN_HEADER = "X-Auth-Token";
+ public static final String X_AUTH_USER_HEADER = "X-Auth-User";
+
+ /**
+ * This should be set by admin.
+ * Example header names might be "X-Forwarded-For", "Proxy-Client-IP",
+ * "WL-Proxy-Client-IP", "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR"
+ */
+ public static String PROXY_HEADER = System
+ .getProperty("ZANATA_PROXY_HEADER");
+
+ public static String getApiKey(HttpRequest request) {
+ return request.getHttpHeaders().getRequestHeaders()
+ .getFirst(X_AUTH_TOKEN_HEADER);
+ }
+
+ @VisibleForTesting
+ static void refreshProxyHeader() {
+ PROXY_HEADER = System.getProperty("ZANATA_PROXY_HEADER");
+ }
+
+ public static String getUsername(HttpRequest request) {
+ return request.getHttpHeaders().getRequestHeaders()
+ .getFirst(X_AUTH_USER_HEADER);
+ }
+
+ /**
+ * Return client ip address according to HttpServletRequest.
+ *
+ * This will also check for the possibility of client behind proxy
+ * before returning default remote address in request.
+ *
+ * NOTE: Not all proxy server include client ip information in http header
+ * and different proxy MIGHT use different http header for such information.
+ * Default remote address in request will be returned if client information
+ * is not found in header.
+ *
+ * see http://stackoverflow.com/questions/4678797/how-do-i-get-the-remote-address-of-a-client-in-servlet
+ * @param request
+ */
+ public static String getClientIp(HttpServletRequest request) {
+ String ip;
+
+ if(StringUtils.isEmpty(PROXY_HEADER)) {
+ return request.getRemoteAddr();
+ }
+
+ // PROXY_HEADER can be list of ip address
+ String[] ipList =
+ StringUtils.split(request.getHeader(PROXY_HEADER), ",");
+
+ if(ipList.length == 1) {
+ return ipList[0];
+ }
+
+ //return last ip address from list if found
+ ip = ipList[ipList.length-1];
+ if(!isIpUnknown(ip)) {
+ return ip;
+ }
+
+ return request.getRemoteAddr();
+ }
+
+ private static boolean isIpUnknown(String ip) {
+ return StringUtils.isEmpty(ip) || StringUtils.equalsIgnoreCase(ip,
+ "unknown") || StringUtils.equalsIgnoreCase(ip, "localhost") ||
+ StringUtils.equals(ip, "127.0.0.1");
+ }
+
+ public static boolean isReadMethod(String httpMethod) {
+ for(String readMethod: HTTP_REQUEST_READ_METHODS) {
+ if(readMethod.equalsIgnoreCase(httpMethod)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/zanata-war/src/test/java/org/zanata/RestTest.java b/zanata-war/src/test/java/org/zanata/RestTest.java
index f680f8714e..788deff06e 100644
--- a/zanata-war/src/test/java/org/zanata/RestTest.java
+++ b/zanata-war/src/test/java/org/zanata/RestTest.java
@@ -35,7 +35,6 @@
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.resteasy.client.ClientRequest;
-import org.jboss.resteasy.client.ProxyFactory;
import org.jboss.seam.util.Naming;
import org.junit.After;
import org.junit.Before;
@@ -46,7 +45,6 @@
import org.zanata.rest.ResourceRequestEnvironment;
import org.zanata.rest.client.ZanataProxyFactory;
import org.zanata.rest.dto.VersionInfo;
-import org.zanata.rest.helper.RemoteTestSignaler;
/**
* Provides basic test utilities to test raw REST APIs and compatibility.
@@ -208,6 +206,18 @@ public static final ResourceRequestEnvironment getAuthorizedEnvironment() {
return ENV_AUTHORIZED;
}
+ /**
+ * Gets an empty header for REST request.
+ */
+ public static final ResourceRequestEnvironment getEmptyHeaderEnvironment() {
+ return new ResourceRequestEnvironment() {
+ @Override
+ public Map getDefaultHeaders() {
+ return new HashMap();
+ }
+ };
+ }
+
/**
* Creates and returns a new instance of a proxy factory for the given
* credentials. This method aids with the testing of Rest API classes.
diff --git a/zanata-war/src/test/java/org/zanata/limits/RateLimitingProcessorTest.java b/zanata-war/src/test/java/org/zanata/limits/RateLimitingProcessorTest.java
index 37bd3ed692..5b1f0adb5d 100644
--- a/zanata-war/src/test/java/org/zanata/limits/RateLimitingProcessorTest.java
+++ b/zanata-war/src/test/java/org/zanata/limits/RateLimitingProcessorTest.java
@@ -41,9 +41,10 @@ public void beforeMethod() throws IOException {
public void restCallLimiterReturnsFalseWillCauseErrorResponse()
throws Exception {
when(restCallLimiter.tryAcquireAndRun(runnable)).thenReturn(false);
- doReturn(restCallLimiter).when(rateLimitManager).getLimiter(API_KEY);
+ doReturn(restCallLimiter).when(rateLimitManager).getLimiter(
+ RateLimiterToken.fromApiKey(API_KEY));
- processor.processApiKey(API_KEY, response, runnable);
+ processor.processForApiKey(API_KEY, response, runnable);
verify(restCallLimiter).tryAcquireAndRun(runnable);
verify(response).sendError(eq(429), anyString());
@@ -53,9 +54,10 @@ public void restCallLimiterReturnsFalseWillCauseErrorResponse()
public void restCallLimiterReturnsTrueWillNotReturnErrorResponse()
throws Exception {
when(restCallLimiter.tryAcquireAndRun(runnable)).thenReturn(true);
- doReturn(restCallLimiter).when(rateLimitManager).getLimiter(API_KEY);
+ doReturn(restCallLimiter).when(rateLimitManager).getLimiter(
+ RateLimiterToken.fromApiKey(API_KEY));
- processor.processApiKey(API_KEY, response, runnable);
+ processor.processForApiKey(API_KEY, response, runnable);
verify(restCallLimiter).tryAcquireAndRun(runnable);
verifyZeroInteractions(response);
diff --git a/zanata-war/src/test/java/org/zanata/rest/ResourceRequestEnvironment.java b/zanata-war/src/test/java/org/zanata/rest/ResourceRequestEnvironment.java
index c68c82685d..615c2bd5fa 100644
--- a/zanata-war/src/test/java/org/zanata/rest/ResourceRequestEnvironment.java
+++ b/zanata-war/src/test/java/org/zanata/rest/ResourceRequestEnvironment.java
@@ -42,7 +42,6 @@ public class ResourceRequestEnvironment {
*/
public Map getDefaultHeaders() {
Map map = Maps.newHashMap();
- map.put("X-Auth-Token", "abc123");
return map;
}
}
diff --git a/zanata-war/src/test/java/org/zanata/rest/RestLimitingSynchronousDispatcherTest.java b/zanata-war/src/test/java/org/zanata/rest/RestLimitingSynchronousDispatcherTest.java
index f60a156264..d50f55259b 100644
--- a/zanata-war/src/test/java/org/zanata/rest/RestLimitingSynchronousDispatcherTest.java
+++ b/zanata-war/src/test/java/org/zanata/rest/RestLimitingSynchronousDispatcherTest.java
@@ -2,6 +2,7 @@
import java.io.IOException;
import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.MultivaluedMap;
import org.jboss.resteasy.core.ResourceInvoker;
@@ -17,8 +18,10 @@
import org.testng.annotations.Test;
import org.zanata.limits.RateLimitingProcessor;
import org.zanata.model.HAccount;
+import org.zanata.util.HttpUtil;
import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.doReturn;
/**
* @author Patrick Huang headers;
private HAccount authenticatedUser;
+ @Mock
+ private HttpServletRequest servletRequest;
+
+ private String clienIP = "255.255.255.0.1";
@BeforeMethod
public void beforeMethod() throws ServletException, IOException {
MockitoAnnotations.initMocks(this);
-
- when(request.getHttpHeaders().getRequestHeaders()).thenReturn(headers);
+ when(request.getHttpHeaders().getRequestHeaders())
+ .thenReturn(headers);
when(request.getHttpMethod()).thenReturn("GET");
- when(headers.getFirst(HeaderHelper.X_AUTH_TOKEN_HEADER)).thenReturn(
- API_KEY);
+ when(headers.getFirst(HttpUtil.X_AUTH_TOKEN_HEADER)).thenReturn(
+ API_KEY);
dispatcher =
spy(new RestLimitingSynchronousDispatcher(providerFactory,
processor));
// this way we can verify the task actually called super.invoke()
+ doReturn(servletRequest).when(dispatcher).getServletRequest();
doReturn(superInvoker).when(dispatcher).getInvoker(request);
doNothing().when(dispatcher).invoke(request, response, superInvoker);
authenticatedUser = null;
@@ -67,56 +75,59 @@ public void beforeMethod() throws ServletException, IOException {
}
@Test
- public void willSkipIfAPIkeyNotPresent() throws IOException,
- ServletException {
- when(headers.getFirst(HeaderHelper.X_AUTH_TOKEN_HEADER)).thenReturn(
- null);
- when(request.getUri().getPath()).thenReturn("/rest/in/peace");
- doReturn(null).when(dispatcher).getAuthenticatedUser();
+ public void willUseAuthenticatedUserApiKeyIfPresent() throws Exception {
+ authenticatedUser = new HAccount();
+ authenticatedUser.setApiKey("apiKeyInAuth");
+ doReturn(authenticatedUser).when(dispatcher).getAuthenticatedUser();
dispatcher.invoke(request, response);
- verify(response).sendError(401,
- RestLimitingSynchronousDispatcher.API_KEY_ABSENCE_WARNING);
- verifyZeroInteractions(processor);
+ verify(processor).processForApiKey(same("apiKeyInAuth"), same(response),
+ taskCaptor.capture());
}
@Test
- public void willCallRateLimitingProcessorIfAllConditionsAreMet()
- throws Exception {
- dispatcher.invoke(request, response);
-
- verify(processor).processApiKey(same(API_KEY), same(response),
- taskCaptor.capture());
+ public void willUseUsernameIfNoApiKeyButAuthenticated() throws Exception {
+ authenticatedUser = new HAccount();
+ authenticatedUser.setUsername("admin");
+ doReturn(authenticatedUser).when(dispatcher).getAuthenticatedUser();
- // verify task is calling super.invoke
- Runnable task = taskCaptor.getValue();
- task.run();
- verify(dispatcher).getInvoker(request);
+ dispatcher.invoke(request, response);
+ verify(processor).processForUser(same("admin"), same(response),
+ taskCaptor.capture());
}
@Test
- public void willUseAuthenticatedUserApiKeyIfPresent() throws Exception {
- authenticatedUser = new HAccount();
- authenticatedUser.setApiKey("apiKeyInAuth");
- doReturn(authenticatedUser).when(dispatcher).getAuthenticatedUser();
+ public void willThrowErrorWithPOSTAndNoApiKey() throws Exception {
+ when(request.getHttpMethod()).thenReturn("POST");
+ when(headers.getFirst(HttpUtil.X_AUTH_TOKEN_HEADER)).thenReturn(
+ null);
+ when(request.getUri().getPath()).thenReturn("/rest/in/peace");
+ doReturn(null).when(dispatcher).getAuthenticatedUser();
dispatcher.invoke(request, response);
- verify(processor).processApiKey(same("apiKeyInAuth"), same(response),
- taskCaptor.capture());
+ verify(response).setStatus(401);
+ verify(response).getOutputStream();
+ verifyZeroInteractions(processor);
}
@Test
- public void willUserUsernameIfNoApiKeyButAuthenticated() throws Exception {
- authenticatedUser = new HAccount();
- authenticatedUser.setUsername("admin");
- doReturn(authenticatedUser).when(dispatcher).getAuthenticatedUser();
+ public void willProcessAnonymousWithGETAndNoApiKey() throws Exception {
+ when(headers.getFirst(HttpUtil.X_AUTH_TOKEN_HEADER)).thenReturn(null);
+ when(request.getUri().getPath()).thenReturn("/rest/in/peace");
+ when(servletRequest.getRemoteAddr()).thenReturn(clienIP);
+ doReturn(null).when(dispatcher).getAuthenticatedUser();
dispatcher.invoke(request, response);
- verify(processor).processUsername(same("admin"), same(response),
- taskCaptor.capture());
+ verify(processor).processForAnonymousIP(same(clienIP), same(response),
+ taskCaptor.capture());
+
+ // verify task is calling super.invoke
+ Runnable task = taskCaptor.getValue();
+ task.run();
+ verify(dispatcher).getInvoker(request);
}
}
diff --git a/zanata-war/src/test/java/org/zanata/rest/compat/ProjectIterationCompatibilityITCase.java b/zanata-war/src/test/java/org/zanata/rest/compat/ProjectIterationCompatibilityITCase.java
index 866e42ad24..2dd8569cd1 100644
--- a/zanata-war/src/test/java/org/zanata/rest/compat/ProjectIterationCompatibilityITCase.java
+++ b/zanata-war/src/test/java/org/zanata/rest/compat/ProjectIterationCompatibilityITCase.java
@@ -38,6 +38,9 @@ public class ProjectIterationCompatibilityITCase extends RestTest {
@Override
protected void prepareDBUnitOperations() {
+ addBeforeTestOperation(new DataSetOperation(
+ "org/zanata/test/model/AccountData.dbunit.xml",
+ DatabaseOperation.CLEAN_INSERT));
addBeforeTestOperation(new DataSetOperation(
"org/zanata/test/model/ProjectsData.dbunit.xml",
DatabaseOperation.CLEAN_INSERT));
diff --git a/zanata-war/src/test/java/org/zanata/rest/compat/ProjectIterationRawCompatibilityITCase.java b/zanata-war/src/test/java/org/zanata/rest/compat/ProjectIterationRawCompatibilityITCase.java
index 211c848426..d3dad379ac 100644
--- a/zanata-war/src/test/java/org/zanata/rest/compat/ProjectIterationRawCompatibilityITCase.java
+++ b/zanata-war/src/test/java/org/zanata/rest/compat/ProjectIterationRawCompatibilityITCase.java
@@ -45,6 +45,9 @@ public class ProjectIterationRawCompatibilityITCase extends RestTest {
@Override
protected void prepareDBUnitOperations() {
+ addBeforeTestOperation(new DataSetOperation(
+ "org/zanata/test/model/AccountData.dbunit.xml",
+ DatabaseOperation.CLEAN_INSERT));
addBeforeTestOperation(new DataSetOperation(
"org/zanata/test/model/ProjectsData.dbunit.xml",
DatabaseOperation.CLEAN_INSERT));
diff --git a/zanata-war/src/test/java/org/zanata/rest/compat/ProjectRawCompatibilityITCase.java b/zanata-war/src/test/java/org/zanata/rest/compat/ProjectRawCompatibilityITCase.java
index 424a758c21..dd21396a97 100644
--- a/zanata-war/src/test/java/org/zanata/rest/compat/ProjectRawCompatibilityITCase.java
+++ b/zanata-war/src/test/java/org/zanata/rest/compat/ProjectRawCompatibilityITCase.java
@@ -66,6 +66,9 @@ public class ProjectRawCompatibilityITCase extends RestTest {
@Override
protected void prepareDBUnitOperations() {
+ addBeforeTestOperation(new DataSetOperation(
+ "org/zanata/test/model/AccountData.dbunit.xml",
+ DatabaseOperation.CLEAN_INSERT));
addBeforeTestOperation(new DataSetOperation(
"org/zanata/test/model/ProjectsData.dbunit.xml",
DatabaseOperation.CLEAN_INSERT));
diff --git a/zanata-war/src/test/java/org/zanata/rest/service/raw/AnonymousUserRawRestITCase.java b/zanata-war/src/test/java/org/zanata/rest/service/raw/AnonymousUserRawRestITCase.java
new file mode 100644
index 0000000000..7f24569fdb
--- /dev/null
+++ b/zanata-war/src/test/java/org/zanata/rest/service/raw/AnonymousUserRawRestITCase.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2015, Red Hat, Inc. and individual contributors as indicated by the
+ * @author tags. See the copyright.txt file in the distribution for a full
+ * listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this software; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF
+ * site: http://www.fsf.org.
+ */
+
+package org.zanata.rest.service.raw;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.zanata.provider.DBUnitProvider.DataSetOperation;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response.Status;
+
+import org.dbunit.operation.DatabaseOperation;
+import org.jboss.arquillian.container.test.api.RunAsClient;
+import org.jboss.resteasy.client.ClientRequest;
+import org.jboss.resteasy.client.ClientResponse;
+import org.junit.Test;
+import org.zanata.RestTest;
+import org.zanata.rest.InvalidApiKeyUtil;
+import org.zanata.rest.MediaTypes;
+import org.zanata.rest.ResourceRequest;
+import org.zanata.rest.ResourceRequestEnvironment;
+
+public class AnonymousUserRawRestITCase extends RestTest {
+
+ private final String invalidAPI = "InvalidAPIKEY";
+
+ //NOTE: keep in sync with RestLimitingSynchronousDispatcher.API_KEY_ABSENCE_WARNING
+ private final String API_KEY_ABSENCE_WARNING =
+ "You must have a valid API key. You can create one by logging in to Zanata and visiting the settings page.";
+
+ @Override
+ protected void prepareDBUnitOperations() {
+ addBeforeTestOperation(new DataSetOperation(
+ "org/zanata/test/model/ClearAllTables.dbunit.xml",
+ DatabaseOperation.DELETE_ALL));
+ addBeforeTestOperation(new DataSetOperation(
+ "org/zanata/test/model/AccountData.dbunit.xml",
+ DatabaseOperation.CLEAN_INSERT));
+ addBeforeTestOperation(new DataSetOperation(
+ "org/zanata/test/model/ProjectsData.dbunit.xml",
+ DatabaseOperation.CLEAN_INSERT));
+ }
+
+ @Test
+ @RunAsClient
+ public void doGETProjectsWithWrongAPI() throws Exception {
+ new ResourceRequest(getRestEndpointUrl("/projects"), "GET",
+ getUnAuthorizedEnvironment()) {
+ @Override
+ protected void prepareRequest(ClientRequest request) {
+ request.header(HttpHeaders.ACCEPT,
+ MediaTypes.APPLICATION_ZANATA_PROJECTS_XML);
+ }
+
+ @Override
+ protected void onResponse(ClientResponse response) {
+ assertThat(response.getEntity(String.class).toString(),
+ is(InvalidApiKeyUtil.getMessage(ADMIN, invalidAPI)));
+
+ assertThat(response.getStatus(),
+ is(Status.UNAUTHORIZED.getStatusCode()));
+ }
+ }.run();
+ }
+
+ @Test
+ @RunAsClient
+ public void doGETProjectsWithCorrectAPI() throws Exception {
+ new ResourceRequest(getRestEndpointUrl("/projects"), "GET",
+ getAuthorizedEnvironment()) {
+ @Override
+ protected void prepareRequest(ClientRequest request) {
+ request.header(HttpHeaders.ACCEPT,
+ MediaTypes.APPLICATION_ZANATA_PROJECTS_XML);
+ }
+
+ @Override
+ protected void onResponse(ClientResponse response) {
+ assertThat(response.getStatus(),
+ is(Status.OK.getStatusCode()));
+ }
+ }.run();
+ }
+
+ @Test
+ @RunAsClient
+ public void doGETProjectsWithAnonymous() throws Exception {
+ new ResourceRequest(getRestEndpointUrl("/projects"), "GET") {
+ @Override
+ protected void prepareRequest(ClientRequest request) {
+ request.header(HttpHeaders.ACCEPT,
+ MediaTypes.APPLICATION_ZANATA_PROJECTS_XML);
+ }
+
+ @Override
+ protected void onResponse(ClientResponse response) {
+ assertThat(response.getStatus(),
+ is(Status.OK.getStatusCode()));
+ }
+ }.run();
+ }
+
+ @Test
+ @RunAsClient
+ public void doPOSTProjectsWithAnonymous() throws Exception {
+ new ResourceRequest(getRestEndpointUrl("/projects"), "POST") {
+ @Override
+ protected void prepareRequest(ClientRequest request) {
+ request.header(HttpHeaders.ACCEPT,
+ MediaTypes.APPLICATION_ZANATA_PROJECTS_XML);
+ }
+
+ @Override
+ protected void onResponse(ClientResponse response) {
+ assertThat(response.getEntity(String.class).toString(),
+ is(InvalidApiKeyUtil.getMessage(
+ API_KEY_ABSENCE_WARNING)));
+
+ assertThat(response.getStatus(),
+ is(Status.UNAUTHORIZED.getStatusCode()));
+ }
+ }.run();
+ }
+
+ private ResourceRequestEnvironment getUnAuthorizedEnvironment() {
+ return new ResourceRequestEnvironment() {
+ @Override
+ public Map getDefaultHeaders() {
+ return new HashMap() {
+ {
+ put("X-Auth-User", ADMIN);
+ put("X-Auth-Token", invalidAPI);
+ }
+ };
+ }
+ };
+ }
+}
diff --git a/zanata-war/src/test/java/org/zanata/util/HttpUtilTest.java b/zanata-war/src/test/java/org/zanata/util/HttpUtilTest.java
new file mode 100644
index 0000000000..601553bbc8
--- /dev/null
+++ b/zanata-war/src/test/java/org/zanata/util/HttpUtilTest.java
@@ -0,0 +1,77 @@
+package org.zanata.util;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.HttpMethod;
+
+import org.mockito.Mockito;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author Alex Eng aeng@redhat.com
+ */
+@Test(groups = { "unit-tests" })
+public class HttpUtilTest {
+
+ @BeforeMethod
+ public void init() {
+ setHeader("");
+ }
+
+ @Test
+ public void getClientIdWithNoHeaderTest() {
+ String expectedIP = "255.255.255.1";
+ HttpServletRequest mockRequest = Mockito.mock(HttpServletRequest.class);
+ when(mockRequest.getRemoteAddr()).thenReturn(expectedIP);
+
+ String ip = HttpUtil.getClientIp(mockRequest);
+ assertThat(ip).isEqualTo(expectedIP);
+ verify(mockRequest).getRemoteAddr();
+ }
+
+ @Test
+ public void getClientIdWithWithHeaderTest() {
+ String proxyHeader = "random-header-from-proxy-server";
+ setHeader(proxyHeader);
+ String expectedIP = "255.255.255.1";
+ HttpServletRequest mockRequest = Mockito.mock(HttpServletRequest.class);
+ when(mockRequest.getHeader(proxyHeader)).thenReturn(expectedIP);
+
+ String ip = HttpUtil.getClientIp(mockRequest);
+ assertThat(ip).isEqualTo(expectedIP);
+ verify(mockRequest).getHeader(proxyHeader);
+ }
+
+ @Test
+ public void getClientIdWithWithHeaderListTest() {
+ String proxyHeader = "random-header-from-proxy-server";
+ setHeader(proxyHeader);
+ String expectedIP = "255.255.255.1,255.255.255.2,255.255.255.3";
+ HttpServletRequest mockRequest = Mockito.mock(HttpServletRequest.class);
+ when(mockRequest.getHeader(proxyHeader)).thenReturn(expectedIP);
+
+ String ip = HttpUtil.getClientIp(mockRequest);
+ assertThat(ip).isEqualTo("255.255.255.3");
+ verify(mockRequest).getHeader(proxyHeader);
+ }
+
+ private void setHeader(String header) {
+ System.setProperty("ZANATA_PROXY_HEADER", header);
+ HttpUtil.refreshProxyHeader();
+ }
+
+ @Test
+ public void isReadMethodTest() {
+ assertThat(HttpUtil.isReadMethod(HttpMethod.DELETE)).isFalse();
+ assertThat(HttpUtil.isReadMethod(HttpMethod.POST)).isFalse();
+ assertThat(HttpUtil.isReadMethod(HttpMethod.PUT)).isFalse();
+
+ assertThat(HttpUtil.isReadMethod(HttpMethod.GET)).isTrue();
+ assertThat(HttpUtil.isReadMethod(HttpMethod.HEAD)).isTrue();
+ assertThat(HttpUtil.isReadMethod(HttpMethod.OPTIONS)).isTrue();
+ }
+}