add AD 2FA group check to instruct Trueconf module

This commit is contained in:
Денис Кривоченко
2026-07-21 23:47:35 +07:00
parent be12045fa8
commit 8fa42206aa
3 changed files with 149 additions and 2 deletions
+33 -1
View File
@@ -3,6 +3,7 @@ from utils.texts import (
EMOJI_DIGITS, EMOJI_DIGITS,
INSTRUCT_MAIN_MENU_TEXT, INSTRUCT_MAIN_MENU_TEXT,
INSTRUCT_TRUECONF_TEXT, INSTRUCT_TRUECONF_TEXT,
INSTRUCT_TRUECONF_WITH_AD,
INSTRUCT_EMAIL_TEXT, INSTRUCT_EMAIL_TEXT,
) )
import logging import logging
@@ -20,6 +21,35 @@ logger = logging.getLogger(__name__)
router = Router() router = Router()
def _get_user_cn(user_id: str) -> str:
"""
Получить CN пользователя из AD по его user_id (email или логин).
user_id из TrueConf — это обычно email, например 'ds.krivochenko@sibcem.ru'
"""
from ldap3 import Server, Connection, ALL
from config.config import AD_SERVER, AD_USER, AD_PASSWORD, AD_BASE
from ldap3.utils.conv import escape_filter_chars
try:
server = Server(AD_SERVER, get_info=ALL)
conn = Connection(server, user=AD_USER, password=AD_PASSWORD, auto_bind=True)
safe_id = escape_filter_chars(user_id)
short_username = user_id.split('@')[0] if '@' in user_id else user_id
search_filter = (
f"(&(objectClass=user)(|"
f"(mail={safe_id})"
f"(userPrincipalName={safe_id})"
f"(userPrincipalName={escape_filter_chars(f'{short_username}@sibcem.ru')}))"
f"(sAMAccountName={escape_filter_chars(short_username)})))"
)
conn.search(AD_BASE, search_filter, attributes=["cn", "sAMAccountName"])
if conn.entries:
return str(conn.entries[0].cn)
except Exception as e:
logger.error(f"Ошибка получения CN из AD для {user_id}: {e}")
return user_id # fallback — используем user_id как CN
@router.message() @router.message()
async def instruct_router_handler(msg: Message): async def instruct_router_handler(msg: Message):
user_id = msg.from_user.id user_id = msg.from_user.id
@@ -55,7 +85,9 @@ async def instruct_router_handler(msg: Message):
elif cmd == "1": elif cmd == "1":
log_menu_stats(user_id, "Инструкции", "Trueconf") log_menu_stats(user_id, "Инструкции", "Trueconf")
set_state(user_id, "INSTRUCT_TRUECONF_VIEW") set_state(user_id, "INSTRUCT_TRUECONF_VIEW")
await msg.answer(INSTRUCT_TRUECONF_TEXT, parse_mode="html") cn = _get_user_cn(user_id)
text = INSTRUCT_TRUECONF_WITH_AD(EMOJI_DIGITS, cn)
await msg.answer(text, parse_mode="html")
elif cmd == "2": elif cmd == "2":
log_menu_stats(user_id, "Инструкции", "Почта") log_menu_stats(user_id, "Инструкции", "Почта")
set_state(user_id, "INSTRUCT_EMAIL_VIEW") set_state(user_id, "INSTRUCT_EMAIL_VIEW")
+73
View File
@@ -0,0 +1,73 @@
# /opt/trueconf_bot/utils/ad_checker.py
# Проверка членства пользователя в группах Active Directory
from ldap3 import Server, Connection, ALL
def _get_ad_connection():
"""Создать соединение с AD."""
from config.config import AD_SERVER, AD_USER, AD_PASSWORD, AD_BASE # noqa: E501
server = Server(AD_SERVER, get_info=ALL)
return Connection(server, user=AD_USER, password=AD_PASSWORD, auto_bind=True), AD_BASE
def get_user_groups(cn: str) -> list[str] | None:
"""
Найти пользователя по CN и вернуть список групп.
Возвращает None если пользователь не найден.
"""
conn, ad_base = _get_ad_connection()
try:
conn.search(
search_base=ad_base,
search_filter=f"(cn={cn})",
attributes=["cn", "sAMAccountName", "memberOf"],
)
if not conn.entries:
return None
entry = conn.entries[0]
return [str(g) for g in entry.memberOf]
except Exception as e:
print(f"❌ Ошибка AD при поиске {cn}: {e}")
return None
def check_group_membership(cn: str, group_pattern: str = "2FA") -> dict:
"""
Проверить членство пользователя в группе.
Args:
cn: CN пользователя (например 'Krivochenko_Denis_Sergeevich')
group_pattern: подстрока для поиска в имени группы (по умолчанию '2FA')
Returns:
dict с полями:
found: bool — найден ли пользователь
cn: str | None — CN пользователя
sam: str | None — sAMAccountName
in_group: bool — в группе ли
groups: list[str] — все группы
matching_groups: list[str] — группы с паттерном
"""
groups = get_user_groups(cn)
if groups is None:
return {
"found": False,
"cn": None,
"sam": None,
"in_group": False,
"groups": [],
"matching_groups": [],
}
matching = [g for g in groups if group_pattern.upper() in g.upper()]
return {
"found": True,
"cn": cn,
"sam": groups[0].split(",")[0].split("=")[-1], # грубо из DN
"in_group": len(matching) > 0,
"groups": groups,
"matching_groups": matching,
}
+43 -1
View File
@@ -586,14 +586,55 @@ def _instruct_main_menu(emojis: dict) -> str:
f"{emojis['2']} — 📧 Почта\n\n" f"{emojis['2']} — 📧 Почта\n\n"
f"<i>{emojis['0']} — В главное меню</i>" f"<i>{emojis['0']} — В главное меню</i>"
) )
def _instruct_trueconf(emojis: dict) -> str: def _instruct_trueconf(emojis: dict) -> str:
"""Статический текст Trueconf — без проверки группы."""
return ( return (
"🖥 <b>Инструкция по Trueconf</b>\n\n" "🖥 <b>Инструкция по Trueconf</b>\n\n"
"Данная функция находится в разработке.\n\n" "Данная функция находится в разработке.\n\n"
f"<i>{emojis['9']} — Назад\n{emojis['0']} — В главное меню</i>" f"<i>{emojis['9']} — Назад\n{emojis['0']} — В главное меню</i>"
) )
def _instruct_trueconf_with_ad(emojis: dict, cn: str) -> str:
"""
Текст Trueconf с проверкой группы 2FA.
cn — CN пользователя из AD.
"""
from utils.ad_checker import check_group_membership
result = check_group_membership(cn, "2FA")
if not result["found"]:
return (
f"🖥 <b>Инструкция по Trueconf</b>\n\n"
f"Пользователь {cn} не найден в Active Directory.\n\n"
f"<i>{emojis['9']} — Назад\n{emojis['0']} — В главное меню</i>"
)
if result["in_group"]:
body = (
f"🖥 <b>Инструкция по Trueconf</b>\n\n"
f"✅ <b>Доступ к Trueconf разрешён.</b>\n\n"
f"У вас есть доступ к функциям Trueconf.\n\n"
f"Детали:\n"
f"• CN: {result['cn']}\n"
f"• Группы 2FA: {', '.join(result['matching_groups'])}\n\n"
f"<i>{emojis['9']} — Назад\n{emojis['0']} — В главное меню</i>"
)
else:
body = (
f"🖥 <b>Инструкция по Trueconf</b>\n\n"
f"❌ <b>Доступ к Trueconf запрещён.</b>\n\n"
f"У вас нет группы 2FA в Active Directory.\n\n"
f"Детали:\n"
f"• CN: {result['cn']}\n"
f"• Групп 2FA: 0\n\n"
f"<i>{emojis['9']} — Назад\n{emojis['0']} — В главное меню</i>"
)
return body
def _instruct_email(emojis: dict) -> str: def _instruct_email(emojis: dict) -> str:
return ( return (
"📧 <b>Инструкция по Почте</b>\n\n" "📧 <b>Инструкция по Почте</b>\n\n"
@@ -605,4 +646,5 @@ def _instruct_email(emojis: dict) -> str:
INSTRUCT_MAIN_MENU_TEXT = _instruct_main_menu(EMOJI_DIGITS) INSTRUCT_MAIN_MENU_TEXT = _instruct_main_menu(EMOJI_DIGITS)
INSTRUCT_TRUECONF_TEXT = _instruct_trueconf(EMOJI_DIGITS) INSTRUCT_TRUECONF_TEXT = _instruct_trueconf(EMOJI_DIGITS)
INSTRUCT_TRUECONF_WITH_AD = _instruct_trueconf_with_ad
INSTRUCT_EMAIL_TEXT = _instruct_email(EMOJI_DIGITS) INSTRUCT_EMAIL_TEXT = _instruct_email(EMOJI_DIGITS)