Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config/cloud_controller.yml
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,8 @@ rate_limiter:
global_general_limit: 20000
per_process_unauthenticated_limit: 100
global_unauthenticated_limit: 1000
per_process_admin_limit: -1
global_admin_limit: -1
reset_interval_in_minutes: 60

rate_limiter_v2_api:
Expand Down
2 changes: 2 additions & 0 deletions lib/cloud_controller/config_schemas/api_schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,8 @@ class ApiSchema < VCAP::Config
global_general_limit: Integer,
per_process_unauthenticated_limit: Integer,
global_unauthenticated_limit: Integer,
optional(:per_process_admin_limit) => Integer,
optional(:global_admin_limit) => Integer,
reset_interval_in_minutes: Integer
},
max_concurrent_service_broker_requests: Integer,
Expand Down
2 changes: 2 additions & 0 deletions lib/cloud_controller/rack_app_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ def build(config, request_metrics, request_logs)
global_general_limit: config.get(:rate_limiter, :global_general_limit),
per_process_unauthenticated_limit: config.get(:rate_limiter, :per_process_unauthenticated_limit),
global_unauthenticated_limit: config.get(:rate_limiter, :global_unauthenticated_limit),
per_process_admin_limit: config.get(:rate_limiter, :per_process_admin_limit),
global_admin_limit: config.get(:rate_limiter, :global_admin_limit),
interval: config.get(:rate_limiter, :reset_interval_in_minutes)
}
end
Expand Down
5 changes: 4 additions & 1 deletion middleware/base_rate_limiter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,10 @@ def per_process_request_limit(_env)
end

def exceeded_rate_limit(count, env)
count > per_process_request_limit(env)
limit = per_process_request_limit(env)
return false if limit == -1

count > limit
end

def estimate_remaining(env, new_count)
Expand Down
12 changes: 11 additions & 1 deletion middleware/rate_limiter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,20 @@ def initialize(app, opts)
@global_general_limit = opts[:global_general_limit]
@per_process_unauthenticated_limit = opts[:per_process_unauthenticated_limit]
@global_unauthenticated_limit = opts[:global_unauthenticated_limit]
@per_process_admin_limit = opts[:per_process_admin_limit] || -1
@global_admin_limit = opts[:global_admin_limit] || -1
super(app, opts[:logger], EXPIRING_REQUEST_COUNTER, opts[:interval])
end

private

def apply_rate_limiting?(env)
request = ActionDispatch::Request.new(env)
!basic_auth?(env) && !internal_api?(request) && !root_api?(request) && !admin?
!basic_auth?(env) && !internal_api?(request) && !root_api?(request) && !admin_unlimited?
end

def admin_unlimited?
admin? && @per_process_admin_limit == -1
end

def root_api?(request)
Expand All @@ -29,10 +35,14 @@ def internal_api?(request)
end

def global_request_limit(env)
return @global_admin_limit if admin?

user_token?(env) ? @global_general_limit : @global_unauthenticated_limit
end

def per_process_request_limit(env)
return @per_process_admin_limit if admin?

user_token?(env) ? @per_process_general_limit : @per_process_unauthenticated_limit
end

Expand Down
53 changes: 53 additions & 0 deletions spec/request/rate_limit_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,57 @@
expect(parsed_response['errors'].first['detail']).to include('Rate Limit Exceeded: Unauthenticated requests from this IP address have exceeded the limit')
end
end

context 'as an admin' do
let(:admin_headers) { admin_headers_for(VCAP::CloudController::User.make) }

context 'when admin_limit is -1 (default, unlimited)' do
it 'is not rate limited' do
20.times do |n|
get '/v3/spaces', nil, admin_headers
expect(last_response.status).to eq(200), "rate limited after #{n} requests"
expect(last_response.headers).not_to include('X-RateLimit-Limit')
end
end
end

context 'when admin_limit is set to a positive value' do
before do
TestConfig.override(
rate_limiter: {
enabled: true,
per_process_general_limit: 10,
global_general_limit: 100,
per_process_unauthenticated_limit: 2,
global_unauthenticated_limit: 20,
per_process_admin_limit: 3,
global_admin_limit: 30,
reset_interval_in_minutes: 60
}
)
end

it 'uses the admin limit' do
3.times do |n|
get '/v3/spaces', nil, admin_headers
expect(last_response.status).to eq(200), "rate limited after #{n} requests"
expect(last_response.headers['X-RateLimit-Limit']).to eq('30')
end

get '/v3/spaces', nil, admin_headers
expect(last_response.status).to eq(429)
end

it 'does not affect regular users' do
4.times { get '/v3/spaces', nil, admin_headers }

space = VCAP::CloudController::Space.make
user = make_developer_for_space(space)
user_headers = headers_for(user)

get '/v3/spaces', nil, user_headers
expect(last_response.status).to eq(200)
end
end
end
end
2 changes: 2 additions & 0 deletions spec/unit/lib/cloud_controller/rack_app_builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ module VCAP::CloudController
global_general_limit: 1230,
per_process_unauthenticated_limit: 1,
global_unauthenticated_limit: 10,
per_process_admin_limit: nil,
global_admin_limit: nil,
interval: 60
)
end
Expand Down
44 changes: 35 additions & 9 deletions spec/unit/middleware/rate_limiter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ module Middleware
global_general_limit:,
per_process_unauthenticated_limit:,
global_unauthenticated_limit:,
per_process_admin_limit:,
global_admin_limit:,
interval:
}
)
Expand All @@ -23,6 +25,8 @@ module Middleware
let(:global_general_limit) { 1000 }
let(:per_process_unauthenticated_limit) { 10 }
let(:global_unauthenticated_limit) { 100 }
let(:per_process_admin_limit) { -1 }
let(:global_admin_limit) { -1 }
let(:interval) { 60 }
let(:logger) { double('logger', info: nil) }
let(:expires_in) { 10.minutes.to_i }
Expand Down Expand Up @@ -230,19 +234,41 @@ module Middleware
end

context 'when user has admin or admin_read_only scopes' do
let(:per_process_general_limit) { 1 }

before do
allow(VCAP::CloudController::SecurityContext).to receive(:admin_read_only?).and_return(true)
end

it 'does not rate limit' do
2.times { middleware.call(user_1_env) }
status, response_headers, = middleware.call(user_1_env)
expect(response_headers).not_to include('X-RateLimit-Remaining')
expect(status).to eq(200)
expect(app).to have_received(:call).at_least(:once)
expect(expiring_request_counter).not_to have_received(:increment)
context 'when admin_limit is -1 (default, unlimited)' do
it 'does not rate limit' do
2.times { middleware.call(user_1_env) }
status, response_headers, = middleware.call(user_1_env)
expect(response_headers).not_to include('X-RateLimit-Remaining')
expect(status).to eq(200)
expect(app).to have_received(:call).at_least(:once)
expect(expiring_request_counter).not_to have_received(:increment)
end
end

context 'when admin_limit is set to a positive value' do
let(:per_process_admin_limit) { 2 }
let(:global_admin_limit) { 20 }

it 'rate limits admin users using the admin limits' do
_, response_headers, = middleware.call(user_1_env)
expect(response_headers['X-RateLimit-Limit']).to eq(global_admin_limit.to_s)
end

it 'returns 429 when admin limit is exceeded' do
allow(expiring_request_counter).to receive(:increment).and_return([per_process_admin_limit + 1, expires_in])
status, = middleware.call(user_1_env.merge('PATH_INFO' => '/v3/foo'))
expect(status).to eq(429)
end

it 'does not rate limit regular users with admin limits' do
allow(VCAP::CloudController::SecurityContext).to receive_messages(admin_read_only?: false, admin?: false)
_, response_headers, = middleware.call(user_1_env)
expect(response_headers['X-RateLimit-Limit']).to eq(global_general_limit.to_s)
end
end
end

Expand Down
Loading