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

Commit

Permalink
Implement anonymous pull:
Browse files Browse the repository at this point in the history
https://bugzilla.redhat.com/show_bug.cgi?id=1172618

Squashed commit of the following:

commit 7dcf92e
Author: Alex Eng <aeng@redhat.com>
Date:   Thu Mar 26 15:29:00 2015 +1000

    Fix 'download-tmx missing quote'

commit 9297d20
Author: Alex Eng <aeng@redhat.com>
Date:   Tue Mar 24 16:20:33 2015 +1000

    Remove single quote on API Key in error message

commit 4127de2
Author: Alex Eng <aeng@redhat.com>
Date:   Tue Mar 24 15:17:29 2015 +1000

    Add release notes

commit e390c86
Merge: bd0d3e8 fe9939d
Author: Alex Eng <aeng@redhat.com>
Date:   Tue Mar 24 15:16:33 2015 +1000

    Merge branch 'release' into rhbz1172618

commit bd0d3e8
Author: Alex Eng <aeng@redhat.com>
Date:   Tue Mar 24 13:17:07 2015 +1000

    Add authentication check for TMX REST

commit e7ec0cf
Author: Alex Eng <aeng@redhat.com>
Date:   Tue Mar 24 07:16:37 2015 +1000

    Fix RawRestTest

commit ff97eab
Merge: 65b6188 fd1eab8
Author: Alex Eng <aeng@redhat.com>
Date:   Tue Mar 24 06:53:20 2015 +1000

    Merge branch 'release' into rhbz1172618

commit 65b6188
Author: Alex Eng <aeng@redhat.com>
Date:   Mon Mar 23 13:04:36 2015 +1000

    Refactor rate limiter key, fix functional test

commit 8a7343b
Author: Alex Eng <aeng@redhat.com>
Date:   Mon Mar 23 11:43:26 2015 +1000

    Add ZANATA_PROXY_HEADER for proxy-header scan

commit 30a3ae7
Author: Alex Eng <aeng@redhat.com>
Date:   Sun Mar 22 01:04:40 2015 +1000

    Refactor test and RestLimitingSynchronousDispatcher from PR review

commit a49a516
Author: Alex Eng <aeng@redhat.com>
Date:   Fri Mar 20 15:56:16 2015 +1000

    Fix checkstyle

commit ec6bea8
Author: Alex Eng <aeng@redhat.com>
Date:   Fri Mar 20 15:55:02 2015 +1000

    Add unit test and integration test for anonymous REST request

commit 3bf93ab
Author: Alex Eng <aeng@redhat.com>
Date:   Thu Mar 19 09:18:22 2015 +1000

    Allow anonymous REST request for READ: https://bugzilla.redhat.com/show_bug.cgi?id=1172618
  • Loading branch information
Alex Eng committed Mar 29, 2015
1 parent 97545b4 commit 8ccbfa1
Show file tree
Hide file tree
Showing 20 changed files with 745 additions and 113 deletions.
2 changes: 1 addition & 1 deletion docs/release-notes.md
Expand Up @@ -13,7 +13,7 @@
-----------------------

<h5>New Features</h5>
*
* [1172618](https://bugzilla.redhat.com/show_bug.cgi?id=1172618) - Allow anonymous pull from Zanata

----

Expand Down
17 changes: 10 additions & 7 deletions zanata-war/src/main/java/org/zanata/limits/RateLimitManager.java
Expand Up @@ -37,7 +37,7 @@ public class RateLimitManager implements Introspectable {

public static final String INTROSPECTABLE_FIELD_RATE_LIMITERS =
"RateLimiters";
private final Cache<String, RestCallLimiter> activeCallers = CacheBuilder
private final Cache<RateLimiterToken, RestCallLimiter> activeCallers = CacheBuilder
.newBuilder().maximumSize(100).build();

@Getter(AccessLevel.PROTECTED)
Expand Down Expand Up @@ -111,21 +111,24 @@ public String getFieldValueAsString(String fieldName) {
}

private Iterable<String> peekCurrentBuckets() {
ConcurrentMap<String, RestCallLimiter> map = activeCallers.asMap();
ConcurrentMap<RateLimiterToken, RestCallLimiter> map = activeCallers.asMap();
return Iterables.transform(map.entrySet(),
new Function<Map.Entry<String, RestCallLimiter>, String>() {
new Function<Map.Entry<RateLimiterToken, RestCallLimiter>, String>() {

@Override
public String
apply(Map.Entry<String, RestCallLimiter> input) {
apply(Map.Entry<RateLimiterToken, RestCallLimiter> input) {

RestCallLimiter rateLimiter = input.getValue();
return input.getKey() + ":" + rateLimiter;
}
});
}

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) {
Expand All @@ -135,10 +138,10 @@ public RestCallLimiter getLimiter(final String apiKey) {
return NoLimitLimiter.INSTANCE;
}
try {
return activeCallers.get(apiKey, new Callable<RestCallLimiter>() {
return activeCallers.get(key, new Callable<RestCallLimiter>() {
@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());
}
Expand Down
72 changes: 72 additions & 0 deletions 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 <a href="mailto:aeng@redhat.com">aeng@redhat.com</a>
*/
@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);
}
}
Expand Up @@ -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);
Expand All @@ -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);
}
}
25 changes: 0 additions & 25 deletions zanata-war/src/main/java/org/zanata/rest/HeaderHelper.java

This file was deleted.

31 changes: 31 additions & 0 deletions 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 <a href="mailto:aeng@redhat.com">aeng@redhat.com</a>
*/
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();
}
}
@@ -1,21 +1,49 @@
/*
* 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;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
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;

/**
Expand Down Expand Up @@ -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;
}

Expand All @@ -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);
Expand Down Expand Up @@ -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);
}
}

0 comments on commit 8ccbfa1

Please sign in to comment.