diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index a9b577d1..f176fff4 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -98,7 +98,7 @@ class Api::V1::BaseController < ApplicationController @current_user = @api_key.user @api_key.update_last_used! @authentication_method = :api_key - @rate_limiter = ApiRateLimiter.new(@api_key) + @rate_limiter = ApiRateLimiter.limit(@api_key) setup_current_context_for_api true end diff --git a/app/services/api_rate_limiter.rb b/app/services/api_rate_limiter.rb index 9ceb9e79..d3a771cf 100644 --- a/app/services/api_rate_limiter.rb +++ b/app/services/api_rate_limiter.rb @@ -67,7 +67,17 @@ class ApiRateLimiter # Class method to get usage for an API key without incrementing def self.usage_for(api_key) - new(api_key).usage_info + limit(api_key).usage_info + end + + def self.limit(api_key) + if Rails.application.config.app_mode.self_hosted? + # Use NoopApiRateLimiter for self-hosted mode + # This means no rate limiting is applied + NoopApiRateLimiter.new(api_key) + else + new(api_key) + end end private diff --git a/app/services/noop_api_rate_limiter.rb b/app/services/noop_api_rate_limiter.rb new file mode 100644 index 00000000..116b6537 --- /dev/null +++ b/app/services/noop_api_rate_limiter.rb @@ -0,0 +1,39 @@ +class NoopApiRateLimiter + def initialize(api_key) + @api_key = api_key + end + + def rate_limit_exceeded? + false + end + + def increment_request_count! + # No operation + end + + def current_count + 0 + end + + def rate_limit + Float::INFINITY + end + + def reset_time + 0 + end + + def usage_info + { + current_count: 0, + rate_limit: Float::INFINITY, + remaining: Float::INFINITY, + reset_time: 0, + tier: :noop + } + end + + def self.usage_for(api_key) + new(api_key).usage_info + end +end diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb index 93f55288..3d225e58 100644 --- a/config/initializers/rack_attack.rb +++ b/config/initializers/rack_attack.rb @@ -9,8 +9,11 @@ class Rack::Attack request.ip if request.path == "/oauth/token" end + # Determine limits based on self-hosted mode + self_hosted = Rails.application.config.app_mode.self_hosted? + # Throttle API requests per access token - throttle("api/requests", limit: 100, period: 1.hour) do |request| + throttle("api/requests", limit: self_hosted ? 10_000 : 100, period: 1.hour) do |request| if request.path.start_with?("/api/") # Extract access token from Authorization header auth_header = request.get_header("HTTP_AUTHORIZATION") @@ -25,7 +28,7 @@ class Rack::Attack end # More permissive throttling for API requests by IP (for development/testing) - throttle("api/ip", limit: 200, period: 1.hour) do |request| + throttle("api/ip", limit: self_hosted ? 20_000 : 200, period: 1.hour) do |request| request.ip if request.path.start_with?("/api/") end diff --git a/test/services/noop_api_rate_limiter_test.rb b/test/services/noop_api_rate_limiter_test.rb new file mode 100644 index 00000000..9c7105b1 --- /dev/null +++ b/test/services/noop_api_rate_limiter_test.rb @@ -0,0 +1,58 @@ +require "test_helper" + +class NoopApiRateLimiterTest < ActiveSupport::TestCase + setup do + @user = users(:family_admin) + # Clean up any existing API keys for this user to ensure tests start fresh + @user.api_keys.destroy_all + + @api_key = ApiKey.create!( + user: @user, + name: "Noop Rate Limiter Test Key", + scopes: [ "read" ], + display_key: "noop_rate_limiter_test_#{SecureRandom.hex(8)}" + ) + @rate_limiter = NoopApiRateLimiter.new(@api_key) + end + + test "should never be rate limited" do + assert_not @rate_limiter.rate_limit_exceeded? + end + + test "should not increment request count" do + @rate_limiter.increment_request_count! + assert_equal 0, @rate_limiter.current_count + end + + test "should always have zero request count" do + assert_equal 0, @rate_limiter.current_count + end + + test "should have infinite rate limit" do + assert_equal Float::INFINITY, @rate_limiter.rate_limit + end + + test "should have zero reset time" do + assert_equal 0, @rate_limiter.reset_time + end + + test "should provide correct usage info" do + usage_info = @rate_limiter.usage_info + + assert_equal 0, usage_info[:current_count] + assert_equal Float::INFINITY, usage_info[:rate_limit] + assert_equal Float::INFINITY, usage_info[:remaining] + assert_equal 0, usage_info[:reset_time] + assert_equal :noop, usage_info[:tier] + end + + test "class method usage_for should work" do + usage_info = NoopApiRateLimiter.usage_for(@api_key) + + assert_equal 0, usage_info[:current_count] + assert_equal Float::INFINITY, usage_info[:rate_limit] + assert_equal Float::INFINITY, usage_info[:remaining] + assert_equal 0, usage_info[:reset_time] + assert_equal :noop, usage_info[:tier] + end +end