/**
* Copyright (c) 2013-2016, The SeedStack authors <http://seedstack.org>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.seedstack.seed.web.security.internal;
import com.google.inject.Key;
import com.google.inject.Scopes;
import com.google.inject.name.Names;
import org.apache.shiro.config.ConfigurationException;
import org.apache.shiro.guice.web.GuiceShiroFilter;
import org.apache.shiro.util.StringUtils;
import org.apache.shiro.web.filter.PathMatchingFilter;
import org.seedstack.seed.security.internal.SecurityGuiceConfigurer;
import org.seedstack.seed.web.SecurityFilter;
import org.seedstack.seed.web.security.WebSecurityConfig;
import org.seedstack.seed.web.security.internal.shiro.ShiroWebModule;
import org.seedstack.seed.web.spi.AntiXsrfService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.Filter;
import javax.servlet.ServletContext;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class WebSecurityModule extends ShiroWebModule {
private static final Logger LOGGER = LoggerFactory.getLogger(ShiroWebModule.class);
private static final Map<String, Key<? extends Filter>> DEFAULT_FILTERS = new HashMap<>();
static {
// Shiro filters
DEFAULT_FILTERS.put("anon", ANON);
DEFAULT_FILTERS.put("authc", AUTHC);
DEFAULT_FILTERS.put("authcBasic", AUTHC_BASIC);
DEFAULT_FILTERS.put("logout", LOGOUT);
DEFAULT_FILTERS.put("noSessionCreation", NO_SESSION_CREATION);
DEFAULT_FILTERS.put("perms", PERMS);
DEFAULT_FILTERS.put("port", PORT);
DEFAULT_FILTERS.put("rest", REST);
DEFAULT_FILTERS.put("roles", ROLES);
DEFAULT_FILTERS.put("ssl", SSL);
DEFAULT_FILTERS.put("user", USER);
// Seed filters
DEFAULT_FILTERS.put("xsrf", Key.get(AntiXsrfFilter.class));
DEFAULT_FILTERS.put("cert", Key.get(X509CertificateFilter.class));
}
private final String applicationName;
private final WebSecurityConfig securityConfig;
private final Collection<Class<? extends Filter>> customFilters;
private final SecurityGuiceConfigurer securityGuiceConfigurer;
WebSecurityModule(ServletContext servletContext, WebSecurityConfig securityConfig, Collection<Class<? extends Filter>> customFilters, String applicationName, SecurityGuiceConfigurer securityGuiceConfigurer) {
super(servletContext);
this.securityConfig = securityConfig;
this.customFilters = customFilters;
this.applicationName = applicationName;
this.securityGuiceConfigurer = securityGuiceConfigurer;
}
@Override
protected void configureShiroWeb() {
for (WebSecurityConfig.UrlConfig urlConfig : securityConfig.getUrls()) {
String pattern = urlConfig.getPattern();
List<String> filters = urlConfig.getFilters();
LOGGER.trace("Binding {} to security filter chain {}", pattern, filters);
addFilterChain(pattern, getFilterKeys(filters));
}
LOGGER.debug("{} URL(s) bound to security filters", securityConfig.getUrls().size());
bind(WebSecurityConfig.class);
// Bind filters which are not PatchMatchingFilters
bind(AntiXsrfFilter.class);
// Bind custom filters not extending PathMatchingFilter as Shiro doesn't do it
for (Class<? extends Filter> customFilter : customFilters) {
if (!PathMatchingFilter.class.isAssignableFrom(customFilter)) {
bind(customFilter);
}
}
// Additional web security bindings
bind(AntiXsrfService.class).to(StatelessAntiXsrfService.class);
bindConstant().annotatedWith(Names.named("shiro.applicationName")).to(applicationName);
// Shiro global configuration
securityGuiceConfigurer.configure(binder());
// Shiro filter
bind(GuiceShiroFilter.class).in(Scopes.SINGLETON);
// Exposed binding
expose(AntiXsrfService.class);
}
@SuppressWarnings("unchecked")
private FilterKey[] getFilterKeys(List<String> filters) {
FilterKey[] keys = new FilterKey[filters.size()];
int index = 0;
for (String filter : filters) {
String[] nameConfig = toNameConfigPair(filter);
Key<? extends Filter> key = findKey(nameConfig[0]);
if (key != null) {
keys[index] = new FilterKey(key, nameConfig[1] == null ? "" : nameConfig[1]);
} else {
addError("The filter [" + nameConfig[0] + "] could not be found as a default filter or as a class annotated with SecurityFilter");
}
index++;
}
return keys;
}
private Key<? extends Filter> findKey(String filterName) {
Key<? extends Filter> currentKey = null;
if (DEFAULT_FILTERS.containsKey(filterName)) {
currentKey = DEFAULT_FILTERS.get(filterName);
} else {
for (Class<? extends Filter> filterClass : customFilters) {
String name = filterClass.getAnnotation(SecurityFilter.class).value();
if (filterName.equals(name)) {
currentKey = Key.get(filterClass);
}
}
}
return currentKey;
}
/**
* This method is copied from the same method in Shiro in class DefaultFilterChainManager.
*/
private String[] toNameConfigPair(String token) throws ConfigurationException {
String[] pair = token.split("\\[", 2);
String name = StringUtils.clean(pair[0]);
if (name == null) {
throw new IllegalArgumentException("Filter name not found for filter chain definition token: " + token);
}
String config = null;
if (pair.length == 2) {
config = StringUtils.clean(pair[1]);
//if there was an open bracket, it assumed there is a closing bracket, so strip it too:
config = config.substring(0, config.length() - 1);
config = StringUtils.clean(config);
//backwards compatibility prior to implementing SHIRO-205:
//prior to SHIRO-205 being implemented, it was common for end-users to quote the config inside brackets
//if that config required commas. We need to strip those quotes to get to the interior quoted definition
//to ensure any existing quoted definitions still function for end users:
if (config != null && config.startsWith("\"") && config.endsWith("\"")) {
String stripped = config.substring(1, config.length() - 1);
stripped = StringUtils.clean(stripped);
//if the stripped value does not have any internal quotes, we can assume that the entire config was
//quoted and we can use the stripped value.
if (stripped != null && stripped.indexOf('"') == -1) {
config = stripped;
}
//else:
//the remaining config does have internal quotes, so we need to assume that each comma delimited
//pair might be quoted, in which case we need the leading and trailing quotes that we stripped
//So we ignore the stripped value.
}
}
return new String[]{name, config};
}
}