From d57a4c3eaebdfdede885d13d659c6faf85f8c292 Mon Sep 17 00:00:00 2001 From: Henrik Nygren Date: Wed, 7 Oct 2026 15:58:34 +0300 Subject: [PATCH] Send rate-limit bypass key to courses.mooc.fi --- app/models/user.rb | 2 +- app/services/courses_mooc_fi_rate_limit_bypass.rb | 12 ++++++++++++ app/services/courses_mooc_fi_token_introspector.rb | 2 +- spec/models/user_spec.rb | 12 ++++++++++++ .../courses_mooc_fi_token_introspector_spec.rb | 12 ++++++++++++ 5 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 app/services/courses_mooc_fi_rate_limit_bypass.rb diff --git a/app/models/user.rb b/app/models/user.rb index 6157bc11b..781abaa59 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -601,7 +601,7 @@ def processing_submissions_count_for_exercise(exercise_name, course_id) private def courses_mooc_fi_connection - Faraday.new(request: { open_timeout: 2, timeout: 10 }) do |f| + Faraday.new(headers: CoursesMoocFiRateLimitBypass.headers, request: { open_timeout: 2, timeout: 10 }) do |f| f.request :json f.response :json end diff --git a/app/services/courses_mooc_fi_rate_limit_bypass.rb b/app/services/courses_mooc_fi_rate_limit_bypass.rb new file mode 100644 index 000000000..7f96bcd51 --- /dev/null +++ b/app/services/courses_mooc_fi_rate_limit_bypass.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# Headers for tmc-server's requests to courses.mooc.fi: sp331 skips its per-IP rate limits when this +# header matches its RATELIMIT_PROTECTION_SAFE_API_KEY, the same shared value as RACK_ATTACK_SAFE_API_KEY. +module CoursesMoocFiRateLimitBypass + HEADER = 'RATELIMIT-PROTECTION-SAFE-API-KEY' + + def self.headers + key = ENV['RACK_ATTACK_SAFE_API_KEY'] + key.present? ? { HEADER => key } : {} + end +end diff --git a/app/services/courses_mooc_fi_token_introspector.rb b/app/services/courses_mooc_fi_token_introspector.rb index 3556135b9..5b168c565 100644 --- a/app/services/courses_mooc_fi_token_introspector.rb +++ b/app/services/courses_mooc_fi_token_introspector.rb @@ -64,7 +64,7 @@ def request_introspection(token) raise Unavailable, 'courses_mooc_fi_base_url, COURSES_MOOC_FI_INTROSPECTION_CLIENT_ID or COURSES_MOOC_FI_INTROSPECTION_SECRET is not set' end - connection = Faraday.new(request: { open_timeout: 2, timeout: 5 }) do |f| + connection = Faraday.new(headers: CoursesMoocFiRateLimitBypass.headers, request: { open_timeout: 2, timeout: 5 }) do |f| f.request :url_encoded f.response :json end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index d7176eb60..75a22ca75 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -342,6 +342,18 @@ def stub_courses_mooc_fi_authentication(status:, body:) allow(Faraday).to receive(:new).and_return(connection) end + { 'bypass-key' => { 'RATELIMIT-PROTECTION-SAFE-API-KEY' => 'bypass-key' }, '' => {}, nil => {} }.each do |configured, headers| + it "sends rate-limit bypass headers #{headers} when RACK_ATTACK_SAFE_API_KEY is #{configured.inspect}" do + allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:[]).with('RACK_ATTACK_SAFE_API_KEY').and_return(configured) + stub_courses_mooc_fi_authentication(status: 200, body: true) + + User.authenticate_with_status('manageduser', 'secret123') + + expect(Faraday).to have_received(:new).with(hash_including(headers: headers)) + end + end + it 'rejects a wrong local password' do User.create!(login: 'localuser', password: 'secret123', email: 'localuser@example.com') expect(User.authenticate_with_status('localuser', 'wrongpassword').last).to eq(:rejected) diff --git a/spec/services/courses_mooc_fi_token_introspector_spec.rb b/spec/services/courses_mooc_fi_token_introspector_spec.rb index eae4b8b20..295dc8dbf 100644 --- a/spec/services/courses_mooc_fi_token_introspector_spec.rb +++ b/spec/services/courses_mooc_fi_token_introspector_spec.rb @@ -44,6 +44,18 @@ def introspect expect(sent.request_headers['Accept']).to eq('application/json') end + { 'bypass-key' => 'bypass-key', '' => nil, nil => nil }.each do |configured, sent| + it "sends rate-limit bypass key #{sent.inspect} when RACK_ATTACK_SAFE_API_KEY is #{configured.inspect}" do + allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:[]).with('RACK_ATTACK_SAFE_API_KEY').and_return(configured) + stub_provider(status: 200, body: active_body) + + introspect + + expect(provider_requests.last.request_headers['RATELIMIT-PROTECTION-SAFE-API-KEY']).to eq(sent) + end + end + it 'derives the endpoint and issuer from courses_mooc_fi_base_url' do allow(SiteSetting).to receive(:value).with('courses_mooc_fi_base_url').and_return('http://project-331.local/') provider.post('http://project-331.local/api/v0/main-frontend/oauth/introspect') do