throttling, abuse-protection & logging/monitoring pass
This commit is contained in:
@@ -6,6 +6,7 @@ from drf_spectacular.utils import OpenApiParameter, extend_schema
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.throttling import ScopedRateThrottle
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from .models import DiscountCode, Redemption
|
||||
@@ -19,6 +20,41 @@ from .serializers import (
|
||||
from .services import redeem_offer_code
|
||||
|
||||
|
||||
class GuestAwareDiscountThrottle(ScopedRateThrottle):
|
||||
"""
|
||||
⚠️ اضافه شده: همون مشکلِ GuestAwareAIThrottle (ai_tutor/views.py) و
|
||||
GuestAwareFamilyThrottle (family/views.py) اینجا هم هست.
|
||||
ScopedRateThrottle استاندارد برای درخواستِ احرازهویتشده از
|
||||
request.user.pk بهعنوانِ کلیدِ throttle استفاده میکنه؛ چون
|
||||
GuestSessionView با هر deviceId جدید یک کاربرِ مهمانِ تازه (pk جدید)
|
||||
میسازه، یک مهاجم میتونست با ساختنِ پیدرپیِ حسابهای مهمان (تا سقفِ
|
||||
guest_session=20/hour)، برای هر حساب یک bucket تازه از throttle_scope
|
||||
'discount_redeem' بگیره. ریسکِ این endpoint از بقیه هم بیشتره، چون
|
||||
DiscountCode یک رکوردِ سراسری با max_uses مشترکِ بینِ همهی کاربرهاست؛
|
||||
یک حدسِ درست صرفاً اطلاعات لو نمیده، بلکه یک Redemption واقعی
|
||||
میسازه و مبلغِ سفارش رو کم میکنه — یعنی throttle دورزدهشده به
|
||||
مهاجم guessهای واقعاً بیشتری روی یک کدِ سراسری میده.
|
||||
|
||||
دقیقاً طبقِ همون الگو: فقط برای کاربرهای مهمان (is_guest=True) کلیدِ
|
||||
cache از pk کاربر به IP کلاینت (get_ident) تغییر میکنه، پس همهی
|
||||
حسابهای مهمانِ ساختهشده از یک IP یک bucket مشترک دارن و چرخشِ حساب
|
||||
دیگه bucket تازه نمیده. رفتارِ throttle برای کاربرهای غیرمهمان
|
||||
(ثبتنامشده) دقیقاً مثلِ قبل (کلید بر اساسِ pk) باقی میمونه؛ منطقِ
|
||||
lookup/redemption/order دستنخوردهست.
|
||||
"""
|
||||
|
||||
def get_cache_key(self, request, view):
|
||||
user = request.user
|
||||
if user and user.is_authenticated and getattr(user, 'is_guest', False):
|
||||
ident = f'guest-ip:{self.get_ident(request)}'
|
||||
elif user and user.is_authenticated:
|
||||
ident = user.pk
|
||||
else:
|
||||
ident = self.get_ident(request)
|
||||
|
||||
return self.cache_format % {'scope': self.scope, 'ident': ident}
|
||||
|
||||
|
||||
def _is_usable(discount: DiscountCode) -> bool:
|
||||
return bool(
|
||||
discount.redemptions.count() < discount.max_uses
|
||||
@@ -33,6 +69,24 @@ class ValidateDiscountView(APIView):
|
||||
# صورت کلاینت بدون احراز هویت میتواند کدهای تخفیف را probe کند.
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
# ⚠️ اضافه شده: این endpoint خودِ type/value کد رو هم برمیگردونه (نه
|
||||
# فقط valid/invalid)، پس بدونِ throttle یک کاربرِ احرازهویتشده میتونست
|
||||
# بینهایت کدهای تخفیف رو probe کنه و جزئیاتشون رو یاد بگیره. طبقِ
|
||||
# همون الگویِ ScopedRateThrottle که برای RedeemOfferCodeView
|
||||
# (offer_redeem) اضافه شد: چون کاربر احرازهویتشده، کلید بر اساسِ pk
|
||||
# کاربره (نه IP). نرخ کمی بالاتر از offer_redeem انتخاب شده چون این
|
||||
# یک چکِ read-only پیش از خریده (ممکنه کاربر چند کد رو قبل از تصمیمِ
|
||||
# نهایی امتحان کنه)، نه خودِ مصرف کد.
|
||||
# ⚠️ اصلاح شده: چون کاربرهای مهمان (GuestSessionView) با هر deviceId
|
||||
# جدید یک pk تازه میگیرن، ScopedRateThrottleِ ساده (کلید بر اساسِ
|
||||
# user.pk) توسطِ چرخشِ حساب مهمان قابلدورزدن بود. حالا از همون
|
||||
# GuestAwareDiscountThrottleِ مشترک (که RedeemDiscountView/
|
||||
# RedeemOfferCodeView هم استفاده میکنن) استفاده میشه که برای
|
||||
# کاربرهای مهمان کلید رو بر اساسِ IP میسازه و برای کاربرهای
|
||||
# غیرمهمان رفتارِ per-user قبلی رو دستنخورده نگه میداره.
|
||||
throttle_scope = 'discount_validate'
|
||||
throttle_classes = [GuestAwareDiscountThrottle]
|
||||
|
||||
@extend_schema(
|
||||
parameters=[OpenApiParameter('code', OpenApiTypes.STR, required=True)],
|
||||
responses=ValidateResponseSerializer,
|
||||
@@ -63,6 +117,26 @@ class RedeemDiscountView(APIView):
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
# ⚠️ اضافه شده: این endpoint هم دقیقاً همون oracle ای که باعث شد
|
||||
# ValidateDiscountView و RedeemOfferCodeView throttle بگیرن رو داره —
|
||||
# کدِ ناموجود ۴۰۴ میگیره، کدِ موجود-ولی-نامعتبر ۴۰۰ میگیره، پس بدونِ
|
||||
# throttle یک کاربرِ احرازهویتشده میتونست کدهای تخفیف رو روی سفارشِ
|
||||
# خودش probe/brute-force کنه. اینجا ریسک از اون دوتا هم بیشتره، چون
|
||||
# حدسِ درست صرفاً اطلاعات لو نمیده؛ یک کدِ واقعی رو مصرف میکنه
|
||||
# (سهمِ max_uses مشترک بین همهی کاربرها رو میخوره) و باعثِ افتِ
|
||||
# واقعیِ مبلغِ سفارش میشه؛ ضمناً هر تلاش دو تا select_for_update و یک
|
||||
# تراکنشِ دیتابیسی اجرا میکنه که گرونتر از GET سبکِ validate است.
|
||||
# طبقِ همون الگو، ScopedRateThrottle با کلیدِ per-user (نه IP) اضافه شد.
|
||||
# ⚠️ اصلاح شده: چون کاربرهای مهمان (GuestSessionView) با هر deviceId
|
||||
# جدید یک pk تازه میگیرن، ScopedRateThrottleِ ساده (کلید بر اساسِ
|
||||
# user.pk) توسطِ چرخشِ حساب مهمان قابلدورزدن بود. حالا از
|
||||
# GuestAwareDiscountThrottle استفاده میشه که برای کاربرهای مهمان
|
||||
# کلید رو بر اساسِ IP میسازه (دقیقاً همون الگویِ GuestAwareAIThrottle
|
||||
# و GuestAwareFamilyThrottle) و برای کاربرهای غیرمهمان رفتارِ per-user
|
||||
# قبلی رو دستنخورده نگه میداره.
|
||||
throttle_scope = 'discount_redeem'
|
||||
throttle_classes = [GuestAwareDiscountThrottle]
|
||||
|
||||
@extend_schema(request=RedeemRequestSerializer, responses=RedeemResponseSerializer,
|
||||
tags=['Discount (discount)'])
|
||||
def post(self, request):
|
||||
@@ -126,6 +200,27 @@ class RedeemOfferCodeView(APIView):
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
# ⚠️ اضافه شده: کدهای نامعتبر فقط یک ValidationError ساده (۴۰۰) برمیگردونن
|
||||
# و هیچ throttleای اینجا نبود؛ یعنی یک کاربرِ احرازهویتشده میتونست
|
||||
# بدونِ هیچ محدودیتی کدهای آفر رو حدس بزنه (offer-code probing). طبقِ
|
||||
# همون الگویِ ScopedRateThrottle که در بقیهی اکشنهایِ کمتکرار/حساسِ
|
||||
# پروژه استفاده شده (مثلِ purchase در subscription، support_create در
|
||||
# support)، اینجا هم یک throttle_scope مجزا اضافه شد.
|
||||
# ⚠️ اصلاح شده: چون کاربرهای مهمان (GuestSessionView) با هر deviceId
|
||||
# جدید یک pk تازه میگیرن، ScopedRateThrottleِ ساده (کلید بر اساسِ
|
||||
# user.pk) توسطِ چرخشِ حساب مهمان قابلدورزدن بود — بهخصوص اینجا
|
||||
# حساستره چون Offer.code سراسری/یکتاست و redeem_offer_code() فقط
|
||||
# جلوی تکرارِ *همون* کاربر رو میگیره، نه کاربرهای مهمانِ جدید (یک
|
||||
# حسابِ مهمانِ چرخشی میتونست همون آفرِ سراسری رو دوباره از
|
||||
# apply_offer() بگیره). حالا از همون GuestAwareDiscountThrottle که
|
||||
# برای RedeemDiscountView اضافه شد استفاده میشه (دقیقاً همون الگویِ
|
||||
# GuestAwareAIThrottle، GuestAwareFamilyThrottle و
|
||||
# GuestAwarePurchaseThrottle) که برای کاربرهای مهمان کلید رو بر
|
||||
# اساسِ IP میسازه و برای کاربرهای غیرمهمان رفتارِ per-user قبلی رو
|
||||
# دستنخورده نگه میداره.
|
||||
throttle_scope = 'offer_redeem'
|
||||
throttle_classes = [GuestAwareDiscountThrottle]
|
||||
|
||||
@extend_schema(
|
||||
request=RedeemOfferCodeRequestSerializer, responses=UserOfferResponseSerializer,
|
||||
tags=['Discount (discount)'],
|
||||
|
||||
Reference in New Issue
Block a user