Скрытый текст

Перед началом небольшой дисклеймер: данный плагин не является официальным. Он не поддерживается технической поддержкой и продуктовой командой ALD Pro. Перед его использованием, необходимо оценить возможные риски и протестировать, до использования в продакш среде.

Введение: Почему автоматизация рутины стала необходимостью

ALD PRO предоставляет управление ролями через WebUI и API. В WebUI массовое создание/клонирование ролей занимает много времени и плохо подходит для интеграции в автоматизацию. Требование было практичным: быстро копировать существующую роль для новых организационных подразделений (OU), сохраняя привилегии/политики и приводя роль в корректное состояние.

В этой статье я расскажу о четырёх этапах эволюции решения:

  1. Первая версия на bash с работой через LDAP

  2. Переход на API ALD PRO с тем же bash

  3. Рефакторинг на Python для надёжности и гибкости

  4. Полная интеграция как плагин FreeIPA

Каждый этап решал конкретные проблемы предыдущего и добавлял новые возможности.

Этап 1: Наивный подход — работа напрямую с LDAP

Первая версия скрипта пыталась работать напрямую с LDAP-деревом FreeIPA. Идея была проста: найти запись роли, скопировать её атрибуты и создать новую запись в нужном OU.

Проблемы, с которыми я столкнулся:

  • Сложность определения корректного DN для целевого OU

  • Проблемы с копированием привилегий и политик

  • Отсутствие валидации данных на стороне ALD PRO

  • Необходимость ручного управления состоянием роли (активация, деактивация)

Этот подход быстро показал свою ограниченность. ALD PRO — не просто LDAP-каталог, а сложная система с собственной логикой, которая не отражается напрямую в атрибутах LDAP.

Этап 2: Переход на API ALD PRO — bash-скрипт с поддержкой Kerberos

Осознав ограничения LDAP-подхода, я изучил REST API ALD PRO. Оказалось, что система предоставляет полноценный JSON API для управления ролями.

Аутентификация: Kerberos или логин/пароль

  • Kerberos (negotiate) удобен для SSO в инфраструктуре FreeIPA/ALD.

  • Логин/пароль полезен для изолированных стендов или при отсутствии Kerberos.

Ключевые особенности bash-реализации:

#!/bin/bash
# Функция для аутентификации с поддержкой Kerberos и логина/пароля
authenticate() {
    if [[ -n "$ADMIN_USER" && -n "$ADMIN_PASSWORD" ]]; then
        use_kerberos="false"
    fi
    
    if [[ "$use_kerberos" != "true" ]]; then
        # Базовая аутентификация
        response=$(curl -X 'POST' \
            "https://$SERVER_URL/ad/api/ds/login" \
            -d "{\"data\":{\"login\":\"$username\",\"password\":\"$password\"}}" \
            -c curl.cookie -s --insecure -w "|%{http_code}")
    else
        # Kerberos-аутентификация
        response=$(curl -X 'POST' \
            -H "referer:https://$SERVER_URL/ad/ui" \
            -c curl.cookie --negotiate -u : \
            --insecure -w "|%{http_code}" \
            "https://$SERVER_URL/ad/api/ds/login/kerberos")
    fi
}

Что было реализовано в bash-скрипте:

  1. Двойная аутентификация: поддержка как Kerberos (для интегрированных систем), так и логина/пароля

  2. Безопасный ввод пароля: маскировка вводимых символов

  3. Полный цикл работы с ролью: получение → создание → обновление → активация

  4. Обработка ошибок: анализ HTTP-кодов и JSON-ответов

  5. Работа с пробелами в именах ролей: URL-кодирование специальных символов

Пример использования:

./clone_api_roles.sh \
  -r 'ALDPRO - Automation Tasks Administrators' \
  -n 'New Automation Role' \
  -o 'ou=buh,ou=ald.pro,cn=orgunits,cn=accounts,dc=ald,dc=pro' \
  -U aldprodc1.ald.pro

Недостатки bash-реализации:

  • Сложная работа с JSON (через jq, но всё равно громоздко)

  • Ограниченная обработка ошибок

  • Трудности с поддержкой и расширением

  • Зависимость от внешних утилит (curl, jq, sed)

Этап 3: Рефакторинг на Python — структурирование логики и нормальная обработка ошибок

Переход на Python решает основные проблемы bash-скрипта:

  • нативный JSON;

  • исключения и типизированные структуры;

  • модульность (разделение на аутентификацию, работу с ролью, валидацию входных данных);

  • возможность тестов (моки API).

Важная оговорка: Kerberos negotiate

В первом варианте, мне удобнее было использовать curl --negotiate как проверенный способ аутентификации в конкретной среде. Поэтому Python-версия выступала как "оркестратор", а HTTP-вызов делался через subprocess.

Если окружение позволяет, этот слой можно заменить на requests + GSSAPI/SSPI (в зависимости от платформы, можно использовать второй плагин в репозитории plagun_with_req).

Архитектура Python-реализации:

class ALDProRoleCloner:
    """Класс для работы с ролями в ALD PRO через API с использованием curl"""
    
    def _run_curl(self, method: str, url: str, data: Optional[Dict] = None) -> Optional[Dict]:
        """Выполняет curl запрос"""
        try:
            cmd = [
                'curl', '-s', '-k', '--negotiate', '-u', ':',
                '-X', method,
                '-H', 'Accept: application/json',
                '-H', 'Content-Type: application/json',
                '-b', self.cookie_file.name,
                '-c', self.cookie_file.name,
            ]
            # ... выполнение запроса и обработка ответа

Ключевые улучшения:

  1. Объектно-ориентированный подход: инкапсуляция логики в классы

  2. Типизация: аннотации типов для лучшей читаемости

  3. Временные файлы: автоматическая очистка cookie-файлов

  4. Гибкая конфигурация: возможность задания всех параметров через аргументы

Но оставалась проблема: скрипт всё ещё был внешним инструментом, не интегрированным в экосистему FreeIPA. Пользователям приходилось запоминать отдельную команду, не было единого интерфейса.


Этап 4: Архитектура плагинов FreeIPA - как это работает изнутри

Прежде чем перейти к нашему кейсу, давайте разберемся, как вообще устроены плагины FreeIPA. Понимание архитектуры поможет оценить, почему интеграция в виде плагина — это наиболее правильный подход для подобных задач.

Основные компоненты плагина FreeIPA

FreeIPA построена на модульной архитектуре, где каждый плагин — это Python-класс, расширяющий базовые классы фреймворка. Вот ключевые элементы:

1. Класс команды (наследник Command):

from ipalib import Command, Str, _

class my_command(Command):
    """Документация команды"""
    
    # Имя команды (будет доступно как ipa my-command)
    name = 'my_command'
    
    # Определение параметров
    takes_args = (
        Str('username',
            cli_name='user',
            label=_('Username'),
            doc=_('User to process')),
    )
    
    # Определение вывода
    has_output = (
        ('result', dict),
        ('summary', str),
    )
    
    def execute(self, username, **options):
        # Логика команды
        return {
            'result': {'processed': username},
            'summary': _('User %s processed') % username
        }

2. Регистрация плагина:

from ipalib import api

def register():
    # Регистрируем команду в API FreeIPA
    api.add_plugin(my_command)

3. Типы параметров:

  • Str — строковые значения

  • Int — целые числа

  • Flag — флаги (True/False)

  • Bytes — бинарные данные

  • Password — пароли (с маскировкой)

Знак вопроса в конце имени параметра ('username?') делает его необязательным.

Жизненный цикл плагина

  1. Загрузка: FreeIPA при старте сканирует директорию /usr/lib/python3/dist-packages/ipaserver/plugins/ и импортирует все модули, указанные в init.py.

  2. Инициализация: Вызывается функция register() каждого плагина, которая добавляет команды в общее пространство имён FreeIPA.

  3. Выполнение:

    • Пользователь вызывает команду через CLI, WebUI или API

    • FreeIPA находит соответствующий плагин

    • Автоматически валидируются параметры

    • Вызывается метод execute() с переданными аргументами

    • Результат форматируется согласно has_output

  4. Интеграция: После регистрации команда становится доступной через:

    • CLI: ipa my-command --user=test

    • WebUI: появляется в интерфейсе (если настроено)

    • JSON-RPC API: для удалённых вызовов

Преимущества подхода с плагинами

  1. Единый интерфейс: Все команды, как встроенные, так и кастомные, доступны через единый CLI ipa.

  2. Наследование инфраструктуры: Плагины автоматически получают:

    • Аутентификацию и авторизацию FreeIPA

    • Логирование в стандартные журналы

    • Поддержку internationalization (i18n)

    • Валидацию параметров

  3. Интеграция с WebUI: При необходимости плагин можно подключить к веб-интерфейсу FreeIPA.

  4. Безопасность: Команды наследуют систему привилегий FreeIPA, можно тонко настраивать права доступа через ipa privilege-* и ipa permission-*.

Лучшие практики разработки плагинов

  • Используйте i18n: Все строки, видимые пользователю, оборачивайте в _():

summary = _('Operation completed successfully')
  • Валидируйте входные данные: Используйте ограничения параметров:

Int('port', minvalue=1, maxvalue=65535)
  • Правильно обрабатывайте ошибки: Используйте стандартные исключения FreeIPA:

from ipalib import errors
raise errors.NotFound(reason=_('User not found'))
  • Логируйте действия: Используйте встроенный логгер:

import logging
log = logging.getLogger(__name__)
log.debug('Processing user: %s', username)
  • Пишите документацию: Подробные docstring становятся справкой в CLI.

Теперь, понимая основы архитектуры плагинов FreeIPA, давайте посмотрим, как эти принципы были применены в нашем случае с клонированием ролей ALD PRO.


Этап 5: Плагин FreeIPA: команда в ipa CLI и единая авторизация

Финальный этап — создание полноценного плагина FreeIPA. Это позволило:

  • Интегрировать команду в стандартный CLI FreeIPA (ipa)

  • Использовать встроенную аутентификацию и авторизацию

  • Обеспечить единый интерфейс со всеми другими командами FreeIPA

  • Автоматически генерировать документацию и справку

Структура нашего плагина для клонирования ролей:

@register()
class role_clone(Command):
    """Копирование ролей в ALD PRO"""
    
    name = 'role_clone'
    takes_args = ()
    
    takes_options = (
        Str('srcrole',
            cli_name='srcrole',
            label=_('Source role name'),
            doc=_('Имя существующей роли для копирования (например: "ALDPRO - Automation Tasks Administrators")'),
            no_convert=True,
        ),
        Str('newrole',
            cli_name='newrole',
            label=_('New role name'),
            doc=_('Имя новой роли'),
            no_convert=True,
        ),
        # ... другие параметры
    )
    
    has_output = (
        output.Output('summary',
            type=str,
            doc=_('Summary of the operation'),
        ),
        output.Output('result',
            type=dict,
            doc=_('Result information'),
        ),
    )
    
    def execute(self, *args, **options):
        """Основная логика команда"""
        # Проверяем корректность статуса
        # Проверяем Kerberos билет
        # Шаг 1: Аутентификация через Kerberos
        # Шаг 2: Получение данных о существующей роли
        # Извлекаем необходимые данные
        # Определяем какой OU использовать
        # Определяем какое описание использовать
        # Шаг 3: Создание новой роли
        # Шаг 4: Обновление роли через PATCH
        # Шаг 5: Активация роли (выполняется только если статус active)
        # Возвращаем результат

Как работает плагин изнутри:

  1. Регистрация в FreeIPA: Плагин добавляется в /usr/lib/python3/dist-packages/ipaserver/plugins/ и регистрируется через декоратор @register().

  2. Интеграция с CLI: После установки плагина команда становится доступной через стандартный интерфейс:

ipa help role-clone  # просмотр справки
ipa role-clone --srcrole="Source Role" --newrole="New Role" --server="aldpro.example.com"

3. Использование Kerberos: Плагин автоматически использует текущий Kerberos-билет пользователя, что обеспечивает единый вход (SSO):

    def check_kerberos_ticket(self) -> bool:
        """Проверка наличия Kerberos билета"""
        try:
            result = subprocess.run(
                ['klist'],
                capture_output=True,
                text=True,
                timeout=5
            )
            return result.returncode == 0
        except Exception:
            return False

4. Полный цикл работы с API: Плагин выполняет те же этапы, что и bash-скрипт, но с лучшей обработкой ошибок и интеграцией в FreeIPA.

Пример использования плагина:

# Получение Kerberos-билета (если ещё нет)
kinit admin

# Клонирование роли с указанием целевого OU
ipa role-clone \
  --srcrole="ALDPRO - Automation Tasks Administrators" \
  --newrole="ALDPRO - Automation Tasks Administrators OU1" \
  --targetou="ou=ou1,ou=ald.pro,cn=orgunits,cn=accounts,dc=ald,dc=pro" \
  --server="aldprodc2.ald.pro" \
  --description="Автоматизация задач для OU1" \
  --status="active"

Преимущества плагина перед standalone-скриптом:

  1. Единый интерфейс: Используется привычный ipa CLI

  2. Интегрированная аутентификация: Работает с Kerberos-билетами FreeIPA

  3. Автоматическая документация: --help генерируется автоматически

  4. Консистентность ошибок: Ошибки форматируются так же, как в других командах FreeIPA

  5. Возможность интеграции с WebUI: Теоретически плагин можно подключить и к веб-интерфейсу

Технические детали реализации

Обработка пробелов в именах ролей

ALD PRO допускает пробелы в именах роле��, что требует специальной обработки в URL:

def encode_role_name(role_name):
    """Кодирование имени роли для URL"""
    return role_name.replace(' ', '%20')

# Использование в запросах
encoded_role = encode_role_name("Role with spaces")
url = f"https://{server}/ad/api/ds/roles/{encoded\\_role}"

Работа с состоянием ролей

Роли в ALD PRO имеют несколько состояний:

  • editing — роль создана, но не активирована

  • active — роль активна и применяется

  • inactive — роль неактивна

Плагин управляет этим состоянием через PATCH-запросы:

update_data = {
    "role_description": description,
    "role_ou_dn": target_ou,
    "role_ou_nested": True,
    "role_state": "active"  # или "inactive"
}

Обработка ошибок API

Каждый запрос к API проверяется на HTTP-код и содержание JSON-ответа:

def _run_curl(self, method, url, data=None):
    # Выполнение запроса
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
    
    if result.returncode != 0:
        raise Exception(f"curl error: {result.stderr}")
    
    # Парсинг JSON
    response = json.loads(result.stdout)
    
    if not response.get('success'):
        error_msg = response.get('error', {}).get('message', 'Unknown error')
        raise Exception(f"API error: {error_msg}")
    
    return response

Сравнение подходов

Критерий

Bash-скрипт

Python-скрипт

FreeIPA плагин

Сложность разработки

Средняя

Высокая

Очень высокая

Интеграция с FreeIPA

Нет

Нет

Полная

Аутентификация

Логин/пароль или Kerberos

Логин/пароль или Kerberos

Kerberos (е��иный вход)

Обработка ошибок

Базовая

Продвинутая

Интегрированная

Удобство использования

Отдельная команда

Отдельная команда

ipa role-clone

Поддержка

Требует bash-знаний

Требует Python

Стандартный интерфейс FreeIPA

Заключение

Путь от простого bash-скрипта до полноценного плагина FreeIPA занял несколько месяцев, но результат того стоил.

  1. Экономию времени: Клонирование роли теперь занимает секунды вместо минут ручной работы

  2. Автоматизацию: Процесс интегрирован в CI/CD и скрипты развёртывания

  3. Надёжность: Встроенная обработка ошибок и проверка данных

  4. Единый интерфейс: Администраторы работают через привычный ipa CLI

Этот проект также показал важность выбора правильного уровня интеграции при работе со сложными системами управления идентификацией. Начав с внешнего скрипта, я постепенно пришел к пониманию, что настоящая ценность — в глубокой интеграции с платформой.

Советы для разработчиков, которые хотят повторить этот путь:

  1. Начинайте с изучения API системы, а не прямого доступа к данным (LDAP, БД);

  2. Используйте язык, который хорошо интегрируется с целевой платформой (Python для FreeIPA);

  3. Не бойтесь рефакторинга — каждый этап улучшал плагин и фильтровал ошибки;

  4. Документируйте процесс — это поможет и вам, и другим разработчикам.