Files
sd_dispatcher/ad/test_ad_lookup.py

337 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Тестовый скрипт — поиск пользователя в Active Directory.
Аналог find_user_in_ad() из sd_dispatcher.py.
Использует Passwork для загрузки учётных данных AD (как в основном коде).
Если Passwork недоступен — просит ввести учётные данные вручную.
Единственная зависимость: ldap3
pip install ldap3
Запуск:
python3 test_ad_lookup.py --login ivanov
python3 test_ad_lookup.py --email ivanov@sibcem.ru
python3 test_ad_lookup.py --phone 79131234567
python3 test_ad_lookup.py --test-all
"""
import argparse
import os
import re
import sys
# =========================================================================
# 1. Загрузка учётных данных AD
# =========================================================================
def load_ad_credentials():
"""
Загружает AD_USER и AD_PASSWORD из Passwork (как в config.py).
fallback: ручной ввод.
"""
# --- Путь 1: через модуль Passwork ---
passwork_dir = "/opt/passwork"
card_name = os.getenv("PW_ID_AD", "AD").strip()
if passwork_dir not in sys.path:
sys.path.insert(0, passwork_dir)
try:
from passwork import get_passwork_secrets
print(f"[Passwork] Загрузка карточки '{card_name}'...")
pool = get_passwork_secrets(required_cards=[card_name])
card = pool.get(card_name)
if card:
ad_user = card.get("login", "").strip()
ad_pass = card.get("password", "").strip()
if ad_user and ad_pass:
print(f"[Passwork] ✅ AD_USER: {ad_user}")
return ad_user, ad_pass
except Exception:
pass
# --- Путь 2: env-переменные ---
ad_user = os.getenv("AD_USER", "").strip()
ad_pass = os.getenv("AD_PASS", "").strip()
if ad_user and ad_pass:
print("[Env] ✅ Учётные данные загружены из окружения")
return ad_user, ad_pass
# --- Путь 3: ручной ввод ---
print("\n❌ Passwork недоступен.")
print("Введите учётные данные AD вручную:\n")
ad_user = input(" AD user (DOMAIN\\account): ").strip()
ad_pass = input(" AD password: ").strip()
if not ad_user or not ad_pass:
print("❌ Отмена.")
sys.exit(1)
print(f"[Manual] ✅ AD_USER: {ad_user}")
return ad_user, ad_pass
# =========================================================================
# 2. Подключение к LDAP
# =========================================================================
def connect_to_ad(ad_user, ad_pass):
from ldap3 import Server, Connection
server = Server(
"ldap://172.16.20.20",
get_info=None,
connect_timeout=5,
)
conn = Connection(
server,
user=ad_user,
password=ad_pass,
auto_bind=True,
receive_timeout=5,
)
return conn
# =========================================================================
# 3. Поиск пользователя
# =========================================================================
def find_user(conn, login=None, email=None, phone=None):
"""
Приоритет: телефон > логин > email.
Возвращает dict атрибутов или None.
"""
if phone:
filt = (
f"(|(telephoneNumber=*{phone}*)(mobile=*{phone}*) "
f"(homePhone=*{phone}*)(pager=*{phone}*) "
f"(facsimileTelephoneNumber=*{phone}*)(ipPhone=*{phone}*))"
)
by = f"телефону {phone}"
elif login:
clean = login.split("\\")[-1].split("/")[-1]
filt = f"(sAMAccountName={clean})"
by = f"логину {clean}"
elif email:
filt = f"(mail={email})"
by = f"почте {email}"
else:
print("❌ Укажите --login, --email или --phone")
sys.exit(1)
print(f"\n[LDAP] Фильтр: {filt}")
print(f"[LDAP] Base: OU=-Пользователи,DC=sibcem,DC=ru")
conn.search(
search_base="OU=-Пользователи,DC=sibcem,DC=ru",
search_filter=filt,
attributes=[
"sAMAccountName", "displayName", "givenName", "sn",
"mail", "title", "department", "l", "company",
"manager", "telephoneNumber", "mobile",
],
)
if not conn.entries:
print(f"\n❌ Не найден по {by}")
return None
user = conn.entries[0]
print(f"\n✅ Найдено: {user.entry_dn}")
result = {
"sam": user.sAMAccountName.value if "sAMAccountName" in user else None,
"given_name": user.givenName.value if "givenName" in user else None,
"surname": user.sn.value if "sn" in user else None,
"display_name": user.displayName.value if "displayName" in user else None,
"mail": user.mail.value if "mail" in user else None,
"title": user.title.value if "title" in user else None,
"department": user.department.value if "department" in user else None,
"city": user.l.value if "l" in user and user.l.value else None,
"company": user.company.value if "company" in user and user.company.value else None,
"dn": user.entry_dn,
"phone": user.telephoneNumber.value if "telephoneNumber" in user else None,
"mobile": user.mobile.value if "mobile" in user else None,
}
# Город из OU если атрибут l пустой
if not result["city"]:
match = re.search(r"OU=([^,]+),OU=-Пользователи", result["dn"], re.IGNORECASE)
if match:
result["city"] = match.group(1).strip()
if result["city"].lower() == "красноярск" and "OU=ООО Комбинат Волна" in result["dn"]:
result["company"] = "ООО Комбинат Волна"
print(f" Город из OU: {result['city']}")
# Руководитель — второй запрос
manager_dn = user.manager.value if "manager" in user else None
if manager_dn:
conn.search(
search_base=manager_dn,
search_filter="(objectClass=user)",
attributes=["mail", "displayName"],
search_scope="BASE",
)
if conn.entries and "mail" in conn.entries[0]:
result["manager_email"] = conn.entries[0].mail.value
result["manager_name"] = (
conn.entries[0].displayName.value if "displayName" in conn.entries[0] else None
)
else:
result["manager_email"] = None
else:
result["manager_email"] = None
# Проверка department на исключение из автосогласования
excluded_depts = [
# "IT-Администраторы",
# "Системные аналитики",
# Добавьте нужные отделы из config.py: APPROVAL_EXCLUDED_DEPARTMENTS
]
if result["department"] and any(
dept.lower() in result["department"].lower() or
result["department"].lower() in dept.lower()
for dept in excluded_depts
):
result["approval_excluded"] = True
result["excluded_department"] = result["department"]
else:
result["approval_excluded"] = False
result["excluded_department"] = None
return result
# =========================================================================
# 4. Вывод
# =========================================================================
def print_report(data):
print("\n" + "=" * 60)
print(" РЕЗУЛЬТАТ ПОИСКА В ACTIVE DIRECTORY")
print("=" * 60)
print(f" sAMAccountName: {data['sam']}")
print(f" Имя: {data['given_name']}")
print(f" Фамилия: {data['surname']}")
print(f" ФИО: {data['display_name']}")
print(f" Email: {data['mail']}")
print(f" Должность: {data['title']}")
print(f" Отдел: {data['department'] or '—'}")
print(f" Город: {data['city'] or '—'}")
print(f" Компания: {data['company'] or '—'}")
print(f" Телефон: {data['phone'] or '—'}")
print(f" Мобильный: {data['mobile'] or '—'}")
print(f" Руководитель: {data['manager_name'] or '—'}")
if data.get("manager_email"):
print(f" Email руково.: {data['manager_email']}")
# Проверка на исключение из автосогласования
if data.get("approval_excluded"):
print("\n" + "!" * 60)
print(f" ⚠️ ОТДЕЛ: {data['excluded_department']}")
print(f" ⚠️ АВТОСОГЛАСОВАНИЕ ОТМЕНЕНО")
print(f" Пользователь находится в исключённом отделе.")
print("!" * 60)
print("-" * 60)
print(f" DN: {data['dn']}")
# OU-дерево
ou_parts = [p.strip() for p in data["dn"].split(",") if p.strip().startswith("OU=")]
if ou_parts:
print(f"\n OU-структура:")
for i, ou in enumerate(reversed(ou_parts)):
print(f" {' ' * i}└── {ou}")
print("=" * 60)
# =========================================================================
# 5. Тест всех методов
# =========================================================================
def test_all_methods(conn):
print("\n" + "─" * 60)
print(" ТЕСТИРОВАНИЕ: все методы поиска")
print("─" * 60)
conn.search(
search_base="OU=-Пользователи,DC=sibcem,DC=ru",
search_filter="(objectClass=user)",
attributes=["sAMAccountName", "mail", "telephoneNumber"],
size_limit=1,
)
if not conn.entries:
print("❌ Нет пользователей в AD.")
return
sample = conn.entries[0]
sam = sample.sAMAccountName.value
mail = sample.mail.value if "mail" in sample else None
phone = sample.telephoneNumber.value if "telephoneNumber" in sample else None
print(f"\n📌 Тестовый: {sam} ({mail})")
for label, kwargs in [("логину", {"login": sam}),
("email", {"email": mail}) if mail else None,
("телефону", {"phone": phone}) if phone else None]:
if kwargs is None:
continue
print(f"\n[Тест] По {label}...")
r = find_user(conn, **kwargs)
if r:
print_report(r)
print("\n" + "─" * 60)
print(" ✅ Готово")
print("─" * 60)
# =========================================================================
# MAIN
# =========================================================================
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Поиск пользователя в Active Directory",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Примеры:
python3 test_ad_lookup.py --login ivanov
python3 test_ad_lookup.py --email ivanov@sibcem.ru
python3 test_ad_lookup.py --phone 79131234567
python3 test_ad_lookup.py --test-all
Учётные данные AD загружаются из Passwork (как в основном коде).
Если Passwork недоступен — запрашиваются вручную.
Проверка department: если department пользователя совпадает с
APPROVAL_EXCLUDED_DEPARTMENTS из config.py — автосогласование отменяется.
""",
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--login", help="sAMAccountName пользователя")
group.add_argument("--email", help="Email пользователя")
group.add_argument("--phone", help="Номер телефона")
parser.add_argument("--test-all", action="store_true",
help="Найти первого пользователя и проверить все методы")
args = parser.parse_args()
# Загрузка учётных данных (Passwork → env → ручной ввод)
ad_user, ad_pass = load_ad_credentials()
conn = connect_to_ad(ad_user, ad_pass)
print(f"[LDAP] Подключено к ldap://172.16.20.20 как {ad_user}")
if args.test_all:
test_all_methods(conn)
else:
data = find_user(conn, login=args.login, email=args.email, phone=args.phone)
if data:
print_report(data)
conn.unbind()