From a57a9193b18ea571b57a8a4414eeddee556a277a Mon Sep 17 00:00:00 2001 From: Wei Quan Date: Fri, 29 May 2026 14:27:31 +0200 Subject: [PATCH 1/3] Add admin_limit to cc.rate_limiter configuration --- config/cloud_controller.yml | 2 + .../config_schemas/api_schema.rb | 2 + lib/cloud_controller/rack_app_builder.rb | 2 + middleware/base_rate_limiter.rb | 5 +- middleware/rate_limiter.rb | 12 ++++- spec/request/rate_limit_spec.rb | 53 +++++++++++++++++++ spec/unit/middleware/rate_limiter_spec.rb | 45 ++++++++++++---- 7 files changed, 110 insertions(+), 11 deletions(-) diff --git a/config/cloud_controller.yml b/config/cloud_controller.yml index 0983c7f0673..a7ffaf5bc7e 100644 --- a/config/cloud_controller.yml +++ b/config/cloud_controller.yml @@ -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: diff --git a/lib/cloud_controller/config_schemas/api_schema.rb b/lib/cloud_controller/config_schemas/api_schema.rb index a9c8665639b..53413e21553 100644 --- a/lib/cloud_controller/config_schemas/api_schema.rb +++ b/lib/cloud_controller/config_schemas/api_schema.rb @@ -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, diff --git a/lib/cloud_controller/rack_app_builder.rb b/lib/cloud_controller/rack_app_builder.rb index a725ad0fa9b..7ceae938cda 100644 --- a/lib/cloud_controller/rack_app_builder.rb +++ b/lib/cloud_controller/rack_app_builder.rb @@ -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 diff --git a/middleware/base_rate_limiter.rb b/middleware/base_rate_limiter.rb index ca00db858be..1c19d370946 100644 --- a/middleware/base_rate_limiter.rb +++ b/middleware/base_rate_limiter.rb @@ -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) diff --git a/middleware/rate_limiter.rb b/middleware/rate_limiter.rb index f2a7705aebb..5c6c196c2b3 100644 --- a/middleware/rate_limiter.rb +++ b/middleware/rate_limiter.rb @@ -10,6 +10,8 @@ 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 @@ -17,7 +19,11 @@ def initialize(app, opts) 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) @@ -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 diff --git a/spec/request/rate_limit_spec.rb b/spec/request/rate_limit_spec.rb index 56bb60c2e1d..65eac051f98 100644 --- a/spec/request/rate_limit_spec.rb +++ b/spec/request/rate_limit_spec.rb @@ -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 diff --git a/spec/unit/middleware/rate_limiter_spec.rb b/spec/unit/middleware/rate_limiter_spec.rb index eba05ca77d5..a882ba68e41 100644 --- a/spec/unit/middleware/rate_limiter_spec.rb +++ b/spec/unit/middleware/rate_limiter_spec.rb @@ -12,6 +12,8 @@ module Middleware global_general_limit:, per_process_unauthenticated_limit:, global_unauthenticated_limit:, + per_process_admin_limit:, + global_admin_limit:, interval: } ) @@ -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 } @@ -230,19 +234,42 @@ 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(:admin_read_only?).and_return(false) + allow(VCAP::CloudController::SecurityContext).to receive(:admin?).and_return(false) + _, response_headers, = middleware.call(user_1_env) + expect(response_headers['X-RateLimit-Limit']).to eq(global_general_limit.to_s) + end end end From b9069d69d82039b199e7c4c322e63c8aeaa7c433 Mon Sep 17 00:00:00 2001 From: Wei Quan Date: Fri, 5 Jun 2026 11:51:23 +0200 Subject: [PATCH 2/3] Fix rubocop: use receive_messages instead of multiple stubs --- spec/unit/middleware/rate_limiter_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/unit/middleware/rate_limiter_spec.rb b/spec/unit/middleware/rate_limiter_spec.rb index a882ba68e41..462848ddb87 100644 --- a/spec/unit/middleware/rate_limiter_spec.rb +++ b/spec/unit/middleware/rate_limiter_spec.rb @@ -265,8 +265,7 @@ module Middleware end it 'does not rate limit regular users with admin limits' do - allow(VCAP::CloudController::SecurityContext).to receive(:admin_read_only?).and_return(false) - allow(VCAP::CloudController::SecurityContext).to receive(:admin?).and_return(false) + 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 From 77122c6c85ed067c8e02b8e686922a9c7e9e4644 Mon Sep 17 00:00:00 2001 From: Wei Quan Date: Fri, 5 Jun 2026 12:33:20 +0200 Subject: [PATCH 3/3] Fix rack_app_builder spec to expect per_process_admin_limit and global_admin_limit params --- spec/unit/lib/cloud_controller/rack_app_builder_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/unit/lib/cloud_controller/rack_app_builder_spec.rb b/spec/unit/lib/cloud_controller/rack_app_builder_spec.rb index 51b689ce25e..b31b8ca408f 100644 --- a/spec/unit/lib/cloud_controller/rack_app_builder_spec.rb +++ b/spec/unit/lib/cloud_controller/rack_app_builder_spec.rb @@ -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