Skip to content

Commit

Permalink
Add rate limiting based on user-role evalutation (#130)
Browse files Browse the repository at this point in the history
Fixes #127
  • Loading branch information
marcosbarbero authored Oct 30, 2018
1 parent da12509 commit 8db11b0
Show file tree
Hide file tree
Showing 20 changed files with 416 additions and 31 deletions.
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
<module>spring-cloud-zuul-ratelimit-core</module>
<module>spring-cloud-zuul-ratelimit-tests/consul</module>
<module>spring-cloud-zuul-ratelimit-tests/springdata</module>
<module>spring-cloud-zuul-ratelimit-tests/security-context</module>
<module>spring-cloud-zuul-ratelimit-tests/redis</module>
<module>spring-cloud-zuul-ratelimit-tests/bucket4j-jcache</module>
<module>spring-cloud-zuul-ratelimit-tests/bucket4j-hazelcast</module>
Expand Down
13 changes: 13 additions & 0 deletions spring-cloud-zuul-ratelimit-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<scope>provided</scope>
<optional>true</optional>
</dependency>

<dependency>
<groupId>com.github.vladimir-bukhtoyarov</groupId>
<artifactId>bucket4j-core</artifactId>
Expand Down Expand Up @@ -153,6 +160,12 @@
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>

</dependencies>


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.filters.RateLimitPreFilter;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.DefaultRateLimitKeyGenerator;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.DefaultRateLimitUtils;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.SecuredRateLimitUtils;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.StringToMatchTypeConverter;
import com.netflix.zuul.ZuulFilter;
import io.github.bucket4j.grid.GridBucketState;
Expand All @@ -49,6 +50,7 @@
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;
Expand Down Expand Up @@ -90,12 +92,6 @@ public RateLimiterErrorHandler rateLimiterErrorHandler() {
return new DefaultRateLimiterErrorHandler();
}

@Bean
@ConditionalOnMissingBean(RateLimitUtils.class)
public RateLimitUtils rateLimitUtils(final RateLimitProperties rateLimitProperties) {
return new DefaultRateLimitUtils(rateLimitProperties);
}

@Bean
public ZuulFilter rateLimiterPreFilter(final RateLimiter rateLimiter, final RateLimitProperties rateLimitProperties,
final RouteLocator routeLocator, final RateLimitKeyGenerator rateLimitKeyGenerator,
Expand All @@ -119,6 +115,23 @@ public RateLimitKeyGenerator ratelimitKeyGenerator(final RateLimitProperties pro
return new DefaultRateLimitKeyGenerator(properties, rateLimitUtils);
}

@Configuration
@ConditionalOnMissingBean(RateLimitUtils.class)
public static class RateLimitUtilsConfiguration {

@Bean
@ConditionalOnClass(name = "org.springframework.security.core.Authentication")
public RateLimitUtils securedRateLimitUtils(final RateLimitProperties rateLimitProperties) {
return new SecuredRateLimitUtils(rateLimitProperties);
}

@Bean
@ConditionalOnMissingClass("org.springframework.security.core.Authentication")
public RateLimitUtils rateLimitUtils(final RateLimitProperties rateLimitProperties) {
return new DefaultRateLimitUtils(rateLimitProperties);
}
}

@Configuration
@ConditionalOnClass(RedisTemplate.class)
@ConditionalOnMissingBean(RateLimiter.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config;

import javax.servlet.http.HttpServletRequest;
import java.util.Set;

/**
* @author Liel Chayoun
Expand All @@ -38,4 +39,12 @@ public interface RateLimitUtils {
* @return The remote IP address
*/
String getRemoteAddress(HttpServletRequest request);

/**
* Returns the authenticated user's roles.
*
* @return The authenticated user's roles or empty
*/
Set<String> getUserRoles();

}
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public boolean apply(HttpServletRequest request, Route route, RateLimitUtils rat
}

public String key(HttpServletRequest request, Route route, RateLimitUtils rateLimitUtils) {
return type.key(request, route, rateLimitUtils) +
return type.key(request, route, rateLimitUtils, matcher) +
(StringUtils.isEmpty(matcher) ? StringUtils.EMPTY : (":" + matcher));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@
package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties;

import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimitUtils;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.netflix.zuul.filters.Route;

import javax.servlet.http.HttpServletRequest;
import java.util.Optional;

public enum RateLimitType {
/**
* Rate limit policy considering the user's origin.
Expand All @@ -33,7 +34,7 @@ public boolean apply(HttpServletRequest request, Route route, RateLimitUtils rat
}

@Override
public String key(HttpServletRequest request, Route route, RateLimitUtils rateLimitUtils) {
public String key(HttpServletRequest request, Route route, RateLimitUtils rateLimitUtils, String matcher) {
return rateLimitUtils.getRemoteAddress(request);
}
},
Expand All @@ -48,7 +49,7 @@ public boolean apply(HttpServletRequest request, Route route, RateLimitUtils rat
}

@Override
public String key(HttpServletRequest request, Route route, RateLimitUtils rateLimitUtils) {
public String key(HttpServletRequest request, Route route, RateLimitUtils rateLimitUtils, String matcher) {
return rateLimitUtils.getUser(request);
}
},
Expand All @@ -63,14 +64,30 @@ public boolean apply(HttpServletRequest request, Route route, RateLimitUtils rat
}

@Override
public String key(HttpServletRequest request, Route route, RateLimitUtils rateLimitUtils) {
public String key(HttpServletRequest request, Route route, RateLimitUtils rateLimitUtils, String matcher) {
return Optional.ofNullable(route).map(Route::getPath).orElse(StringUtils.EMPTY);
}
},

/**
* Rate limit policy considering the authenticated user's role.
*/
ROLE {
@Override
public boolean apply(HttpServletRequest request, Route route, RateLimitUtils rateLimitUtils, String matcher) {
return rateLimitUtils.getUserRoles().contains(matcher.toUpperCase());
}

@Override
public String key(HttpServletRequest request, Route route, RateLimitUtils rateLimitUtils, String matcher) {
return matcher;
}
},
;

public abstract boolean apply(HttpServletRequest request, Route route,
RateLimitUtils rateLimitUtils, String matcher);

public abstract String key(HttpServletRequest request, Route route, RateLimitUtils rateLimitUtils);
public abstract String key(HttpServletRequest request, Route route,
RateLimitUtils rateLimitUtils, String matcher);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@
package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.validators;

import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties.Policy;
import java.util.Collection;
import java.util.Map;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Collection;
import java.util.Map;

import static com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitType.ROLE;

/**
* Validates the rate limit policies.
Expand Down Expand Up @@ -49,14 +52,19 @@ public boolean isValid(Object value, ConstraintValidatorContext context) {

private boolean isValidCollection(Collection<?> objects) {
return objects.isEmpty()
|| objects.stream().allMatch(this::isValidObject);
|| objects.stream().allMatch(this::isValidObject);
}

private boolean isValidObject(Object o) {
return (o instanceof Policy) && isValidPolicy((Policy) o);
}

private boolean isValidPolicy(Policy p) {
return p.getLimit() != null || p.getQuota() != null;
private boolean isValidPolicy(Policy policy) {
return (policy.getLimit() != null || policy.getQuota() != null) && isValidRoles(policy);
}

private boolean isValidRoles(Policy policy) {
return policy.getType().stream()
.noneMatch(type -> type.getType().equals(ROLE) && type.getMatcher() == null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@

package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support;

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.X_FORWARDED_FOR_HEADER;

import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimitUtils;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties;
import javax.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;

import javax.servlet.http.HttpServletRequest;
import java.util.Set;

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.X_FORWARDED_FOR_HEADER;

/**
* @author Liel Chayoun
*/
Expand All @@ -46,4 +48,9 @@ public String getRemoteAddress(final HttpServletRequest request) {
}
return request.getRemoteAddr();
}

@Override
public Set<String> getUserRoles() {
throw new UnsupportedOperationException("Not supported");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2012-2018 the original author or authors.
*
* 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.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support;

import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;

import java.util.Set;

import static java.util.Collections.emptySet;

/**
* @author Marcos Barbero
*/
public class SecuredRateLimitUtils extends DefaultRateLimitUtils {

public SecuredRateLimitUtils(final RateLimitProperties properties) {
super(properties);
}

@Override
public Set<String> getUserRoles() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return emptySet();
}
return AuthorityUtils.authorityListToSet(authentication.getAuthorities());
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.X_FORWARDED_FOR_HEADER;

import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties.Policy;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties.Policy.MatchType;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitType;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.DefaultRateLimitKeyGenerator;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.DefaultRateLimitUtils;
import java.util.Collections;
import javax.servlet.http.HttpServletRequest;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.cloud.netflix.zuul.filters.Route;

import javax.servlet.http.HttpServletRequest;
import java.util.Collections;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.X_FORWARDED_FOR_HEADER;

public class DefaultRateLimitKeyGeneratorTest {

private DefaultRateLimitKeyGenerator target;
Expand Down Expand Up @@ -151,4 +152,12 @@ public void testKeyUserWithMatcher() {
String key = target.key(httpServletRequest, route, policy);
assertThat(key).isEqualTo("key-prefix:id:user:matcherUser");
}

@Test
public void testKeyUserRoleWithMatcher() {
Policy policy = new Policy();
policy.getType().add(new MatchType(RateLimitType.ROLE, "user"));
String key = target.key(httpServletRequest, route, policy);
assertThat(key).isEqualTo("key-prefix:id:user:user");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@
import javax.servlet.http.HttpServletRequest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.cloud.netflix.zuul.filters.Route;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringRunner;

public class RateLimitTypeTest {

Expand Down Expand Up @@ -46,7 +51,7 @@ public void applyUserNoMatch() {
public void keyUser() {
when(httpServletRequest.getRemoteUser()).thenReturn("testUser");

String key = RateLimitType.USER.key(httpServletRequest, route, rateLimitUtils);
String key = RateLimitType.USER.key(httpServletRequest, route, rateLimitUtils, null);
assertThat(key).isEqualTo("testUser");
}

Expand All @@ -70,7 +75,7 @@ public void applyOriginNoMatch() {
public void keyOrigin() {
when(httpServletRequest.getRemoteAddr()).thenReturn("testAddr");

String key = RateLimitType.ORIGIN.key(httpServletRequest, route, rateLimitUtils);
String key = RateLimitType.ORIGIN.key(httpServletRequest, route, rateLimitUtils, null);
assertThat(key).isEqualTo("testAddr");
}

Expand All @@ -88,7 +93,12 @@ public void applyURLNoMatch() {

@Test
public void keyURL() {
String key = RateLimitType.URL.key(httpServletRequest, route, rateLimitUtils);
String key = RateLimitType.URL.key(httpServletRequest, route, rateLimitUtils, null);
assertThat(key).isEqualTo("/test");
}

@Test(expected = UnsupportedOperationException.class)
public void doNotApplyRoleWithoutMatcher() {
RateLimitType.ROLE.apply(httpServletRequest, route, rateLimitUtils, null);
}
}
Loading

0 comments on commit 8db11b0

Please sign in to comment.