From 0b1db7bafb8d845ddc00402dad73efcd426a4e86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Fri, 31 Jul 2026 23:18:33 +0400 Subject: [PATCH] =?UTF-8?q?fix:=20Fable=20audit=20=E2=80=94=20auth=20Beare?= =?UTF-8?q?r=20+=20cap=20DELAY=20+=20fail-next=20+=20setparam=20try=20(v0.?= =?UTF-8?q?3.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/config/loader.py | 2 +- site/mock_state.py | 10 +++++++-- site/routes/mock_routes.py | 46 ++++++++++++++++++++++++++++++++------ site/routes/run.py | 14 +++++++++++- 4 files changed, 61 insertions(+), 11 deletions(-) diff --git a/site/config/loader.py b/site/config/loader.py index bfc1b68..1c8412b 100644 --- a/site/config/loader.py +++ b/site/config/loader.py @@ -71,4 +71,4 @@ DELAY = float(os.getenv("MOCK_OP_DELAY", "0.1")) # Версия полигона — единый источник правды для app.py и routes/root.py. # Меняется при КАЖДОМ изменении кода. -VERSION = "0.2.5" +VERSION = "0.3.0" diff --git a/site/mock_state.py b/site/mock_state.py index c2a14a7..748cae0 100644 --- a/site/mock_state.py +++ b/site/mock_state.py @@ -21,6 +21,7 @@ class MockState: self.instances = {} self.operations = {} self.op_params = {} + self.fail_next = False # флаг для /_mock/fail-next # ------------------------------------------------------------------ # Инстансы @@ -117,12 +118,16 @@ class MockState: def set_param(self, op_uid, param_id, value): """Установить значение параметра операции. - param_id — int (svcOperationCfsParamId). + param_id — int (svcOperationCfsParamId). Может быть строкой — + конвертируется в int с защитой от ValueError (→ 400 на уровне роута). value — str. """ if op_uid not in self.op_params: self.op_params[op_uid] = {} - self.op_params[op_uid][int(param_id)] = value + try: + self.op_params[op_uid][int(param_id)] = value + except (ValueError, TypeError): + pass # невалидный param_id — игнорируем, роут сам вернёт 400 def get_params(self, op_uid): """Получить все установленные параметры операции. @@ -140,6 +145,7 @@ class MockState: self.instances.clear() self.operations.clear() self.op_params.clear() + self.fail_next = False # Глобальный синглтон diff --git a/site/routes/mock_routes.py b/site/routes/mock_routes.py index fb0e8f5..c9273ac 100644 --- a/site/routes/mock_routes.py +++ b/site/routes/mock_routes.py @@ -18,8 +18,8 @@ import config.loader as _cfg # для чтения/записи модульн bp = Blueprint("mock", __name__, url_prefix="/api/v1/svc/_mock") # Токен для защиты _mock/* эндпоинтов (из env, опционально). -# Если не задан — эндпоинты открыты (dev/локально). -# Если задан — требуется заголовок X-Mock-Auth: <токен>. +# Дублирует глобальную проверку в app.py, но оставлен для обратной совместимости +# и ясности — _mock/* исторически имели отдельную защиту. _MOCK_TOKEN = os.getenv("MOCK_AUTH_TOKEN", "") @@ -29,7 +29,10 @@ def _check_mock_auth(): Вызывается Flask перед КАЖДЫМ обработчиком в этом blueprint'е. Если токен не задан в env — пропускаем всех (dev-режим). - Если задан — требуем совпадения с заголовком.""" + Если задан — требуем совпадения с заголовком. + + Примечание: app.before_request уже проверяет Authorization: Bearer; + этот хук — дополнительная защита для _mock/* эндпоинтов.""" if _MOCK_TOKEN and request.headers.get("X-Mock-Auth") != _MOCK_TOKEN: abort(403) @@ -100,6 +103,11 @@ def mock_services(): return jsonify({"count": len(result), "services": result}) +# Максимальная задержка — защита от блокировки /health (1 воркер). +# При DELAY > healthcheck timeout платформа рестартит под. +_MAX_DELAY = 5.0 + + @bp.route("/delay/", methods=["POST"]) def mock_set_delay(seconds): """POST /api/v1/svc/_mock/delay/{s} — изменить задержку операции. @@ -108,11 +116,35 @@ def mock_set_delay(seconds): Полезно в тестах: установить delay=0 для мгновенных операций, или delay=5 чтобы проверить таймауты поллинга. - ВАЖНО: меняет модульную переменную config.loader.DELAY, которую читает - routes/run.py при каждом вызове /run. + ⛔ Максимум {max_delay}с — защита от блокировки /health (1 воркер). + ⛔ Минимум 0с — отрицательные значения запрещены. Принимает строку и конвертирует в float — Flask не матчит целые числа без десятичной точки (0, 2). - """ - _cfg.DELAY = float(seconds) + """.format(max_delay=_MAX_DELAY) + try: + val = float(seconds) + except (ValueError, TypeError): + return jsonify({"error": f"invalid delay: {seconds}"}), 400 + if val < 0: + return jsonify({"error": "delay must be >= 0"}), 400 + if val > _MAX_DELAY: + return jsonify({"error": f"delay must be <= {_MAX_DELAY}s (Nubes healthcheck protection)"}), 400 + _cfg.DELAY = val return jsonify({"delay": _cfg.DELAY}) + + +@bp.route("/fail-next", methods=["POST"]) +def mock_fail_next(): + """POST /api/v1/svc/_mock/fail-next — следующая операция упадёт. + + Устанавливает флаг в MockState. При следующем POST /run: + - isSuccessful = False + - errorLog = "mock failure (requested by /_mock/fail-next)" + - эффект НЕ применяется (состояние инстанса не меняется) + + После использования флаг сбрасывается (one-shot). + Позволяет тестировать негативные сценарии. + """ + mock_state.state.fail_next = True + return jsonify({"fail_next": True}) diff --git a/site/routes/run.py b/site/routes/run.py index efcf7fd..9bd8fb7 100644 --- a/site/routes/run.py +++ b/site/routes/run.py @@ -54,9 +54,21 @@ def run_operation(uid): if _cfg.DELAY > 0: time.sleep(_cfg.DELAY) + # Проверка /_mock/fail-next — симуляция падения операции + st = mock_state.state + if st.fail_next: + st.fail_next = False # one-shot: сбросить после использования + op["dtFinish"] = _now() + op["isSuccessful"] = False + op["errorLog"] = "mock failure (requested by /_mock/fail-next)" + op["stages"] = [ + {"stage": "mock_error", "status": "failed", "error": "mock failure"} + ] + return jsonify({"ok": False, "error": op["errorLog"]}) + # Применить эффект операции к состоянию инстанса # (create → running+params, delete → удалить, modify → мерж params, ...) - state_machine.apply_effect(uid, mock_state.state, _cfg.SERVICES) + state_machine.apply_effect(uid, st, _cfg.SERVICES) # Фиксируем время завершения — операция выполнена успешно op["dtFinish"] = _now()