From 25d910cef75e89ada8de4941341dc48970a32ef9 Mon Sep 17 00:00:00 2001 From: stakost Date: Sun, 1 Jun 2025 14:03:16 +0300 Subject: [PATCH] Fix Vault integration and ALLOWED_USER_IDS handling --- __pycache__/vault_client.cpython-313.pyc | Bin 0 -> 4682 bytes bot.py | 57 ++++++++++----- docker-compose.portainer.yml | 72 +++++++++---------- docker-compose.production.yml | 46 +++++++++++++ test_vault.py | 40 +++++++++++ vault_client.py | 84 +++++++++++++++++++++++ 6 files changed, 247 insertions(+), 52 deletions(-) create mode 100644 __pycache__/vault_client.cpython-313.pyc create mode 100644 docker-compose.production.yml create mode 100644 test_vault.py create mode 100644 vault_client.py diff --git a/__pycache__/vault_client.cpython-313.pyc b/__pycache__/vault_client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6ce4709debed1e7a9ecd1c43a58769c0bbe4cebb GIT binary patch literal 4682 zcmb_fZ)_9E6(8HX{ujrABw#xMvI&8nBZ!krLPH2#oIhL$cgfY8P-qaV#dZ=O#`evw zX)nhipzYPt(W7@N0$=|)d@12eoBlzdqerFm3m?}?B+?~P5k-~mQ*#%ptJ*JpZ`XDl zNKPl(kvu!|-prdfK+4TlNUzXod1 z`bKJmuko;HfT7F)OIeP%Ngd%$RfK2SjJgGC9yfT|&#*J-HL6T+Bq*!ap@bZX$HZt* zAEei8uCsg+8du11g6qH<25RDsltH_c<(cE=RwK_=kvAD?fu8la?YRB8z14`*@dsM7 zrY!+&kq+b*P43{Wv~=9&wW}pZ#AHmkCF8d?p7L2*2+~)j=L?9jwbIMHi``L@S z8Rbs)2Vn9Oki7@(4}u0*CtvWaP4HXZ1JWyGG%qKC+!+=G&TqStJKiog* zmcl_AmVF^n7Ja89r)XS`oZ5XdE_Sad`sz!m71> z`Z7T+a2peEt5!jX#3HgFsFg(_-I$jm&Lu?Ed9qlwCDV{@$TX%Kr?)8I9Gt5fN->wp z7G0jJlOIlAJ@?_csra0056B#gp6X0hx+=3Ry-j&(@0_OtgwDn4t(lH=N9I8KfYPvE zdF9w#^>aC24F`m}3}dVg6Gs%tuH7hdxtY%Js#K1peZ+ z0FWOM0?HaKgcfhe2OLMnjoWZ;Ay`(HZ~{5C7-NRD*Is))o@q43jHA}Pm4HT?`JVo* zBS*`5cCRsJ8ZiY*SL?9XmJ@LV^kwVoW6TJ{n{iXT1#1ZM!n#F!wHEEwR!c_9^A;^c zbQ2O|i^keVSSM*QuFR+*d`XP;v*SknJ7yRo`%N`u%;0q-ePBuX$-2A+Jop^XC3|7c z<6m6jR$+?+wgVE)2@440luwmg9N51A%s2y`d&=)%98&H32PZfC&NTbPq&(ph6A45Q z;CDO{^BO5c1EIK0M9Z5Jz>UFiszZl>pdl%V$Nc!`Kg?0`SMV?F16)x41e~1$CuWBA zsjvyRO^icHx4X1HY(*u z=N-ouD%?xVMw@4$YFoya_9>nQrJ+l4`BVLms%tWx>CT0%wV4y?6Q8ptk27`XshzmF ztM7jF?$pt&>p-f1!BP3Zu_No)G4J@sqpG^h?)2`T`BH}$JoQsYvYuVjC$gT-RPUt$ zZM{SDj)8>=*T0^WxtEF2=0RcQq5HM>E0prr=N*HK74H8mv7q?31W`_A{H@(P$m|Y# zkHs|WbM_cbe>SqvUYm5ze+|+J!O;N;6z)%0Bv}PE%X)9v zX7)qg$7|mrg0UtW!K{)2*xv;N0+#^#Ak%`33Ywo_0tLJPoPhLkTZh=-Wa-h}TKw)F z#`|)TL6!g>uuO!-5L}HagV;#!2g?Y4<@$;#%+-p4evbpp7p)EYm4%Xf(86;ZuxbM2 zZonPSINQMi2Ox~87b^rmt-y-I3hAEKVWq*n23y;q@>(>Y(_PS3?YX=F@J@H4AfG5` zu(#-1iDLLknD*#U=X9)>K;|LjhN!M$s23?3aOw)uCofM<)jz0f$=05CduJ*iH2bs7ex<2f@${qyp15kRp8e?T z)IiqNwLa1}*G}!sZuU+?)tq|u3G&Lf<{g2BimKFL9_elk=>zwh?{8Pi2j?9_ixn;n z>24kAH~r1s)nvBX-o4*6yTjStZklapp}h*}h$t<+n9U*xzvGS4AP_n@-nKeduUh~ z9y;7NsFnnVMuz(WLjMpyTM;LbGg0Kl!h_Xy0Cn3NjLAXgY7d67B z2xzTBDwlwM$a~~3%;_%}maSTHx>(P(&{G1RrI#yN_qC%_q3M bool: + # Если список пуст, разрешаем всем (для тестирования) + if not ALLOWED_USER_IDS: + return True if message.from_user.id not in ALLOWED_USER_IDS: await message.answer("⛔️ Нет доступа") return False return True # Подключение к MikroTik API - def get_mt_api(): - return connect(username=MT_API_USER, password=MT_API_PASS, host=MT_API_HOST) + return connect(username=ROUTER_USER, password=ROUTER_PASSWORD, host=ROUTER_HOST) @dp.message(Command("start")) async def start_cmd(msg: types.Message): @@ -111,8 +127,10 @@ async def event(request: Request): # Сохраняем клиента и сессию (fetch) await upsert_client(mac, name=None) await start_session(mac, ip, source='fetch') - for uid in ALLOWED_USER_IDS: - await bot.send_message(uid, text) + # Отправляем уведомления только если есть разрешённые пользователи + if ALLOWED_USER_IDS: + for uid in ALLOWED_USER_IDS: + await bot.send_message(uid, text) return {"status": "ok"} # --- Периодический polling MikroTik API для сбора статистики --- @@ -167,8 +185,7 @@ async def history_cmd(msg: types.Message): @dp.callback_query(lambda c: c.data.startswith("history_")) async def history_period(callback: types.CallbackQuery): - if callback.from_user.id not in ALLOWED_USER_IDS: - await callback.answer("⛔️ Нет доступа", show_alert=True) + if not await check_user_callback(callback): return parts = callback.data.split("_") days = int(parts[1]) @@ -219,8 +236,7 @@ async def clients_cmd(msg: types.Message): @dp.callback_query(lambda c: c.data.startswith("clients_")) async def clients_page_cb(callback: types.CallbackQuery): - if callback.from_user.id not in ALLOWED_USER_IDS: - await callback.answer("⛔️ Нет доступа", show_alert=True) + if not await check_user_callback(callback): return parts = callback.data.split("_") page = int(parts[1]) if len(parts) > 1 else 1 @@ -272,8 +288,7 @@ async def stats_cmd(msg: types.Message): @dp.callback_query(lambda c: c.data.startswith("stats_")) async def stats_period(callback: types.CallbackQuery): - if callback.from_user.id not in ALLOWED_USER_IDS: - await callback.answer("⛔️ Нет доступа", show_alert=True) + if not await check_user_callback(callback): return parts = callback.data.split("_") days = int(parts[1]) @@ -313,5 +328,15 @@ async def send_stats_page(message, period_days, page, edit=False, callback=None) else: await message.answer(text, parse_mode="HTML", reply_markup=kb) +# Добавляем вспомогательную функцию для проверки callback'ов +async def check_user_callback(callback: types.CallbackQuery) -> bool: + # Если список пуст, разрешаем всем + if not ALLOWED_USER_IDS: + return True + if callback.from_user.id not in ALLOWED_USER_IDS: + await callback.answer("⛔️ Нет доступа", show_alert=True) + return False + return True + if __name__ == "__main__": asyncio.run(main()) \ No newline at end of file diff --git a/docker-compose.portainer.yml b/docker-compose.portainer.yml index f4ce3bb..2025f57 100644 --- a/docker-compose.portainer.yml +++ b/docker-compose.portainer.yml @@ -1,44 +1,44 @@ version: '3.8' -services: - telegram-bot: - build: . - container_name: mikrotik-telegram-bot - env_file: - - .env - restart: unless-stopped - volumes: - - ./data:/app/data - - /var/run/docker.sock:/var/run/docker.sock:ro # для watchtower - labels: - - "com.centurylinklabs.watchtower.enable=true" - - "com.centurylinklabs.watchtower.scope=mikrotik-bot" - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 15s - networks: - - mikrotik-net - # Автообновление контейнеров при изменении кода - watchtower: - image: containrrr/watchtower:latest - container_name: watchtower-mikrotik +services: + mikrotik-bot: + image: 10.10.30.121:5000/mikrotik-bot:latest + container_name: mikrotik-bot-production restart: unless-stopped environment: - - WATCHTOWER_CLEANUP=true - - WATCHTOWER_POLL_INTERVAL=300 # проверка каждые 5 мин - - WATCHTOWER_SCOPE=mikrotik-bot - - WATCHTOWER_INCLUDE_STOPPED=true - - WATCHTOWER_REVIVE_STOPPED=true + # Vault AppRole credentials (безопасно) + - VAULT_ADDR=http://10.10.30.121:8200 + - VAULT_ROLE_ID=ba8d3d21-263e-4d92-8ffe-ef803017cef5 + - VAULT_SECRET_ID=6b3ecc3c-9436-7f04-022f-8b1ce0ac09ee + - VAULT_SECRET_PATH=secret/data/mikrotik-bot + - DATABASE_PATH=/app/data/bot.db + volumes: + - mikrotik_bot_data:/app/data + ports: + - "8001:8000" # Health check endpoint + networks: + - bot-network + labels: + - "com.centurylinklabs.watchtower.enable=true" + + # Автоматические обновления + watchtower: + image: containrrr/watchtower + container_name: watchtower-mikrotik + restart: unless-stopped volumes: - /var/run/docker.sock:/var/run/docker.sock - labels: - - "com.centurylinklabs.watchtower.scope=mikrotik-bot" + environment: + - WATCHTOWER_POLL_INTERVAL=60 # Проверять каждые 60 секунд + - WATCHTOWER_LABEL_ENABLE=true + - WATCHTOWER_CLEANUP=true + command: --interval 60 --label-enable --cleanup networks: - - mikrotik-net - + - bot-network + networks: - mikrotik-net: - driver: bridge \ No newline at end of file + bot-network: + driver: bridge + +volumes: + mikrotik_bot_data: \ No newline at end of file diff --git a/docker-compose.production.yml b/docker-compose.production.yml new file mode 100644 index 0000000..3a94535 --- /dev/null +++ b/docker-compose.production.yml @@ -0,0 +1,46 @@ +version: '3.8' + +services: + mikrotik-bot: + image: 10.10.30.121:5000/mikrotik-bot:latest + container_name: mikrotik-bot-production + restart: unless-stopped + environment: + # Vault AppRole credentials (безопасно) + - VAULT_ADDR=http://10.10.30.121:8200 + - VAULT_ROLE_ID=ba8d3d21-263e-4d92-8ffe-ef803017cef5 + - VAULT_SECRET_ID=6b3ecc3c-9436-7f04-022f-8b1ce0ac09ee + - VAULT_SECRET_PATH=secret/data/mikrotik-bot + - DATABASE_PATH=/app/data/bot.db + volumes: + - mikrotik_bot_data:/app/data + ports: + - "8000:8000" # Health check endpoint + networks: + - bot-network + depends_on: + - watchtower + labels: + - "com.centurylinklabs.watchtower.enable=true" + + # Автоматические обновления + watchtower: + image: containrrr/watchtower + container_name: watchtower-mikrotik + restart: unless-stopped + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + - WATCHTOWER_POLL_INTERVAL=60 # Проверять каждые 60 секунд + - WATCHTOWER_LABEL_ENABLE=true + - WATCHTOWER_CLEANUP=true + command: --interval 60 --label-enable --cleanup + networks: + - bot-network + +networks: + bot-network: + driver: bridge + +volumes: + mikrotik_bot_data: \ No newline at end of file diff --git a/test_vault.py b/test_vault.py new file mode 100644 index 0000000..137a65e --- /dev/null +++ b/test_vault.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +from vault_client import VaultClient +import os + +# Устанавливаем переменные окружения для тестирования +os.environ['VAULT_ADDR'] = 'http://10.10.30.121:8200' +os.environ['VAULT_ROLE_ID'] = 'ba8d3d21-263e-4d92-8ffe-ef803017cef5' +os.environ['VAULT_SECRET_ID'] = '6b3ecc3c-9436-7f04-022f-8b1ce0ac09ee' +os.environ['VAULT_SECRET_PATH'] = 'secret/data/mikrotik-bot' + +def test_vault_secrets(): + """Тестирование получения секретов из Vault""" + print('🔍 Проверка получения секретов из Vault:') + print('=' * 50) + + vault = VaultClient() + config = vault.get_config() + + if not config: + print('❌ Не удалось получить конфигурацию') + return False + + success = True + for key, value in config.items(): + if value: + masked_value = value[:10] + '...' if len(value) > 10 else value + print(f'✅ {key}: {masked_value}') + else: + print(f'❌ {key}: не найден') + success = False + + return success + +if __name__ == '__main__': + if test_vault_secrets(): + print('\n🎉 Все секреты успешно получены!') + print('✅ Готов к production deployment') + else: + print('\n❌ Есть проблемы с получением секретов') \ No newline at end of file diff --git a/vault_client.py b/vault_client.py new file mode 100644 index 0000000..0c2da88 --- /dev/null +++ b/vault_client.py @@ -0,0 +1,84 @@ +import os +import requests +import json +from typing import Dict, Optional + + +class VaultClient: + """Клиент для работы с HashiCorp Vault через AppRole аутентификацию""" + + def __init__(self): + self.vault_addr = os.environ.get('VAULT_ADDR', 'http://localhost:8200') + self.role_id = os.environ.get('VAULT_ROLE_ID') + self.secret_id = os.environ.get('VAULT_SECRET_ID') + self.secret_path = os.environ.get('VAULT_SECRET_PATH', 'secret/data/mikrotik-bot') + self.token = None + + def authenticate(self) -> bool: + """Аутентификация через AppRole""" + if not self.role_id or not self.secret_id: + print("❌ VAULT_ROLE_ID или VAULT_SECRET_ID не установлены") + return False + + try: + auth_url = f"{self.vault_addr}/v1/auth/approle/login" + auth_data = { + "role_id": self.role_id, + "secret_id": self.secret_id + } + + response = requests.post(auth_url, json=auth_data) + response.raise_for_status() + + auth_result = response.json() + self.token = auth_result['auth']['client_token'] + print("✅ Vault аутентификация успешна") + return True + + except Exception as e: + print(f"❌ Ошибка аутентификации Vault: {e}") + return False + + def get_secrets(self) -> Optional[Dict[str, str]]: + """Получение секретов из Vault""" + if not self.token and not self.authenticate(): + return None + + try: + headers = {"X-Vault-Token": self.token} + secret_url = f"{self.vault_addr}/v1/{self.secret_path}" + + response = requests.get(secret_url, headers=headers) + response.raise_for_status() + + secret_data = response.json() + secrets = secret_data['data']['data'] + + print("✅ Секреты успешно получены из Vault") + return secrets + + except Exception as e: + print(f"❌ Ошибка получения секретов: {e}") + return None + + def get_config(self) -> Dict[str, str]: + """Получение конфигурации с fallback на environment variables""" + # Сначала пытаемся получить из Vault + secrets = self.get_secrets() + + if secrets: + return { + 'BOT_TOKEN': secrets.get('bot_token'), + 'ROUTER_HOST': secrets.get('router_host'), + 'ROUTER_USER': secrets.get('router_user'), + 'ROUTER_PASSWORD': secrets.get('router_password'), + } + + # Fallback на environment variables + print("⚠️ Используются environment variables вместо Vault") + return { + 'BOT_TOKEN': os.environ.get('BOT_TOKEN'), + 'ROUTER_HOST': os.environ.get('ROUTER_HOST'), + 'ROUTER_USER': os.environ.get('ROUTER_USER'), + 'ROUTER_PASSWORD': os.environ.get('ROUTER_PASSWORD'), + } \ No newline at end of file