001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.shiro.web.env;
020
021import org.apache.shiro.config.ConfigurationException;
022import org.apache.shiro.config.Ini;
023import org.apache.shiro.config.IniFactorySupport;
024import org.apache.shiro.io.ResourceUtils;
025import org.apache.shiro.util.CollectionUtils;
026import org.apache.shiro.util.Destroyable;
027import org.apache.shiro.util.Initializable;
028import org.apache.shiro.util.StringUtils;
029import org.apache.shiro.web.config.IniFilterChainResolverFactory;
030import org.apache.shiro.web.config.WebIniSecurityManagerFactory;
031import org.apache.shiro.web.filter.mgt.FilterChainResolver;
032import org.apache.shiro.web.mgt.WebSecurityManager;
033import org.apache.shiro.web.util.WebUtils;
034import org.slf4j.Logger;
035import org.slf4j.LoggerFactory;
036
037import javax.servlet.ServletContext;
038import java.io.IOException;
039import java.io.InputStream;
040import java.util.Map;
041
042/**
043 * {@link WebEnvironment} implementation configured by an {@link Ini} instance or {@code Ini} resource locations.
044 *
045 * @since 1.2
046 */
047public class IniWebEnvironment extends ResourceBasedWebEnvironment implements Initializable, Destroyable {
048
049    public static final String DEFAULT_WEB_INI_RESOURCE_PATH = "/WEB-INF/shiro.ini";
050
051    private static final Logger log = LoggerFactory.getLogger(IniWebEnvironment.class);
052
053    /**
054     * The Ini that configures this WebEnvironment instance.
055     */
056    private Ini ini;
057
058    /**
059     * Initializes this instance by resolving any potential (explicit or resource-configured) {@link Ini}
060     * configuration and calling {@link #configure() configure} for actual instance configuration.
061     */
062    public void init() {
063        Ini ini = getIni();
064
065        String[] configLocations = getConfigLocations();
066
067        if (log.isWarnEnabled() && !CollectionUtils.isEmpty(ini) &&
068                configLocations != null && configLocations.length > 0) {
069            log.warn("Explicit INI instance has been provided, but configuration locations have also been " +
070                    "specified.  The {} implementation does not currently support multiple Ini config, but this may " +
071                    "be supported in the future. Only the INI instance will be used for configuration.",
072                    IniWebEnvironment.class.getName());
073        }
074
075        if (CollectionUtils.isEmpty(ini)) {
076            log.debug("Checking any specified config locations.");
077            ini = getSpecifiedIni(configLocations);
078        }
079
080        if (CollectionUtils.isEmpty(ini)) {
081            log.debug("No INI instance or config locations specified.  Trying default config locations.");
082            ini = getDefaultIni();
083        }
084
085        if (CollectionUtils.isEmpty(ini)) {
086            String msg = "Shiro INI configuration was either not found or discovered to be empty/unconfigured.";
087            throw new ConfigurationException(msg);
088        }
089
090        setIni(ini);
091
092        configure();
093    }
094
095    protected void configure() {
096
097        this.objects.clear();
098
099        WebSecurityManager securityManager = createWebSecurityManager();
100        setWebSecurityManager(securityManager);
101
102        FilterChainResolver resolver = createFilterChainResolver();
103        if (resolver != null) {
104            setFilterChainResolver(resolver);
105        }
106    }
107
108    protected Ini getSpecifiedIni(String[] configLocations) throws ConfigurationException {
109
110        Ini ini = null;
111
112        if (configLocations != null && configLocations.length > 0) {
113
114            if (configLocations.length > 1) {
115                log.warn("More than one Shiro .ini config location has been specified.  Only the first will be " +
116                        "used for configuration as the {} implementation does not currently support multiple " +
117                        "files.  This may be supported in the future however.", IniWebEnvironment.class.getName());
118            }
119
120            //required, as it is user specified:
121            ini = createIni(configLocations[0], true);
122        }
123
124        return ini;
125    }
126
127    protected Ini getDefaultIni() {
128
129        Ini ini = null;
130
131        String[] configLocations = getDefaultConfigLocations();
132        if (configLocations != null) {
133            for (String location : configLocations) {
134                ini = createIni(location, false);
135                if (!CollectionUtils.isEmpty(ini)) {
136                    log.debug("Discovered non-empty INI configuration at location '{}'.  Using for configuration.",
137                            location);
138                    break;
139                }
140            }
141        }
142
143        return ini;
144    }
145
146    /**
147     * Creates an {@link Ini} instance reflecting the specified path, or {@code null} if the path does not exist and
148     * is not required.
149     * <p/>
150     * If the path is required and does not exist or is empty, a {@link ConfigurationException} will be thrown.
151     *
152     * @param configLocation the resource path to load into an {@code Ini} instance.
153     * @param required       if the path must exist and be converted to a non-empty {@link Ini} instance.
154     * @return an {@link Ini} instance reflecting the specified path, or {@code null} if the path does not exist and
155     *         is not required.
156     * @throws ConfigurationException if the path is required but results in a null or empty Ini instance.
157     */
158    protected Ini createIni(String configLocation, boolean required) throws ConfigurationException {
159
160        Ini ini = null;
161
162        if (configLocation != null) {
163            ini = convertPathToIni(configLocation, required);
164        }
165        if (required && CollectionUtils.isEmpty(ini)) {
166            String msg = "Required configuration location '" + configLocation + "' does not exist or did not " +
167                    "contain any INI configuration.";
168            throw new ConfigurationException(msg);
169        }
170
171        return ini;
172    }
173
174    protected FilterChainResolver createFilterChainResolver() {
175
176        FilterChainResolver resolver = null;
177
178        Ini ini = getIni();
179
180        if (!CollectionUtils.isEmpty(ini)) {
181            //only create a resolver if the 'filters' or 'urls' sections are defined:
182            Ini.Section urls = ini.getSection(IniFilterChainResolverFactory.URLS);
183            Ini.Section filters = ini.getSection(IniFilterChainResolverFactory.FILTERS);
184            if (!CollectionUtils.isEmpty(urls) || !CollectionUtils.isEmpty(filters)) {
185                //either the urls section or the filters section was defined.  Go ahead and create the resolver:
186                IniFilterChainResolverFactory factory = new IniFilterChainResolverFactory(ini, this.objects);
187                resolver = factory.getInstance();
188            }
189        }
190
191        return resolver;
192    }
193
194    protected WebSecurityManager createWebSecurityManager() {
195        WebIniSecurityManagerFactory factory;
196        Ini ini = getIni();
197        if (CollectionUtils.isEmpty(ini)) {
198            factory = new WebIniSecurityManagerFactory();
199        } else {
200            factory = new WebIniSecurityManagerFactory(ini);
201        }
202
203        WebSecurityManager wsm = (WebSecurityManager)factory.getInstance();
204
205        //SHIRO-306 - get beans after they've been created (the call was before the factory.getInstance() call,
206        //which always returned null.
207        Map<String, ?> beans = factory.getBeans();
208        if (!CollectionUtils.isEmpty(beans)) {
209            this.objects.putAll(beans);
210        }
211
212        return wsm;
213    }
214
215    /**
216     * Returns an array with two elements, {@code /WEB-INF/shiro.ini} and {@code classpath:shiro.ini}.
217     *
218     * @return an array with two elements, {@code /WEB-INF/shiro.ini} and {@code classpath:shiro.ini}.
219     */
220    protected String[] getDefaultConfigLocations() {
221        return new String[]{
222                DEFAULT_WEB_INI_RESOURCE_PATH,
223                IniFactorySupport.DEFAULT_INI_RESOURCE_PATH
224        };
225    }
226
227    /**
228     * Converts the specified file path to an {@link Ini} instance.
229     * <p/>
230     * If the path does not have a resource prefix as defined by {@link org.apache.shiro.io.ResourceUtils#hasResourcePrefix(String)}, the
231     * path is expected to be resolvable by the {@code ServletContext} via
232     * {@link javax.servlet.ServletContext#getResourceAsStream(String)}.
233     *
234     * @param path     the path of the INI resource to load into an INI instance.
235     * @param required if the specified path must exist
236     * @return an INI instance populated based on the given INI resource path.
237     */
238    private Ini convertPathToIni(String path, boolean required) {
239
240        //TODO - this logic is ugly - it'd be ideal if we had a Resource API to polymorphically encaspulate this behavior
241
242        Ini ini = null;
243
244        if (StringUtils.hasText(path)) {
245            InputStream is = null;
246
247            //SHIRO-178: Check for servlet context resource and not only resource paths:
248            if (!ResourceUtils.hasResourcePrefix(path)) {
249                is = getServletContextResourceStream(path);
250            } else {
251                try {
252                    is = ResourceUtils.getInputStreamForPath(path);
253                } catch (IOException e) {
254                    if (required) {
255                        throw new ConfigurationException(e);
256                    } else {
257                        if (log.isDebugEnabled()) {
258                            log.debug("Unable to load optional path '" + path + "'.", e);
259                        }
260                    }
261                }
262            }
263            if (is != null) {
264                ini = new Ini();
265                ini.load(is);
266            } else {
267                if (required) {
268                    throw new ConfigurationException("Unable to load resource path '" + path + "'");
269                }
270            }
271        }
272
273        return ini;
274    }
275
276    //TODO - this logic is ugly - it'd be ideal if we had a Resource API to polymorphically encaspulate this behavior
277    private InputStream getServletContextResourceStream(String path) {
278        InputStream is = null;
279
280        path = WebUtils.normalize(path);
281        ServletContext sc = getServletContext();
282        if (sc != null) {
283            is = sc.getResourceAsStream(path);
284        }
285
286        return is;
287    }
288
289    /**
290     * Returns the {@code Ini} instance reflecting this WebEnvironment's configuration.
291     *
292     * @return the {@code Ini} instance reflecting this WebEnvironment's configuration.
293     */
294    public Ini getIni() {
295        return this.ini;
296    }
297
298    /**
299     * Allows for configuration via a direct {@link Ini} instance instead of via
300     * {@link #getConfigLocations() config locations}.
301     * <p/>
302     * If the specified instance is null or empty, the fallback/default resource-based configuration will be used.
303     *
304     * @param ini the ini instance to use for creation.
305     */
306    public void setIni(Ini ini) {
307        this.ini = ini;
308    }
309}