Авторизация в Netbox через Keycloak (кастомный pipeline)
В сети есть хороший мануал по настройке авторизации в Netbox через Keycloak, однако он хорошо подходит для новых инсталляций, когда у вас чистая БД, ну или размер таблицы с пользователями и группами не очень большой и вам не составит труда все переделать.
Сложность, с которой я столкнулся, заключалась в том, что используемая нами инсталляция Netbox имеет множество активных пользователей импортированных из Active Directory, активную ролевую модель основанную на группах из Active Directory и потерять все эти ассоциации очень не хотелось бы.
Изучая документацию на модуль python-social-auth (а также код самого модуля и его бэкэнд к Keycloak) я наткнулся на раздел Extending the Piplene и понял что весь конвеер авторизации легко настраивается и изменяется, однако придётся немного по-питонить.
Проблемы с которыми я столкнулся
- Дефолтный Pipeline падает при наличии в Netbox пользователей с одинаковыми email (это специфический случай, который характерен для нашей организации и с которым вы можете никогда не столкнуться. Для снятия лишних вопросов скажу что, у нас есть правило записывать в поле email соответствующий email владельца технической учётной записи)
- Дефолтный Pipeline не обновляет членство в группах (а вот это уже критично для ролевой модели доступа основанной на группах).
Дефолтный Pipeline
Дефолтный pipeline для Netbox определяется в модуле <your-netbox-folder>/netbox/netbox/settings.py и выглядит так:
SOCIAL_AUTH_PIPELINE = (
'social_core.pipeline.social_auth.social_details',
'social_core.pipeline.social_auth.social_uid',
'social_core.pipeline.social_auth.social_user',
'social_core.pipeline.user.get_username',
'social_core.pipeline.social_auth.associate_by_email',
'social_core.pipeline.user.create_user',
'social_core.pipeline.social_auth.associate_user',
'netbox.authentication.user_default_groups_handler',
'social_core.pipeline.social_auth.load_extra_data',
'social_core.pipeline.user.user_details',
)Он содержит функции из дефолтного, для модуля python-social-auth, конвеера, за исключением функции user_default_groups_handler из модуля <your-netbox-folder>/netbox/netbox/authentication.py (строка 'netbox.authentication.user_default_groups_handler').
Решение проблемы №1
В первых трех функциях Pipeline происходит получение и декодирование JWT access_token из которого извлекаются различные поля, которые передаются клиенту (в нашем случае Netbox) от Keycloak, и помещаются в переменную details (dict содержащий данные о пользователе, на базе которых пайплайн должен будет создать или обновить данные пользователя Netbox).
Функция get_username создает на базе совокупности различных настроек приложения и модуля python-social-auth имя пользователя, а функция associate_by_email пытается по email найти в БД Netbox соответствующего пользователя.
Функция associate_by_email выглядит так:
def associate_by_email(backend, details, user=None, *args, **kwargs):
"""
Associate current auth with a user with the same email address in the DB.
This pipeline entry is not 100% secure unless you know that the providers
enabled enforce email verification on their side, otherwise a user can
attempt to take over another user account by using the same (not validated)
email address on some provider. This pipeline entry is disabled by
default.
"""
if user:
return None
email = details.get("email")
if email:
# Try to associate accounts registered with the same email address,
# only if it's a single object. AuthException is raised if multiple
# objects are returned.
users = list(backend.strategy.storage.user.get_users_by_email(email))
if len(users) == 0:
return None
elif len(users) > 1:
raise AuthException(
backend, "The given email address is associated with another account"
)
else:
return {"user": users[0], "is_new": False}Очевидно, что при наличии в в БД нескольких пользователей в одинаковым email эта функция упадёт.
Чтобы решить эту проблему я создал новый модуль и переписал эту функцию так, чтобы использовать для ассоциации не только поле email но username.
import re
from social_core.exceptions import AuthException
def associate_by_email_and_username(backend, details, user=None, *args, **kwargs):
"""
Associate current auth with a user with the same email address and username in the DB.
"""
if user:
return None
email = details.get("email")
username = details.get("username")
# If username in upn format, get username part
if m:=re.match(r"^(?:(?P<username>[^@]+)@(?P<domain>.+))quot;, username):
username = m.group('username')
if email:
users = list(backend.strategy.storage.user.get_users_by_email(email))
if len(users) == 0:
return None
elif len(users) > 1:
for u in users:
if u.username == username:
return {"user": u, "is_new": False}
raise AuthException(
backend, "The given email address is associated with another account"
)
else:
return {"user": users[0], "is_new": False}
else:
raise AuthException(backend, "No email address returned")Функция извлекает из словаря details username, если он в формате userPrincipalName - извлекает из него username часть и при наличии в БД Netbox пользователей с одинаковым email проводит дополнительную проверку по полю username.
Остается только собрать пакет из модуля, импортировать его в venv окружение, в котором работает Netbox, и переопределить в настройках приложения Pipeline (об этом расскажу ниже).
Решение проблемы №2
Изучив встроенную функцию netbox.authentication.user_default_groups_handler я понял что она назначает пользователю только группы определенные в переменной REMOTE_AUTH_DEFAULT_GROUPS из configuration.py. Я принял решение заменить эту функцию в конвеере совместив с функциональностью обновления групп, полученных от Keycloak.
Код этой функции будет выглядеть так:
Важно! Для того чтобы эта функция корректно работала access_token, который возвращает ваш Keycloak после авторизации, должен содержать поле groups с массивом имён групп. Если этого поля нет или оно содержит пустой массив - пользователю Netbox будут сброшены все связи с группами, а новых связей создано не будет.
import logging
from django.conf import settings
from django.contrib.auth.models import Group
def update_user_groups(backend, response, user=None, *args, **kwargs):
"""
Custom pipline handler with adds remote auth users to extist groups or
create non-exist groups
"""
logger = logging.getLogger(
"netbox-social-auth-custom.auth_username.update_user_groups_handler"
)
if not user:
return None
else:
user_groups = []
auth_groups = response.get("groups", [])
user.groups.clear()
# Add or Create DEFAULT USER GROUPS defined in configuration.py
for name in settings.REMOTE_AUTH_DEFAULT_GROUPS:
group, created = Group.objects.get_or_create(name=name)
user_groups.append(group)
# Add or Create Social Auth user groups
# groups should be added to access token as array (list) of names
for name in auth_groups:
group, created = Group.objects.get_or_create(name=name)
user_groups.append(group)
if user_groups:
user.groups.add(*user_groups)
else:
logger.info(
f"No any groups assignments for {user}. May be REMOTE_AUTH_DEFAULT_GROUPS incorrectly set\
or not returned from OAuth provider"
)Переопределение Pipeline
Итак, обе функции помещены в файл, который я назвал auth_username.py, сложены в папки netbox-social-auth-custom/pipeline (вторая папка больше для красоты в названии - обозначения что это часть pipeline по аналогии с модулем social_core) и из этого собран python package (как собирать python-пакеты я описывать не буду, т.к. для этого есть куча разного инструментария и соответствующих гайдов - например такой, разве что подскажу, что не плохо бы при сборке указать зависимость от social-auth-core>=4.5).
Пакет установлен в окружении venv нашего Netbox и остается только переопределить собственно сам Pipline. Для этого в файле configuration.py переопределяется переменная SOCIAL_AUTH_PIPELINE в виде вот такого массива:
SOCIAL_AUTH_PIPELINE = (
'social_core.pipeline.social_auth.social_details',
'social_core.pipeline.social_auth.social_uid',
'social_core.pipeline.social_auth.social_user',
'social_core.pipeline.user.get_username',
'netbox-social-auth-custom.pipeline.auth_username.associate_by_email_and_username',
'social_core.pipeline.user.create_user',
'social_core.pipeline.social_auth.associate_user',
'social_core.pipeline.social_auth.load_extra_data',
'social_core.pipeline.user.user_details',
'netbox-social-auth-custom.pipeline.auth_username.update_user_groups',
)Собственно на этом всё. Спасибо что дочитали, надеюсь это кому-то пригодится.
Забыл написать про отладку вашего (или вообще) конвеера авторизации. Чтобы в увидеть в логах или dev-консоли сервера Netbox
UPD: Отладка Pipline
Забыл написать про отладку вашего (или вообще) конвеера авторизации. Чтобы в увидеть в логах или dev-консоли сервера Netbox отладочную информацию добавьте в конец (или перед падающей функцией конвеера) строку
'social_core.pipeline.debug.debug',