/**
* Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.core.user.impl;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.Permission;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.MapCache;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.util.SoftHashMap;
import org.threeten.bp.ZoneId;
import com.google.common.collect.ImmutableList;
import com.opengamma.DataNotFoundException;
import com.opengamma.core.change.ChangeEvent;
import com.opengamma.core.change.ChangeListener;
import com.opengamma.core.change.ChangeType;
import com.opengamma.core.user.DateStyle;
import com.opengamma.core.user.TimeStyle;
import com.opengamma.core.user.UserAccount;
import com.opengamma.core.user.UserPrincipals;
import com.opengamma.core.user.UserProfile;
import com.opengamma.core.user.UserSource;
import com.opengamma.id.ExternalIdBundle;
import com.opengamma.util.ArgumentChecker;
import com.opengamma.util.auth.AuthUtils;
import com.opengamma.util.auth.ShiroPermissionResolver;
/**
* A security {@code Realm} that accesses the user source.
* <p>
* The {@code UserSource} insulates the main application from Apache Shiro.
*/
public class UserSourceRealm extends AuthorizingRealm {
/**
* The user profiles.
* This cache operates with {@code ProxyProfile} to ensure that changes to a user are correctly propagated.
*/
private final MapCache<String, UserProfile> _profiles = new MapCache<>("profiles", new SoftHashMap<String, UserProfile>());
/**
* The user principals.
* This cache operates with {@code ProxyPrincipals} to ensure that changes to a user are correctly propagated.
*/
private final MapCache<String, UserPrincipals> _principals = new MapCache<>("principals", new SoftHashMap<String, UserPrincipals>());
/**
* The user master.
*/
private final UserSource _userSource;
/**
* Creates an instance.
*
* @param userSource the user source, not null
*/
public UserSourceRealm(UserSource userSource) {
setName("UserSourceRealm");
_userSource = ArgumentChecker.notNull(userSource, "userSource");
// clear everything if any user changed
_userSource.changeManager().addChangeListener(new ChangeListener() {
@Override
public void entityChanged(ChangeEvent event) {
if (event.getType() == ChangeType.CHANGED || event.getType() == ChangeType.REMOVED) {
Cache<Object, AuthenticationInfo> authnCache = getAuthenticationCache();
if (authnCache != null) {
authnCache.clear();
}
Cache<Object, AuthorizationInfo> authzCache = getAuthorizationCache();
if (authzCache != null) {
authzCache.clear();
}
_profiles.clear();
_principals.clear();
}
}
});
}
//-------------------------------------------------------------------------
/**
* Gets the user profile.
* <p>
* This method binds the proxy profile that is stored in the user session
* to the real profile stored in the cache.
*
* @param userName the user name, not null
* @return the user profile, null if not found
*/
UserProfile getUserProfile(String userName) {
UserProfile profile = _profiles.get(userName);
if (profile == null) {
try {
profile = _userSource.getAccount(userName).getProfile();
_profiles.put(userName, profile);
} catch (DataNotFoundException ex) {
// ignored
}
}
return profile;
}
/**
* Gets the user principals.
* <p>
* This method binds the proxy profile that is stored in the user session
* to the real profile stored in the cache.
*
* @param userName the user name, not null
* @return the user principals, null if not found
*/
UserPrincipals getUserPrincipals(String userName) {
UserPrincipals principals = _principals.get(userName);
if (principals == null) {
try {
UserAccount account = _userSource.getAccount(userName);
_principals.put(userName, SimpleUserPrincipals.from(account));
} catch (DataNotFoundException ex) {
// ignored
}
}
return principals;
}
//-------------------------------------------------------------------------
@Override
public boolean isAuthenticationCachingEnabled() {
// additional work performed in doGetAuthenticationInfo() which must not be skipped by caching
return false;
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
try {
// load and validate
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String enteredUserName = upToken.getUsername();
UserAccount account = loadUserByName(enteredUserName);
account.getStatus().check();
// make data available in the session
String userName = account.getUserName();
_profiles.put(userName, account.getProfile());
_principals.put(userName, SimpleUserPrincipals.from(account));
AuthUtils.getSubject().getSession().setAttribute(UserProfile.ATTRIBUTE_KEY, new ProxyProfile(userName));
AuthUtils.getSubject().getSession().setAttribute(UserPrincipals.ATTRIBUTE_KEY, new ProxyPrincipals(userName, upToken.getHost()));
// return Shiro data
SimplePrincipalCollection principals = new SimplePrincipalCollection();
principals.add(userName, getName());
return new SimpleAuthenticationInfo(principals, account.getPasswordHash());
} catch (AuthenticationException ex) {
throw ex;
} catch (RuntimeException ex) {
throw new AuthenticationException("Unable to load authentication data: " + token, ex);
}
}
@Override
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
// cleanup after login failure
// Apache Shiro should provide a protected method to handle this better
try {
super.assertCredentialsMatch(token, info);
} catch (AuthenticationException ex) {
String userName = info.getPrincipals().getPrimaryPrincipal().toString();
_profiles.remove(userName);
_principals.remove(userName);
AuthUtils.getSubject().getSession().removeAttribute(UserProfile.ATTRIBUTE_KEY);
AuthUtils.getSubject().getSession().removeAttribute(UserPrincipals.ATTRIBUTE_KEY);
throw ex;
}
}
//-------------------------------------------------------------------------
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
if (principals == null) {
throw new AuthorizationException("PrincipalCollection must not be null");
}
try {
// try UniqueId
Collection<String> userNames = principals.byType(String.class);
if (userNames.size() == 0) {
return null;
}
if (userNames.size() > 1) {
throw new AuthorizationException("PrincipalCollection must not contain two UserAccount instances");
}
String userName = userNames.iterator().next();
UserAccount account = loadUserByName(userName);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRoles(account.getRoles());
for (String permStr : account.getPermissions()) {
info.addObjectPermission(getPermissionResolver().resolvePermission(permStr));
}
return info;
} catch (AuthorizationException ex) {
throw ex;
} catch (RuntimeException ex) {
throw new AuthorizationException("Unable to load authorization data: " + principals, ex);
}
}
private UserAccount loadUserByName(String userName) {
try {
return _userSource.getAccount(userName);
} catch (DataNotFoundException ex) {
throw new UnknownAccountException("User not found: " + userName, ex);
}
}
//-------------------------------------------------------------------------
/**
* The proxy profile.
*/
class ProxyProfile implements UserProfile {
private final String _userName;
ProxyProfile(String userName) {
_userName = userName;
}
@Override
public String getDisplayName() {
return getUserProfile(_userName).getDisplayName();
}
@Override
public Locale getLocale() {
return getUserProfile(_userName).getLocale();
}
@Override
public ZoneId getZone() {
return getUserProfile(_userName).getZone();
}
@Override
public DateStyle getDateStyle() {
return getUserProfile(_userName).getDateStyle();
}
@Override
public TimeStyle getTimeStyle() {
return getUserProfile(_userName).getTimeStyle();
}
@Override
public Map<String, String> getExtensions() {
return getUserProfile(_userName).getExtensions();
}
@Override
public String toString() {
return String.format("ProxyProfile[%s]", _userName);
}
}
//-------------------------------------------------------------------------
/**
* The proxy principals.
*/
class ProxyPrincipals implements UserPrincipals {
private final String _userName;
private final String _networkAddress;
ProxyPrincipals(String userName, String networkAddress) {
_userName = userName;
_networkAddress = networkAddress;
}
@Override
public String getUserName() {
return getUserPrincipals(_userName).getUserName();
}
@Override
public ExternalIdBundle getAlternateIds() {
return getUserPrincipals(_userName).getAlternateIds();
}
@Override
public String getNetworkAddress() {
return _networkAddress;
}
@Override
public String getEmailAddress() {
return getUserPrincipals(_userName).getEmailAddress();
}
@Override
public String toString() {
return String.format("ProxyPrincipals[%s]", _userName);
}
}
//-------------------------------------------------------------------------
// override Authorizer permission methods
// all interesting methods are overridden to insulate against changes in superclass
// the array versions of the methods are not overridden as they are not used
@Override
public ShiroPermissionResolver getPermissionResolver() {
return (ShiroPermissionResolver) super.getPermissionResolver();
}
@Override
public boolean isPermitted(PrincipalCollection subjectPrincipal, String requiredPermission) {
Permission required = getPermissionResolver().resolvePermission(requiredPermission);
return isPermitted(subjectPrincipal, required);
}
@Override
public boolean isPermitted(PrincipalCollection subjectPrincipal, Permission requiredPermission) {
return isPermittedAll(subjectPrincipal, ImmutableList.of(requiredPermission));
}
@Override
public boolean isPermittedAll(PrincipalCollection subjectPrincipal, String... requiredPermissions) {
if (requiredPermissions.length == 0) {
return true;
}
List<Permission> required = getPermissionResolver().resolvePermissions(requiredPermissions);
return isPermittedAll(subjectPrincipal, required);
}
@Override
public boolean isPermittedAll(PrincipalCollection subjectPrincipal, Collection<Permission> requiredPermissions) {
AuthorizationInfo info = getAuthorizationInfo(subjectPrincipal);
if (info == null) {
return false;
}
return getPermissionResolver().isPermittedAll(info.getObjectPermissions(), requiredPermissions);
}
//-------------------------------------------------------------------------
@Override
public void checkPermission(PrincipalCollection subjectPrincipal, String requiredPermission) throws AuthorizationException {
Permission required = getPermissionResolver().resolvePermission(requiredPermission);
checkPermission(subjectPrincipal, required);
}
@Override
public void checkPermission(PrincipalCollection subjectPrincipal, Permission requiredPermission) throws AuthorizationException {
checkPermissions(subjectPrincipal, ImmutableList.of(requiredPermission));
}
@Override
public void checkPermissions(PrincipalCollection subjectPrincipal, String... requiredPermissions) throws AuthorizationException {
if (requiredPermissions.length > 0) {
List<Permission> required = getPermissionResolver().resolvePermissions(requiredPermissions);
checkPermissions(subjectPrincipal, required);
}
}
@Override
public void checkPermissions(PrincipalCollection subjectPrincipal, Collection<Permission> requiredPermissions) throws AuthorizationException {
AuthorizationInfo info = getAuthorizationInfo(subjectPrincipal);
if (info == null) {
throw new UnauthenticatedException("Permission denied, user not authenticated");
}
getPermissionResolver().checkPermissions(info.getObjectPermissions(), requiredPermissions);
}
}