/**
* Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright ownership. Apereo
* licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the License at the
* following location:
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apereo.portal.layout.profile;
import com.google.common.collect.ImmutableMap;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.Validate;
import org.apereo.portal.security.IPerson;
import org.apereo.portal.security.IdentitySwapperManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.context.ApplicationListener;
/**
* Profile mapper implementing sticky profile selection.
*
* <p>Stores selected profile fname (as looked up in injected Map keyed by profile key) into an
* injected IProfileSelectionRegistry, but does not store under impersonation.
*
* <p>Translates a configured profile key (default: "default") to mean apathy, which is to say
* clearing any stored profile selection. This allows users selecting the default profile key (as
* "default") to have no stored selection at all and thereby get the default profile selection
* behavior, where just what that default selection behavior in the rest of the profile mapping
* chain might change over time. This apathy translation takes priority over key->fname mapping --
* if a key is both a key in the map and is the key configured to mean apathy, it is treated as
* meaning apathy.
*
* <p>Reflects stored profile selection from the IProfileSelectionRegistry.
*
* <p>DOES NOT HAVE A DEFAULT PROFILE FNAME TO RETURN. If this mapper doesn't have anything
* interesting to say about what user profile a user ought to have, it simply returns null, which is
* the API defined way to say nothing.
*
* <p>Subsequent or upstream profile mappers can fall back on a default, see also
* ChainingProfileMapperImpl.
*
* <p>Fails gracefully. If persisted profile selection if any cannot be determined, logs and returns
* null indicating no available opinion about desired profile mapping.
*
* <p>Typically this mapper will be in the profile mapping chain within a ChainingProfileMapperImpl.
*
* @since 4.2
*/
public class StickyProfileMapperImpl
implements IProfileMapper, ApplicationListener<ProfileSelectionEvent> {
protected final Logger logger = LoggerFactory.getLogger(getClass());
// autowired
private IProfileSelectionRegistry profileSelectionRegistry;
// dependency injected as "mappings", required.
private Map<String, String> immutableMappings;
// autowired
private IdentitySwapperManager identitySwapperManager;
// dependency injected, optional, may be null.
private String profileKeyForNoSelection = "default";
/**
* Log a warning when configured such that a profile key both means apathy and means a
* particular profile fname.
*/
@PostConstruct
public void warnOnApathyKeyInMappings() {
if (null != profileKeyForNoSelection
&& immutableMappings.containsKey(profileKeyForNoSelection)) {
logger.warn(
"Configured to treat profile key {} as apathy, "
+ "yet also configured to map that key to profile fname {}. Apathy wins. "
+ "This is likely just fine, but it might be a misconfiguration.",
profileKeyForNoSelection,
immutableMappings.get(profileKeyForNoSelection));
}
}
@Override
public void onApplicationEvent(ProfileSelectionEvent event) {
try {
final String userName = event.getPerson().getUserName();
if (event.getPerson().isGuest()) {
logger.warn(
"Ignoring a profile selection event fired by guest user. "
+ "Should the Guest user be firing profile selections in your uPortal "
+ "implementation? Likely not.");
return;
}
if (identitySwapperManager.isImpersonating(event.getRequest())) {
logger.debug(
"Ignoring selection of profile by key {} in the context of user {} because impersonated.",
event.getRequestedProfileKey(),
userName);
return;
}
if (profileKeyForNoSelection != null
&& profileKeyForNoSelection.equals(event.getRequestedProfileKey())) {
logger.trace(
"Translating {} selection of profile key {} to apathy about profile selection.",
userName,
event.getRequestedProfileKey());
profileSelectionRegistry.registerUserProfileSelection(userName, null);
return;
}
if (!immutableMappings.containsKey(event.getRequestedProfileKey())) {
logger.warn(
"User desired a profile by a key {} that does not map to any profile fname. Ignoring.",
event.getRequestedProfileKey());
return;
}
final String profileFName = immutableMappings.get(event.getRequestedProfileKey());
logger.trace(
"Storing {} selection of profile fname {} (keyed by profile key {})",
userName,
profileFName,
event.getRequestedProfileKey());
profileSelectionRegistry.registerUserProfileSelection(userName, profileFName);
} catch (final Exception e) {
logger.error("Something went wrong handling profile selection event " + event);
}
}
@Override
public String getProfileFname(IPerson person, HttpServletRequest request) {
Validate.notNull(person, "Cannot get profile fname for a null person.");
Validate.notNull(request, "Cannot get profile fname for a null request.");
final String userName = person.getUserName();
Validate.notNull(userName, "Cannot get profile fname for a null username.");
try {
return this.profileSelectionRegistry.profileSelectionForUser(userName);
} catch (Exception e) {
logger.error(
"Failed to read persisted profile selection for user "
+ userName
+ " if any. Ignoring.",
e);
}
return null; // default to no opinion if registry lookup failed.
}
@Autowired
public void setProfileSelectionRegistry(
final IProfileSelectionRegistry profileSelectionRegistry) {
this.profileSelectionRegistry = profileSelectionRegistry;
}
@Required
public void setMappings(Map<String, String> mappings) {
Validate.notNull(mappings);
this.immutableMappings = ImmutableMap.copyOf(mappings);
}
@Autowired
public void setIdentitySwapperManager(final IdentitySwapperManager manager) {
this.identitySwapperManager = manager;
}
/**
* Set the profile key that should be translated to "no selection".
*
* <p>Optional. Setting to null means no key will have this behavior. Defaults to "default".
*
* <p>Typically, this will be the value "default". "default" will typically both map to whatever
* profile fname is your default (so, "resopndr") AND a user selecting "default" (rather than
* "respondr") is saying "I have no selection and want to accept the default behavior", NOT "I
* prefer whatever default maps to right now forver even if default changes in the future."
*
* @param profileKeyForNoSelection potentially null key that you want to mean apathy about
* profile selection
*/
public void setProfileKeyForNoSelection(final String profileKeyForNoSelection) {
this.profileKeyForNoSelection = profileKeyForNoSelection;
}
}