Система аутентификации¶
Архитектура¶
Система аутентификации построена на основе нескольких бэкендов, каждый из которых отвечает за свой метод аутентификации. Это позволяет гибко настраивать способы входа пользователей в систему.
Основные компоненты¶
- Django ModelBackend (
CustomModelBackend) - Реализован в
authentification_backend.py - Стандартная аутентификация Django
- Проверяет учетные данные в локальной базе данных
-
Используется как fallback, если другие методы недоступны
-
LDAP Backend (
LDAPBackend) - Реализован в
authentification_ldap.py - Использует библиотеку
python-ldap - Поддерживает конфигурацию через переменные окружения
- Может работать как в эксклюзивном режиме, так и с fallback на локальную БД
-
При определении имени пользователя для LDAP использует следующий приоритет:
sso_login- если заполненldap_username- если заполненemail- если заполненusername- как последний вариант
-
SSO Backend (
SSOBackend) - Реализован в
authentification_sso.py - Поддерживает несколько провайдеров SSO (LDAP, Azure AD, Omada IDM)
- Использует иерархическую стратегию выбора провайдера:
- Эксклюзивный SSO (если настроен)
- Пользовательский SSO (если привязан к пользователю)
- Локальная аутентификация (fallback)
- Поддерживает fallback на email-аутентификацию
- Реализует детальное логирование всех попыток аутентификации
- Классифицирует ошибки аутентификации для аналитики
Модели данных¶
Основная модель SSO¶
class SSO(Model):
TYPE_CHOICES = [
('ldap', 'LDAP'),
('azure', 'Azure AD'),
('omada', 'Omada IDM')
]
name = models.CharField(max_length=255)
type = models.CharField(max_length=50, choices=TYPE_CHOICES)
exclusive = models.BooleanField(default=False)
Провайдер-специфичные модели¶
-
SSOLDAP
class SSOLDAP(models.Model): sso = models.OneToOneField(SSO, on_delete=models.CASCADE, primary_key=True) server_uri = models.CharField(max_length=255) base_dn = models.CharField(max_length=255) -
SSOAzure
class SSOAzure(models.Model): sso = models.OneToOneField(SSO, on_delete=models.CASCADE, primary_key=True) tenant_id = models.CharField(max_length=255) client_id = models.CharField(max_length=255) client_secret = models.CharField(max_length=255) -
SSOOmada
class SSOOmada(models.Model): sso = models.OneToOneField(SSO, on_delete=models.CASCADE, primary_key=True) server_uri = models.CharField(max_length=255) client_id = models.CharField(max_length=255) client_secret = models.CharField(max_length=255)
Процесс аутентификации¶
1. Инициализация¶
def authenticate(self, request, username=None, password=None, **kwargs):
if not username or not password:
return None
try:
user = User.objects.get(username=username)
exclusive_sso = SSO.objects.filter(exclusive=True).first()
# ...
2. Выбор метода аутентификации¶
def authenticate(self, request, username=None, password=None, **kwargs):
user = User.objects.get(username=username)
exclusive_sso = SSO.objects.filter(exclusive=True).first()
if exclusive_sso:
return self._authenticate_with_sso(user, password, exclusive_sso, "exclusive")
elif user.sso:
return self._authenticate_with_sso(user, password, user.sso, "user")
else:
return self._authenticate_local(user, password)
3. Определение имени пользователя¶
def _get_auth_username(self, user, sso_record):
"""
Определяет имя пользователя для аутентификации и способ аутентификации.
Приоритет: sso_login, ldap_username (для LDAP), username.
"""
if not user:
return None, 'username'
# Для LDAP учитываем ldap_username (устаревшее поле для совместимости)
if sso_record and sso_record.type == 'ldap':
if user.sso_login:
return user.sso_login, 'username'
elif user.ldap_username:
return user.ldap_username, 'username'
else:
return user.username, 'username'
elif sso_record:
# Для остальных SSO (Azure, Omada)
if user.sso_login:
return user.sso_login, 'username'
else:
return user.username, 'username'
else:
# Для локальной аутентификации используем только username
return user.username, 'username'
4. SSO-аутентификация с fallback¶
def _authenticate_with_sso(self, user, password, sso_record, sso_type):
# Определяем имя пользователя для аутентификации
username, auth_method = self._get_auth_username(user, sso_record)
# Первая попытка: аутентификация по username
result, error_msg = sso_record.authenticate(username, password)
# Fallback на email, если первая попытка не удалась
if not result and user.email and sso_record.supports_email_auth() and auth_method == "username":
result, error_msg = sso_record.authenticate(user.email, password, email=user.email)
if result:
auth_method = "email"
username = user.email
return result, error_msg, auth_method, username
Архитектура SSO-провайдеров¶
Принцип работы¶
SSO-провайдеры реализованы через паттерн Strategy с использованием дочерних моделей:
- Базовая модель
SSO— содержит общие поля и выступает как диспетчер - Дочерние модели (
SSOLDAP,SSOAzure,SSOOmada) — содержат специфичные настройки и реализуют методы аутентификации - Метод
get_implementation()— возвращает конкретную реализацию провайдера
Структура провайдера¶
class SSO(Model):
def get_implementation(self):
"""Возвращает конкретную реализацию SSO-провайдера"""
if self.type == 'ldap':
return self.ssoldap
elif self.type == 'azure':
return self.ssoazure
elif self.type == 'omada':
return self.ssoomada
return None
def authenticate(self, username, password, **kwargs):
"""Делегирует аутентификацию конкретной реализации"""
impl = self.get_implementation()
if impl:
return impl.authenticate(username, password, **kwargs)
return False, "SSO implementation not found"
def supports_email_auth(self):
"""Проверяет поддержку email-аутентификации"""
impl = self.get_implementation()
if impl:
return impl.supports_email_auth()
return False
Реализация конкретного провайдера¶
class SSONewProvider(models.Model):
sso = models.OneToOneField(SSO, on_delete=models.CASCADE, primary_key=True)
api_url = models.CharField(max_length=255)
api_key = models.CharField(max_length=255)
def authenticate(self, username, password, **kwargs):
"""Реализация аутентификации для нового провайдера"""
try:
response = requests.post(
self.api_url,
json={
'username': username,
'password': password,
'api_key': self.api_key
},
timeout=30
)
return response.status_code == 200, response.text
except requests.exceptions.RequestException as e:
return False, str(e)
def supports_email_auth(self):
"""Поддерживает ли провайдер email-аутентификацию"""
return True # или False, в зависимости от провайдера
def get_server_info(self):
"""Возвращает информацию о сервере для логирования"""
return {
"type": self.sso.type,
"name": self.sso.name,
"exclusive": self.sso.exclusive,
"api_url": self.api_url
# НЕ включаем api_key для безопасности
}
Добавление нового провайдера SSO¶
Для добавления нового провайдера SSO необходимо выполнить следующие шаги:
-
Создать модель настроек провайдера (см. пример выше)
-
Добавить тип в
SSO.TYPE_CHOICES:TYPE_CHOICES = [ ('ldap', 'LDAP'), ('azure', 'Azure AD'), ('omada', 'Omada IDM'), ('new_provider', 'New Provider Name') ] -
Обновить метод
get_implementation()в моделиSSO:def get_implementation(self): if self.type == 'new_provider': return self.ssonewprovider # ... existing providers ... -
Создать миграцию для новой модели
-
Добавить тесты для нового провайдера в
test_sso_integration.py
Безопасность¶
1. Хранение учетных данных¶
- Пароли хранятся только в хэшированном виде с использованием Django password hashers
- Секретные ключи провайдеров хранятся в зашифрованном виде
- Реализована история паролей для отслеживания повторного использования
2. Защита от атак¶
- Реализована защита от брутфорс-атак через ограничение попыток входа
- Ведется журнал попыток входа с IP-адресами
- Поддерживается временная блокировка по IP при превышении лимита попыток
3. Аудит безопасности¶
- Все действия с аутентификацией логируются
- Поддерживается отслеживание активных сессий
- Реализован механизм принудительного завершения сессий
- Ведется журнал изменений учетных данных
Тестирование¶
При добавлении нового провайдера или изменении существующей логики аутентификации необходимо:
- Создать unit-тесты для нового провайдера
- Обновить интеграционные тесты
- Проверить работу fallback механизмов
- Протестировать сценарии с эксклюзивным SSO
- Проверить корректность обработки ошибок
Пример тест-кейса:
def test_successful_login(client, create_users):
"""
Проверяет успешный вход пользователя в систему.
"""
user, _ = create_users
response = client.post(
"/accounts/login/?next=/",
{"username": "user", "password": "pass"},
REMOTE_ADDR="1.1.1.1"
)
assert response.wsgi_request.user.is_authenticated
Рекомендации по разработке¶
- Расширение функционала
- Используйте существующие абстракции при добавлении новых провайдеров
- Следуйте паттерну существующих реализаций
-
Документируйте все специфичные для провайдера настройки
-
Безопасность
- Всегда используйте HTTPS для внешних провайдеров
- Не храните секретные ключи в коде
-
Используйте переменные окружения для конфигурации
-
Производительность
- Кэшируйте результаты внешних запросов где это возможно
- Используйте таймауты для внешних запросов
-
Обрабатывайте сетевые ошибки корректно
-
Поддержка
- Ведите подробное логирование
- Добавляйте метрики для мониторинга
- Документируйте все изменения в API
Регистрация попыток аутентификации ¶
См. также: Журнал попыток аутентификации для администраторов
Модели¶
- AuthAttemptLog — основная модель для хранения попыток входа (src/planiqum/core/authcustom/models.py).
- Связи: ForeignKey на User, SSO; JSON-поля для credentials_info и sso_settings.
- Основные поля: user, username, sso, sso_repr, credentials_info, sso_settings, ip_address, user_agent, auth_backend, success, error_type, error_message, timestamp.
- User — стандартная модель пользователя.
- SSO, SSOLDAP, SSOAzure, SSOOmada — модели для описания SSO-провайдеров и их параметров.
Где происходит регистрация¶
- Вся логика регистрации попыток централизована в backend-классах:
SSOBackend.authenticate(src/planiqum/core/authcustom/authentification_sso.py)CustomModelBackend.authenticate(src/planiqum/core/authcustom/authentification_backend.py)- (устаревшее)
LDAPBackend.authenticate(src/planiqum/core/authcustom/authentification_ldap.py) - Для SSO — используется метод
_authenticate_with_ssoс fallback на email-аутентификацию. - Для каждой попытки вызывается
_log_auth_attempt()с передачей всех ключевых параметров.
Алгоритм регистрации¶
- Определение имени пользователя через
_get_auth_username(): - Для SSO:
sso_login→ldap_username(только LDAP) →username - Для локальной аутентификации: только
username -
Возвращает
(username_value, auth_method)гдеauth_methodможет быть 'username' или 'email' -
Формирование структуры
credentials_info:credentials_info = { "auth_method": auth_method, # "username" или "email" "login_value": login_value, # фактическое значение для входа "sso_server": sso_server_info # настройки SSO-сервера (без секретов) } -
Аутентификация с fallback:
- Первая попытка: аутентификация по username
- Fallback на email: если первая попытка не удалась и SSO поддерживает email-аутентификацию
-
Обновление
credentials_infoпри успешном fallback -
Классификация ошибок через
_determine_error_type(): connection— ошибки подключения к серверуtimeout— таймауты соединенияcredentials— неверные учетные данныеuser_not_found— пользователь не найденserver_error— ошибки сервера (5xx)technical— технические ошибки-
unknown— неклассифицированные ошибки -
Сохранение в журнал через
_log_auth_attempt(): - Результат (success: bool)
- Тип ошибки (error_type: str)
- Оригинальное сообщение об ошибке (error_message: str)
- Технические детали: IP, user-agent, backend, время
- Вся регистрация происходит независимо от результата
Особенности для разных сценариев¶
- Локальная аутентификация — логируется только
username,sso_serverпустой,auth_method= "username". - SSO (LDAP, Azure, Omada) — логируется реальный логин, способ аутентификации, параметры подключения, результат, ошибка.
- Эксклюзивная SSO — параметры берутся из эксклюзивного SSO,
sso_type= "exclusive". - Пользовательский SSO — параметры берутся из привязанного к пользователю SSO,
sso_type= "user". - Fallback на email —
auth_method= "email",login_value= email пользователя. - Пользователь не найден — логируется попытка с отсутствующим user, фиксируется ошибка.
- Техническая ошибка — в
error_messageсохраняется оригинальный текст, вerror_type— классифицированный тип.
Расширение и тестирование¶
- Для добавления новых сценариев регистрации — расширяйте логику в backend-классах.
- Для тестирования используйте pytest-тесты (см. src/planiqum/core/authcustom/tests/test_sso_integration.py).
- Все тесты проверяют структуру
credentials_info,sso_settings,success,error_type,error_messageи корректность логирования.
Примеры логирования¶
Успешная аутентификация через LDAP¶
{
"auth_method": "username",
"login_value": "john.doe",
"sso_server": {
"type": "ldap",
"name": "Corporate LDAP",
"exclusive": false,
"server_uri": "ldap://ldap.company.com:389",
"base_dn": "ou=users,dc=company,dc=com"
}
}
Fallback на email-аутентификацию¶
{
"auth_method": "email",
"login_value": "john.doe@company.com",
"sso_server": {
"type": "azure",
"name": "Azure AD",
"exclusive": true,
"tenant_id": "12345678-1234-1234-1234-123456789012",
"client_id": "87654321-4321-4321-4321-210987654321"
}
}
Ошибка аутентификации¶
{
"auth_method": "username",
"login_value": "john.doe",
"sso_server": { /* настройки SSO */ },
"error_type": "credentials",
"error_message": "Invalid credentials"
}
См. также: Журнал попыток аутентификации для администраторов