Security review fixes: admin permissions, guest sessions, payment edge cases, assistant leaks
- Admin: refund/cancel/mark-resolved/Bazaar-link actions need change permission; question and curriculum import need add/delete; content browser needs view_question; guest device ids are shown to superusers only. - Accounts: registering a guest's phone revokes the guest's earlier sessions; self-deletion keeps Redemption, UserOffer and the phone's OTP rows; the public demo guest cannot be deleted (DEMO_GUEST_DEVICE_IDS); display-name contact filter covers any separator and social handles. - Payment: callback without Authority is 400; a second Bazaar purchase on a paid order is 409; orders snapshot credits/hearts/days at creation (payment.0011). - Assistant: a broken diagram no longer makes a streamed answer free; a late off-topic marker refuses the answer (not stored, not cached); a disconnect saves the partial answer; only KNOWN_COLLECTIONS are searched. - Infra: secret-key file is created atomically (workers no longer split keys); no default INTERNAL_SERVICE_KEY; Kavenegar key and Bazaar token kept out of error logs; Swagger/ReDoc pinned; schema throttled; docker compose bound to 127.0.0.1; non-ASCII digit ids are 400; ticket text capped at 5000. - docs/security-before-production.md: remaining open items 24-31 and the guard tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -152,7 +152,9 @@ def _is_db_ref_id(value) -> bool:
|
||||
"""
|
||||
if value in (None, ''):
|
||||
return False
|
||||
return str(value).strip().lstrip('-').isdigit()
|
||||
# isascii: '²'.isdigit() هم True است ولی int('²') خطا میدهد.
|
||||
text = str(value).strip().lstrip('-')
|
||||
return text.isascii() and text.isdigit()
|
||||
|
||||
|
||||
def _db_ref_or_none(value):
|
||||
@@ -606,7 +608,7 @@ class AskAIView(APIView):
|
||||
event: error data: {"detail": "..."}
|
||||
فاز ۱ (رزرو کوتا + conversation + user_message) قبلاً در post() انجام شده.
|
||||
"""
|
||||
from rag.services import answer_cache
|
||||
from rag.services import answer_cache, guardrails
|
||||
from rag.services.generation import (
|
||||
PROMPT_VERSION, chat_model_for, default_follow_ups, stream_answer,
|
||||
)
|
||||
@@ -654,12 +656,18 @@ class AskAIView(APIView):
|
||||
stream_result: dict = {}
|
||||
started = time.monotonic()
|
||||
effective_grade = retrieval_meta.get('gradeTitle') or grade_title or ''
|
||||
for delta in stream_answer(
|
||||
v['userQuestion'], used_chunks or None, effective_grade,
|
||||
history=history, focus=focus, result=stream_result, age=age,
|
||||
):
|
||||
parts.append(delta)
|
||||
yield _sse('chunk', {'delta': delta})
|
||||
try:
|
||||
for delta in stream_answer(
|
||||
v['userQuestion'], used_chunks or None, effective_grade,
|
||||
history=history, focus=focus, result=stream_result, age=age,
|
||||
):
|
||||
parts.append(delta)
|
||||
yield _sse('chunk', {'delta': delta})
|
||||
except GeneratorExit:
|
||||
# کاربر صفحه را وسطِ جواب بست. سوال خرج میماند (مدل هزینهاش را گرفته)، ولی
|
||||
# همان بخشی که ساخته شد ثبت میشود تا گفتگو سوالِ بیجواب نداشته باشد.
|
||||
self._save_partial_answer(conversation, user, v, ''.join(parts))
|
||||
raise
|
||||
provider_status.record_success()
|
||||
answer_text = ''.join(parts).strip() or 'متأسفم، الان نتونستم پاسخِ مناسبی بسازم.'
|
||||
follow_ups = default_follow_ups()
|
||||
@@ -678,6 +686,7 @@ class AskAIView(APIView):
|
||||
}
|
||||
if stream_result.get('refused'):
|
||||
used_chunks = []
|
||||
answer_text = guardrails.REFUSAL
|
||||
elif cacheable:
|
||||
answer_cache.set(cache_key, {
|
||||
'answer': answer_text, 'followUps': follow_ups,
|
||||
@@ -752,6 +761,20 @@ class AskAIView(APIView):
|
||||
response['X-Accel-Buffering'] = 'no'
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def _save_partial_answer(conversation, user, v, text):
|
||||
logger.info('stream closed by client mid-answer: user_id=%s conversation_id=%s chars=%s',
|
||||
user.id, conversation.id, len(text))
|
||||
if not text.strip():
|
||||
return
|
||||
try:
|
||||
Message.objects.create(
|
||||
conversation=conversation, role='assistant', text=text.strip() + ' …',
|
||||
question_id=_db_ref_or_none(v.get('questionId')),
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('saving partial answer failed: conversation_id=%s', conversation.id)
|
||||
|
||||
def _release_reserved_slot(self, conversation, user_message, conversation_is_new):
|
||||
"""پاککردن رزرو فاز ۱ وقتی فاز بعدی شکست میخورد، تا نه پیام یتیم بماند و نه کوتا مصرفشده حساب شود."""
|
||||
from subscription.services import refund_credit
|
||||
|
||||
Reference in New Issue
Block a user