P1 通用查询:viewsets / app / menu / mail / gitea / mc / stats / sync_apps 等视图与命令的裸查询改委托 SDK Manager(Role/Company/Department/Position/Menu/Tenant/App)。 P2 App 体系:app_manager.py 与 app_manifests.py 实现零裸 ORM,职务多值/虚拟职务/公司下拉全部走 SDK。 P3 工具下沉:money/json_encoder/celery_utils/exception_handler/redis_lock 改为纯转发展开;ip_filter/storage 转发并注入 app.ini 配置源;common 下 DateTime/String/Naming/UUID 转发 SDK。 删除死代码 commission/strategies/engine.py(SDK 已有同物且零引用)与 payment/models.py 的 FromTenantAndPlatform;单测 patch 目标同步为 gvsdsdk.*,test_attack_detect fixture 补齐限频去重状态。
241 lines
9.2 KiB
Python
241 lines
9.2 KiB
Python
"""模块:AttackDetectMiddleware 单元测试
|
||
|
||
覆盖 _classify 状态码分类 / ENABLED 开关 / 状态码过滤 /
|
||
exempt_paths 前缀豁免 / _log_attack 写入 AbnormalUserLog(mock)。
|
||
"""
|
||
import uuid
|
||
from unittest import mock
|
||
|
||
from django.test import RequestFactory, SimpleTestCase
|
||
|
||
from gvsds import middleware as mw
|
||
|
||
|
||
class ClassifyTestCase(SimpleTestCase):
|
||
"""_classify 状态码 → 攻击类型映射。"""
|
||
|
||
def testClassify_401_AuthFailure(self):
|
||
self.assertEqual(
|
||
mw.AttackDetectMiddleware._classify(401),
|
||
'auth_failure',
|
||
)
|
||
|
||
def testClassify_403_PermissionDenied(self):
|
||
self.assertEqual(
|
||
mw.AttackDetectMiddleware._classify(403),
|
||
'permission_denied',
|
||
)
|
||
|
||
def testClassify_429_RateLimited(self):
|
||
self.assertEqual(
|
||
mw.AttackDetectMiddleware._classify(429),
|
||
'rate_limited',
|
||
)
|
||
|
||
def testClassify_UnknownStatus_Suspicious(self):
|
||
self.assertEqual(
|
||
mw.AttackDetectMiddleware._classify(500),
|
||
'suspicious',
|
||
)
|
||
|
||
def testClassify_400_Suspicious(self):
|
||
self.assertEqual(
|
||
mw.AttackDetectMiddleware._classify(400),
|
||
'suspicious',
|
||
)
|
||
|
||
|
||
class AttackDetectMiddlewareTestCase(SimpleTestCase):
|
||
"""AttackDetectMiddleware 核心流程。"""
|
||
|
||
def setUp(self):
|
||
self.factory = RequestFactory()
|
||
|
||
def _BuildMiddleware(self, enabled=True, status_codes=None,
|
||
exempt_paths=None, max_body_len=1024):
|
||
"""构造中间件实例,绕过 app.ini 读取。"""
|
||
mwf = mw.AttackDetectMiddleware.__new__(mw.AttackDetectMiddleware)
|
||
mwf.get_response = lambda req: _FakeResponse(200)
|
||
mwf.enabled = enabled
|
||
mwf.status_codes = set(status_codes or [401, 403, 429])
|
||
mwf.exempt_paths = tuple(exempt_paths or ['/API/Auth', '/health'])
|
||
mwf.max_body_len = max_body_len
|
||
#: 绕过 __init__ 时需补齐限频去重状态,否则 _log_attack 内 AttributeError 被吞
|
||
mwf._recent_attacks = {}
|
||
return mwf
|
||
|
||
def testDisabled_PassesThrough(self):
|
||
mwf = self._BuildMiddleware(enabled=False)
|
||
req = self.factory.get('/data', REMOTE_ADDR='1.2.3.4')
|
||
resp = mwf(req)
|
||
self.assertEqual(resp.status_code, 200)
|
||
|
||
def testNormalStatus_PassesThrough(self):
|
||
mwf = self._BuildMiddleware()
|
||
# 200 不在检测列表 → 不记录
|
||
req = self.factory.get('/data', REMOTE_ADDR='1.2.3.4')
|
||
resp = mwf(req)
|
||
self.assertEqual(resp.status_code, 200)
|
||
|
||
def testExemptPath_PassesThrough(self):
|
||
mwf = self._BuildMiddleware()
|
||
# 构造一个 401 响应触发检测,但路径在豁免列表
|
||
mwf.get_response = lambda req: _FakeResponse(401)
|
||
req = self.factory.get('/API/Auth/Login', REMOTE_ADDR='1.2.3.4')
|
||
resp = mwf(req)
|
||
# 豁免路径 → 不记录,直接返回原 401
|
||
self.assertEqual(resp.status_code, 401)
|
||
|
||
def testExemptPath_ExactMatch_PassesThrough(self):
|
||
mwf = self._BuildMiddleware()
|
||
mwf.get_response = lambda req: _FakeResponse(401)
|
||
req = self.factory.get('/health', REMOTE_ADDR='1.2.3.4')
|
||
resp = mwf(req)
|
||
self.assertEqual(resp.status_code, 401)
|
||
|
||
def testDetectedStatus_CallsLogAttack(self):
|
||
mwf = self._BuildMiddleware()
|
||
mwf.get_response = lambda req: _FakeResponse(401)
|
||
req = self.factory.get('/data', REMOTE_ADDR='1.2.3.4')
|
||
with mock.patch.object(mwf, '_log_attack') as mock_log:
|
||
mwf(req)
|
||
mock_log.assert_called_once()
|
||
|
||
def testNonExemptPath_403_CallsLogAttack(self):
|
||
mwf = self._BuildMiddleware()
|
||
mwf.get_response = lambda req: _FakeResponse(403)
|
||
req = self.factory.get('/secure/data', REMOTE_ADDR='1.2.3.4')
|
||
with mock.patch.object(mwf, '_log_attack') as mock_log:
|
||
mwf(req)
|
||
mock_log.assert_called_once()
|
||
|
||
def testCustomStatusCodes_500_NotTriggered(self):
|
||
mwf = self._BuildMiddleware(status_codes=[401, 403])
|
||
mwf.get_response = lambda req: _FakeResponse(500)
|
||
req = self.factory.get('/data', REMOTE_ADDR='1.2.3.4')
|
||
with mock.patch.object(mwf, '_log_attack') as mock_log:
|
||
mwf(req)
|
||
mock_log.assert_not_called()
|
||
|
||
def testCustomStatusCodes_429_Triggered(self):
|
||
mwf = self._BuildMiddleware(status_codes=[429])
|
||
mwf.get_response = lambda req: _FakeResponse(429)
|
||
req = self.factory.get('/data', REMOTE_ADDR='1.2.3.4')
|
||
with mock.patch.object(mwf, '_log_attack') as mock_log:
|
||
mwf(req)
|
||
mock_log.assert_called_once()
|
||
|
||
|
||
class LogAttackTestCase(SimpleTestCase):
|
||
"""_log_attack 写入 AbnormalUserLog(mock 数据库)。"""
|
||
|
||
def setUp(self):
|
||
self.factory = RequestFactory()
|
||
|
||
def _BuildMiddleware(self, max_body_len=1024):
|
||
mwf = mw.AttackDetectMiddleware.__new__(mw.AttackDetectMiddleware)
|
||
mwf.max_body_len = max_body_len
|
||
#: 绕过 __init__ 时需补齐限频去重状态,否则 _log_attack 内 AttributeError 被吞
|
||
mwf._recent_attacks = {}
|
||
return mwf
|
||
|
||
def testLogAttack_WritesToAbnormalUserLog(self):
|
||
mwf = self._BuildMiddleware()
|
||
req = self.factory.get(
|
||
'/secure/data',
|
||
REMOTE_ADDR='1.2.3.4',
|
||
HTTP_USER_AGENT='BadBot/1.0',
|
||
HTTP_X_REAL_IP='5.6.7.8',
|
||
)
|
||
req.user = None
|
||
resp = _FakeResponse(401)
|
||
|
||
with mock.patch('gvsds.apps.gerp.models.AbnormalUserLog') as MockLog:
|
||
MockManager = MockLog.objects.using.return_value
|
||
mwf._log_attack(req, resp)
|
||
MockManager.create.assert_called_once()
|
||
CallKwargs = MockManager.create.call_args.kwargs
|
||
self.assertEqual(CallKwargs['IPAddress'], '1.2.3.4')
|
||
self.assertEqual(CallKwargs['RealIP'], '5.6.7.8')
|
||
self.assertEqual(CallKwargs['UserAgent'], 'BadBot/1.0')
|
||
self.assertEqual(CallKwargs['RequestMethod'], 'GET')
|
||
self.assertEqual(CallKwargs['RequestPath'], '/secure/data')
|
||
self.assertEqual(CallKwargs['ResponseStatus'], 401)
|
||
self.assertEqual(CallKwargs['AttemptType'], 'auth_failure')
|
||
|
||
def testLogAttack_WithRequestUser_ExtractsUserUUID(self):
|
||
mwf = self._BuildMiddleware()
|
||
req = self.factory.get('/data', REMOTE_ADDR='1.2.3.4')
|
||
UserUUID = uuid.uuid4().bytes
|
||
FakeUser = type('FakeUser', (), {'UserUUID': UserUUID})()
|
||
req.user = FakeUser
|
||
resp = _FakeResponse(403)
|
||
|
||
with mock.patch('gvsds.apps.gerp.models.AbnormalUserLog') as MockLog:
|
||
MockManager = MockLog.objects.using.return_value
|
||
mwf._log_attack(req, resp)
|
||
CallKwargs = MockManager.create.call_args.kwargs
|
||
self.assertEqual(CallKwargs['UserUUID'], UserUUID)
|
||
self.assertEqual(CallKwargs['AttemptType'], 'permission_denied')
|
||
|
||
def testLogAttack_PostWithBody_CapturesBodySummary(self):
|
||
mwf = self._BuildMiddleware(max_body_len=64)
|
||
req = self.factory.post(
|
||
'/data',
|
||
data={'secret': 'leaked'},
|
||
REMOTE_ADDR='1.2.3.4',
|
||
)
|
||
req.user = None
|
||
resp = _FakeResponse(429)
|
||
|
||
with mock.patch('gvsds.apps.gerp.models.AbnormalUserLog') as MockLog:
|
||
MockManager = MockLog.objects.using.return_value
|
||
mwf._log_attack(req, resp)
|
||
CallKwargs = MockManager.create.call_args.kwargs
|
||
self.assertEqual(CallKwargs['AttemptType'], 'rate_limited')
|
||
# 请求体摘要应非空
|
||
self.assertTrue(CallKwargs['RequestBody'])
|
||
|
||
def testLogAttack_Exception_SwallowsAndLogs(self):
|
||
"""_log_attack 内部异常不应影响响应返回。"""
|
||
mwf = self._BuildMiddleware()
|
||
req = self.factory.get('/data', REMOTE_ADDR='1.2.3.4')
|
||
req.user = None
|
||
resp = _FakeResponse(401)
|
||
|
||
with mock.patch('gvsds.apps.gerp.models.AbnormalUserLog') as MockLog:
|
||
MockLog.objects.using.side_effect = RuntimeError('db down')
|
||
# 不应抛异常
|
||
mwf._log_attack(req, resp)
|
||
|
||
def testLogAttack_SafeHeaders_OnlyWhitelistedKeys(self):
|
||
"""请求头摘要仅保留安全相关头,不记录 Authorization 等。"""
|
||
mwf = self._BuildMiddleware()
|
||
req = self.factory.get(
|
||
'/data',
|
||
REMOTE_ADDR='1.2.3.4',
|
||
HTTP_USER_AGENT='Bot',
|
||
HTTP_HOST='example.com',
|
||
HTTP_REFERER='http://evil.com',
|
||
HTTP_AUTHORIZATION='Bearer secret-token',
|
||
)
|
||
req.user = None
|
||
resp = _FakeResponse(401)
|
||
|
||
with mock.patch('gvsds.apps.gerp.models.AbnormalUserLog') as MockLog:
|
||
MockManager = MockLog.objects.using.return_value
|
||
mwf._log_attack(req, resp)
|
||
CallKwargs = MockManager.create.call_args.kwargs
|
||
Headers = CallKwargs['RequestHeaders']
|
||
# 安全相关头应被保留
|
||
self.assertIn('HTTP_USER_AGENT', Headers)
|
||
self.assertIn('HTTP_HOST', Headers)
|
||
# Authorization 不应出现在摘要中
|
||
self.assertNotIn('HTTP_AUTHORIZATION', Headers)
|
||
|
||
|
||
class _FakeResponse:
|
||
"""简易响应对象。"""
|
||
def __init__(self, status_code):
|
||
self.status_code = status_code
|