Skip to content

Commit

Permalink
feat(ratelimit): Add enforcing & ignoring principal list configs (#356)
Browse files Browse the repository at this point in the history
  • Loading branch information
robzienert committed Mar 28, 2017
1 parent 43e4cff commit 19c4668
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ public class GateWebConfig extends WebMvcConfigurerAdapter {
@Autowired
Registry registry

@Autowired(required = false)
RateLimiterConfiguration rateLimiterConfiguration

@Autowired(required = false)
RateLimiter rateLimiter

Expand All @@ -62,7 +65,7 @@ public class GateWebConfig extends WebMvcConfigurerAdapter {
)

if (rateLimiter != null) {
registry.addInterceptor(new RateLimitingInterceptor(rateLimiter, spectatorRegistry, rateLimitLearningMode))
registry.addInterceptor(new RateLimitingInterceptor(rateLimiter, spectatorRegistry, rateLimiterConfiguration))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Component
Expand Down Expand Up @@ -49,4 +51,31 @@ public class RateLimiterConfiguration {
* rateSeconds than the default.
*/
Map<String, Integer> capacityByPrincipal = new HashMap<>();

/**
* A list of principals whose capacities are being enforced. This
* setting will only be used when learning mode is ENABLED, allowing
* operators to incrementally enable rate limiting on a per-principal
* basis.
*/
List<String> enforcing = new ArrayList<>();

/**
* A list of principals whose capacities are not being enforced. This
* setting will only be used when learning mode is DISABLED, allowing
* operators to enable learning-mode on a per-principal basis.
*/
List<String> ignoring = new ArrayList<>();

public boolean isLearning() {
return learning;
}

public List<String> getEnforcing() {
return enforcing;
}

public List<String> getIgnoring() {
return ignoring;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import com.netflix.spectator.api.Counter;
import com.netflix.spectator.api.Registry;
import com.netflix.spinnaker.gate.config.RateLimiterConfiguration;
import com.netflix.spinnaker.security.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -37,13 +38,13 @@ public class RateLimitingInterceptor extends HandlerInterceptorAdapter {
private static String UNKNOWN_PRINCIPAL = "unknown";

RateLimiter rateLimiter;
boolean learning;
RateLimiterConfiguration rateLimiterConfiguration;

private Counter throttlingCounter;

public RateLimitingInterceptor(RateLimiter rateLimiter, Registry registry, boolean learning) {
public RateLimitingInterceptor(RateLimiter rateLimiter, Registry registry, RateLimiterConfiguration rateLimiterConfiguration) {
this.rateLimiter = rateLimiter;
this.learning = learning;
this.rateLimiterConfiguration = rateLimiterConfiguration;
throttlingCounter = registry.counter("rateLimit.throttling");
}

Expand All @@ -58,6 +59,8 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons

Rate rate = rateLimiter.incrementAndGetRate(principal);

boolean learning = isLearning(principal);

rate.assignHttpHeaders(response, learning);

if (learning) {
Expand Down Expand Up @@ -113,4 +116,8 @@ private String sourceIpAddress(HttpServletRequest request) {
}
return ip;
}

private boolean isLearning(String principal) {
return !rateLimiterConfiguration.getEnforcing().contains(principal) && (rateLimiterConfiguration.getIgnoring().contains(principal) || rateLimiterConfiguration.isLearning());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright 2017 Netflix, Inc.
*
* 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.netflix.spinnaker.gate.ratelimit

import com.netflix.spectator.api.Counter
import com.netflix.spectator.api.Registry
import com.netflix.spinnaker.gate.config.RateLimiterConfiguration
import com.netflix.spinnaker.security.User
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContext
import org.springframework.security.core.context.SecurityContextHolder
import spock.lang.Specification
import spock.lang.Unroll

import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

class RateLimitingInterceptorSpec extends Specification {

Registry registry = Mock() {
counter(_) >> Mock(Counter)
}

RateLimiter rateLimiter = Mock()

Authentication authentication = Mock()

SecurityContext securityContext = Mock() {
getAuthentication() >> authentication
}

def setup() {
SecurityContextHolder.context = securityContext
}

def cleanup() {
SecurityContextHolder.clearContext()
}

@Unroll
def 'should conditionally enforce rate limiting'() {
given:
def config = new RateLimiterConfiguration().with {
it.learning = learning
it.enforcing = ['foo@example.com']
it.ignoring = ['bar@example.com']
return it
}
def subject = new RateLimitingInterceptor(rateLimiter, registry, config)

and:
def request = Mock(HttpServletRequest)
def response = Mock(HttpServletResponse)

when:
subject.preHandle(request, response, null)

then:
noExceptionThrown()
2 * authentication.getPrincipal() >> new User(email: principal)
1 * rateLimiter.incrementAndGetRate(_) >> {
new Rate().with {
it.capacity = -1
it.rateSeconds = -1
it.remaining = -1
it.reset = -1
it.throttled = true
return it
}
}
(shouldEnforce ? 1 : 0) * response.sendError(429, "Rate capacity exceeded")


where:
learning | principal || shouldEnforce
true | 'foo@example.com' || true
false | 'foo@example.com' || true
true | 'bar@example.com' || false
false | 'bar@example.com' || false
true | 'baz@example.com' || false
false | 'baz@example.com' || true
}
}

0 comments on commit 19c4668

Please sign in to comment.