Design Security
Security and Identity Management
Flowable relies on the Spring Security framework to provide authentication and access control capabilities. Spring Security is an open source library that provides connectors and adapters to many security standards such as OAuth, SAML, basic authentication, etc. As with all Spring frameworks, it is designed to be highly customizable and adaptable to many security requirements.
Authentication and Authorization
The out of the box security configuration for Flowable provides a simple configuration that can be extended and adapted according to your needs.
Upon an authentication attempt, the user is loaded through the
DesignUserDetailsService
(which is called from DaoAuthenticationProvider
as this class delegates to an instance of the UserDetailsService
) and given that
the user exists, an instance of org.springframework.security.core.userdetails.UserDetails
is created. The UserDetails
object contains the following information:
The username and password of the user
Granted authorities for the user:
Group keys prefixed with
GROUP_
Tenant id prefixed with
TENANT_
After the Spring Security filter chain is executed, the Spring Security Context contains the authenticated user information, which is used for access control or other purposes.
Flowable uses the com.flowable.design.security.spring.DesignSecurityUtils
to retrieve
all the needed information for the Flowable Authorization.
Every action uses com.flowable.design.security.api.DesignSecurityScope
to get the
required information.
DesignSecurityScope#getUserId()
provides the id of the current user.DesignSecurityScope#getTenantId()
provides the tenant id of the current user.DesignSecurityScope#getGroupKeys()
provides the group keys of the current user.DesignSecurityScope#hasAuthority(String)
provides a way to check if the current user has the given authority.DesignSecurityScope#hasAdminRights()
provides a way to check if the current user has admin rights.DesignSecurityScope#hasSuperAdminRights()
provides a way to check if the current user has super admin rights. By default, a user has super admin rights if the user is part of thedefault
tenant and has admin rights.
The DesignSecurityScope
is provided via a bean com.flowable.design.security.api.DesignSecurityScopeProvider
.
The default implementation uses the org.springframework.security.core.Authentication
to get access to the relevant information.
DesignSecurityScope#getUserId()
retrieved viaAuthentication#getName()
.DesignSecurityScope#getTenantId()
the firstGrantedAuthority
that is prefixed withTENANT_
.DesignSecurityScope#getGroupKeys()
all grantedGrantedAuthority
(ies) that have theGROUP_
prefixSecurityScope#hasAuthority(String)
provides a way to check if the current user has the given authority.
For example, if the user has the following GrantedAuthority
(ies)
GROUP_user
GROUP_admin
TENANT_acme
access-reports
The current user group keys are user
and admin
,
and the current user tenant id is acme
.
Administrators
When doing calls over REST Flowable needs to know whether the user doing the request is an admin user or not.
The reason for this is that admin users are allowed to access all data (within their tenant).
For these users it is not checked if they have access to the data via the IdentityLink
(s).
Such users are the technical users used by Flowable Control to get data for a particular Flowable instance.
By default, a user is an admin user if they have the access-admin
granted authority.
However, it is possible to provide a custom logic for determining whether a user is an admin or not.
This can be done by implementing your own DesignSecurityScopeProvider
and providing your own implementation of the DesignSecurityScope
This means that you are in full control to decide how you want to determine what an admin user is.
Super Administrators
Super administrators are admin users which are in the empty or default
tenant.
These users have access to the data across all tenants in the system.
You can also customize this by implementing your own DesignSecurityScopeProvider
and providing your own implementation of the DesignSecurityScope
Customizing the Default Configuration
As other Flowable products, the Design Identity Management (IDM) provides extension
points to override default beans by custom ones. For instance, if in your project,
you provide a custom UserDetailsService, the security auto-configuration
(com.flowable.autoconfigure.design.security.DesignSecurityAutoConfiguration
)
finds the bean and does not use the default bean.
See the following classes for more configuration options:
com.flowable.autoconfigure.design.security.DesignSecurityAutoConfiguration
com.flowable.design.security.spring.userdetails.DesignUserDetailsService
com.flowable.autoconfigure.design.security.FlowableDesignSecurityProperties
In order for Flowable to be able to use the correct userId, group keys, tenant id
then a custom bean implementation of com.flowable.design.security.api.DesignSecurityScopeProvider
should be provided.
Have a look at Custom Security Authentication for more details about how to customize the authentication
and Custom Identity Query Service for more details how to write a custom DesignIdentityQueryService
.
Overriding the Default Identity Query Service
The default DesignIdentityQueryService
is sufficient if the users are synced in the Flowable Database.
However, in most cases you have your own identity store (LDAP, Azure AD, or something else).
In this case you would need to provide your own implementation of the DesignIdentityQueryService
.
In order for your custom DesignIdentityQueryService
to be configured with the DesignEngineConfiguration
,
you need to use an Consumer
as follows:
@Bean
public Consumer<DesignEngineConfiguration> customDesignEngineConfigurer() {
return engineConfiguration -> {
engineConfiguration.setIdentityQueryService(new YourCustomIdentityService());
};
}
Do not define your custom identity query service as a bean.
The DesignIdentityQueryService
is exposed as a bean once the Design engine is created.
For more details about writing your own DesignIdentityQueryService
have a look at Custom Identity Query Service.
Remember Me Services
Flowable provides stateless applications: no information is stored in any of the servers running them. However, sometimes, it is important to store some data to increase the end-user happiness. A good example is user tokens, which are stored by the RememberMe Services in the database so that users do not have to log in again in the case a server is down. The default RememberMe Services of Flowable enrich the tokens with useful information, such as User-Agent and IP addresses, enabling further possibilities for special auditing use cases.
LDAP
This section will explain how to configure your custom Flowable Design application to work with LDAP. For all the configuration properties have a look at the LDAP Installation Guide.
In order to work with LDAP in your application you need to add the following dependencies (on top of the default flowable ones):
<dependency>
<groupId>org.springframework.ldap</groupId>
<artifactId>spring-ldap-core</artifactId>
</dependency>
<!-- LDAP Security needed only if projects need to use the LDAP Security -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
</dependency>
Then in your SecurityConfiguration
you'll need to do the following:
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public DesignLdapAuthenticationProviderFactory ldapAuthenticationProvider() {
return new DesignLdapAuthenticationProviderFactory();
}
@Bean
@Order(10)
public SecurityFilterChain ldapDefaultSecurity(HttpSecurity http, ObjectProvider<DesignHttpSecurityCustomizer> httpSecurityCustomizers) throws Exception {
for (DesignHttpSecurityCustomizer customizer : httpSecurityCustomizers.orderedStream().toList()) {
customizer.customize(http);
}
// ...
return http.build();
}
}
Using the DesignLdapAuthenticationProviderFactory
will make sure that the configuration properties are properly applied for authentication security.
If you need to customize something, you use the DesignLdapAuthenticationProviderFactory
and customize it.
OAuth2
This section will explain how to configure your custom Flowable application to work with OAuth2. For all the configuration properties have a look at the OAuth2 Installation Guide.
Depending on how you want to use OAuth2 for your application you will need to add some or all of the following dependencies (on top of the default flowable ones):
- spring-boot-starter-oauth2-client - When the users will log in to the application by being redirected to the SSO.
- spring-boot-starter-oauth2-resource-server - When the application is accessed using already authenticated users / machines. i.e. machine to machine communication, Flowable Control to Work, Flowable Design to Work, etc.
<!-- OAuth2 Client needed only if projects needs to use OAuth2 Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<!--
OAuth2 Resource Server needed only if projects need to use OAuth2 Security to communicate with the Flowable REST API.
e.g. machine to machine, Flowable Control to Work, Flowable Design to Work
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
Then in your SecurityConfiguration
you'll need to do the following:
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
@Order(10)
public SecurityFilterChain oauthDefaultSecurity(HttpSecurity http, ObjectProvider<DesignHttpSecurityCustomizer> httpSecurityCustomizers) throws Exception {
for (DesignHttpSecurityCustomizer customizer : httpSecurityCustomizers.orderedStream().toList()) {
customizer.customize(http);
}
http.exceptionHandling(exceptionHandling -> exceptionHandling
// Using this entry point as a default entry point if none of the others match (the first default entry point is the default entry point).
// This one will not match any request, so it won't override other defaults from Spring Security
.defaultAuthenticationEntryPointFor(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), RequestMatchers.not(AnyRequestMatcher.INSTANCE))
// using this no op authentication entry point, due to Spring Boot (3.x) no longer using a default error handling
.defaultAuthenticationEntryPointFor((request, response, authException) -> {}, new DispatcherTypeRequestMatcher(DispatcherType.ERROR))
.defaultAuthenticationEntryPointFor(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest"))
);
// We mark all requests as authenticated in order for the redirect to happen when the application is accessed
// We allow the favicon to always be available, since it is invoked by the browser itself
http.authorizeHttpRequests(configurer -> configurer
.requestMatchers(antMatcher("/favicon.ico")).permitAll()
.anyRequest().authenticated()
);
// Currently an HttpSessionSecurityContextRepository is needed for the oauth2 to work
HttpSessionSecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
securityContextRepository.setDisableUrlRewriting(true);
http.securityContext(securityContext -> securityContext.securityContextRepository(securityContextRepository));
http.addFilterAfter(new MatchingRequestParameterNameRemovalRedirectFilter(), RequestCacheAwareFilter.class);
http.oauth2Login(Customizer.withDefaults());
http.oauth2Client(Customizer.withDefaults());
ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
JwtDecoder jwtDecoder = applicationContext
.getBeanProvider(JwtDecoder.class)
.getIfAvailable();
if (jwtDecoder != null) {
// Doing this since Flowable Access token authentication could be enabled through properties.
// if you need to use Bearer authentication, and Flowable Access token then you should register the jwt configurer only if the flowable one isn't already configured
if (http.getConfigurer(FlowableDesignJwtResourceServerConfigurer.class) == null) {
http.oauth2ResourceServer(configurer -> configurer.jwt(Customizer.withDefaults()));
}
}
return http.build();
}
}
Using the FlowableDesignJwtResourceServerConfigurer
will make sure that the configuration properties are applied for authentication security.
The Flowable Design Security auto configurations will make sure that the required mapping properties are applied.
Custom Security Authentication
Flowable relies on the Spring Security framework to provide authentication and access control capabilities. Spring Security is an open source library that provides connectors and adapters to many security standards such as OAuth, SAML, basic authentication, etc. As with all Spring frameworks, it is designed to be highly customizable and adaptable to many security requirements.
This document describes how you can create your own Custom Authentication From Scratch and Custom Authentication With Existing Integration. If you need to create a custom identity query service then have a look at Custom Identity Query Service. We assume that you are already familiar with the basics defined in Security and Identity Management.
Custom Authentication From Scratch
For this example, we assume that you have some kind of Single Sign-On (SSO) system which performs the authorization for the users and attaches the userId, tenantId and groups as HTTP Headers on the incoming requests. Additionally, you don’t have some common libraries in your organization that integrate with Spring Security so you would need to do everything on your own.
These are the headers which are added by your SSO:
Incoming Headers.
X-USER-ID: kermit
X-TENANT-ID: muppets
X-GROUP: Accounting
X-GROUP: IT
X-GROUP: HR
We now need to intercept the request and extract this information into a so-called Authentication
.
Spring Security already has us covered we need to extend the AbstractPreAuthenticatedProcessingFilter
and add our extraction logic.
We will create our own implementation of the principal that would store the information.
CustomPrincipal.
public class CustomPrincipal implements Principal {
protected String userId;
protected String tenantId;
protected Collection<String> groups;
public CustomPrincipal(String userId, String tenantId, Collection<String> groups) {
this.userId = userId;
this.tenantId = tenantId;
this.groups = groups;
}
@Override
public String getName() {
return userId;
}
// getters omitted
}
We are now going to write the authentication filter, which uses the HttpServletRequest
to extract the information from the headers.
Extending from AbstractPreAuthenticatedProcessingFilter
allows us to easily hook in with the Spring Security authentication.
It is going to intercept all requests and perform the authentication via the AuthenticationManager
.
CustomHeaderAuthenticationFilter.
public class CustomHeaderAuthenticationFilter
extends AbstractPreAuthenticatedProcessingFilter {
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
String userId = request.getHeader("X-USER-ID");
String tenantId = request.getHeader("X-TENANT-ID");
Enumeration<String> headersEnumeration = request.getHeaders("X-GROUP-ID");
Set<String> groups = new HashSet<>();
while (headersEnumeration.hasMoreElements()) {
groups.add(headersEnumeration.nextElement());
}
return new CustomPrincipal(userId, tenantId, groups);
}
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
// The request is already authenticated there is nothing we need to do
return "N/A";
}
}
The next step is to write a custom AuthenticationUserDetailsService
that loads the Spring Security UserDetails
from the PreAuthenticationToken
created by our CustomHeaderAuthenticationFilter
.
CustomAuthenticationUserDetailsService.
public class CustomAuthenticationUserDetailsService
implements AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
@Override
public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken token) {
CustomPrincipal customPrincipal = (CustomPrincipal) token.getPrincipal();
Collection<? extends GrantedAuthority> authorities = new ArrayList<>();
// have some custom logic to get more authorities
return User.withUsername(customPrincipal.getUserId())
.password("n/A")
.authorities(authorities)
.build();
}
}
We now have all the needed pieces to make Spring Security work with our authentication.
We are now going to add a custom DesignSecurityScopeProvider
and DesignSecurityScope
so that we can link our custom security with Flowable.
CustomSecurityScope.
public class DesignCustomSecurityScope implements DesignSecurityScope {
protected Authentication authentication;
public CustomSecurityScope(Authentication authentication) {
this.authentication = authentication;
}
@Override
public String getUserId() {
return authentication.getName();
}
@Override
public Set<String> getGroupKeys() {
return new HashSet<>(getCustomPrincipal().getGroups());
}
@Override
public String getTenantId() {
return getCustomPrincipal().getTenantId();
}
@Override
public boolean hasAdminRights() {
return hasAuthority("access-admin")
}
@Override
public boolean hasAuthority(String authority) {
for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
if (Objects.equals(authority, grantedAuthority.getAuthority())) {
return true;
}
}
return false;
}
protected CustomPrincipal getCustomPrincipal() {
return (CustomPrincipal) authentication.getPrincipal();
}
}
CustomSecurityScopeProvider.
public class CustomSecurityProvider implements DesignSecurityScopeProvider {
@Override
public DesignSecurityScope getSecurityScope(Principal principal) {
if (principal instanceof Authentication) {
return new CustomSecurityScope((Authentication) principal);
}
return new FlowablePrincipalSecurityScope(principal);
}
}
We now also have all the pieces to make the Security work with Flowable.
The final step is to actually link all of this together.
To make the linking a bit easier we are going to add a CustomHttpConfigurer
.
public class CustomHttpConfigurer<H extends HttpSecurityBuilder<H>>
extends AbstractHttpConfigurer<CustomHttpConfigurer<H>, H> {
@Override
public void init(H http) {
PreAuthenticatedAuthenticationProvider authenticationProvider
= new PreAuthenticatedAuthenticationProvider();
authenticationProvider.setPreAuthenticatedUserDetailsService(
new CustomAuthenticationUserDetailsService()
);
authenticationProvider = postProcess(authenticationProvider);
http
.authenticationProvider(authenticationProvider)
.setSharedObject(
AuthenticationEntryPoint.class,
new Http403ForbiddenEntryPoint()
);
}
@Override
public void configure(H http) {
AuthenticationManager authenticationManager
= http.getSharedObject(AuthenticationManager.class);
CustomHeaderAuthenticationFilter authenticationFilter
= new CustomHeaderAuthenticationFilter();
authenticationFilter.setAuthenticationManager(authenticationManager);
authenticationFilter = postProcess(authenticationFilter);
http.addFilter(authenticationFilter);
}
}
Register our
CustomAuthenticationUserDetailsService
with the out of the boxPreAuthenticatedAuthenticationProvider
Register the authentication provider with Spring Security
Since all our requests are authenticated via our SSO we send back HTTP 403 if there was no authentication
Create and add our CustomHeaderAuthenticationFilter
Finally, we need to register this as a Spring Configuration. For this we are going to adapt the out of the box security configuration for Flowable so our custom configuration works.
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
@Order(10)
public SecurityFilterChain customDefaultSecurity(HttpSecurity http, ObjectProvider<DesignHttpSecurityCustomizer> httpSecurityCustomizers) throws Exception {
for (DesignHttpSecurityCustomizer customizer : httpSecurityCustomizers.orderedStream().toList()) {
customizer.customize(http);
}
http
.apply(new CustomHttpConfigurer<>())
.and()
.authorizeHttpRequests(configurer -> configurer
// allow context root for all (it triggers the loading of the initial page)
.requestMatchers(antMatcher("/")).permitAll()
.requestMatchers(
antMatcher("/**/*.svg"), antMatcher("/**/*.ico"), antMatcher("/**/*.png"), antMatcher("/**/*.woff2"), antMatcher("/**/*.css"),
antMatcher("/**/*.woff"), antMatcher("/**/*.html"), antMatcher("/**/*.js"),
antMatcher("/**/flowable-frontend-configuration"),
antMatcher("/**/index.html")).permitAll()
.anyRequest().authenticated()
);
return http.build();
}
@Bean
public CustomSecurityProvider() {
return new CustomSecurityProvider();
}
}
Apply our CustomConfigurer to the Spring Security configuration
Register our
CustomSecurityProvider
as a Spring Bean
Custom Authentication With Existing Integration
In the previous section we assumed that your organization does not have any Spring Security Integration.
However, in case that your organization already has some kind of integration then the only thing you need to do is do provide a custom DesignSecurityScopeProvider
and DesignSecurityScope
that would perform the mapping from your Organization Spring Security integration to Flowable.
For the purposes of this example lets imagine that the following stands:
You have a custom implementation of the Spring Security
UserDetails
namedAcmeUserDetails
. It contains thetenantId
for your user.Your groups are mapped as a custom
GroupGrantedAuthority
and that you have otherGrantedAuthority
(ies) which represent something else for your organization.
For this we will have the following implementations
AcmeSecurityScope.
public class AcmeSecurityScope implements DesignSecurityScope {
protected Authentication authentication;
public AcmeSecurityScope(Authentication authentication) {
this.authentication = authentication;
}
@Override
public String getUserId() {
return authentication.getName();
}
@Override
public Set<String> getGroupKeys() {
Set<String> groupKeys = new HashSet<>();
for (GrantedAuthority authority: authentication.getGrantedAuthorities()) {
if (authority instanceof GroupGrantedAuthority) {
groupKeys.add(((GroupGrantedAuthority) authority).getGroupId());
}
}
return groupKeys;
}
@Override
public String getTenantId() {
return getAcmeUserDetails().getTenantId();
}
@Override
public boolean hasAdminRights() {
return hasAuthority("access-admin")
}
@Override
public boolean hasAuthority(String authority) {
for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
if (Objects.equals(authority, grantedAuthority.getAuthority())) {
return true;
}
}
return false;
}
protected AcmeUserDetails getAcmeUserDetails() {
return (AcmeUserDetails) authentication.getDetails();
}
}
AcmeSecurityScopeProvider.
public class AcmeSecurityProvider implements DesignSecurityScopeProvider {
@Override
public DesignSecurityScope getSecurityScope(Principal principal) {
if (principal instanceof Authentication) {
return new AcmeSecurityScope((Authentication) principal);
}
return new FlowablePrincipalSecurityScope(principal);
}
}
Finally, we need to register this as a Spring Configuration.
You would need to consult with your organization documentation for how to configure your security configuration.
The only thing needed for the integration with Flowable to work is to have the AcmeSecurityScopeProvider
registered as a Spring Bean
@Configuration
public class AcmeFlowableConfiguration {
@Bean
public AcmeSecurityProvider() {
return new AcmeSecurityProvider();
}
}
Custom Identity Query Service
This document describes how you can create your own Custom DesignIdentityQueryService
and thus avoid syncing your users into the Flowable database.
We assume that you are already familiar with the basics defined in Security and Identity Management
and Overriding the Default Identity Service.
If you need to create a custom authentication then have a look at Custom Security Authentication.
For this example, we are going to assume that your users are going to be retrieved from LDAP and we are going to use Spring LDAP to do the querying.
:::node Flowable already provides out-of-the-box support for LDAP, but this is still only an example :::
AcmeDesignIdentityQueryService.
public class AcmeDesignIdentityQueryService implements DesignIdentityQueryService {
protected LdapTemplate ldapTemplate;
public AcmeDesignIdentityQueryService(LdapTemplate ldapTemplate) {
super(idmEngineConfiguration);
this.ldapTemplate = ldapTemplate;
}
// Retrieve methods
@Override
public DesignUserQuery createUserQuery() {
return new AcmeUserQuery(ldapTemplate);
}
@Override
public Collection<String> findGroupsForUser(String userId) {
return Collections.emptyList();
}
}
For the purposes of this example we are going to show the implementation of only of
subsets of methods of the AcmeUserQuery
.
AcmePlatformUserQuery.
public class AcmeUserQuery
extends AbstractQuery<DesignUserQuery, DesignUser>
implements DesignUserQuery {
protected LdapTemplate ldapTemplate;
protected String userId;
protected String searchText;
public AcmePlatformUserQuery(LdapTemplate ldapTemplate) {
this.ldapTemplate = ldapTemplate;
}
@Override
public DesignUserQuery id(String id) {
userId = id;
return this;
}
@Override
public DesignUserQuery searchText(String searchText) {
this.searchText = searchText;
return this;
}
// ...
@Override
public long executeCount(CommandContext commandContext) {
return executeList(commandContext).size();
}
@Override
public List<PlatformUser> executeList(CommandContext commandContext) {
LdapQueryBuilder queryBuilder = LdapQueryBuilder.query();
ContainerCriteria criteria = queryBuilder.where("objectClass").is("user");
if (userId != null) {
criteria = criteria.and("sAMAccountName").is(userId);
}
if (searchText != null) {
criteria = criteria.and("displayName").like(displayNameLike.replace('%', '*'));
}
return ldapTemplate.search(criteria, new AttributesMapper<DesignUser>() {
@Override
public PlatformUser mapFromAttributes(Attributes attributes) throws NamingException {
MyDesignUser user = new MyDesignUser();
user.setId(getAttribute("sAMAccountName", attributes));
user.setFirstName(getAttribute("givenName", attributes));
user.setLastName(getAttribute("sn", attributes));
user.setDisplayName(getAttribute("displayName", attributes));
user.setEmail(getAttribute("mail", attributes));
user.setTenantId(getAttribute("tenant", attributes));
return user;
}
});
}
protected static String getAttribute(String attributeName, Attributes attributes) {
Attribute attribute = attributes.get(attributeName);
if (attribute != null && attribute.size() > 0) {
try {
return (String) attribute.get();
} catch (NamingException e) {
throw new FlowableException("Exception getting attribute " + attributeName);
}
} else {
return null;
}
}
protected static class MyDesignUser implements DesignUser {
protected String id;
protected String tenantId;
protected String password;
protected String firstName;
protected String lastName;
protected String displayName;
protected String email;
// getters and setters omitted for brevity
}
}
We now need to register our AcmeDesignIdentityQueryService
with the Design Engine.
This is done by using the Consumer
as follows:
Register AcmeDesignIdentityQueryService
with the Design Engine.
@Configuration
public class AcmeFlowableConfiguration {
@Bean
public Consumer<DesignEngineConfiguration> acmeDesignEngineConfigurer(LdapTemplate ldapTemplate) {
return engineConfiguration -> {
engineConfiguration.setIdentityQueryService(new AcmeDesignIdentityQueryService(ldapTemplate));
};
}
}