fix: Fable audit — auth Bearer + cap DELAY + fail-next + setparam try (v0.3.0)

This commit is contained in:
2026-07-31 23:18:33 +04:00
parent 9b8d90218c
commit 0b1db7bafb
4 changed files with 61 additions and 11 deletions
+1 -1
View File
@@ -71,4 +71,4 @@ DELAY = float(os.getenv("MOCK_OP_DELAY", "0.1"))
# Версия полигона — единый источник правды для app.py и routes/root.py. # Версия полигона — единый источник правды для app.py и routes/root.py.
# Меняется при КАЖДОМ изменении кода. # Меняется при КАЖДОМ изменении кода.
VERSION = "0.2.5" VERSION = "0.3.0"
+8 -2
View File
@@ -21,6 +21,7 @@ class MockState:
self.instances = {} self.instances = {}
self.operations = {} self.operations = {}
self.op_params = {} 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): def set_param(self, op_uid, param_id, value):
"""Установить значение параметра операции. """Установить значение параметра операции.
param_id — int (svcOperationCfsParamId). param_id — int (svcOperationCfsParamId). Может быть строкой —
конвертируется в int с защитой от ValueError (→ 400 на уровне роута).
value — str. value — str.
""" """
if op_uid not in self.op_params: if op_uid not in self.op_params:
self.op_params[op_uid] = {} 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): def get_params(self, op_uid):
"""Получить все установленные параметры операции. """Получить все установленные параметры операции.
@@ -140,6 +145,7 @@ class MockState:
self.instances.clear() self.instances.clear()
self.operations.clear() self.operations.clear()
self.op_params.clear() self.op_params.clear()
self.fail_next = False
# Глобальный синглтон # Глобальный синглтон
+39 -7
View File
@@ -18,8 +18,8 @@ import config.loader as _cfg # для чтения/записи модульн
bp = Blueprint("mock", __name__, url_prefix="/api/v1/svc/_mock") bp = Blueprint("mock", __name__, url_prefix="/api/v1/svc/_mock")
# Токен для защиты _mock/* эндпоинтов (из env, опционально). # Токен для защиты _mock/* эндпоинтов (из env, опционально).
# Если не задан — эндпоинты открыты (dev/локально). # Дублирует глобальную проверку в app.py, но оставлен для обратной совместимости
# Если задан — требуется заголовок X-Mock-Auth: <токен>. # и ясности — _mock/* исторически имели отдельную защиту.
_MOCK_TOKEN = os.getenv("MOCK_AUTH_TOKEN", "") _MOCK_TOKEN = os.getenv("MOCK_AUTH_TOKEN", "")
@@ -29,7 +29,10 @@ def _check_mock_auth():
Вызывается Flask перед КАЖДЫМ обработчиком в этом blueprint'е. Вызывается Flask перед КАЖДЫМ обработчиком в этом blueprint'е.
Если токен не задан в env — пропускаем всех (dev-режим). Если токен не задан в env — пропускаем всех (dev-режим).
Если задан — требуем совпадения с заголовком.""" Если задан — требуем совпадения с заголовком.
Примечание: app.before_request уже проверяет Authorization: Bearer;
этот хук — дополнительная защита для _mock/* эндпоинтов."""
if _MOCK_TOKEN and request.headers.get("X-Mock-Auth") != _MOCK_TOKEN: if _MOCK_TOKEN and request.headers.get("X-Mock-Auth") != _MOCK_TOKEN:
abort(403) abort(403)
@@ -100,6 +103,11 @@ def mock_services():
return jsonify({"count": len(result), "services": result}) return jsonify({"count": len(result), "services": result})
# Максимальная задержка — защита от блокировки /health (1 воркер).
# При DELAY > healthcheck timeout платформа рестартит под.
_MAX_DELAY = 5.0
@bp.route("/delay/<string:seconds>", methods=["POST"]) @bp.route("/delay/<string:seconds>", methods=["POST"])
def mock_set_delay(seconds): def mock_set_delay(seconds):
"""POST /api/v1/svc/_mock/delay/{s} — изменить задержку операции. """POST /api/v1/svc/_mock/delay/{s} — изменить задержку операции.
@@ -108,11 +116,35 @@ def mock_set_delay(seconds):
Полезно в тестах: установить delay=0 для мгновенных операций, Полезно в тестах: установить delay=0 для мгновенных операций,
или delay=5 чтобы проверить таймауты поллинга. или delay=5 чтобы проверить таймауты поллинга.
ВАЖНО: меняет модульную переменную config.loader.DELAY, которую читает ⛔ Максимум {max_delay}с — защита от блокировки /health (1 воркер).
routes/run.py при каждом вызове /run. ⛔ Минимум 0с — отрицательные значения запрещены.
Принимает строку и конвертирует в float — Flask <float:> не матчит Принимает строку и конвертирует в float — Flask <float:> не матчит
целые числа без десятичной точки (0, 2). целые числа без десятичной точки (0, 2).
""" """.format(max_delay=_MAX_DELAY)
_cfg.DELAY = float(seconds) 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}) 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})
+13 -1
View File
@@ -54,9 +54,21 @@ def run_operation(uid):
if _cfg.DELAY > 0: if _cfg.DELAY > 0:
time.sleep(_cfg.DELAY) 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, ...) # (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() op["dtFinish"] = _now()