@@ -74,7 +74,7 @@ object RateLimitingUtil extends MdcLoggable {
7474
7575 def useConsumerLimits = APIUtil .getPropsAsBoolValue(" use_consumer_limits" , false )
7676
77- private def createUniqueKey (consumerKey : String , period : LimitCallPeriod ) = consumerKey + RateLimitingPeriod .toString(period)
77+ private def createUniqueKey (consumerKey : String , period : LimitCallPeriod ) = consumerKey + " _ " + RateLimitingPeriod .toString(period)
7878
7979 private def underConsumerLimits (consumerKey : String , period : LimitCallPeriod , limit : Long ): Boolean = {
8080 if (useConsumerLimits) {
@@ -173,18 +173,51 @@ object RateLimitingUtil extends MdcLoggable {
173173 }
174174
175175 /**
176- * This function checks rate limiting for a Consumer.
177- * It will check rate limiting per minute, hour, day, week and month.
178- * In case any of the above is hit an error is thrown.
179- * In case two or more limits are hit rate limit with lower period has precedence regarding the error message.
180- * @param userAndCallContext is a Tuple (Box[User], Option[CallContext]) provided from getUserAndSessionContextFuture function
181- * @return a Tuple (Box[User], Option[CallContext]) enriched with rate limiting header or an error.
176+ * Rate limiting guard that enforces API call limits for both authorized and anonymous access.
177+ *
178+ * This is the main rate limiting enforcement function that controls access to OBP API endpoints.
179+ * It operates in two modes depending on whether the caller is authenticated or anonymous.
180+ *
181+ * AUTHORIZED ACCESS (with valid consumer credentials):
182+ * - Enforces limits across 6 time periods: per second, minute, hour, day, week, and month
183+ * - Uses consumer_id as the rate limiting key (simplified for current implementation)
184+ * - Note: api_name, api_version, and bank_id may be added to the key in future versions
185+ * - Limits are defined in CallLimit configuration for each consumer
186+ * - Stores counters in Redis with TTL matching the time period
187+ * - Returns 429 status with appropriate error message when any limit is exceeded
188+ * - Lower period limits take precedence in error messages (e.g., per-second over per-minute)
189+ *
190+ * ANONYMOUS ACCESS (no consumer credentials):
191+ * - Only enforces per-hour limits (configurable via "user_consumer_limit_anonymous_access", default: 1000)
192+ * - Uses client IP address as the rate limiting key
193+ * - Designed to prevent abuse while allowing reasonable anonymous usage
194+ *
195+ * REDIS STORAGE MECHANISM:
196+ * - Keys format: {consumer_id}_{PERIOD} (e.g., "consumer123_PER_MINUTE")
197+ * - Values: current call count within the time window
198+ * - TTL: automatically expires keys when time period ends
199+ * - Atomic operations ensure thread-safe counter increments
200+ *
201+ * RATE LIMIT HEADERS:
202+ * - Sets X-Rate-Limit-Limit: maximum allowed requests for the period
203+ * - Sets X-Rate-Limit-Reset: seconds until the limit resets (TTL)
204+ * - Sets X-Rate-Limit-Remaining: requests remaining in current period
205+ *
206+ * ERROR HANDLING:
207+ * - Redis connectivity issues default to allowing the request (fail-open)
208+ * - Rate limiting can be globally disabled via "use_consumer_limits" property
209+ * - Malformed or missing limits default to unlimited access
210+ *
211+ * @param userAndCallContext Tuple containing (Box[User], Option[CallContext]) from authentication
212+ * @return Same tuple structure, either with updated rate limit headers or rate limit exceeded error
182213 */
183214 def underCallLimits (userAndCallContext : (Box [User ], Option [CallContext ])): (Box [User ], Option [CallContext ]) = {
215+ // Configuration and helper functions
184216 def perHourLimitAnonymous = APIUtil .getPropsAsIntValue(" user_consumer_limit_anonymous_access" , 1000 )
185217 def composeMsgAuthorizedAccess (period : LimitCallPeriod , limit : Long ): String = TooManyRequests + s " We only allow $limit requests ${RateLimitingPeriod .humanReadable(period)} for this Consumer. "
186218 def composeMsgAnonymousAccess (period : LimitCallPeriod , limit : Long ): String = TooManyRequests + s " We only allow $limit requests ${RateLimitingPeriod .humanReadable(period)} for anonymous access. "
187219
220+ // Helper function to set rate limit headers in successful responses
188221 def setXRateLimits (c : CallLimit , z : (Long , Long ), period : LimitCallPeriod ): Option [CallContext ] = {
189222 val limit = period match {
190223 case PER_SECOND => c.per_second
@@ -199,6 +232,7 @@ object RateLimitingUtil extends MdcLoggable {
199232 .map(_.copy(xRateLimitReset = z._1))
200233 .map(_.copy(xRateLimitRemaining = limit - z._2))
201234 }
235+ // Helper function to set rate limit headers for anonymous access
202236 def setXRateLimitsAnonymous (id : String , z : (Long , Long ), period : LimitCallPeriod ): Option [CallContext ] = {
203237 val limit = period match {
204238 case PER_HOUR => perHourLimitAnonymous
@@ -209,6 +243,7 @@ object RateLimitingUtil extends MdcLoggable {
209243 .map(_.copy(xRateLimitRemaining = limit - z._2))
210244 }
211245
246+ // Helper function to create rate limit exceeded response with remaining TTL for authorized users
212247 def exceededRateLimit (c : CallLimit , period : LimitCallPeriod ): Option [CallContextLight ] = {
213248 val remain = ttl(c.consumer_id, period)
214249 val limit = period match {
@@ -225,6 +260,7 @@ object RateLimitingUtil extends MdcLoggable {
225260 .map(_.copy(xRateLimitRemaining = 0 )).map(_.toLight)
226261 }
227262
263+ // Helper function to create rate limit exceeded response for anonymous users
228264 def exceededRateLimitAnonymous (id : String , period : LimitCallPeriod ): Option [CallContextLight ] = {
229265 val remain = ttl(id, period)
230266 val limit = period match {
@@ -236,15 +272,14 @@ object RateLimitingUtil extends MdcLoggable {
236272 .map(_.copy(xRateLimitRemaining = 0 )).map(_.toLight)
237273 }
238274
275+ // Main logic: check if we have a CallContext and determine access type
239276 userAndCallContext._2 match {
240277 case Some (cc) =>
241278 cc.rateLimiting match {
242- case Some (rl) => // Authorized access
243- val rateLimitingKey =
244- rl.consumer_id +
245- rl.api_name.getOrElse(" " ) +
246- rl.api_version.getOrElse(" " ) +
247- rl.bank_id.getOrElse(" " )
279+ case Some (rl) => // AUTHORIZED ACCESS - consumer has valid credentials and rate limits
280+ // Create rate limiting key for Redis storage using consumer_id
281+ val rateLimitingKey = rl.consumer_id
282+ // Check if current request would exceed any of the 6 rate limits
248283 val checkLimits = List (
249284 underConsumerLimits(rateLimitingKey, PER_SECOND , rl.per_second),
250285 underConsumerLimits(rateLimitingKey, PER_MINUTE , rl.per_minute),
@@ -253,6 +288,7 @@ object RateLimitingUtil extends MdcLoggable {
253288 underConsumerLimits(rateLimitingKey, PER_WEEK , rl.per_week),
254289 underConsumerLimits(rateLimitingKey, PER_MONTH , rl.per_month)
255290 )
291+ // Return 429 error for first exceeded limit (shorter periods take precedence)
256292 checkLimits match {
257293 case x1 :: x2 :: x3 :: x4 :: x5 :: x6 :: Nil if x1 == false =>
258294 (fullBoxOrException(Empty ~> APIFailureNewStyle (composeMsgAuthorizedAccess(PER_SECOND , rl.per_second), 429 , exceededRateLimit(rl, PER_SECOND ))), userAndCallContext._2)
@@ -267,14 +303,16 @@ object RateLimitingUtil extends MdcLoggable {
267303 case x1 :: x2 :: x3 :: x4 :: x5 :: x6 :: Nil if x6 == false =>
268304 (fullBoxOrException(Empty ~> APIFailureNewStyle (composeMsgAuthorizedAccess(PER_MONTH , rl.per_month), 429 , exceededRateLimit(rl, PER_MONTH ))), userAndCallContext._2)
269305 case _ =>
306+ // All limits passed - increment counters and set rate limit headers
270307 val incrementCounters = List (
271- incrementConsumerCounters(rateLimitingKey, PER_SECOND , rl.per_second), // Responses other than the 429 status code MUST be stored by a cache.
272- incrementConsumerCounters(rateLimitingKey, PER_MINUTE , rl.per_minute), // Responses other than the 429 status code MUST be stored by a cache.
273- incrementConsumerCounters(rateLimitingKey, PER_HOUR , rl.per_hour), // Responses other than the 429 status code MUST be stored by a cache.
274- incrementConsumerCounters(rateLimitingKey, PER_DAY , rl.per_day), // Responses other than the 429 status code MUST be stored by a cache.
275- incrementConsumerCounters(rateLimitingKey, PER_WEEK , rl.per_week), // Responses other than the 429 status code MUST be stored by a cache.
276- incrementConsumerCounters(rateLimitingKey, PER_MONTH , rl.per_month) // Responses other than the 429 status code MUST be stored by a cache.
308+ incrementConsumerCounters(rateLimitingKey, PER_SECOND , rl.per_second),
309+ incrementConsumerCounters(rateLimitingKey, PER_MINUTE , rl.per_minute),
310+ incrementConsumerCounters(rateLimitingKey, PER_HOUR , rl.per_hour),
311+ incrementConsumerCounters(rateLimitingKey, PER_DAY , rl.per_day),
312+ incrementConsumerCounters(rateLimitingKey, PER_WEEK , rl.per_week),
313+ incrementConsumerCounters(rateLimitingKey, PER_MONTH , rl.per_month)
277314 )
315+ // Set rate limit headers based on the most restrictive active period
278316 incrementCounters match {
279317 case first :: _ :: _ :: _ :: _ :: _ :: Nil if first._1 > 0 =>
280318 (userAndCallContext._1, setXRateLimits(rl, first, PER_SECOND ))
@@ -292,17 +330,21 @@ object RateLimitingUtil extends MdcLoggable {
292330 (userAndCallContext._1, userAndCallContext._2)
293331 }
294332 }
295- case None => // Anonymous access
333+ case None => // ANONYMOUS ACCESS - no consumer credentials, use IP-based limiting
334+ // Use client IP address as rate limiting key for anonymous access
296335 val consumerId = cc.ipAddress
336+ // Anonymous access only has per-hour limits to prevent abuse
297337 val checkLimits = List (
298338 underConsumerLimits(consumerId, PER_HOUR , perHourLimitAnonymous)
299339 )
300340 checkLimits match {
301- case x1 :: Nil if x1 == false =>
341+ case x1 :: Nil if ! x1 =>
342+ // Return 429 error if anonymous hourly limit exceeded
302343 (fullBoxOrException(Empty ~> APIFailureNewStyle (composeMsgAnonymousAccess(PER_HOUR , perHourLimitAnonymous), 429 , exceededRateLimitAnonymous(consumerId, PER_HOUR ))), userAndCallContext._2)
303344 case _ =>
345+ // Limit not exceeded - increment counter and set headers
304346 val incrementCounters = List (
305- incrementConsumerCounters(consumerId, PER_HOUR , perHourLimitAnonymous), // Responses other than the 429 status code MUST be stored by a cache.
347+ incrementConsumerCounters(consumerId, PER_HOUR , perHourLimitAnonymous)
306348 )
307349 incrementCounters match {
308350 case x1 :: Nil if x1._1 > 0 =>
@@ -312,7 +354,8 @@ object RateLimitingUtil extends MdcLoggable {
312354 }
313355 }
314356 }
315- case _ => (userAndCallContext._1, userAndCallContext._2)
357+ case _ => // No CallContext available - pass through without rate limiting
358+ (userAndCallContext._1, userAndCallContext._2)
316359 }
317360 }
318361
0 commit comments