Привет! Меня зовут Олег Гетманский, я – старший архитектор информационных систем. Сегодня расскажу, как мы упростили создание и управление бизнес-процесссами в IdM, оставив в прошлом жестко зашитые в систему правила и внедрив гибкий визуальный конструктор бизнес-логики Camunda BPM. Под катом краткое руководство по внедрению движка с моими комментариями – возможно, для кого-то оно сэкономит несколько рабочих часов или даже дней.
Автоматизация в IdM
Развитая IdM-система должна уметь автоматизировать процессы управления доступом даже в крупных компаниях с сотнями тысяч пользователей. Ее можно рассматривать как пункт управления: система интегрируется с другими информационными ресурсами организации и дает возможность централизованно управлять пользователями и их полномочиями. Процессы управления включают в себя такие операции, как предоставление доступа при приеме на работу, запрос дополнительных полномочий, приостановка доступа при увольнении, смена пароля и прочие действия, которые требуются по регламентам компаний.
С точки зрения разработчика самое сложное в системах класса IdM – это требование абсолютной кастомизируемости: заказчики считают, что в IdM все должно быть настраиваемо. Приведу несколько примеров из жизни:
Есть стандартная логика бизнес-процесса: IdM автоматически создает учетную запись для сотрудника при приеме на работу и блокирует УЗ и связанные с ней права при увольнении. Написали код, протестировали, выпустили фичу – все работает. Но у заказчика есть такое понятие, как перевод через увольнение, и теперь наша фича ломает этот выстроенный процесс перевода – IdM воспринимает его как просто прием на работу и создает новую учетную запись, а она сотруднику не нужна. Здесь важно, чтобы осталась прежняя учетка, и, уж тем более, чтобы она не заблокировалась.
IdM автоматически блокирует аккаунт на время отпуска пользователя. Эту фичу можно включить и выключить. Обязательно найдется заказчик, который попросит вместо блокировки отправлять уведомление, или перемещать учетную запись в отдельный каталог.
IdM автоматически назначает базовые роли новым пользователям. Внезапно найдутся «не такие как все» VIP-пользователи, которым нужно назначать роли в обход регламента – по разным алгоритмам в разных организациях.
В IdM на одного сотрудника приходится одна персональная учетная запись в домене организации. Это хорошо, но есть такие организации, в которых сотрудники работают на нескольких должностях по совместительству, и должны иметь несколько доменных аккаунтов.
Список потенциальных требований, которые нельзя предсказать заранее, со временем становится все больше и больше. Из-за этого у нас возникает ощущение, что новые фичи всегда получаются недоделанные, а программисты виноваты в том, что разрабатывают недостаточно гибкий продукт.
Как можно решить эту проблему? Мы пробовали разные варианты:
Inline feature flags. Создаём множество параметров конфигурации и прямо в коде бизнес-логики пишем, как нужно поступать в том или ином случае, в зависимости от этих параметров. В отдельном интерфейсе можно включить те или иные флажки. Идея не взлетела, потому что мы все равно не знаем, какие фича-флаги понадобятся в будущем. К тому же чем больше в коде фича-флагов, тем дороже выходит реализация очередного такого флажка.
Заскриптованная бизнес-логика. Оставить в стабильной ветке лишь общий workflow и архитектурный «каркас» в виде доступных сервисов и API, предоставив командам внедрения возможность самостоятельно закодировать специфичную для заказчика бизнес-логику. В таком случае объектом поставки является комбинация из стандартного продукта и множества скриптовых конструкций (в нашем случае это скрипты на языке Groovy). Кастомизировать и настраивать в такой модели можно практически всё, что угодно, однако при написании скрипта легко поломать то, что работало раньше. К тому же на практике оказалось довольно сложно поддерживать обратную совместимость для старых скриптов в новых версиях продукта.
Плагины. Выделить сервисы-компоненты в виде простых 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 не будет считаться активным процессом.
Для краткости я пропущу написание REST-контроллера и детали реализации фронтенда.
Итоговый вид административной странички управления БП:
Бизнес-процессы разворачиваются в Camunda сразу после загрузки файла. Если снять флажок «Активен» с бизнес-процесса, то деплоймент будет удален из Camunda, но сама сущность BusinessProcessEntity останется в таблице. Так можно включать и отключать БП, не удаляя их из системы. Если загрузить БП с именем, которое уже есть в таблице, тогда старая версия БП будет обновлена в Camunda.
Пишем простой бизнес-процесс
Для составления БП будем использовать редактор Camunda Modeller. Создаем новый файл в формате BPMN diagram (Camunda Platform 7).
Добавляем сигнал начала процесса - userCreatedEvent (как написано в коде: UserCreatedEvent#getSignalName). Добавляем в процесс шаги типа Script Task. Внутри шагов - скриптовые выражения, вызывающие методы атомарных компонентов. В результате получится примерно так:
Сохраняем файл bpmn, загружаем в IdM и таким образом реализуем то, что нужно организации.
Итак, составляя BPMN, можно быстро набросать работающую фичу по желаниям заказчика, возможно, даже сидя вместе с ним за одним столом. Нужно сделать перевод через увольнение? Отлично, только расскажите, с чего оно начинается, и что IdM должна при этом сделать. Если известны стартовые события и правила формирования атрибутов учетных записей, то поддержать множественные доменные аккаунты также будет довольно просто.
В целом, чем больше атомарных бинов, которые можно использовать в БП, – тем более функциональные процессы можно строить в визуальном редакторе.
Теперь мы можем выстроить такой процесс реализации новых фич:
Системный аналитик (или заказчик, архитектор, разработчик) представляет схематичное изображение нового бизнес-процесса. Это может быть BPMN-диаграмма или просто иллюстрация с разноцветными элементами. Самое главное, чтобы было понятно, какие события должны происходить и как на них нужно реагировать.
Превращаем схему в валидную BPMN-диаграмму.
Наполняем диаграмму обращениями к атомарным компонентам через скриптовые выражения.
Реализуем в IdM новые сигналы (события, запросы) и атомарные компоненты, если существующих недостаточно.
Получаем рабочий бизнес-процесс в виде файла с расширением bpmn, проверяем работоспособность.
Устанавливаем готовый 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 принял то или иное решение на конкретном шаге. Без подробного протоколирования бизнес-процессы – это черный ящик.