rate limits and throttles are toggled, not required

This commit is contained in:
seniorswe
2025-10-04 15:16:51 -04:00
parent 5c2bb751ad
commit f5cc7e8168
6 changed files with 127 additions and 38 deletions
@@ -18,11 +18,13 @@ class CreateUserModel(BaseModel):
rate_limit_duration: Optional[int] = Field(None, ge=0, description='Rate limit for the user', example=100)
rate_limit_duration_type: Optional[str] = Field(None, min_length=1, max_length=7, description='Duration for the rate limit', example='hour')
rate_limit_enabled: Optional[bool] = Field(None, description='Whether rate limiting is enabled for this user', example=True)
throttle_duration: Optional[int] = Field(None, ge=0, description='Throttle limit for the user', example=10)
throttle_duration_type: Optional[str] = Field(None, min_length=1, max_length=7, description='Duration for the throttle limit', example='second')
throttle_wait_duration: Optional[int] = Field(None, ge=0, description='Wait time for the throttle limit', example=5)
throttle_wait_duration_type: Optional[str] = Field(None, min_length=1, max_length=7, description='Wait duration for the throttle limit', example='seconds')
throttle_queue_limit: Optional[int] = Field(None, ge=0, description='Throttle queue limit for the user', example=10)
throttle_enabled: Optional[bool] = Field(None, description='Whether throttling is enabled for this user', example=True)
custom_attributes: Optional[dict] = Field(None, description='Custom attributes for the user', example={'custom_key': 'custom_value'})
bandwidth_limit_bytes: Optional[int] = Field(None, ge=0, description='Maximum bandwidth allowed within the window (bytes)', example=1073741824)
bandwidth_limit_window: Optional[str] = Field('day', min_length=1, max_length=10, description='Bandwidth window unit (second/minute/hour/day/month)', example='day')
@@ -17,11 +17,13 @@ class UpdateUserModel(BaseModel):
groups: Optional[List[str]] = Field(None, description='List of groups the user belongs to', example=['client-1-group'])
rate_limit_duration: Optional[int] = Field(None, ge=0, description='Rate limit for the user', example=100)
rate_limit_duration_type: Optional[str] = Field(None, min_length=1, max_length=7, description='Duration for the rate limit', example='hour')
rate_limit_enabled: Optional[bool] = Field(None, description='Whether rate limiting is enabled for this user', example=True)
throttle_duration: Optional[int] = Field(None, ge=0, description='Throttle limit for the user', example=10)
throttle_duration_type: Optional[str] = Field(None, min_length=1, max_length=7, description='Duration for the throttle limit', example='second')
throttle_wait_duration: Optional[int] = Field(None, ge=0, description='Wait time for the throttle limit', example=5)
throttle_wait_duration_type: Optional[str] = Field(None, min_length=1, max_length=7, description='Wait duration for the throttle limit', example='seconds')
throttle_queue_limit: Optional[int] = Field(None, ge=0, description='Throttle queue limit for the user', example=10)
throttle_enabled: Optional[bool] = Field(None, description='Whether throttling is enabled for this user', example=True)
custom_attributes: Optional[dict] = Field(None, description='Custom attributes for the user', example={'custom_key': 'custom_value'})
bandwidth_limit_bytes: Optional[int] = Field(None, ge=0, description='Maximum bandwidth allowed within the window (bytes)', example=1073741824)
bandwidth_limit_window: Optional[str] = Field(None, min_length=1, max_length=10, description='Bandwidth window unit (second/minute/hour/day/month)', example='day')
@@ -17,11 +17,13 @@ class UserModelResponse(BaseModel):
groups: Optional[List[str]] = Field(None, description='List of groups the user belongs to', example=['client-1-group'])
rate_limit_duration: Optional[int] = Field(None, ge=0, description='Rate limit for the user', example=100)
rate_limit_duration_type: Optional[str] = Field(None, min_length=1, max_length=7, description='Duration for the rate limit', example='hour')
rate_limit_enabled: Optional[bool] = Field(None, description='Whether rate limiting is enabled for this user', example=True)
throttle_duration: Optional[int] = Field(None, ge=0, description='Throttle limit for the user', example=10)
throttle_duration_type: Optional[str] = Field(None, min_length=1, max_length=7, description='Duration for the throttle limit', example='second')
throttle_wait_duration: Optional[int] = Field(None, ge=0, description='Wait time for the throttle limit', example=5)
throttle_wait_duration_type: Optional[str] = Field(None, min_length=1, max_length=7, description='Wait duration for the throttle limit', example='seconds')
throttle_queue_limit: Optional[int] = Field(None, ge=0, description='Throttle queue limit for the user', example=10)
throttle_enabled: Optional[bool] = Field(None, description='Whether throttling is enabled for this user', example=True)
custom_attributes: Optional[dict] = Field(None, description='Custom attributes for the user', example={'custom_key': 'custom_value'})
bandwidth_limit_bytes: Optional[int] = Field(None, ge=0, description='Maximum bandwidth allowed within the window (bytes)', example=1073741824)
bandwidth_limit_window: Optional[str] = Field(None, min_length=1, max_length=10, description='Bandwidth window unit (second/minute/hour/day/month)', example='day')
+42 -38
View File
@@ -58,46 +58,50 @@ async def limit_and_throttle(request: Request):
user = doorman_cache.get_cache('user_cache', username)
if not user:
user = user_collection.find_one({'username': username})
rate = int(user.get('rate_limit_duration') or 1)
duration = user.get('rate_limit_duration_type', 'minute')
window = duration_to_seconds(duration)
now_ms = int(time.time() * 1000)
key = f'rate_limit:{username}:{now_ms // (window * 1000)}'
try:
client = redis_client or _fallback_counter
count = await client.incr(key)
if count == 1:
await client.expire(key, window)
except Exception:
# Rate limiting (skip if explicitly disabled)
if user.get('rate_limit_enabled') is not False:
rate = int(user.get('rate_limit_duration') or 1)
duration = user.get('rate_limit_duration_type', 'minute')
window = duration_to_seconds(duration)
key = f'rate_limit:{username}:{now_ms // (window * 1000)}'
try:
client = redis_client or _fallback_counter
count = await client.incr(key)
if count == 1:
await client.expire(key, window)
except Exception:
count = await _fallback_counter.incr(key)
if count == 1:
await _fallback_counter.expire(key, window)
if count > rate:
raise HTTPException(status_code=429, detail='Rate limit exceeded')
count = await _fallback_counter.incr(key)
if count == 1:
await _fallback_counter.expire(key, window)
if count > rate:
raise HTTPException(status_code=429, detail='Rate limit exceeded')
throttle_limit = int(user.get('throttle_duration') or 5)
throttle_duration = user.get('throttle_duration_type', 'second')
throttle_window = duration_to_seconds(throttle_duration)
throttle_key = f'throttle_limit:{username}:{now_ms // (throttle_window * 1000)}'
try:
client = redis_client or _fallback_counter
throttle_count = await client.incr(throttle_key)
if throttle_count == 1:
await client.expire(throttle_key, throttle_window)
except Exception:
throttle_count = await _fallback_counter.incr(throttle_key)
if throttle_count == 1:
await _fallback_counter.expire(throttle_key, throttle_window)
throttle_queue_limit = int(user.get('throttle_queue_limit') or 10)
if throttle_count > throttle_queue_limit:
raise HTTPException(status_code=429, detail='Throttle queue limit exceeded')
if throttle_count > throttle_limit:
throttle_wait = float(user.get('throttle_wait_duration', 0.5) or 0.5)
throttle_wait_duration = user.get('throttle_wait_duration_type', 'second')
if throttle_wait_duration != 'second':
throttle_wait *= duration_to_seconds(throttle_wait_duration)
dynamic_wait = throttle_wait * (throttle_count - throttle_limit)
await asyncio.sleep(dynamic_wait)
# Throttling (skip if explicitly disabled)
if user.get('throttle_enabled') is not False:
throttle_limit = int(user.get('throttle_duration') or 5)
throttle_duration = user.get('throttle_duration_type', 'second')
throttle_window = duration_to_seconds(throttle_duration)
throttle_key = f'throttle_limit:{username}:{now_ms // (throttle_window * 1000)}'
try:
client = redis_client or _fallback_counter
throttle_count = await client.incr(throttle_key)
if throttle_count == 1:
await client.expire(throttle_key, throttle_window)
except Exception:
throttle_count = await _fallback_counter.incr(throttle_key)
if throttle_count == 1:
await _fallback_counter.expire(throttle_key, throttle_window)
throttle_queue_limit = int(user.get('throttle_queue_limit') or 10)
if throttle_count > throttle_queue_limit:
raise HTTPException(status_code=429, detail='Throttle queue limit exceeded')
if throttle_count > throttle_limit:
throttle_wait = float(user.get('throttle_wait_duration', 0.5) or 0.5)
throttle_wait_duration = user.get('throttle_wait_duration_type', 'second')
if throttle_wait_duration != 'second':
throttle_wait *= duration_to_seconds(throttle_wait_duration)
dynamic_wait = throttle_wait * (throttle_count - throttle_limit)
await asyncio.sleep(dynamic_wait)
def reset_counters():
"""Reset in-memory rate/throttle counters (used by tests and cache clears).
@@ -17,11 +17,13 @@ interface User {
groups: string[]
rate_limit_duration: number
rate_limit_duration_type: string
rate_limit_enabled?: boolean
throttle_duration: number
throttle_duration_type: string
throttle_wait_duration: number
throttle_wait_duration_type: string
throttle_queue_limit: number | null
throttle_enabled?: boolean
custom_attributes: Record<string, string>
bandwidth_limit_bytes?: number
bandwidth_limit_window?: string
@@ -38,11 +40,13 @@ interface UpdateUserData {
groups?: string[]
rate_limit_duration?: number
rate_limit_duration_type?: string
rate_limit_enabled?: boolean
throttle_duration?: number
throttle_duration_type?: string
throttle_wait_duration?: number
throttle_wait_duration_type?: string
throttle_queue_limit?: number | null
throttle_enabled?: boolean
custom_attributes?: Record<string, string>
bandwidth_limit_bytes?: number
bandwidth_limit_window?: string
@@ -84,8 +88,13 @@ const UserDetailPage = () => {
groups: [...parsedUser.groups],
rate_limit_duration: parsedUser.rate_limit_duration,
rate_limit_duration_type: parsedUser.rate_limit_duration_type,
rate_limit_enabled: (parsedUser as any).rate_limit_enabled,
throttle_duration: parsedUser.throttle_duration,
throttle_duration_type: parsedUser.throttle_duration_type,
throttle_wait_duration: (parsedUser as any).throttle_wait_duration,
throttle_wait_duration_type: (parsedUser as any).throttle_wait_duration_type,
throttle_queue_limit: (parsedUser as any).throttle_queue_limit,
throttle_enabled: (parsedUser as any).throttle_enabled,
throttle_wait_duration: parsedUser.throttle_wait_duration,
throttle_wait_duration_type: parsedUser.throttle_wait_duration_type,
throttle_queue_limit: parsedUser.throttle_queue_limit,
@@ -107,6 +116,8 @@ const UserDetailPage = () => {
bandwidth_limit_bytes: refreshed.bandwidth_limit_bytes,
bandwidth_limit_window: refreshed.bandwidth_limit_window,
bandwidth_limit_enabled: (refreshed as any).bandwidth_limit_enabled,
rate_limit_enabled: (refreshed as any).rate_limit_enabled,
throttle_enabled: (refreshed as any).throttle_enabled,
}))
} catch {}
})()
@@ -142,14 +153,17 @@ const UserDetailPage = () => {
groups: [...user.groups],
rate_limit_duration: user.rate_limit_duration,
rate_limit_duration_type: user.rate_limit_duration_type,
rate_limit_enabled: (user as any).rate_limit_enabled,
throttle_duration: user.throttle_duration,
throttle_duration_type: user.throttle_duration_type,
throttle_wait_duration: user.throttle_wait_duration,
throttle_wait_duration_type: user.throttle_wait_duration_type,
throttle_queue_limit: user.throttle_queue_limit,
throttle_enabled: (user as any).throttle_enabled,
custom_attributes: { ...user.custom_attributes },
bandwidth_limit_bytes: user.bandwidth_limit_bytes,
bandwidth_limit_window: user.bandwidth_limit_window,
bandwidth_limit_enabled: (user as any).bandwidth_limit_enabled,
active: user.active,
ui_access: user.ui_access
})
@@ -672,6 +686,24 @@ const UserDetailPage = () => {
<FormHelp docHref="/docs/using-fields.html#rate-limit">Limits requests per user over a time window.</FormHelp>
</div>
<div className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Enforcement</label>
{isEditing ? (
<div className="flex items-center">
<input
type="checkbox"
checked={!!editData.rate_limit_enabled}
onChange={(e) => handleInputChange('rate_limit_enabled', e.target.checked)}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
/>
<label className="ml-2 text-sm text-gray-700 dark:text-gray-300">Enforce rate limiting for this user</label>
</div>
) : (
<span className={`badge ${(user as any).rate_limit_enabled === false ? 'badge-gray' : 'badge-success'}`}>
{(user as any).rate_limit_enabled === false ? 'Disabled' : 'Enabled'}
</span>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Rate Limit Duration
@@ -712,6 +744,24 @@ const UserDetailPage = () => {
<FormHelp docHref="/docs/using-fields.html#throttle">Control bursts with duration, wait, and queue size.</FormHelp>
</div>
<div className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Enforcement</label>
{isEditing ? (
<div className="flex items-center">
<input
type="checkbox"
checked={!!editData.throttle_enabled}
onChange={(e) => handleInputChange('throttle_enabled', e.target.checked)}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
/>
<label className="ml-2 text-sm text-gray-700 dark:text-gray-300">Enforce throttling for this user</label>
</div>
) : (
<span className={`badge ${(user as any).throttle_enabled === false ? 'badge-gray' : 'badge-success'}`}>
{(user as any).throttle_enabled === false ? 'Disabled' : 'Enabled'}
</span>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Throttle Duration
+29
View File
@@ -17,11 +17,13 @@ interface CreateUserData {
groups: string[]
rate_limit_duration?: number
rate_limit_duration_type?: string
rate_limit_enabled?: boolean
throttle_duration?: number
throttle_duration_type?: string
throttle_wait_duration?: number
throttle_wait_duration_type?: string
throttle_queue_limit?: number | null
throttle_enabled?: boolean
custom_attributes: Record<string, string>
bandwidth_limit_bytes?: number
bandwidth_limit_window?: string
@@ -39,6 +41,7 @@ const AddUserPage = () => {
role: '',
groups: [],
custom_attributes: {},
rate_limit_enabled: false,
bandwidth_limit_bytes: undefined,
bandwidth_limit_window: 'day',
bandwidth_limit_enabled: false,
@@ -331,6 +334,19 @@ const AddUserPage = () => {
<FormHelp docHref="/docs/using-fields.html#rate-limit">Limits requests per user over a time window.</FormHelp>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Enforcement</label>
<div className="flex items-center">
<input
type="checkbox"
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
checked={!!formData.rate_limit_enabled}
onChange={(e) => handleInputChange('rate_limit_enabled', e.target.checked)}
disabled={loading}
/>
<span className="ml-2 text-sm text-gray-700 dark:text-gray-300">Enforce rate limiting for this user</span>
</div>
</div>
<div>
<label htmlFor="rate_limit_duration" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Rate Limit Duration
@@ -375,6 +391,19 @@ const AddUserPage = () => {
<FormHelp docHref="/docs/using-fields.html#throttle">Control burst behavior with wait, duration, and queue size.</FormHelp>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Enforcement</label>
<div className="flex items-center">
<input
type="checkbox"
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
checked={!!formData.throttle_enabled}
onChange={(e) => handleInputChange('throttle_enabled', e.target.checked)}
disabled={loading}
/>
<span className="ml-2 text-sm text-gray-700 dark:text-gray-300">Enforce throttling for this user</span>
</div>
</div>
<div>
<label htmlFor="throttle_duration" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Throttle Duration