Fix Vault integration and ALLOWED_USER_IDS handling
All checks were successful
Build and Deploy MikroTik Bot / build-and-deploy (push) Successful in 26s

This commit is contained in:
stakost 2025-06-01 14:03:16 +03:00
parent c2125e6736
commit 25d910cef7
6 changed files with 247 additions and 52 deletions

Binary file not shown.

53
bot.py
View File

@ -9,29 +9,45 @@ from librouteros.exceptions import TrapError
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
import uvicorn import uvicorn
from db import init_db, upsert_client, start_session, update_session, close_session, get_history, get_clients, get_stats 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() load_dotenv()
TG_BOT_TOKEN = os.getenv("TG_BOT_TOKEN") # Получаем конфигурацию из Vault или environment variables
MT_API_HOST = os.getenv("MT_API_HOST") vault = VaultClient()
MT_API_USER = os.getenv("MT_API_USER") config = vault.get_config()
MT_API_PASS = os.getenv("MT_API_PASS")
ALLOWED_USER_IDS = set(map(int, os.getenv("ALLOWED_USER_IDS", "").split(",")))
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() dp = Dispatcher()
# Авторизация по user_id # Авторизация по user_id
async def check_user(message: types.Message) -> bool: 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: if message.from_user.id not in ALLOWED_USER_IDS:
await message.answer("⛔️ Нет доступа") await message.answer("⛔️ Нет доступа")
return False return False
return True return True
# Подключение к MikroTik API # Подключение к MikroTik API
def get_mt_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")) @dp.message(Command("start"))
async def start_cmd(msg: types.Message): async def start_cmd(msg: types.Message):
@ -111,6 +127,8 @@ async def event(request: Request):
# Сохраняем клиента и сессию (fetch) # Сохраняем клиента и сессию (fetch)
await upsert_client(mac, name=None) await upsert_client(mac, name=None)
await start_session(mac, ip, source='fetch') await start_session(mac, ip, source='fetch')
# Отправляем уведомления только если есть разрешённые пользователи
if ALLOWED_USER_IDS:
for uid in ALLOWED_USER_IDS: for uid in ALLOWED_USER_IDS:
await bot.send_message(uid, text) await bot.send_message(uid, text)
return {"status": "ok"} return {"status": "ok"}
@ -167,8 +185,7 @@ async def history_cmd(msg: types.Message):
@dp.callback_query(lambda c: c.data.startswith("history_")) @dp.callback_query(lambda c: c.data.startswith("history_"))
async def history_period(callback: types.CallbackQuery): async def history_period(callback: types.CallbackQuery):
if callback.from_user.id not in ALLOWED_USER_IDS: if not await check_user_callback(callback):
await callback.answer("⛔️ Нет доступа", show_alert=True)
return return
parts = callback.data.split("_") parts = callback.data.split("_")
days = int(parts[1]) days = int(parts[1])
@ -219,8 +236,7 @@ async def clients_cmd(msg: types.Message):
@dp.callback_query(lambda c: c.data.startswith("clients_")) @dp.callback_query(lambda c: c.data.startswith("clients_"))
async def clients_page_cb(callback: types.CallbackQuery): async def clients_page_cb(callback: types.CallbackQuery):
if callback.from_user.id not in ALLOWED_USER_IDS: if not await check_user_callback(callback):
await callback.answer("⛔️ Нет доступа", show_alert=True)
return return
parts = callback.data.split("_") parts = callback.data.split("_")
page = int(parts[1]) if len(parts) > 1 else 1 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_")) @dp.callback_query(lambda c: c.data.startswith("stats_"))
async def stats_period(callback: types.CallbackQuery): async def stats_period(callback: types.CallbackQuery):
if callback.from_user.id not in ALLOWED_USER_IDS: if not await check_user_callback(callback):
await callback.answer("⛔️ Нет доступа", show_alert=True)
return return
parts = callback.data.split("_") parts = callback.data.split("_")
days = int(parts[1]) days = int(parts[1])
@ -313,5 +328,15 @@ async def send_stats_page(message, period_days, page, edit=False, callback=None)
else: else:
await message.answer(text, parse_mode="HTML", reply_markup=kb) 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__": if __name__ == "__main__":
asyncio.run(main()) asyncio.run(main())

View File

@ -1,44 +1,44 @@
version: '3.8' 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
# Автообновление контейнеров при изменении кода services:
watchtower: mikrotik-bot:
image: containrrr/watchtower:latest image: 10.10.30.121:5000/mikrotik-bot:latest
container_name: watchtower-mikrotik container_name: mikrotik-bot-production
restart: unless-stopped restart: unless-stopped
environment: environment:
- WATCHTOWER_CLEANUP=true # Vault AppRole credentials (безопасно)
- WATCHTOWER_POLL_INTERVAL=300 # проверка каждые 5 мин - VAULT_ADDR=http://10.10.30.121:8200
- WATCHTOWER_SCOPE=mikrotik-bot - VAULT_ROLE_ID=ba8d3d21-263e-4d92-8ffe-ef803017cef5
- WATCHTOWER_INCLUDE_STOPPED=true - VAULT_SECRET_ID=6b3ecc3c-9436-7f04-022f-8b1ce0ac09ee
- WATCHTOWER_REVIVE_STOPPED=true - 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: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
labels: environment:
- "com.centurylinklabs.watchtower.scope=mikrotik-bot" - WATCHTOWER_POLL_INTERVAL=60 # Проверять каждые 60 секунд
- WATCHTOWER_LABEL_ENABLE=true
- WATCHTOWER_CLEANUP=true
command: --interval 60 --label-enable --cleanup
networks: networks:
- mikrotik-net - bot-network
networks: networks:
mikrotik-net: bot-network:
driver: bridge driver: bridge
volumes:
mikrotik_bot_data:

View File

@ -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:

40
test_vault.py Normal file
View File

@ -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❌ Есть проблемы с получением секретов')

84
vault_client.py Normal file
View File

@ -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'),
}