IniWebEnvironment.java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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
*
* 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 org.apache.shiro.web.env;
import org.apache.shiro.config.ConfigurationException;
import org.apache.shiro.config.Ini;
import org.apache.shiro.config.IniFactorySupport;
import org.apache.shiro.io.ResourceUtils;
import org.apache.shiro.util.*;
import org.apache.shiro.web.config.IniFilterChainResolverFactory;
import org.apache.shiro.web.config.WebIniSecurityManagerFactory;
import org.apache.shiro.web.filter.mgt.FilterChainResolver;
import org.apache.shiro.web.mgt.WebSecurityManager;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletContext;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
/**
* {@link WebEnvironment} implementation configured by an {@link Ini} instance or {@code Ini} resource locations.
*
* @since 1.2
*/
public class IniWebEnvironment extends ResourceBasedWebEnvironment implements Initializable, Destroyable {
public static final String DEFAULT_WEB_INI_RESOURCE_PATH = "/WEB-INF/shiro.ini";
public static final String FILTER_CHAIN_RESOLVER_NAME = "filterChainResolver";
private static final Logger log = LoggerFactory.getLogger(IniWebEnvironment.class);
/**
* The Ini that configures this WebEnvironment instance.
*/
private Ini ini;
private WebIniSecurityManagerFactory factory;
public IniWebEnvironment() {
factory = new WebIniSecurityManagerFactory();
}
/**
* Initializes this instance by resolving any potential (explicit or resource-configured) {@link Ini}
* configuration and calling {@link #configure() configure} for actual instance configuration.
*/
public void init() {
setIni(parseConfig());
configure();
}
/**
* Loads configuration {@link Ini} from {@link #getConfigLocations()} if set, otherwise falling back
* to the {@link #getDefaultConfigLocations()}. Finally any Ini objects will be merged with the value returned
* from {@link #getFrameworkIni()}
* @return Ini configuration to be used by this Environment.
* @since 1.4
*/
protected Ini parseConfig() {
Ini ini = getIni();
String[] configLocations = getConfigLocations();
if (log.isWarnEnabled() && !CollectionUtils.isEmpty(ini) &&
configLocations != null && configLocations.length > 0) {
log.warn("Explicit INI instance has been provided, but configuration locations have also been " +
"specified. The {} implementation does not currently support multiple Ini config, but this may " +
"be supported in the future. Only the INI instance will be used for configuration.",
IniWebEnvironment.class.getName());
}
if (CollectionUtils.isEmpty(ini)) {
log.debug("Checking any specified config locations.");
ini = getSpecifiedIni(configLocations);
}
if (CollectionUtils.isEmpty(ini)) {
log.debug("No INI instance or config locations specified. Trying default config locations.");
ini = getDefaultIni();
}
// Allow for integrations to provide default that will be merged other configuration.
// to retain backwards compatibility this must be a different method then 'getDefaultIni()'
ini = mergeIni(getFrameworkIni(), ini);
if (CollectionUtils.isEmpty(ini)) {
String msg = "Shiro INI configuration was either not found or discovered to be empty/unconfigured.";
throw new ConfigurationException(msg);
}
return ini;
}
protected void configure() {
this.objects.clear();
WebSecurityManager securityManager = createWebSecurityManager();
setWebSecurityManager(securityManager);
FilterChainResolver resolver = createFilterChainResolver();
if (resolver != null) {
setFilterChainResolver(resolver);
}
}
/**
* Extension point to allow subclasses to provide an {@link Ini} configuration that will be merged into the
* users configuration. The users configuration will override anything set here.
* <p>
* <strong>NOTE:</strong> Framework developers should use with caution. It is possible a user could provide
* configuration that would conflict with the frameworks configuration. For example: if this method returns an
* Ini object with the following configuration:
* <pre><code>
* [main]
* realm = com.myco.FoobarRealm
* realm.foobarSpecificField = A string
* </code></pre>
* And the user provides a similar configuration:
* <pre><code>
* [main]
* realm = net.differentco.MyCustomRealm
* </code></pre>
*
* This would merge into:
* <pre><code>
* [main]
* realm = net.differentco.MyCustomRealm
* realm.foobarSpecificField = A string
* </code></pre>
*
* This may cause a configuration error if <code>MyCustomRealm</code> does not contain the field <code>foobarSpecificField</code>.
* This can be avoided if the Framework Ini uses more unique names, such as <code>foobarRealm</code>. which would result
* in a merged configuration that looks like:
* <pre><code>
* [main]
* foobarRealm = com.myco.FoobarRealm
* foobarRealm.foobarSpecificField = A string
* realm = net.differentco.MyCustomRealm
* </code></pre>
*
* </p>
*
* @return Ini configuration used by the framework integrations.
* @since 1.4
*/
protected Ini getFrameworkIni() {
return null;
}
protected Ini getSpecifiedIni(String[] configLocations) throws ConfigurationException {
Ini ini = null;
if (configLocations != null && configLocations.length > 0) {
if (configLocations.length > 1) {
log.warn("More than one Shiro .ini config location has been specified. Only the first will be " +
"used for configuration as the {} implementation does not currently support multiple " +
"files. This may be supported in the future however.", IniWebEnvironment.class.getName());
}
//required, as it is user specified:
ini = createIni(configLocations[0], true);
}
return ini;
}
protected Ini mergeIni(Ini ini1, Ini ini2) {
if (ini1 == null) {
return ini2;
}
if (ini2 == null) {
return ini1;
}
// at this point we have two valid ini objects, create a new one and merge the contents of 2 into 1
Ini iniResult = new Ini(ini1);
iniResult.merge(ini2);
return iniResult;
}
protected Ini getDefaultIni() {
Ini ini = null;
String[] configLocations = getDefaultConfigLocations();
if (configLocations != null) {
for (String location : configLocations) {
ini = createIni(location, false);
if (!CollectionUtils.isEmpty(ini)) {
log.debug("Discovered non-empty INI configuration at location '{}'. Using for configuration.",
location);
break;
}
}
}
return ini;
}
/**
* Creates an {@link Ini} instance reflecting the specified path, or {@code null} if the path does not exist and
* is not required.
* <p/>
* If the path is required and does not exist or is empty, a {@link ConfigurationException} will be thrown.
*
* @param configLocation the resource path to load into an {@code Ini} instance.
* @param required if the path must exist and be converted to a non-empty {@link Ini} instance.
* @return an {@link Ini} instance reflecting the specified path, or {@code null} if the path does not exist and
* is not required.
* @throws ConfigurationException if the path is required but results in a null or empty Ini instance.
*/
protected Ini createIni(String configLocation, boolean required) throws ConfigurationException {
Ini ini = null;
if (configLocation != null) {
ini = convertPathToIni(configLocation, required);
}
if (required && CollectionUtils.isEmpty(ini)) {
String msg = "Required configuration location '" + configLocation + "' does not exist or did not " +
"contain any INI configuration.";
throw new ConfigurationException(msg);
}
return ini;
}
protected FilterChainResolver createFilterChainResolver() {
FilterChainResolver resolver = null;
Ini ini = getIni();
if (!CollectionUtils.isEmpty(ini)) {
//only create a resolver if the 'filters' or 'urls' sections are defined:
Ini.Section urls = ini.getSection(IniFilterChainResolverFactory.URLS);
Ini.Section filters = ini.getSection(IniFilterChainResolverFactory.FILTERS);
if (!CollectionUtils.isEmpty(urls) || !CollectionUtils.isEmpty(filters)) {
//either the urls section or the filters section was defined. Go ahead and create the resolver:
Factory<FilterChainResolver> factory = (Factory<FilterChainResolver>) this.objects.get(FILTER_CHAIN_RESOLVER_NAME);
if (factory instanceof IniFactorySupport) {
IniFactorySupport iniFactory = (IniFactorySupport) factory;
iniFactory.setIni(ini);
iniFactory.setDefaults(this.objects);
}
resolver = factory.getInstance();
}
}
return resolver;
}
protected WebSecurityManager createWebSecurityManager() {
Ini ini = getIni();
if (!CollectionUtils.isEmpty(ini)) {
factory.setIni(ini);
}
Map<String, Object> defaults = getDefaults();
if (!CollectionUtils.isEmpty(defaults)) {
factory.setDefaults(defaults);
}
WebSecurityManager wsm = (WebSecurityManager)factory.getInstance();
//SHIRO-306 - get beans after they've been created (the call was before the factory.getInstance() call,
//which always returned null.
Map<String, ?> beans = factory.getBeans();
if (!CollectionUtils.isEmpty(beans)) {
this.objects.putAll(beans);
}
return wsm;
}
/**
* Returns an array with two elements, {@code /WEB-INF/shiro.ini} and {@code classpath:shiro.ini}.
*
* @return an array with two elements, {@code /WEB-INF/shiro.ini} and {@code classpath:shiro.ini}.
*/
protected String[] getDefaultConfigLocations() {
return new String[]{
DEFAULT_WEB_INI_RESOURCE_PATH,
IniFactorySupport.DEFAULT_INI_RESOURCE_PATH
};
}
/**
* Converts the specified file path to an {@link Ini} instance.
* <p/>
* If the path does not have a resource prefix as defined by {@link org.apache.shiro.io.ResourceUtils#hasResourcePrefix(String)}, the
* path is expected to be resolvable by the {@code ServletContext} via
* {@link javax.servlet.ServletContext#getResourceAsStream(String)}.
*
* @param path the path of the INI resource to load into an INI instance.
* @param required if the specified path must exist
* @return an INI instance populated based on the given INI resource path.
*/
private Ini convertPathToIni(String path, boolean required) {
//TODO - this logic is ugly - it'd be ideal if we had a Resource API to polymorphically encaspulate this behavior
Ini ini = null;
if (StringUtils.hasText(path)) {
InputStream is = null;
//SHIRO-178: Check for servlet context resource and not only resource paths:
if (!ResourceUtils.hasResourcePrefix(path)) {
is = getServletContextResourceStream(path);
} else {
try {
is = ResourceUtils.getInputStreamForPath(path);
} catch (IOException e) {
if (required) {
throw new ConfigurationException(e);
} else {
if (log.isDebugEnabled()) {
log.debug("Unable to load optional path '" + path + "'.", e);
}
}
}
}
if (is != null) {
ini = new Ini();
ini.load(is);
} else {
if (required) {
throw new ConfigurationException("Unable to load resource path '" + path + "'");
}
}
}
return ini;
}
//TODO - this logic is ugly - it'd be ideal if we had a Resource API to polymorphically encaspulate this behavior
private InputStream getServletContextResourceStream(String path) {
InputStream is = null;
path = WebUtils.normalize(path);
ServletContext sc = getServletContext();
if (sc != null) {
is = sc.getResourceAsStream(path);
}
return is;
}
/**
* Returns the {@code Ini} instance reflecting this WebEnvironment's configuration.
*
* @return the {@code Ini} instance reflecting this WebEnvironment's configuration.
*/
public Ini getIni() {
return this.ini;
}
/**
* Allows for configuration via a direct {@link Ini} instance instead of via
* {@link #getConfigLocations() config locations}.
* <p/>
* If the specified instance is null or empty, the fallback/default resource-based configuration will be used.
*
* @param ini the ini instance to use for creation.
*/
public void setIni(Ini ini) {
this.ini = ini;
}
protected Map<String, Object> getDefaults() {
Map<String, Object> defaults = new HashMap<String, Object>();
defaults.put(FILTER_CHAIN_RESOLVER_NAME, new IniFilterChainResolverFactory());
return defaults;
}
/**
* Returns the SecurityManager factory used by this WebEnvironment.
*
* @return the SecurityManager factory used by this WebEnvironment.
* @since 1.4
*/
@SuppressWarnings("unused")
protected WebIniSecurityManagerFactory getSecurityManagerFactory() {
return factory;
}
/**
* Allows for setting the SecurityManager factory which will be used to create the SecurityManager.
*
* @param factory the SecurityManager factory to used.
* @since 1.4
*/
protected void setSecurityManagerFactory(WebIniSecurityManagerFactory factory) {
this.factory = factory;
}
}