Custom Session Pools - A Case Study
Kenan Salic
2017-10-02
Developing a high performant intranet, or any solution which uses authentication with an explicit authorization model, with Hippo CMS and the Hippo Delivery Tier (HST) will require some research about the practical use of Custom Session Pools within the HST.
The case we will look at in this documentation is a high performant intranet solution that involves the use of a subject based session and a tremendous amount of security domains (100+). We will show how to work with this combination without dramatically decreasing performance, especially when executing complex queries on faceted navigation.
Previously, we have introduced a subject based session which the developer can set as a property on the mount.
HST also provides Repository level authorization integration. This means the JCR session used during the request will be tightly integrated with the authenticated user's subject.
If you want this subject-based session option, set the following property for a mount:
- hst:subjectbasedsession = true
With the configuration above, the JCR session during the request processing is created with the JCR credentials of the authenticated user's subject per each request. In this case, the JCR session is not borrowed from the internal JCR session pool.
This will make sure that when the visitor logins into the site the website will only show content the visitor is allowed to have access to - according to the security domains configured within the repository.
The problem with subject-based session is that having a tremendous amount of security domains configured will slow down performance because there are several things happening in the background. Lucene queries, which can get quite large, need to be calculated for each user during login. Additionally, the security domains have wildcards which need to be recalculated for each login. For a small user base, these background actions wouldn’t be a problem but having thousands of users logging in simultaneously can break the stability of the application very quickly.
Case Study
For the case we will walk through here, we implemented an intranet solution with over 10,000 users that have been split up into 16 different site groups. These 16 site groups contain a combination of 100+ custom security domains. Each user is a member of only one “site group”(please see the “limitations” section for more information on supporting multiple site groups per user).
We implement authentication with Spring Security, using the HST Spring Security Plugin.
The first approach we tried was using the subject-based session on the mount, as described in the text above. During the load test, we noticed that the stability of the website significantly decreased after 6 users logged in and performed HST queries on faceted navigation nodes.
After an investigation, we found out that the Lucene queries being created after login led to a significant performance decrease which resulted in application instability.
Session Pools
Before we discuss the solution, let us first investigate and elaborate on what a session pool is, how are they used, which ones already exist, and how you can create/configure a new one.
You’ve most likely already worked with session pools. The following ones are probably familiar:
- liveuser
- previewuser
- configuser
- sitewriter
These are users within the CMS which have access to certain parts of the repository. Each of these users belongs to a certain group that has custom security domains that define access restrictions.
For example, the liveuser has access to all live (published) documents in the repository. This is the default user when a visitor requests a page via the Hippo Delivery Tier.
In contrast, the previewuser has access to all preview (unpublished) documents in the repository. This is the default user for requests to the website through the channel manager in Hippo CMS.
Notice these are (technical) users which need to be highly performant.
For each request of the website visitor, a (JCR) session is being retrieved from the appropriate session pools. Which session needs to be retrieved for which user is determined by the ContextCredentialsProvider:
org.hippoecm.hst.site.request.DefaultContextCredentialsProvider
This ContextCredentialsProvider is configured to resemble the (technical) users (liveuser, previewuser, etc.). This is configured within Spring:
# session pooling repository for default live site access. (typically disallowed on unpublished contents.)
default.repository.address = vm://
default.repository.user.name = liveuser
default.repository.pool.name = default
default.repository.password =
<bean id="javax.jcr.Credentials.default" class="org.hippoecm.hst.core.jcr.SimpleCredentialsFactoryBean">
<property name="userId" value="${default.repository.user.name}"/>
<property name="separator" value="${repository.pool.user.name.separator}"/>
<property name="poolName" value="${default.repository.pool.name}"/>
<property name="password" value="${default.repository.password}"/>
<property name="hstJmvEnabledUsers" ref="hstJmvEnabledUsers"/>
</bean>
<!-- Default request context based credentials provider -->
<bean id="org.hippoecm.hst.core.request.ContextCredentialsProvider" class="org.hippoecm.hst.site.request.DefaultContextCredentialsProvider">
<constructor-arg ref="javax.jcr.Credentials.default" />
<constructor-arg ref="javax.jcr.Credentials.preview" />
<constructor-arg ref="javax.jcr.Credentials.writable" />
</bean>
Solution
Now let's look at how we can leverage the high-performing session pools to use in this intranet setup.
Instead of having only the default users (liveuser, previewuser, etc. ). We will create custom session pools for each of our 16 site groups. These custom session pools are configured similarly to the default users in the HST configuration.
To indicate in which group each user belongs to we need to set a flag in the httpsession during login. With the Spring Security Support, we can take complete control of user authentication and perform calculations to determine which session pool is right for each logged in user.
Known limitations
These limitations should be considered and discussed with the product owner before implementing this solution.
- A user can only belong to 1 site group at the time. The sessions of multiple groups cannot be combined. For example, group A has access to content C and group B has access to content D. User X can’t see content C and D at the same time because he can not have a membership to group A and B at the same time. A logged in user always needs to retrieve credentials for the session pool which belongs to a single group.
- Wildcards in the security domain configuration won’t work. In the facet rules within the security domain, it is possible to configure the username wildcard “__user__” to create a domain which can use this wildcard to retrieve nodes from the repository with a facet rule that matches the username of the logged in user.
To summarize: This solution contains the creation of 16 technical users, which belong to 16 (site) groups. When the visitor logs in, they are mapped to 1 of the 16 technical users. This is going to perform much better than a subject-based session solution!
Implementation
The following section shows the implementation of this solution. There are two main parts; Repository and HST.
Repository
Users
First, create a technical user for each site group:
<?xml version="1.0" encoding="UTF-8"?>
<sv:node sv:name="sitegroup-A-liveuser" xmlns:sv="http://www.jcp.org/jcr/sv/1.0">
<sv:property sv:name="jcr:primaryType" sv:type="Name">
<sv:value>hipposys:user</sv:value>
</sv:property>
<sv:property sv:name="hipposys:active" sv:type="Boolean">
<sv:value>true</sv:value>
</sv:property>
<sv:property sv:name="hipposys:passkey" sv:type="String">
<sv:value>jvm://</sv:value>
</sv:property>
<sv:property sv:name="hipposys:securityprovider" sv:type="String">
<sv:value>internal</sv:value>
</sv:property>
<sv:property sv:name="hipposys:system" sv:type="Boolean">
<sv:value>true</sv:value>
</sv:property>
</sv:node>
Please, note that this is a system user and the passkey is provided by the JVM.
Groups
Then add the membership of the user to the group as usual:
<?xml version="1.0" encoding="UTF-8"?>
<sv:node sv:name="sitegroup-A-group" xmlns:sv="http://www.jcp.org/jcr/sv/1.0">
<sv:property sv:name="jcr:primaryType" sv:type="Name">
<sv:value>hipposys:group</sv:value>
</sv:property>
<sv:property sv:multiple="true" sv:name="hipposys:members" sv:type="String">
<sv:value>sitegroup-A-liveuser</sv:value>
</sv:property>
<sv:property sv:name="hipposys:securityprovider" sv:type="String">
<sv:value>internal</sv:value>
</sv:property>
</sv:node>
Domains
Create any custom security domains and add the defined group or user membership for it to work.
HST
The following example adds 1 new session pool. For the case study we’ve discussed, we implemented 16 of these custom session pools.
Custom Session Pools
Add new entries to the hst-config.properties file.
sitegroupAliveuser.repository.address = vm://
sitegroupAliveuser.repository.user.name = sitegroup-A-liveuser
sitegroupAliveuser.repository.pool.name = sitegroupAliveuser
sitegroupAliveuser.repository.password =
Create a new xml file in META-INF/hst-assembly/overrides.
<bean id="addSitegroupALiveuserJvmEnabled" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetObject" ref="hstJmvEnabledUsers"/>
<property name="targetMethod" value="add"/>
<property name="arguments">
<value>${sitegroupAliveuser.repository.user.name}</value>
</property>
</bean>
…
<bean id="javax.jcr.Credentials.sitegroupAliveuser" class="org.hippoecm.hst.core.jcr.SimpleCredentialsFactoryBean"
depends-on="addSitegroupALiveuserJvmEnabled">
<property name="userId" value="${sitegroupAliveuser.repository.user.name}"/>
<property name="separator" value="${repository.pool.user.name.separator}"/>
<property name="poolName" value="${sitegroupAliveuser.repository.pool.name}"/>
<property name="password" value="${sitegroupAliveuser.repository.password}"/>
<property name="hstJmvEnabledUsers" ref="hstJmvEnabledUsers"/>
</bean>
...
Abstract configuration only needs to be added once:
<bean id="_abstractUserSessionPool" abstract="true" class="org.hippoecm.hst.core.jcr.pool.BasicPoolingRepository"
init-method="initialize" destroy-method="close">
<!-- delegated JCR repository -->
<property name="repositoryProviderClassName" value="${repositoryProviderClassName}"/>
<property name="repositoryAddress" value="${default.repository.address}"/>
<property name="defaultCredentialsUserIDSeparator" value="${repository.pool.user.name.separator}"/>
<property name="hstJmvEnabledUsers" ref="hstJmvEnabledUsers"/>
<!-- Pool properties. Refer to the GenericObjectPool of commons-pool library. -->
<property name="maxActive" value="${default.repository.maxActive}"/>
<property name="maxIdle" value="${default.repository.maxIdle}"/>
<property name="minIdle" value="${default.repository.minIdle}"/>
<property name="initialSize" value="${default.repository.initialSize}"/>
<property name="maxWait" value="${default.repository.maxWait}"/>
<property name="whenExhaustedAction" value="${default.repository.whenExhaustedAction}"/>
<property name="testOnBorrow" value="${default.repository.testOnBorrow}"/>
<property name="testOnReturn" value="${default.repository.testOnReturn}"/>
<property name="testWhileIdle" value="${default.repository.testWhileIdle}"/>
<property name="timeBetweenEvictionRunsMillis" value="${default.repository.timeBetweenEvictionRunsMillis}"/>
<property name="numTestsPerEvictionRun" value="${default.repository.numTestsPerEvictionRun}"/>
<property name="minEvictableIdleTimeMillis" value="${default.repository.minEvictableIdleTimeMillis}"/>
<property name="refreshOnPassivate" value="${default.repository.refreshOnPassivate}"/>
<property name="maxRefreshIntervalOnPassivate" value="${sessionPool.maxRefreshIntervalOnPassivate}"/>
<property name="poolingCounter" ref="defaultPoolingCounter"/>
<property name="maxTimeToLiveMillis" value="${default.repository.maxTimeToLiveMillis}"/>
</bean>
<bean id="_sitegroupAliveuserSessionPool" parent="_abstractUserSessionPool" class="org.hippoecm.hst.core.jcr.pool.BasicPoolingRepository"
init-method="initialize" destroy-method="close">
<!-- delegated JCR repository -->
<property name="defaultCredentialsUserID" value="${sitegroupAliveuser.repository.user.name}${repository.pool.user.name.separator}${sitegroupAliveuser.repository.pool.name}"/>
<property name="defaultCredentialsPassword" value="${sitegroupAliveuser.repository.password}"/>
</bean>
<bean id="addSitegroupALiveuserSessionPool" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetObject" ref="javax.jcr.Repository"/>
<property name="targetMethod" value="addRepository"/>
<property name="arguments">
<list>
<ref bean="javax.jcr.Credentials.sitegroupAliveuser"/>
<ref bean="_sitegroupAliveuserSessionPool"/>
</list>
</property>
</bean>
The following snippet needs to be added once, “depends-on” is a comma separated file for each session pool:
<bean id="resourceLifecycleManagementList" class="org.springframework.beans.factory.config.ListFactoryBean"
depends-on="addSitegroupALiveuserSessionPool">
<property name="sourceList">
<bean class="org.springframework.beans.factory.config.PropertyPathFactoryBean">
<property name="targetBeanName" value="javax.jcr.Repository"/>
<property name="propertyPath" value="resourceLifecycleManagements"/>
</bean>
</property>
</bean>
Authentication with Spring Security
In the solution of this case study, we are using Spring Security to customize the authentication and access control within the project. If you would like to see Spring Security in action, the demo project contains an example. Custom Context Credentials Provider
Create an implementation of the ContextCredentialsProvider.
public class CustomContextCredentialsProvider implements ContextCredentialsProvider {
private Credentials defaultCredentials;
private Credentials sitegroupAliveuser;
private Credentials sitegroupBliveuser;
private Credentials defaultCredentialsForPreviewMode;
private Credentials writableCredentials;
private final Map<String, Credentials> credentialsMap = new HashMap<>();
public CustomContextCredentialsProvider(final Credentials defaultCredentials,
final Credentials sitegroupAliveuser,
final Credentials sitegroupBliveuser,
final Credentials defaultCredentialsForPreviewMode,
final Credentials writableCredentials) {
this.defaultCredentials = defaultCredentials;
this.sitegroupAliveuser = sitegroupAliveuser;
this.sitegroupBliveuser = sitegroupBliveuser;
this.defaultCredentialsForPreviewMode = defaultCredentialsForPreviewMode;
this.writableCredentials = writableCredentials;
credentialsMap.put("sitegroup-A-group", this.sitegroupAliveuser);
credentialsMap.put("sitegroup-B-group", this.sitegroupBliveuser);
credentialsMap.put("admin", this.defaultCredentials);
}
public Credentials getDefaultCredentials(HstRequestContext requestContext) {
if (defaultCredentialsForPreviewMode != null && requestContext.isPreview()) {
return defaultCredentialsForPreviewMode;
}
final HttpSession session = requestContext.getServletRequest().getSession(false);
if (session == null || requestContext.getServletRequest().getUserPrincipal() == null) {
return defaultCredentials;
}
final HippoUser user = UserUtils.getUser(requestContext);
String group = user.getGroup();
if (credentialsMap.containsKey(group)) {
return credentialsMap.get(group);
}
throw new CustomSessionPoolException("not allowed to access the credentials map with group " + user.getUsername());
}
public Credentials getWritableCredentials(HstRequestContext requestContext) {
return writableCredentials;
}
}
Add the following code to the same XML file as the configuration for the session pools and overwrite the default ContextCredentialsProvider:
<bean id="org.hippoecm.hst.core.request.ContextCredentialsProvider" class="org.onehippo.demo.security.CustomContextCredentialsProvider">
<constructor-arg ref="javax.jcr.Credentials.default"/>
<constructor-arg ref="javax.jcr.Credentials.sitegroupAliveuser"/>
...
<constructor-arg ref="javax.jcr.Credentials.preview"/>
<constructor-arg ref="javax.jcr.Credentials.writable"/>
</bean>
Demo and Source
https://github.com/ksalic/hippo-custom-session-pool-demo