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:
- Pull request proffering code implementing this design
- JIRA issue UP-4223
- uportal-dev@ thread
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 UserInstance
s, and UserInstance
s 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.