Remove the guest system entirely: no is_guest/device_id, no guest throttles, phone login is the only way in

accounts.0008 drops the columns and refuses to run while guest rows exist.
Docs describe the current state; tests that only proved guest behaviour are gone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 18:20:10 +03:30
parent 8835e01595
commit 663252a260
61 changed files with 357 additions and 1518 deletions

View File

@@ -24,7 +24,7 @@ class BugReportCreateThrottleTests(TestCase):
def setUp(self):
cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='device-bugreport-throttle-1', is_guest=True, name='')
self.user = User.objects.create(phone='09000000060', name='')
self.token = issue_access_token(self.user.id)
def _create(self, i=0, remote_addr=None):
@@ -56,7 +56,7 @@ class BugReportCreateThrottleTests(TestCase):
def test_changing_client_ip_does_not_bypass_the_user_limit(self):
# کاربرِ غیرمهمان (نه self.user که مهمانه)، چون کلیدِ throttleِ
# مهمان‌ها عمداً بر اساسِ IP است؛ این تست فقط رفتارِ per-user رو می‌سنجه.
registered_user = User.objects.create(is_guest=False, name='', phone='+989120000901')
registered_user = User.objects.create(name='', phone='+989120000901')
registered_token = issue_access_token(registered_user.id)
def _create_as(i, remote_addr):
@@ -97,7 +97,7 @@ class TicketCreateThrottleTests(TestCase):
def setUp(self):
cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='device-ticket-throttle-1', is_guest=True, name='')
self.user = User.objects.create(phone='09000000061', name='')
self.token = issue_access_token(self.user.id)
def _create(self, i=0, remote_addr=None):
@@ -129,7 +129,7 @@ class TicketCreateThrottleTests(TestCase):
def test_changing_client_ip_does_not_bypass_the_user_limit(self):
# کاربرِ غیرمهمان (نه self.user که مهمانه)، چون کلیدِ throttleِ
# مهمان‌ها عمداً بر اساسِ IP است؛ این تست فقط رفتارِ per-user رو می‌سنجه.
registered_user = User.objects.create(is_guest=False, name='', phone='+989120000902')
registered_user = User.objects.create(name='', phone='+989120000902')
registered_token = issue_access_token(registered_user.id)
def _create_as(i, remote_addr):
@@ -170,7 +170,7 @@ class TicketMessageCreateThrottleTests(TestCase):
def setUp(self):
cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='device-ticketmsg-throttle-1', is_guest=True, name='')
self.user = User.objects.create(phone='09000000062', name='')
self.token = issue_access_token(self.user.id)
self.ticket = Ticket.objects.create(user_id=self.user.id, subject='موضوع', category='billing')
@@ -203,7 +203,7 @@ class TicketMessageCreateThrottleTests(TestCase):
def test_changing_client_ip_does_not_bypass_the_user_limit(self):
# کاربرِ غیرمهمان (نه self.user که مهمانه)، چون کلیدِ throttleِ
# مهمان‌ها عمداً بر اساسِ IP است؛ این تست فقط رفتارِ per-user رو می‌سنجه.
registered_user = User.objects.create(is_guest=False, name='', phone='+989120000903')
registered_user = User.objects.create(name='', phone='+989120000903')
registered_token = issue_access_token(registered_user.id)
registered_ticket = Ticket.objects.create(
user_id=registered_user.id, subject='موضوع', category='billing',
@@ -238,21 +238,8 @@ class TicketMessageCreateThrottleTests(TestCase):
self.assertEqual(TicketMessage.objects.filter(ticket=self.ticket).count(), count_before)
class SupportCreateGuestThrottleBypassTests(TestCase):
"""
🔒 اثبات فیکسِ امنیتی: قبلاً کلیدِ throttle_scope='support_create'
(مشترک بینِ BugReportCreateView، TicketCreateView و
TicketMessageCreateView) برای هر کاربرِ احرازهویت‌شده (مهمان یا نه)
user.pk بود. چون GuestSessionView با هر deviceId جدید یک کاربرِ
مهمانِ تازه (pk جدید) می‌سازه، یک مهاجم می‌تونست با ساختنِ پی‌درپیِ
حساب‌های مهمان، هر بار یک bucket throttle تازه برای این scopeِ
مشترک بگیره و سقفِ ۳/ساعتِ ساختِ رکوردهای پشتیبانی (BugReport/
Ticket/TicketMessage) رو چند برابر کنه. حالا GuestAwareSupportThrottle
برای کاربرهای مهمان کلید رو بر اساسِ IP (همون get_ident که
guest_session هم استفاده می‌کنه) می‌سازه، پس چرخشِ حساب مهمان دیگه
bucket تازه نمی‌ده — این هم روی bucket مشترکِ بینِ هر سه Endpoint و
هم روی رفتارِ per-user برای کاربرهای غیرمهمان تست می‌شه.
"""
class SupportCreatePerUserThrottleTests(TestCase):
"""هر کاربرِ ثبت‌نام‌شده سطلِ throttleِ جدای خودش را دارد؛ مصرفِ حسابِ دیگر به او نمی‌رسد."""
def setUp(self):
cache.clear()
@@ -277,84 +264,21 @@ class SupportCreateGuestThrottleBypassTests(TestCase):
HTTP_AUTHORIZATION=f'Bearer {token}',
)
def test_rotating_guest_accounts_share_the_same_throttle_bucket(self):
# سه حساب مهمانِ متفاوت (شبیه‌سازیِ چرخشِ حساب برای دورزدنِ throttle)،
# همه از همون کلاینت/IP — باید همه یک bucket مشترک رو مصرف کنند،
# حتی وقتی از دو Endpointِ متفاوتِ همون scope استفاده می‌کنن.
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'support_create': '2/hour'}):
guest1 = User.objects.create(device_id='device-support-guest-rot-1', is_guest=True, name='')
response1 = self._create_bug_report(issue_access_token(guest1.id), 1)
self.assertEqual(response1.status_code, 200)
guest2 = User.objects.create(device_id='device-support-guest-rot-2', is_guest=True, name='')
response2 = self._create_ticket(issue_access_token(guest2.id), 2)
self.assertEqual(response2.status_code, 200)
# سومین حسابِ مهمانِ تازه (pk جدید) باید throttle بشه، چون
# bucket بر اساسِ IP به‌اشتراک گذاشته شده، نه user.pk — یعنی
# سقفِ ۲/ساعت هنوز اعمال می‌شه، صرف‌نظر از تعدادِ حساب‌های
# مهمانِ ساخته‌شده یا این‌که کدوم Endpoint صدا زده شده.
guest3 = User.objects.create(device_id='device-support-guest-rot-3', is_guest=True, name='')
response3 = self._create_bug_report(issue_access_token(guest3.id), 3)
self.assertEqual(response3.status_code, 429)
def test_rotated_guest_account_cannot_get_a_fresh_bucket_after_limit(self):
# حتی وقتی حسابِ مهمانِ جدید به سقف رسیده، یک حسابِ مهمانِ دیگر
# (چهارمین pk) هم نباید bucket تازه‌ای بگیره — سقف هنوز به IP
# مشترک متصل است.
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'support_create': '1/hour'}):
guest1 = User.objects.create(device_id='device-support-guest-rot-4', is_guest=True, name='')
response1 = self._create_bug_report(issue_access_token(guest1.id), 4)
self.assertEqual(response1.status_code, 200)
guest2 = User.objects.create(device_id='device-support-guest-rot-5', is_guest=True, name='')
blocked = self._create_bug_report(issue_access_token(guest2.id), 5)
self.assertEqual(blocked.status_code, 429)
guest3 = User.objects.create(device_id='device-support-guest-rot-6', is_guest=True, name='')
still_blocked = self._create_bug_report(issue_access_token(guest3.id), 6)
self.assertEqual(still_blocked.status_code, 429)
def test_registered_users_keep_independent_per_user_buckets(self):
# عدمِ رگرسیون: کاربرهای غیرمهمان (ثبت‌نام‌شده با شماره) باید مثلِ
# قبل هرکدوم bucket جداگانه‌ی خودشون رو داشته باشن — throttle
# برای اونا هنوز بر اساسِ user.pk است، نه IP.
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'support_create': '1/hour'}):
user_a = User.objects.create(is_guest=False, name='', phone='+989120001001')
user_a = User.objects.create(name='', phone='+989120001001')
response_a = self._create_bug_report(issue_access_token(user_a.id), 'a')
self.assertEqual(response_a.status_code, 200)
user_b = User.objects.create(is_guest=False, name='', phone='+989120001002')
user_b = User.objects.create(name='', phone='+989120001002')
response_b = self._create_bug_report(issue_access_token(user_b.id), 'b')
self.assertEqual(response_b.status_code, 200)
def test_blocked_guest_request_does_not_create_support_record(self):
# درخواستِ بلاک‌شده (از حسابِ مهمانِ چرخشی) نباید هیچ BugReport یا
# Ticket جدیدی بسازه — throttle باید *قبل* از رسیدن به منطقِ
# ساختِ رکورد رد کنه.
with patch.dict(ScopedRateThrottle.THROTTLE_RATES, {'support_create': '1/hour'}):
guest1 = User.objects.create(device_id='device-support-guest-rot-7', is_guest=True, name='')
first_response = self._create_bug_report(issue_access_token(guest1.id), 7)
self.assertEqual(first_response.status_code, 200)
# حسابِ مهمانِ تازه‌ی دوم باید بلاک بشه (bucket مشترکِ IP از
# قبل مصرف شده) و نباید BugReport یا Ticket جدیدی بسازد.
guest2 = User.objects.create(device_id='device-support-guest-rot-8', is_guest=True, name='')
bug_count_before = BugReport.objects.count()
ticket_count_before = Ticket.objects.count()
blocked_bug_response = self._create_bug_report(issue_access_token(guest2.id), 8)
self.assertEqual(blocked_bug_response.status_code, 429)
blocked_ticket_response = self._create_ticket(issue_access_token(guest2.id), 8)
self.assertEqual(blocked_ticket_response.status_code, 429)
self.assertEqual(BugReport.objects.count(), bug_count_before)
self.assertEqual(Ticket.objects.count(), ticket_count_before)
class TicketListTests(TestCase):
"""GET /api/support/tickets: فقط تیکت‌های خودِ کاربر، تازه‌ترین اول، با شمارِ پاسخِ پشتیبانی."""
@@ -362,8 +286,8 @@ class TicketListTests(TestCase):
def setUp(self):
cache.clear()
self.client = APIClient()
self.user = User.objects.create(device_id='device-ticket-list-1', is_guest=True, name='')
self.other = User.objects.create(device_id='device-ticket-list-2', is_guest=True, name='')
self.user = User.objects.create(phone='09000000063', name='')
self.other = User.objects.create(phone='09000000064', name='')
self.auth = {'HTTP_AUTHORIZATION': f'Bearer {issue_access_token(self.user.id)}'}
def test_lists_only_own_tickets_with_reply_count(self):

View File

@@ -19,53 +19,14 @@ from .serializers import (
)
class GuestAwareSupportThrottle(ScopedRateThrottle):
"""
همان مشکلِ GuestAwareAIThrottle (ai_tutor/views.py) و GuestAwareDiscountThrottle/
GuestAwarePurchaseThrottle (discount/views.py، subscription/views.py)
اینجا هم هست. ScopedRateThrottle استاندارد برای درخواستِ
احرازهویت‌شده از request.user.pk به‌عنوانِ کلیدِ throttle استفاده
می‌کنه؛ چون GuestSessionView با هر deviceId جدید یک کاربرِ مهمانِ
تازه (pk جدید) می‌سازه، یک مهاجم می‌تونست با ساختنِ پی‌درپیِ حساب‌های
مهمان، برای هر حساب یک bucket تازه از throttle_scope 'support_create'
بگیره. چون هر سه Endpointِ این اپ (BugReportCreateView،
TicketCreateView، TicketMessageCreateView) این scope رو به‌اشتراک
می‌ذارن و هرکدوم واقعاً یک رکورد در دیتابیس می‌سازن (BugReport،
Ticket، یا TicketMessage)، چرخشِ حساب مهمان می‌تونست محدودیتِ
۳/ساعتِ در نظر گرفته‌شده رو چند برابر کنه و باعثِ spam بشه.
این کلاسِ throttle یک‌بار اینجا تعریف شده و بینِ هر سه Endpoint
به‌اشتراک گذاشته می‌شه (به‌جایِ تکرارِ یک کلاسِ مشابه برای هرکدوم)،
دقیقاً چون هر سه از یک throttle_scope مشترک استفاده می‌کنن. فقط
برای کاربرهای مهمان (is_guest=True) کلیدِ cache از pk کاربر به IP
کلاینت (get_ident) تغییر می‌کنه، پس همه‌ی حساب‌های مهمانِ ساخته‌شده
از یک IP یک bucket مشترک دارن و چرخشِ حساب دیگه bucket تازه نمی‌ده.
رفتارِ throttle برای کاربرهای غیرمهمان (ثبت‌نام‌شده) دقیقاً مثلِ قبل
(کلید بر اساسِ pk) باقی می‌مونه؛ منطقِ ساختِ رکورد/ownership/serializer
در هر سه ویو دست‌نخورده‌ست.
"""
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}
class BugReportCreateView(APIView):
"""POST /api/support/bug-reports — ثبت گزارش باگ"""
permission_classes = [IsAuthenticated]
# بدونِ idempotency/throttle، یک کاربر می‌تونست تعدادِ نامحدودی BugReport
# بسازه؛ GuestAwareSupportThrottle برای مهمان‌ها کلید رو بر اساسِ IP
# می‌سازه تا چرخشِ حساب bucket تازه نده.
# بسازه.
throttle_scope = 'support_create'
throttle_classes = [GuestAwareSupportThrottle]
throttle_classes = [ScopedRateThrottle]
@extend_schema(
request=BugReportCreateRequestSerializer,
@@ -103,10 +64,8 @@ class TicketCreateView(APIView):
# مثلِ BugReportCreateView: هیچ idempotency ای نیست، پس این endpoint هم
# همون scope=support_create رو به‌اشتراک می‌ذاره تا محدودیتِ ساختِ
# رکوردهای پشتیبانی، مستقل از این‌که کدوم نوع رکورده، رعایت بشه.
# ⚠️ اصلاح شده: از همون GuestAwareSupportThrottleِ مشترک استفاده می‌شه
# تا چرخشِ حساب مهمان bucket تازه ندهد.
throttle_scope = 'support_create'
throttle_classes = [GuestAwareSupportThrottle]
throttle_classes = [ScopedRateThrottle]
def get_throttles(self):
# GET سهمِ خواندن را می‌خورد، نه سقفِ ۳۰ در ساعتِ ساختِ رکورد.
@@ -200,10 +159,8 @@ class TicketMessageCreateView(APIView):
permission_classes = [IsAuthenticated]
# مثلِ دو Endpoint بالا: بدونِ throttle، یک کاربر می‌تونست تعدادِ
# نامحدودی پیام در یک تیکت بسازه.
# ⚠️ اصلاح شده: از همون GuestAwareSupportThrottleِ مشترک استفاده می‌شه
# تا چرخشِ حساب مهمان bucket تازه ندهد.
throttle_scope = 'support_create'
throttle_classes = [GuestAwareSupportThrottle]
throttle_classes = [ScopedRateThrottle]
@extend_schema(
request=TicketMessageCreateRequestSerializer,