Migrating jBPM images secured by LDAP to Elytron

“It’s like having wings, like flying sometimes because you go off into another realm” (Paul Rodgers)

Elytron is the new security framework offered by JBoss EAP/Wildfly, which tries to unify security management and application access in a single subsystem. 

Legacy security subsystem has been deprecated and maybe removed or limited in future versions of JBoss EAP/Wildfly, while now it’s shipping Elytron as its replacement. In this post, we cover how to migrate current jBPM images for kie-server-showcase and jbpm-server-full (includes also Business-Central) from legacy PicketBox (with security subsystem based on JAAS login modules) to Elytron.

The new images should incorporate the configuration for LDAP authentication and authorization instead of the default one which is properties-based. 

For each image, we are going to follow a different strategy:

  • Partial migration: maintains the legacy login modules at the security subsystem but exposes them to Elytron. 
  • Full migration: Login modules are completely replaced by Security Domain at Elytron.

All the code and configuration for these examples can be found here.

Environment setup

Our test class (with scenarios for testing authentication and process variable change authorization in jbpm) will make use of testcontainers:

  • openLDAP populated with ldif (LDAP Data Interchange Format) containing fixture;
  • KIE Server plus a business application, that will be built on-the-fly, with a multi-stage strategy in the dockerfile:
    • First, maven installs the kjar (other option would have been to fetch it from GitHub);
    • Then, the jboss-cli scripts tune standalone configuration including LDAP support and Elytron;

In this setup, both containers will share the same network and will communicate with each other using the network-alias.

INFO: The business application used in the test is the same described in the following post, but it’s now using Wildfly: Easing The Keycloak Integration Tests In Kie Server (With Testcontainers).

LDAP structure 

For this example, we will use osixia/openldap image as the LDAP backend. We set up  the following users and roles structure within it:

We will start the openLDAP container with following environment variables:

withEnv("LDAP_DOMAIN","jbpm.org");
withEnv("LDAP_ADMIN_PASSWORD","admin"); //default

On the jBPM side, we need to define two files for LDAP authentication and role mapping:

  • jbpm.user.info.properties:
ldap.user.ctx=ou\=People,dc\=jbpm,dc\=org
ldap.role.ctx=ou\=Roles,dc\=jbpm,dc\=org
ldap.user.filter=(uid\={0})
ldap.role.filter=(cn\={0})
  • jbpm.usergroup.callback.properties:
ldap.user.ctx=ou\=People,dc\=jbpm,dc\=org
ldap.role.ctx=ou\=Roles,dc\=jbpm,dc\=org
ldap.user.roles.ctx=ou\=Roles,dc\=jbpm,dc\=org
ldap.user.filter=(uid\={0})
ldap.role.filter=(cn\={0})
ldap.user.roles.filter=(member\={0})
ldap.bind.user=cn\=admin,dc\=jbpm,dc\=org
ldap.bind.pwd=admin
java.naming.provider.url=ldap://ldap-alias:389

TIP: Notice that if the LDAP server doesn’t allow anonymous binding (as in the current image), then ldap.bind.user and ldap.bind.pwd parameters are mandatory in this file.

Partial migration

In this case, we are going to use jbpm-server-full image as it uses KieLoginModule for business-central.war and jbpm-casemgmt.war deployments. The KieLoginModule is in charge of keeping BASIC Authorization header as a principal for the upcoming REST API invocations from these clients.

So, the idea is to add a new legacy login module for LDAP auth, belonging to WildFly’s security subsystem, and then expose this domain as an Elytron security realm so that it can be part of the Elytron subsystem.

We’ll do these actions by using the jboss-cli script:  

TIP: jboss-cli is a script available by default in WildFly’s bin directory. You can find it in .sh and .bat files, so you can run on Unix based OS and Windows respectively.

1.- Let’s define a LdapExtLoginModule that matches our LDAP configuration:

/subsystem=security/security-domain=other/authentication=classic/login-module=LdapExtended:add(code="org.jboss.security.auth.spi.LdapExtLoginModule", flag=required, module-options=[ \
   ("searchScope" => "SUBTREE_SCOPE"), \
   ("java.naming.factory.initial" => "com.sun.jndi.ldap.LdapCtxFactory"), \
   ("java.naming.provider.url" => "ldap://ldap-alias:389"), \
   ("roleAttributeIsDN" => "true"), \
   ("rolesCtxDN" => "ou=Roles,dc=jbpm,dc=org"), \
   ("roleFilter" => "(member=uid={0},ou=People,dc=jbpm,dc=org)"), \
   ("roleNameAttributeID" => "cn"), \
   ("searchTimeLimit" => "5000"), \
   ("java.naming.security.authentication" => "simple"), \
   ("roleRecursion" => "0"), \
   ("java.naming.referral" => "follow"), \
   ("bindDN" => "cn=admin,dc=jbpm,dc=org"), \
   ("bindCredential" => "admin"), \
   ("baseCtxDN" => "ou=People,dc=jbpm,dc=org"), \
   ("allowEmptyPasswords" => "false"), \
   ("throwValidateError" => "true"), \
   ("baseFilter" => "(uid={0})")])

Notice that the security-domain has to be called other because it is the same name protected by the KIE application security domain, as you can see inthe images, in the file jboss-web.xml.

This name is the same as preconfigured security domain for other login modules, so it’s better to remove these legacy ones:

/subsystem=security/security-domain=other/authentication=classic/login-module=UsersRoles:remove
/subsystem=security/security-domain=other/authentication=classic/login-module=Remoting:remove
/subsystem=security/security-domain=other/authentication=classic/login-module=RealmDirect:remove

2.- Next, we will link legacy security domain (other) with a new elytron-realm, that we are going to call LegacyRealm:

/subsystem=security/elytron-realm=LegacyRealm:add(legacy-jaas-config=other)

This way we are creating a dependency from the legacy security subsystem into the elytron subsystem.

3.- This LegacyRealm will be part of the new elytron security domain we are going to name as KIEDomain:

/subsystem=elytron/security-domain=KIEDomain:add(realms=[{realm=LegacyRealm}], default-realm=LegacyRealm, permission-mapper=default-permission-mapper)

TIP: In this case, there is no need for a simple-role-decoder to associate roles, as these ones are retrieved by legacy login modules.

4.- Configure an http-authentication-factory (here called ldap-http-auth) for the KIEDomain and add BASIC (linked to LegacyRealm) and FORM authentication mechanisms used by KIE application to it.

/subsystem=elytron/http-authentication-factory=ldap-http-auth:add(http-server-mechanism-factory=global,security-domain=KIEDomain,mechanism-configurations=[{mechanism-name=BASIC,mechanism-realm-configurations=[{realm-name=LegacyRealm}]}, {mechanism-name=FORM}])

5.- Next, add it to undertow subsystem:

/subsystem=undertow/application-security-domain=KIEDomain:add(http-authentication-factory=ldap-http-auth)
/subsystem=undertow:write-attribute(name=default-security-domain, value=KIEDomain)

It’s time to check that everything worked fine: at runtime, from jboss-cli, read the protected deployments (remember that other is the name for the security-domain in the jboss-web.xml of these wars):

/subsystem=undertow/application-security-domain=other:read-resource(include-runtime=true)
{
    "outcome" => "success",
    "result" => {
        "enable-jacc" => false,
        "enable-jaspi" => true,
        "http-authentication-factory" => "ldap-http-auth",
        "integrated-jaspi" => true,
        "override-deployment-config" => false,
        "referencing-deployments" => [
            "jbpm-casemgmt.war",
            "business-central.war",
            "kie-server.war"
        ],
        "security-domain" => undefined,
        "setting" => undefined
    }
}

For the authorization scenarios, the authenticated subject should contain the principals represented on the image below. These are populated by LoginModules, and will be used by JACC mechanism to obtain the roles for the IdentityProvider:

Full migration

In the case of kie-server-showcase image, only the kie-server.war is present (no KieLoginModule dependencies) and therefore, it’s possible to make a full migration to Elytron.

Elytron is based on a security-domain concept,  in other words, on the representation of a security policy. It is backed by security-realm/s, and resources to make transformations (role-decoder, permission-mapper and others).

In this practical example, we are going to use Elytron LDAP Security Realm to access LDAP backend and verify credentials as well as obtain attributes associated with an identity.

More complex scenarios would allow having several security realms, and by means of a security-mapper, determine which attributes would be retrieved from each security realm.

1.- First, let’s remove the security-domain called other at legacy security subsystem, as it will be no longer used:

/subsystem=security/security-domain=other:remove

2.- Let’s add elytron subsystem from scratch (if not present):

/extension=org.wildfly.extension.elytron:add
/subsystem=elytron:add

3.- Define the directory context to connect with LDAP and the LDAP Realm into Elytron:

/subsystem=elytron/dir-context=ldap-connection:add(url=ldap://ldap-alias:389, principal="cn=admin,dc=jbpm,dc=org", credential-reference={clear-text=admin})
/subsystem=elytron/ldap-realm="KieLdap":add(dir-context=ldap-connection, \
direct-verification=true, \
identity-mapping={search-base-dn="ou=People,dc=jbpm,dc=org", \
rdn-identifier="uid", \
attribute-mapping=[{filter-base-dn="ou=Roles,dc=jbpm,dc=org",filter="(member=uid={0},ou=People,dc=jbpm,dc=org)",from="cn",to="Roles"}]})

Notice that the LDAP connection needs the principal (bindDN) and its password as the used LDAP server doesn’t allow anonymous binding.

Retrieved roles are mapped from “cn” to “Roles”, where the RoleDecoder will take them.

This RoleDecoder component (as its name indicates) is in charge of decoding user’s roles.

Our simple-role-decoder (from-roles-attribute) is pretty straightforward: roles are obtained directly from the attribute “Roles”.

<simple-role-decoder name="from-roles-attribute" attribute="Roles"/>

4.- Create the security domain in Elytron, named KIEDomain, (any name is valid, as we will map it later to the one defined at application level) and add it the previous LDAP realm, and the default-permission-mapper:

/subsystem=elytron/security-domain=KIEDomain:add(realms=[{realm=KieLdap,role-decoder=from-roles-attribute}], default-realm="KieLdap", permission-mapper=default-permission-mapper)

TIP: The default-permission-mapper gives “login permission” to all users but the one with anonymous principal, excluded for login. This means that it doesn’t matter if the verification with the backend LDAP is successful (and valid roles), login action won’t be allowed.

<permission-mapping>
    <principal name="anonymous"/>
    <!-- No permissions: Deny any permission to anonymous! -->
</permission-mapping>

It will produce following logs:

Identity [anonymous] attributes are:
    Attribute [Roles] value [user].
Authorizing principal anonymous.
Authorizing against the following attributes: [Roles] => [user]
Permission mapping: identity [anonymous] with roles [user] implies ("org.wildfly.security.auth.permission.LoginPermission" "") = false
Authorization failed - identity does not have required LoginPermission

5.- Next, we need to define the HTTP authentication factory: for kie-server, it’s needed to link the mechanisms for BASIC and FORM authentications:

/subsystem=elytron/http-authentication-factory=ldap-http-auth:add(http-server-mechanism-factory=global,security-domain=KIEDomain,mechanism-configurations=[{mechanism-name=BASIC,mechanism-realm-configurations=[{realm-name=KieLdap}]}, {mechanism-name=FORM}])

6.- Map the application security domain (other, as it is the one specified at jboss-web.xml) to our Elytron security domain (KIEDomain) for the undertow and ejb3 subsystems:

/subsystem=undertow/application-security-domain=other:add(security-domain=KIEDomain)

/subsystem=ejb3/application-security-domain=other:add(security-domain=KIEDomain)

7.- Update the messaging-activemq (JMS) to point to our Elytron security domain (KIEDomain) and undefine (remove) the default security domain given by WildFly:

/subsystem=messaging-activemq/server=default:write-attribute(name=elytron-domain, value=KIEDomain)

/subsystem=messaging-activemq/server=default:undefine-attribute(name=security-domain)

8.- Disable JACC from legacy security subsystem and enable it at elytron by adding the default policy:

/subsystem=security:write-attribute(name=initialize-jacc, value=false)

/subsystem=elytron/policy=jacc:add(jacc-policy={})

That’s all. Now, let’s see how it works: After a request to the KIE server is filtered and assigned to HTTP mechanism, it’s assigned to the KieLdap Realm. Once a user has been authenticated against LDAP retrieving its roles, the security domain produces a security identity as you can see on the logs below:

Obtaining authorization identity attributes for principal [Bartlet]:
Identity [Bartlet] attributes are:
    Attribute [Roles] value [President].
    Attribute [Roles] value [kie-server].

These roles will be retrieved by JACC IdentityProvider to authorize actions inside KIE server.

Conclusion

Legacy security subsystem has been deprecated from EAP/Wildfly, and in the future, it will be totally removed. Then, Elytron will become the one and unified subsystem for authentication and authorization.

For easing the transition, a partial migration is offered to link both subsystems, but the full migration is preferred. KIE server is ready to migrate with a few jboss-cli operations. Give it a try, really worth it!

Featured photo by Martin Cimbalek

Author

5 1 vote
Article Rating
Subscribe
Notify of
guest
3 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments