diff --git a/instruct/handlers.py b/instruct/handlers.py
index 85ea36a..5f00ca8 100644
--- a/instruct/handlers.py
+++ b/instruct/handlers.py
@@ -3,6 +3,7 @@ from utils.texts import (
EMOJI_DIGITS,
INSTRUCT_MAIN_MENU_TEXT,
INSTRUCT_TRUECONF_TEXT,
+ INSTRUCT_TRUECONF_WITH_AD,
INSTRUCT_EMAIL_TEXT,
)
import logging
@@ -20,6 +21,35 @@ logger = logging.getLogger(__name__)
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()
async def instruct_router_handler(msg: Message):
user_id = msg.from_user.id
@@ -55,7 +85,9 @@ async def instruct_router_handler(msg: Message):
elif cmd == "1":
log_menu_stats(user_id, "Инструкции", "Trueconf")
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":
log_menu_stats(user_id, "Инструкции", "Почта")
set_state(user_id, "INSTRUCT_EMAIL_VIEW")
diff --git a/utils/ad_checker.py b/utils/ad_checker.py
new file mode 100644
index 0000000..484c663
--- /dev/null
+++ b/utils/ad_checker.py
@@ -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,
+ }
diff --git a/utils/texts.py b/utils/texts.py
index 2fe0b20..5f9bacf 100644
--- a/utils/texts.py
+++ b/utils/texts.py
@@ -586,14 +586,55 @@ def _instruct_main_menu(emojis: dict) -> str:
f"{emojis['2']} — 📧 Почта\n\n"
f"{emojis['0']} — В главное меню"
)
-
def _instruct_trueconf(emojis: dict) -> str:
+ """Статический текст Trueconf — без проверки группы."""
return (
"🖥 Инструкция по Trueconf\n\n"
"Данная функция находится в разработке.\n\n"
f"{emojis['9']} — Назад\n{emojis['0']} — В главное меню"
)
+
+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"🖥 Инструкция по Trueconf\n\n"
+ f"Пользователь {cn} не найден в Active Directory.\n\n"
+ f"{emojis['9']} — Назад\n{emojis['0']} — В главное меню"
+ )
+
+ if result["in_group"]:
+ body = (
+ f"🖥 Инструкция по Trueconf\n\n"
+ f"✅ Доступ к Trueconf разрешён.\n\n"
+ f"У вас есть доступ к функциям Trueconf.\n\n"
+ f"Детали:\n"
+ f"• CN: {result['cn']}\n"
+ f"• Группы 2FA: {', '.join(result['matching_groups'])}\n\n"
+ f"{emojis['9']} — Назад\n{emojis['0']} — В главное меню"
+ )
+ else:
+ body = (
+ f"🖥 Инструкция по Trueconf\n\n"
+ f"❌ Доступ к Trueconf запрещён.\n\n"
+ f"У вас нет группы 2FA в Active Directory.\n\n"
+ f"Детали:\n"
+ f"• CN: {result['cn']}\n"
+ f"• Групп 2FA: 0\n\n"
+ f"{emojis['9']} — Назад\n{emojis['0']} — В главное меню"
+ )
+
+ return body
+
+
def _instruct_email(emojis: dict) -> str:
return (
"📧 Инструкция по Почте\n\n"
@@ -605,4 +646,5 @@ def _instruct_email(emojis: dict) -> str:
INSTRUCT_MAIN_MENU_TEXT = _instruct_main_menu(EMOJI_DIGITS)
INSTRUCT_TRUECONF_TEXT = _instruct_trueconf(EMOJI_DIGITS)
+INSTRUCT_TRUECONF_WITH_AD = _instruct_trueconf_with_ad
INSTRUCT_EMAIL_TEXT = _instruct_email(EMOJI_DIGITS)