Making alternate profile selection sticky

In which I share a design for making ad-hoc profile selection sticky in uPortal.

What problem is being solved?

See also:

Users can specify a desired profile key on their login request. Profiles essentially select uPortal themes. So, they select respondr or universality or muniversality or bucky or so. Different themes render the portal in different ways.

In MyUW specifically there is a transition period where universality remains the default theme but users can try out the beta bucky theme which if all goes well will eventually become the default. During this transition period users are invited to try out the new theme but the default remains the old theme.

To communicate their desired profile, users (click links that) put a profile request parameter on the /Login request with a value that keys to a desired theme.

my.wisc.edu/portal/Login?profile=bucky

The portal stores this into the user's session and a SessionAttributeProfileMapperImpl considers that stored profile key when determining the profile mapping.

This amounts to an ad-hoc profile selection, where the hoc is that session. On the next session, the selection is forgotten.

Forgetting the selection can be disconcerting, and new sessions are easy to get. Any time the user hits /Login uPortal helpfully zaps the user's session even if the current session was otherwise alive and well, so there's a path to inadvertently zap back to the default profile in the middle of user interaction. (That session handling is something I hope to work to tweak in a future line of inquiry). If the session expires and the user interacts, then even if the authentication can be re-constituted from an existing single sign-on session or Shibboleth SP session, the newly created session will not reflect the ad-hoc profile selection, because the hoc (old session) is gone.

The problem to be solved is to remember the user's selection until he or she makes another selection.

And, of course, to implement this cleanly and in a way where uPortal adopters can differ on just what profile selection algorithms they desire to apply.

Context - How Profile Mapping works

This problem space is about profile selection, not about storing and modeling actual profiles. What needs modeling and applying here is a user desire for a profile, not the profile itself. That nuance turns out to greatly simply the problem space and the solution.

How Login remembers your desired profile

uPortal is actually partially using Spring Security and the /Login path is fronted by Spring Security which then has a uPortal-specific Spring Security processing filter plugged in. That processing filter reads the requested profile off of the request and stores it into the session.

HttpSession s = request.getSession(true);

final String requestedProfile = request.getParameter(
  LoginController.REQUESTED_PROFILE_KEY);

if (requestedProfile != null) {
  s.setAttribute(
    DEFAULT_SESSION_ATTRIBUTE_NAME, requestedProfile);
} 

(A proposed changeset under review would adjust this a bit in @Autowired-ing in the SessionAttributeProfileMapperImpl and calling upon it to store the requested profile into the session rather than PortalPreAuthenticatedProcessingFilter relying upon knowledge of how that session storage is implemented.)

How SessionAttributeProfileMapperImpl reflects your desired profile

SessionAttributeProfileMapperImpl simply reads the desired profile key back out of the session, mapping it to a profile fname.

  final String requestedProfileKey = 
    (String) session.getAttribute(attributeName);

  if (requestedProfileKey != null) {

    final String profileName =
      mappings.get(requestedProfileKey);

    if (profileName != null) {
      return profileName;
    }

  }

How ProfileMappers are consulted

The short version is that there is a UserInstanceManagerImpl responsible for spinning up UserInstances, and UserInstances contain among other things the actual resolved Profile for the user.

That profile mapper is @Autowired into UserInstanceManager.

How ProfileMappers are configured

The ProfileMapper is configured in userContext.xml.

<bean id="profileMapper"
  class="org.jasig.portal.layout.ChainingProfileMapperImpl">
  <property name="defaultProfileName" value="default" />
  <property name="subMappers">
    <util:list>
      <ref bean="sessionAttributeProfileMapper" />
    </util:list>
  </property>
</bean>

Other profile mappers

Ad-hoc user selection is not necessarily controlling: other profile mappers are available including one that drives selection by user-agent (for defaulting "mobile devices" to a "mobile theme", i.e., the dedicated-mobile-experience approach alternative to the one-responsive-experience approach).

Proposed solution

Generalize /Login registering of profile selection

While the change to make /Login handling rely less on implementation details of how SessionAttributeProfileMapperImpl stores profile desire into the session and to instead rely upon that profile mapper to manage its own session storage was a good step, it is time to generalize further. Rather than having /Login treat that particular profile mapper specially, instead fire a ProfileSelectionEvent and make SessionAttributeProfileMapperImpl handle that event. This makes it feasible to replace or supplement SessionAttributeProfileMapperImpl with alternative implementations that handle that user profile selection differently.

Add a StickyProfileMapper

Add a StickyProfileMapper that implements IProfileMapper and handles the profile selection event that instead of storing the profile selection into the Session instead stores it into the database (via components described next) and that reflects the stored selection when asked about the profile mapping for the user.

Add a Service-Registry-DAO-JPA-implementation for profile selection

Under the uPortal architecture, the way data is stored into a database is via the Service-Registry-DAO-JPA architecture. So, implement that architecture. IProfileMapper is already the Service layer of such an architecture.

Handle selection of "default" specially in that "default" forgets any remembered sticky selection.

Adopters choose profile stickiness through configuration.

Wire up either the SessionAttributeProfileMapperImpl or the StickyProfileMapper or both in userContext.xml.

Characteristics of proposed solution

Meets local requirements

This solution solves MyUW's need to make opting-in to the non-default bucky profile sticky during the Beta period.

Generally useful in uPortal product

The resulting sticky profile mapping implementation is plausibly interesting and useful to other uPortal adopters and becomes part of the toolkit for local wiring together of profile mapping to achieve desired behavior.

Architectural adherence

While the Service-Registry-DAO-JPA architecture creates a slew of objects and layers for what is ultimately storing String-->String pairs, implementing this in this way makes it like other things in uPortal.

Straightforward

The solution keeps to small, few-responsibilities objects that are eminently unit testable. Does not complicate the ChainingProfileMapper implementation (which remains only responsible for delegating to sub-mappers.)

Schema change

The proposed solution requires a database schema change to add the new table that remembers the user's profile selection. Schema changes are annoying, but it seems for the best -- what this feature fundamentally is is storing and applying a new bit of data.

Image credits