diff --git a/__pycache__/vault_client.cpython-313.pyc b/__pycache__/vault_client.cpython-313.pyc new file mode 100644 index 0000000..6ce4709 Binary files /dev/null and b/__pycache__/vault_client.cpython-313.pyc differ diff --git a/bot.py b/bot.py index cefcb91..f75a556 100644 --- a/bot.py +++ b/bot.py @@ -9,29 +9,45 @@ from librouteros.exceptions import TrapError from fastapi import FastAPI, Request import uvicorn from db import init_db, upsert_client, start_session, update_session, close_session, get_history, get_clients, get_stats +from vault_client import VaultClient load_dotenv() -TG_BOT_TOKEN = os.getenv("TG_BOT_TOKEN") -MT_API_HOST = os.getenv("MT_API_HOST") -MT_API_USER = os.getenv("MT_API_USER") -MT_API_PASS = os.getenv("MT_API_PASS") -ALLOWED_USER_IDS = set(map(int, os.getenv("ALLOWED_USER_IDS", "").split(","))) +# Получаем конфигурацию из Vault или environment variables +vault = VaultClient() +config = vault.get_config() -bot = Bot(token=TG_BOT_TOKEN) +# Используем правильные имена переменных +BOT_TOKEN = config.get('BOT_TOKEN') or os.getenv("BOT_TOKEN") +ROUTER_HOST = config.get('ROUTER_HOST') or os.getenv("ROUTER_HOST") +ROUTER_USER = config.get('ROUTER_USER') or os.getenv("ROUTER_USER") +ROUTER_PASSWORD = config.get('ROUTER_PASSWORD') or os.getenv("ROUTER_PASSWORD") + +# Безопасная обработка ALLOWED_USER_IDS +allowed_ids_str = os.getenv("ALLOWED_USER_IDS", "") +if allowed_ids_str.strip(): + ALLOWED_USER_IDS = set(map(int, allowed_ids_str.split(","))) +else: + # Если не установлено, используем пустое множество или значение по умолчанию + ALLOWED_USER_IDS = set() + print("⚠️ ALLOWED_USER_IDS не установлено - доступ для всех пользователей!") + +bot = Bot(token=BOT_TOKEN) dp = Dispatcher() # Авторизация по user_id async def check_user(message: types.Message) -> 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