/* Copyright 2006-2014 SpringSource.
*
* Licensed 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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 grails.plugin.springsecurity;
import grails.plugin.springsecurity.web.SecurityRequestHolder;
import grails.plugin.springsecurity.web.filter.DebugFilter;
import grails.util.Environment;
import groovy.lang.Closure;
import groovy.lang.GroovyClassLoader;
import groovy.util.ConfigObject;
import groovy.util.ConfigSlurper;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import javax.servlet.Filter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.apache.commons.lang.StringEscapeUtils;
import org.codehaus.groovy.grails.commons.GrailsApplication;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserCache;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.authentication.switchuser.SwitchUserFilter;
import org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartHttpServletRequest;
/**
* Helper methods.
*
* @author <a href='mailto:burt@burtbeckwith.com'>Burt Beckwith</a>
*/
public final class SpringSecurityUtils {
private static ConfigObject _securityConfig;
private static GrailsApplication application;
private static List<String> providerNames = new ArrayList<String>();
private static List<String> logoutHandlerNames = new ArrayList<String>();
private static List<String> voterNames = new ArrayList<String>();
private static List<String> afterInvocationManagerProviderNames = new ArrayList<String>();
private static Map<Integer, String> orderedFilters = new HashMap<Integer, String>();
private static SortedMap<Integer, Filter> configuredOrderedFilters = new TreeMap<Integer, Filter>();
// HttpSessionRequestCache.SAVED_REQUEST is package-scope
public static final String SAVED_REQUEST = "SPRING_SECURITY_SAVED_REQUEST"; // TODO use requestCache
// UsernamePasswordAuthenticationFilter.SPRING_SECURITY_LAST_USERNAME_KEY is deprecated
public static final String SPRING_SECURITY_LAST_USERNAME_KEY = "SPRING_SECURITY_LAST_USERNAME";
// AbstractAuthenticationTargetUrlRequestHandler.DEFAULT_TARGET_PARAMETER was removed
public static final String DEFAULT_TARGET_PARAMETER = "spring-security-redirect";
/**
* Default value for the name of the Ajax header.
*/
public static final String AJAX_HEADER = "X-Requested-With";
/**
* Used to ensure that all authenticated users have at least one granted authority to work
* around Spring Security code that assumes at least one. By granting this non-authority,
* the user can't do anything but gets past the somewhat arbitrary restrictions.
*/
public static final String NO_ROLE = "ROLE_NO_ROLES";
private SpringSecurityUtils() {
// static only
}
/**
* Set at startup by plugin.
* @param app the application
*/
public static void setApplication(GrailsApplication app) {
application = app;
initializeContext();
}
/**
* Extract the role names from authorities.
* @param authorities the authorities (a collection or array of {@link GrantedAuthority}).
* @return the names
*/
public static Set<String> authoritiesToRoles(final Object authorities) {
Set<String> roles = new HashSet<String>();
for (Object authority : ReflectionUtils.asList(authorities)) {
String authorityName = ((GrantedAuthority)authority).getAuthority();
if (null == authorityName) {
throw new IllegalArgumentException(
"Cannot process GrantedAuthority objects which return null " +
"from getAuthority() - attempting to process " + authority);
}
roles.add(authorityName);
}
return roles;
}
/**
* Get the current user's authorities.
* @return a list of authorities (empty if not authenticated).
*/
public static Collection<GrantedAuthority> getPrincipalAuthorities() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return Collections.emptyList();
}
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
if (authorities == null) {
return Collections.emptyList();
}
// remove the fake role if it's there
Collection<GrantedAuthority> copy = new ArrayList<GrantedAuthority>(authorities);
for (Iterator<GrantedAuthority> iter = copy.iterator(); iter.hasNext();) {
if (iter.next().getAuthority().equals(NO_ROLE)) {
iter.remove();
}
}
return copy;
}
/**
* Split the role names and create {@link GrantedAuthority}s for each.
* @param roleNames comma-delimited role names
* @return authorities (possibly empty)
*/
public static List<GrantedAuthority> parseAuthoritiesString(final String roleNames) {
List<GrantedAuthority> requiredAuthorities = new ArrayList<GrantedAuthority>();
for (String auth : StringUtils.commaDelimitedListToStringArray(roleNames)) {
auth = auth.trim();
if (auth.length() > 0) {
requiredAuthorities.add(new SimpleGrantedAuthority(auth));
}
}
return requiredAuthorities;
}
/**
* Find authorities in <code>granted</code> that are also in <code>required</code>.
* @param granted the granted authorities (a collection or array of {@link SpringSecurityUtils}).
* @param required the required authorities (a collection or array of {@link SpringSecurityUtils}).
* @return the authority names
*/
public static Set<String> retainAll(final Object granted, final Object required) {
Set<String> grantedRoles = authoritiesToRoles(granted);
Set<String> requiredRoles = authoritiesToRoles(required);
grantedRoles.retainAll(requiredRoles);
return grantedRoles;
}
/**
* Check if the current user has all of the specified roles.
* @param roles a comma-delimited list of role names
* @return <code>true</code> if the user is authenticated and has all the roles
*/
public static boolean ifAllGranted(final String roles) {
return ifAllGranted(parseAuthoritiesString(roles));
}
public static boolean ifAllGranted(final Collection<? extends GrantedAuthority> roles) {
Set<String> inferredNames = authoritiesToRoles(findInferredAuthorities(getPrincipalAuthorities()));
return inferredNames.containsAll(authoritiesToRoles(roles));
}
/**
* Check if the current user has none of the specified roles.
* @param roles a comma-delimited list of role names
* @return <code>true</code> if the user is authenticated and has none the roles
*/
public static boolean ifNotGranted(final String roles) {
return ifNotGranted(parseAuthoritiesString(roles));
}
public static boolean ifNotGranted(final Collection<? extends GrantedAuthority> roles) {
Collection<? extends GrantedAuthority> inferred = findInferredAuthorities(getPrincipalAuthorities());
Set<String> grantedCopy = retainAll(inferred, roles);
return grantedCopy.isEmpty();
}
/**
* Check if the current user has any of the specified roles.
* @param roles a comma-delimited list of role names
* @return <code>true</code> if the user is authenticated and has any the roles
*/
public static boolean ifAnyGranted(final String roles) {
return ifAnyGranted(parseAuthoritiesString(roles));
}
public static boolean ifAnyGranted(final Collection<? extends GrantedAuthority> roles) {
Collection<? extends GrantedAuthority> inferred = findInferredAuthorities(getPrincipalAuthorities());
Set<String> grantedCopy = retainAll(inferred, roles);
return !grantedCopy.isEmpty();
}
/**
* Parse and load the security configuration.
* @return the configuration
*/
public static synchronized ConfigObject getSecurityConfig() {
if (_securityConfig == null) {
reloadSecurityConfig();
}
return _securityConfig;
}
/**
* For testing only.
* @param config the config
*/
public static void setSecurityConfig(ConfigObject config) {
_securityConfig = config;
}
/**
* Reset the config for testing or after a dev mode Config.groovy change.
*/
public static synchronized void resetSecurityConfig() {
_securityConfig = null;
}
/**
* Allow a secondary plugin to add config attributes.
* @param className the name of the config class.
*/
public static synchronized void loadSecondaryConfig(final String className) {
mergeConfig(getSecurityConfig(), className);
}
/**
* Force a reload of the security configuration.
*/
public static void reloadSecurityConfig() {
mergeConfig(ReflectionUtils.getSecurityConfig(), "DefaultSecurityConfig");
}
/**
* Check if the request was triggered by an Ajax call.
* @param request the request
* @return <code>true</code> if Ajax
*/
public static boolean isAjax(final HttpServletRequest request) {
String ajaxHeaderName = (String)ReflectionUtils.getConfigProperty("ajaxHeader");
// check the current request's headers
if ("XMLHttpRequest".equals(request.getHeader(ajaxHeaderName))) {
return true;
}
Object ajaxCheckClosure = ReflectionUtils.getConfigProperty("ajaxCheckClosure");
if (ajaxCheckClosure instanceof Closure) {
Object result = ((Closure<?>)ajaxCheckClosure).call(request);
if (result instanceof Boolean && ((Boolean)result)) {
return true;
}
}
// look for an ajax=true parameter
if ("true".equals(request.getParameter("ajax"))) {
return true;
}
// process multipart requests
MultipartHttpServletRequest multipart = ((MultipartHttpServletRequest)request.getAttribute("org.springframework.web.multipart.MultipartHttpServletRequest"));
if (multipart != null && "true".equals(multipart.getParameter("ajax"))) {
return true;
}
// check the SavedRequest's headers
HttpSession httpSession = request.getSession(false);
if (httpSession != null) {
SavedRequest savedRequest = (SavedRequest)httpSession.getAttribute(SAVED_REQUEST);
if (savedRequest != null) {
return !savedRequest.getHeaderValues(ajaxHeaderName).isEmpty();
}
}
return false;
}
/**
* Register a provider bean name.
* <p/>
* Note - only for use by plugins during bean building.
*
* @param beanName the Spring bean name of the provider
*/
public static void registerProvider(final String beanName) {
providerNames.add(0, beanName);
}
/**
* Authentication provider names. Plugins add or remove them, and can be overridden by config.
* @return the names
*/
public static List<String> getProviderNames() {
return providerNames;
}
/**
* Register a logout handler bean name.
* <p/>
* Note - only for use by plugins during bean building.
*
* @param beanName the Spring bean name of the handler
*/
public static void registerLogoutHandler(final String beanName) {
logoutHandlerNames.add(0, beanName);
}
/**
* Logout handler names. Plugins add or remove them, and can be overridden by config.
* @return the names
*/
public static List<String> getLogoutHandlerNames() {
return logoutHandlerNames;
}
/**
* Register an AfterInvocationProvider bean name.
* <p/>
* Note - only for use by plugins during bean building.
*
* @param beanName the Spring bean name of the provider
*/
public static void registerAfterInvocationProvider(final String beanName) {
afterInvocationManagerProviderNames.add(0, beanName);
}
/**
* AfterInvocationProvider names. Plugins add or remove them, and can be overridden by config.
* @return the names
*/
public static List<String> getAfterInvocationManagerProviderNames() {
return afterInvocationManagerProviderNames;
}
/**
* Register a voter bean name.
* <p/>
* Note - only for use by plugins during bean building.
*
* @param beanName the Spring bean name of the voter
*/
public static void registerVoter(final String beanName) {
voterNames.add(0, beanName);
}
/**
* Voter names. Plugins add or remove them and can be overridden by config.
* @return the names
*/
public static List<String> getVoterNames() {
return voterNames;
}
/**
* Register a filter bean name in a specified position in the chain.
* <p/>
* Note - only for use by plugins during bean building - to register at runtime
* (preferably in BootStrap) use <code>clientRegisterFilter</code>.
*
* @param beanName the Spring bean name of the filter
* @param order the position
*/
public static void registerFilter(final String beanName, final SecurityFilterPosition order) {
registerFilter(beanName, order.getOrder());
}
/**
* Register a filter bean name in a specified position in the chain.
* <p/>
* Note - only for use by plugins during bean building - to register at runtime
* (preferably in BootStrap) use <code>clientRegisterFilter</code>.
*
* @param beanName the Spring bean name of the filter
* @param order the position (see {@link SecurityFilterPosition})
*/
public static void registerFilter(final String beanName, final int order) {
String oldName = getOrderedFilters().get(order);
if (oldName != null) {
throw new IllegalArgumentException("Cannot register filter '" + beanName +
"' at position " + order + "; '" + oldName +
"' is already registered in that position");
}
getOrderedFilters().put(order, beanName);
}
/**
* Ordered filter names. Plugins add or remove them, and can be overridden by config.
* @return the names
*/
public static Map<Integer, String> getOrderedFilters() {
return orderedFilters;
}
/**
* Register a filter in a specified position in the chain.
* <p/>
* Note - this is for use in application code after the plugin has initialized,
* e.g. in BootStrap where you want to register a custom filter in the correct
* order without dealing with the existing configured filters.
*
* @param beanName the Spring bean name of the filter
* @param order the position
*/
public static void clientRegisterFilter(final String beanName, final SecurityFilterPosition order) {
clientRegisterFilter(beanName, order.getOrder());
}
/**
* Register a filter in a specified position in the chain.
* <p/>
* Note - this is for use in application code after the plugin has initialized,
* e.g. in BootStrap where you want to register a custom filter in the correct
* order without dealing with the existing configured filters.
*
* @param beanName the Spring bean name of the filter
* @param order the position (see {@link SecurityFilterPosition})
*/
@SuppressWarnings("deprecation")
public static void clientRegisterFilter(final String beanName, final int order) {
Map<Integer, Filter> orderedFilters = SpringSecurityUtils.getConfiguredOrderedFilters();
Filter oldFilter = orderedFilters.get(order);
if (oldFilter != null) {
throw new IllegalArgumentException("Cannot register filter '" + beanName + "' at position " + order + "; '"
+ oldFilter + "' is already registered in that position");
}
Filter filter = getBean(beanName);
orderedFilters.put(order, filter);
FilterChainProxy filterChain = getFilterChainProxy();
Map<RequestMatcher, List<Filter>> filterChainMap = filterChain.getFilterChainMap();
Map<RequestMatcher, List<Filter>> fixedFilterChainMap = mergeFilterChainMap(orderedFilters, filter, order,
filterChainMap);
filterChain.setFilterChainMap(fixedFilterChainMap);
}
private static FilterChainProxy getFilterChainProxy() {
FilterChainProxy filterChain;
Object bean = getBean("springSecurityFilterChain");
if (bean instanceof DebugFilter) {
filterChain = ((DebugFilter)bean).getFilterChainProxy();
}
else {
filterChain = (FilterChainProxy)bean;
}
return filterChain;
}
private static Map<RequestMatcher, List<Filter>> mergeFilterChainMap(Map<Integer, Filter> orderedFilters,
Filter filter, final int order, Map<RequestMatcher, List<Filter>> filterChainMap) {
Map<Filter, Integer> filterToPosition = new HashMap<Filter, Integer>();
for (Map.Entry<Integer, Filter> entry : orderedFilters.entrySet()) {
filterToPosition.put(entry.getValue(), entry.getKey());
}
Map<RequestMatcher, List<Filter>> fixedFilterChainMap = new LinkedHashMap<RequestMatcher, List<Filter>>();
for (Entry<RequestMatcher, List<Filter>> entry : filterChainMap.entrySet()) {
List<Filter> filters = new ArrayList<Filter>(entry.getValue());
int indexOfFilterBeforeTargetFilter = 0;
while (indexOfFilterBeforeTargetFilter < filters.size()
&& filterToPosition.get(filters.get(indexOfFilterBeforeTargetFilter)) < order) {
indexOfFilterBeforeTargetFilter++;
}
filters.add(indexOfFilterBeforeTargetFilter, filter);
fixedFilterChainMap.put(entry.getKey(), filters);
}
return fixedFilterChainMap;
}
/**
* Set by SpringSecurityCoreGrailsPlugin; contains the actual filter beans in order.
* @return the filters
*/
public static SortedMap<Integer, Filter> getConfiguredOrderedFilters() {
return configuredOrderedFilters;
}
/**
* Check if the current user is switched to another user.
* @return <code>true</code> if logged in and switched
*/
public static boolean isSwitched() {
Collection<? extends GrantedAuthority> inferred = findInferredAuthorities(getPrincipalAuthorities());
for (GrantedAuthority authority : inferred) {
if (authority instanceof SwitchUserGrantedAuthority) {
return true;
}
if (SwitchUserFilter.ROLE_PREVIOUS_ADMINISTRATOR.equals(authority.getAuthority())) {
return true;
}
}
return false;
}
/**
* Get the username of the original user before switching to another.
* @return the original login name
*/
public static String getSwitchedUserOriginalUsername() {
if (isSwitched()) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
for (GrantedAuthority auth : authentication.getAuthorities()) {
if (auth instanceof SwitchUserGrantedAuthority) {
return ((SwitchUserGrantedAuthority)auth).getSource().getName();
}
}
}
return null;
}
/**
* Lookup the security type as a String to avoid dev mode reload issues.
* @return the name of the <code>SecurityConfigType</code>
*/
public static String getSecurityConfigType() {
return getSecurityConfig().get("securityConfigType").toString();
}
/**
* Rebuild an Authentication for the given username and register it in the security context.
* Typically used after updating a user's authorities or other auth-cached info.
* <p/>
* Also removes the user from the user cache to force a refresh at next login.
*
* @param username the user's login name
* @param password optional
*/
public static void reauthenticate(final String username, final String password) {
UserDetailsService userDetailsService = getBean("userDetailsService");
UserCache userCache = getBean("userCache");
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
userDetails, password == null ? userDetails.getPassword() : password, userDetails.getAuthorities()));
userCache.removeUserFromCache(username);
}
/**
* Execute a closure with the current authentication. Assumes that there's an authentication in the
* http session and that the closure is running in a separate thread from the web request, so the
* context and authentication aren't available to the standard ThreadLocal.
*
* @param closure the code to run
* @return the closure's return value
*/
public static Object doWithAuth(@SuppressWarnings("rawtypes") final Closure closure) {
boolean set = false;
if (SecurityContextHolder.getContext().getAuthentication() == null && SecurityRequestHolder.getRequest() != null) {
HttpSession httpSession = SecurityRequestHolder.getRequest().getSession(false);
SecurityContext securityContext = null;
if (httpSession != null) {
securityContext = (SecurityContext)httpSession.getAttribute(
HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
if (securityContext != null) {
SecurityContextHolder.setContext(securityContext);
set = true;
}
}
}
try {
return closure.call();
}
finally {
if (set) {
SecurityContextHolder.clearContext();
}
}
}
/**
* Authenticate as the specified user and execute the closure with that authentication. Restores
* the authentication to the one that was active if it exists, or clears the context otherwise.
* <p/>
* This is similar to run-as and switch-user but is only local to a Closure.
*
* @param username the username to authenticate as
* @param closure the code to run
* @return the closure's return value
*/
public static Object doWithAuth(final String username, @SuppressWarnings("rawtypes") final Closure closure) {
Authentication previousAuth = SecurityContextHolder.getContext().getAuthentication();
reauthenticate(username, null);
try {
return closure.call();
}
finally {
if (previousAuth == null) {
SecurityContextHolder.clearContext();
}
else {
SecurityContextHolder.getContext().setAuthentication(previousAuth);
}
}
}
public static SecurityContext getSecurityContext(final HttpSession session) {
Object securityContext = session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
if (securityContext instanceof SecurityContext) {
return (SecurityContext)securityContext;
}
return null;
}
/**
* Get the last auth exception.
* @param session the session
* @return the exception
*/
public static Throwable getLastException(final HttpSession session) {
return (Throwable)session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
}
/**
* Get the last attempted username.
* @param session the session
* @return the username
*/
public static String getLastUsername(final HttpSession session) {
String username = (String)session.getAttribute(SPRING_SECURITY_LAST_USERNAME_KEY);
if (username != null) {
username = StringEscapeUtils.unescapeHtml(username);
}
return username;
}
/**
* Get the saved request from the session.
* @param session the session
* @return the saved request
*/
public static SavedRequest getSavedRequest(final HttpSession session) {
return (SavedRequest)session.getAttribute(SAVED_REQUEST);
}
/**
* Merge in a secondary config (provided by a plugin as defaults) into the main config.
* @param currentConfig the current configuration
* @param className the name of the config class to load
*/
private static void mergeConfig(final ConfigObject currentConfig, final String className) {
GroovyClassLoader classLoader = new GroovyClassLoader(SpringSecurityUtils.class.getClassLoader());
ConfigSlurper slurper = new ConfigSlurper(Environment.getCurrent().getName());
ConfigObject secondaryConfig;
try {
secondaryConfig = slurper.parse(classLoader.loadClass(className));
}
catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
_securityConfig = mergeConfig(currentConfig, (ConfigObject)secondaryConfig.getProperty("security"));
ReflectionUtils.setSecurityConfig(_securityConfig);
}
/**
* Merge two configs together. The order is important; if <code>secondary</code> is not null then
* start with that and merge the main config on top of that. This lets the <code>secondary</code>
* config act as default values but let user-supplied values in the main config override them.
*
* @param currentConfig the main config, starting from Config.groovy
* @param secondary new default values
* @return the merged configs
*/
@SuppressWarnings("unchecked")
private static ConfigObject mergeConfig(final ConfigObject currentConfig, final ConfigObject secondary) {
ConfigObject config = new ConfigObject();
if (secondary == null) {
if (currentConfig != null) {
config.putAll(currentConfig);
}
}
else {
if (currentConfig == null) {
config.putAll(secondary);
}
else {
config.putAll(secondary.merge(currentConfig));
}
}
return config;
}
private static Collection<? extends GrantedAuthority> findInferredAuthorities(
final Collection<GrantedAuthority> granted) {
RoleHierarchy roleHierarchy = getBean("roleHierarchy");
Collection<? extends GrantedAuthority> reachable = roleHierarchy.getReachableGrantedAuthorities(granted);
if (reachable == null) {
return Collections.emptyList();
}
return reachable;
}
@SuppressWarnings("unchecked")
private static <T> T getBean(final String name) {
return (T)application.getMainContext().getBean(name);
}
/**
* Called each time doWithApplicationContext() is invoked, so it's important to reset
* to default values when running integration and functional tests together.
*/
private static void initializeContext() {
voterNames.clear();
voterNames.add("authenticatedVoter");
voterNames.add("roleVoter");
voterNames.add("webExpressionVoter");
voterNames.add("closureVoter");
logoutHandlerNames.clear();
logoutHandlerNames.add("rememberMeServices");
logoutHandlerNames.add("securityContextLogoutHandler");
providerNames.clear();
providerNames.add("daoAuthenticationProvider");
providerNames.add("anonymousAuthenticationProvider");
providerNames.add("rememberMeAuthenticationProvider");
orderedFilters.clear();
configuredOrderedFilters.clear();
afterInvocationManagerProviderNames.clear();
}
}