Привет! Меня зовут Олег Гетманский, я – старший архитектор информационных систем. Сегодня расскажу, как мы упростили создание и управление бизнес-процесссами в IdM, оставив в прошлом жестко зашитые в систему правила и внедрив гибкий визуальный конструктор бизнес-логики Camunda BPM. Под катом краткое руководство по внедрению движка с моими комментариями – возможно, для кого-то оно сэкономит несколько рабочих часов или даже дней.

Автоматизация в IdM

Развитая IdM-система должна уметь автоматизировать процессы управления доступом даже в крупных компаниях с сотнями тысяч пользователей. Ее можно рассматривать как пункт управления: система интегрируется с другими информационными ресурсами организации и дает возможность централизованно управлять пользователями и их полномочиями. Процессы управления включают в себя такие операции, как предоставление доступа при приеме на работу, запрос дополнительных полномочий, приостановка доступа при увольнении, смена пароля и прочие действия, которые требуются по регламентам компаний.

С точки зрения разработчика самое сложное в системах класса IdM – это требование абсолютной кастомизируемости: заказчики считают, что в IdM все должно быть настраиваемо. Приведу несколько примеров из жизни:

  • Есть стандартная логика бизнес-процесса: IdM автоматически создает учетную запись для сотрудника при приеме на работу и блокирует УЗ и связанные с ней права при увольнении. Написали код, протестировали, выпустили фичу – все работает. Но у заказчика есть такое понятие, как перевод через увольнение, и теперь наша фича ломает этот выстроенный процесс перевода – IdM воспринимает его как просто прием на работу и создает новую учетную запись, а она сотруднику не нужна. Здесь важно, чтобы осталась прежняя учетка, и, уж тем более, чтобы она не заблокировалась.

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

  • IdM автоматически назначает базовые роли новым пользователям. Внезапно найдутся «не такие как все» VIP-пользователи, которым нужно назначать роли в обход регламента – по разным алгоритмам в разных организациях.

  • В IdM на одного сотрудника приходится одна персональная учетная запись в домене организации. Это хорошо, но есть такие организации, в которых сотрудники работают на нескольких должностях по совместительству, и должны иметь несколько доменных аккаунтов.

Список потенциальных требований, которые нельзя предсказать заранее, со временем становится все больше и больше. Из-за этого у нас возникает ощущение, что новые фичи всегда получаются недоделанные, а программисты виноваты в том, что разрабатывают недостаточно гибкий продукт.

Настроение разработчика, когда появляется новое непредвиденное требование
Настроение разработчика, когда появляется новое непредвиденное требование

Как можно решить эту проблему? Мы пробовали разные варианты:

  1. Inline feature flags. Создаём множество параметров конфигурации и прямо в коде бизнес-логики пишем, как нужно поступать в том или ином случае, в зависимости от этих параметров. В отдельном интерфейсе можно включить те или иные флажки. Идея не взлетела, потому что мы все равно не знаем, какие фича-флаги понадобятся в будущем. К тому же чем больше в коде фича-флагов, тем дороже выходит реализация очередного такого флажка.

  2. Заскриптованная бизнес-логика. Оставить в стабильной ветке лишь общий workflow и архитектурный «каркас» в виде доступных сервисов и API, предоставив командам внедрения возможность самостоятельно закодировать специфичную для заказчика бизнес-логику. В таком случае объектом поставки является комбинация из стандартного продукта и множества скриптовых конструкций (в нашем случае это скрипты на языке Groovy). Кастомизировать и настраивать в такой модели можно практически всё, что угодно, однако при написании скрипта легко поломать то, что работало раньше. К тому же на практике оказалось довольно сложно поддерживать обратную совместимость для старых скриптов в новых версиях продукта.

  3. Плагины. Выделить сервисы-компоненты в виде простых Spring-бинов, каждый из которых отвечает за свой участок бизнес-логики. Реализацию компонента можно дополнить или вовсе заменить, если подложить в classpath приложения jar-файл, в котором есть альтернативная реализация бина. В таком случае мы получаем высокую кастомизируемость, в том смысле, что позволяем разработчикам самостоятельно написать плагин под конкретного заказчика. Однако при изменении кода оригинальных бинов нам придётся адаптировать все существующие плагины под новую версию продукта.

BPM-движок в основе бизнес-логики

На самом деле продвинутая IdM-система должна уметь  управлять аккаунтами и привилегиями, назначать и отзывать роли, посылать уведомления, выявлять нарушения и много другого полезного. Остается только уточнить, когда именно и как именно это всё нужно делать в отдельно взятой организации: когда создавать аккаунт для сотрудника (и сколько их будет), когда назначать роли и какие они будут, когда посылать уведомления, и о чем они должны быть и тому подобное.

Если посмотреть на IdM как на систему, автоматизирующую бизнес-процессы, то получается, что:

  • Можно выделить контекстно-независимые компоненты для проведения атомарных операций (создать аккаунт, назначить роль, отправить уведомление). Эти компоненты простые, они очень редко изменяются, и их можно переиспользовать. В своей системе мы иногда создаем новые компоненты, когда нужна новая функциональность.

  • Задача располагает к выстраиванию событийно-ориентированной архитектуры. При различных действиях (автоматических или пользовательских) порождаются события. В ответ на различные события происходят автоматические действия, определяемые бизнес-процессами организации («Новый пользователь? Создать доменный аккаунт», «Сотрудник ушел в отпуск? Временно заблокировать аккаунт», и много другого, что может потребоваться в отдельно взятой организации). Любую автоматику можно представить как последовательность простых шагов.

  • Максимальная гибкость настройки требуется именно на уровне принятия решений. Это не касается компонентов, выполняющих атомарные операции. Очень редко нужно изменять реализацию конкретных операций, и наоборот - очень часто мы будем изменять алгоритмы принятия решений – то есть добавлять условия на выполнение тех или иных операций, менять их последовательность, убирать из процессов ненужные шаги и добавлять нужные.

Так почему бы не возложить автоматизацию бизнес-процессов на предназначенный для этого движок BPM, оставив непосредственно в IdM только компоненты атомарных операций? Давайте посмотрим, что из этого получилось у нас в нашей IdM-системе.

В качестве BPM-системы мы выбрали Camunda v.7 – это open-source движок, его можно встроить в Java-приложение, он хорошо интегрируется со Spring. Есть визуальный редактор Camunda Modeller, в котором можно рисовать бизнес-процессы (БП). Сами БП у нас будут в формате BPMN (Business Process Management Notation).

Ниже под спойлером будет немного кода в технологическом стеке Java 17 + Maven + Spring Framework + Hibernate.

Компоненты атомарных операций

Сначала создадим атомарные компоненты. Каждый компонент – это небольшой stateless-бин, который отвечает за свою ограниченную область применения. Методы бина должны быть максимально простые и как можно более универсальные, мы должны иметь возможность вызвать их без знания контекста.

// Все атомарные компоненты называются по шаблону [Область применения]Ops 
// Так проще отличить их от любых других компонентов 
@Component 
public class RoleOps { 
// Компоненты полагаются на другие готовые сервисы из инфраструктуры системы 
    @Autowired 
    protected RoleService roleService; 
 
    @Autowired 
    protected UserRepository userRepository; 
 
    // Методы принимают и возвращают простые типы 
    /** 
     * Назначить роль на пользователя 
     * @param roleId id роли 
     * @param userId id пользователя 
     * @return id назначения 
     */ 
    public String assignRoleToUser(String roleId, String userId) { 
        var role = roleService.getRole(roleId); 
        var user = userRepository.getUser(userId); 
        return roleService.createAssignment(role, user).getId(); 
    } 
 
    /** 
     * Отозвать назначение 
     * @param assignmentId Id назначения 
     */ 
    public void removeAssignment(String assignmentId) { 
        roleService.removeAssignment(assignmentId); 
    } 
}

Теперь мы можем вызывать компонент RoleOps в скриптовом обработчике Camunda:

roleOps.assignRoleToUser("id-role-admin", userId)

Получается лаконично, такие однострочники мы затем будем использовать в BPMN-диаграммах.

Интегрируем Camunda в проект

Создаем новый Maven проект, добавляем зависимости на Camunda, Spring и сервисы инфраструктуры IdM:

<!-- Camunda, Spring -->
<dependency>
    <groupId>org.camunda.bpm</groupId>
    <artifactId>camunda-engine</artifactId>
</dependency>
<dependency>
    <groupId>org.camunda.bpm</groupId>
    <artifactId>camunda-engine-spring</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
</dependency>
<!-- Инфраструктура IdM -->
<dependency>
    <groupId>ru.solarsecurity.inRights.model</groupId>
    <artifactId>model-common</artifactId>
    <scope>provided</scope>
</dependency>

В нашем случае мы создаем отдельный Maven-модуль, который живет в составе проекта, однако можно сделать отдельный BPM-микросервис. Сегодня это даже предпочтительно: принятие решений – отдельно, исполнители – отдельно.

Создаем конфигурацию Camunda:

@Configuration
public class BpmBeanConfiguration {

    @Qualifier("transactionManager")
    @Autowired
    protected PlatformTransactionManager transactionManager;

    @Bean
    public SpringProcessEngineConfiguration engineConfiguration(DataSource dataSource) {
        SpringProcessEngineConfiguration cfg = new SpringProcessEngineConfiguration();
        cfg.setProcessEngineName("engine");
        
        // Используем стандартные dataSource и transactionManager из проекта
        // Camunda будет использовать ту же БД, что и остальная система
        cfg.setDataSource(dataSource);
        cfg.setDatabaseSchemaUpdate("true");
        cfg.setTransactionManager(transactionManager);
        cfg.setScriptEngineResolver(new DefaultScriptEngineResolver(new ScriptEngineManager()));
        cfg.setInitializeTelemetry(false); // Отключаем телеметрию Camunda       
        return cfg;
    }

    @Bean
    public ProcessEngineFactoryBean engineFactory(SpringProcessEngineConfiguration engineConfiguration) {
        ProcessEngineFactoryBean factoryBean = new ProcessEngineFactoryBean();
        factoryBean.setProcessEngineConfiguration(engineConfiguration);
        return factoryBean;
    }
}

Итак, сейчас у нас есть движок Camunda, в нем можно развернуть свои бизнес-процессы и стартовать их.

В нашей IdM бизнес-процессы будут стартовать по сигналу, где сигнал – это какое-либо пользовательское действие или внешнее событие.

Все сигналы наследуются от нашего интерфейса CamundaSignal:

public interface CamundaSignal extends Serializable {
    String getSignalName();
}

Для Camunda важно, чтобы сигналы были сериализуемыми и имели название.

Пример сигнала, который говорит о том, что в IdM появился новый пользователь:

public record UserCreatedEvent(
	String id,
    String login,
    String name,
    Map<String, Serializable> attributes
) implements CamundaSignal {
    @Override
    public String getSignalName() {
        return "userCreated";
    }
}

Создаем сервис для запуска бизнес-процессов по сигналам. Поскольку запуск БП и выполнение всех его шагов – это дорогостоящая операция, то лучше не блокировать поток обработки пользовательских запросов и сделать запуск БП асинхронным. Мы отправляем сигналы для Camunda в отдельном ThreadPoolExecutor:

@Component
public class CamundaSignalService {

    @Autowired
    protected RuntimeService runtimeService;

    // 128 потоков выполнения БП по умолчанию.
    // Так много, потому что внутри них IdM занята в основном ожиданием I/O:
    //  обращения к СУБД, запросы к внешним системам, отправка уведомлений
    @Value("${inRights.camunda.threadPoolSize:128}")
    protected int threadPoolSize;

    protected ThreadPoolExecutor singalSubmitterThreadExecutor;

    @PostConstruct
    protected void init() {
        var threadFactory = new CustomizableThreadFactory();
        threadFactory.setDaemon(true);
        threadFactory.setThreadNamePrefix("camunda-signal-thread-");

        singalSubmitterThreadExecutor = new ThreadPoolExecutor(
            threadPoolSize, threadPoolSize, 60, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(Integer.MAX_VALUE), threadFactory
        );
    }

    /**
     * Отправить сигнал в Camunda. Это запустит БП, которые должны отреагировать на данный сигнал.
     * @param camundaSignal сигнал
     */
    public void sendSignal(CamundaSignal camundaSignal) {
        singalSubmitterThreadExecutor.execute(() ->
            runtimeService.createSignalEvent(camundaSignal.getSignalName())
                .setVariables(Map.of("signal", camundaSignal))
                .send()
        );
    }
}

Теперь отправляем сигналы в Camunda там, где возникают соответствующие события в системе:

// В БД сохранен новый пользователь
userRepository.save(user);
// Сообщаем об этом Camunda
camundaSignalService.sendSignal(new UserCreatedEvent(user.getId(), user.getLogin(), user.getName(), user.getAttributes()));

Далее - разворачиваем в Camunda наш стандартный БП, чтобы наполнить уже движок каким-нибудь полезным функционалом. В идеале мы хотим сделать отдельную административную web-страничку для управления бизнес-процессами – разворачивать новые БП, скачивать / включать / выключать существующие. Интерфейсы Camunda для этого не подходят, поэтому создадим свой простой репозиторий сущностей БП.

Сущность бизнес-процесса имеет название, случайный id как UUID, флаг развернут / не развернут, и контент в формате BPMN:

@Entity
@Table(name = "bpm_process")
public class BusinessProcessEntity {

    @GeneratedValue(generator = "custom-generator", strategy = GenerationType.IDENTITY)
    @GenericGenerator(name = "custom-generator", strategy = UuidGenerator.STRATEGY_NAME)
    @Id
    protected String uuid;

    @Column
    protected String name;

    @Column
    protected boolean deployed;

    @Version
    @Column
    protected Integer version;

    @Column
    protected byte[] content;

    // getters, setters
}

Репозиторий для сущностей БП:

public interface BusinessProcessRepository extends CrudRepository<BusinessProcessEntity, String> {
    Optional<BusinessProcessEntity> findByName(String name);
}

BusinessProcessDeploymentService отвечает за фактическое соответствие сущностей BusinessProcessEntity реальному состоянию БП в Camunda BPM. С помощью него и только него мы будем управлять БП на инсталляциях нашей IdM:

@Component 
public class BusinessProcessDeploymentService { 
 
    @Autowired 
    protected BusinessProcessRepository bpRepo; 
 
    @Autowired 
    protected RepositoryService camundaRepo; 
 
    /** 
     * Активировать БП в Camunda BPM 
     */ 
    @Transactional 
    public void deploy(BusinessProcessEntity bp) { 
        bp.setDeployed(true); 
        bpRepo.save(bp); 
        deleteDeployments(bp.getName()); // Удаляем старую версию деплоймента 
        camundaRepo.createDeployment()   // Создаем новый деплоймент 
            .source(bp.getName()) 
            .addInputStream(bp.getName(), new ByteArrayInputStream(bp.getContent())) 
            .deploy(); 
    } 
 
    /** 
     * Деактивировать БП в Camunda BPM 
     */ 
    @Transactional 
    public void undeploy(BusinessProcessEntity bp) { 
        bp.setDeployed(false); 
        bpRepo.save(bp); 
        deleteDeployments(bp.getName()); 
    } 
 
    protected void deleteDeployments(String deploymentSource) { 
        camundaRepo.createDeploymentQuery().deploymentSource(deploymentSource).list().stream() 
            .map(Deployment::getId) 
            .forEach(camundaRepo::deleteDeployment); 
    } 
}

Установка флага deployed = true и деплоймент в Camunda происходят в одной транзакции, поэтому если в процессе деплоя что-то пойдет не так, то сущность BusinessProcessEntity не будет считаться активным процессом.

Представление архитектуры модуля BPM
Представление архитектуры модуля BPM

Для краткости я пропущу написание REST-контроллера и детали реализации фронтенда.

Итоговый вид административной странички управления БП:

Бизнес-процессы разворачиваются в Camunda сразу после загрузки файла. Если снять флажок «Активен» с бизнес-процесса, то деплоймент будет удален из Camunda, но сама сущность BusinessProcessEntity останется в таблице. Так можно включать и отключать БП, не удаляя их из системы. Если загрузить БП с именем, которое уже есть в таблице, тогда старая версия БП будет обновлена в Camunda.

Пишем простой бизнес-процесс

Для составления БП будем использовать редактор Camunda Modeller. Создаем новый файл в формате BPMN diagram (Camunda Platform 7).

Добавляем сигнал начала процесса - userCreatedEvent (как написано в коде: UserCreatedEvent#getSignalName). Добавляем в процесс шаги типа Script Task. Внутри шагов - скриптовые выражения, вызывающие методы атомарных компонентов. В результате получится примерно так:

Пример бизнес-процесса в IdM-системе
Пример бизнес-процесса в IdM-системе

Сохраняем файл bpmn, загружаем в IdM и таким образом реализуем то, что нужно организации.

Итак, составляя BPMN, можно быстро набросать работающую фичу по желаниям заказчика, возможно, даже сидя вместе с ним за одним столом. Нужно сделать перевод через увольнение? Отлично, только расскажите, с чего оно начинается, и что IdM должна при этом сделать. Если известны стартовые события и правила формирования атрибутов учетных записей, то поддержать множественные доменные аккаунты также будет довольно просто.

В целом, чем больше атомарных бинов, которые можно использовать в БП, – тем более функциональные процессы можно строить в визуальном редакторе.

Теперь мы можем выстроить такой процесс реализации новых фич:

  1. Системный аналитик (или заказчик, архитектор, разработчик) представляет схематичное изображение нового бизнес-процесса. Это может быть BPMN-диаграмма или просто иллюстрация с разноцветными элементами. Самое главное, чтобы было понятно, какие события должны происходить и как на них нужно реагировать.

  2. Превращаем схему в валидную BPMN-диаграмму.

  3. Наполняем диаграмму обращениями к атомарным компонентам через скриптовые выражения.

  4. Реализуем в IdM новые сигналы (события, запросы) и атомарные компоненты, если существующих недостаточно.

  5. Получаем рабочий бизнес-процесс в виде файла с расширением bpmn, проверяем работоспособность.

  6. Устанавливаем готовый bpmn у заказчика.

Готовый БП – это легковесный переносимый артефакт. Файлы bpmn можно копировать, изменять, можно положить их в систему контроля версий и использовать как основу для других кастомных БП.

В заключение

BPM-движок, такой как Camunda, вполне может использоваться как визуальный конструктор в окружении, где требуется 100% кастомизируемость алгоритмов и сложно предсказать, в какую сторону пойдет развитие той или иной фичи.

Плюсы этого решения:

  • Это «микросервисно»! Модуль BPM взаимодействует с остальной системой посредством обмена сигналами и событиями, это позволяет выделить его в отдельный микросервис. Инфраструктура исполнения команд, поступающих от BPM, может быть масштабирована отдельно.

  • Поощряет написание простого кода. BPMN-диаграммы будут простые и понятные, если таковыми будут атомарные компоненты, которые их поддерживают. Желательно, чтобы Ops-бины принимали и возвращали простые типы данных: строки, числа, enum, record.

  • Наглядность. Бизнес-логика в BPMN отображена визуально. Не требуется умение программировать, чтобы понимать диаграммы бизнес-процессов.

  • Изменения on-the-fly. Для изменения бизнес-логики на конкретной инсталляции достаточно только загрузить новый файл bpmn.

  • Обратная совместимость. Поскольку БП работают только с Ops-компонентами, то для поддержки обратной совместимости нам достаточно того, что публичное API Ops-компонентов не изменяется от версии к версии. Если нам нужны более продвинутые возможности от операционных компонентов, тогда мы просто напишем новые бины.

  • Производительность. Мы не используем в Camunda асинхронные процессы, таймеры и пользовательские задачи. Выполнение всего БП укладывается в одну транзакцию. Это значит, что не требуется сохранять промежуточное состояние процесса в базе. Скорость выполнения кода бизнес-логики практически равна скорости выполнения обычного Java-кода. Подробнее о транзакциях в Camunda.

На что нужно обратить внимание, если вы решитесь на внедрение движка BPM:

  • Тяжеловесность. BPM-движок усложняет систему как минимум одним своим присутствием. Поэтому если ваша предметная область достаточно предсказуема, и вы можете сохранить гибкость вашего продукта просто посредством написания качественного кода, тогда вам не нужен BPM.

  • Нужно знать нотацию. Для того, чтобы грамотно составлять бизнес-процессы, кто-то в команде должен изучить нотацию BPMN 2.0.

  • BPM – это автоматические действия. Описанный способ применения движка не подходит для решения общих проблем в области кастомизации. К примеру, если нужно сделать настраиваемые графические формы или слегка изменить сценарий взаимодействия программы с пользователем – тогда BPM не поможет, а более подходящим инструментом будет модульный конструктор фронтенда. Наиболее сильная сторона движка BPM – это автоматизированные процессы и действия, происходящие без участия человека.

  • Обязательное протоколирование. Довольно сложно проводить отладку кода бизнес-логики, когда он заключен в BPMN-диаграммы. Крайне желательно делать так, чтобы любое совершенное действие было зафиксировано в файлах логов или в журнале событий, или в системе аудита. По этим данным позже можно будет отследить, почему движок BPM принял то или иное решение на конкретном шаге. Без подробного протоколирования бизнес-процессы – это черный ящик.

Комментарии (10)


  1. aborouhin
    06.04.2023 10:31

    А тот факт, что 8-я версия Камунды уже не очень опенсорс, на Ваш выбор не повлиял? Я вот как-то ознакомившись с их новой лицензионной политикой, сделал вывод, что Камунда всё (по крайней мере, для меня) :(


  1. inRights Автор
    06.04.2023 10:31
    +1

    Мы не рассматривали Camunda 8, потому что он может работать только в виде независимого оркестратора, а нам важно сохранить BPM-движок в виде embedded engine.
    Сами разработчики говорят, что будут поддерживать Camunda 7 еще несколько лет.

    "If you’re using an embedded engine, you might want to stick with Camunda Platform 7, unless you have good reason to migrate. This is a perfectly reasonable choice as Camunda Platform 7 will remain fully supported for at least the next five years".

    В целом, Camunda 8 - это уже другой продукт (больше не форк Activity, совершенно другая модель данных, ориентир на SaaS и т.д.)


    1. aborouhin
      06.04.2023 10:31

      Понятно. Ну будем надеяться, что обещания про 5 лет не останутся обещаниями :)


  1. areful
    06.04.2023 10:31

    Коллеги, а каким мнханизмом у вас в системе согласовывались заяввки на доступ - там же должно быть что-то очень похожее на Camunda по функциональности?


    1. inRights Автор
      06.04.2023 10:31

      Раньше согласование заявок происходило по "hardcoded" бизнес-процессу Activity. Теперь за это отвечает Camunda (собственно, тут ничего не пришлось переделывать, потому что Camunda - это форк Activity)


  1. areful
    06.04.2023 10:31

    Работаем по схеме Idm+Camunda с середины 2020г. При этом Idm в схеме уже поменялся, а Camunda осталась. Idm - для управления подключенными системами, Camunda - для реализации высокоуровневой бизнес-логики.


  1. BigD
    06.04.2023 10:31

    • В IdM на одного сотрудника приходится одна персональная учетная запись в домене организации. Это хорошо, но есть такие организации, в которых сотрудники работают на нескольких должностях по совместительству, и должны иметь несколько доменных аккаунтов.

    Чего? Один человек = один аккаунт, а на него роли.


    1. ggo
      06.04.2023 10:31

      Есть легаси системы, где права раздают в зависимости от OU.


      1. BigD
        06.04.2023 10:31

        Просто это принципиальное нарушение всего, чего можно.


  1. ggo
    06.04.2023 10:31

    Использовать процессный движок - (жирное) да.

    Использовать Camunda для idm-процессов - нет.

    Есть ли процессные движки, кроме Camunda - конечно есть.