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.*;
026import org.apache.shiro.web.config.IniFilterChainResolverFactory;
027import org.apache.shiro.web.config.WebIniSecurityManagerFactory;
028import org.apache.shiro.web.filter.mgt.FilterChainResolver;
029import org.apache.shiro.web.mgt.WebSecurityManager;
030import org.apache.shiro.web.util.WebUtils;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034import javax.servlet.ServletContext;
035import java.io.IOException;
036import java.io.InputStream;
037import java.util.HashMap;
038import java.util.Map;
039
040/**
041 * {@link WebEnvironment} implementation configured by an {@link Ini} instance or {@code Ini} resource locations.
042 *
043 * @since 1.2
044 */
045public class IniWebEnvironment extends ResourceBasedWebEnvironment implements Initializable, Destroyable {
046
047    public static final String DEFAULT_WEB_INI_RESOURCE_PATH = "/WEB-INF/shiro.ini";
048    public static final String FILTER_CHAIN_RESOLVER_NAME = "filterChainResolver";
049
050    private static final Logger log = LoggerFactory.getLogger(IniWebEnvironment.class);
051
052    /**
053     * The Ini that configures this WebEnvironment instance.
054     */
055    private Ini ini;
056
057    private WebIniSecurityManagerFactory factory;
058
059    public IniWebEnvironment() {
060        factory = new WebIniSecurityManagerFactory();
061    }
062
063    /**
064     * Initializes this instance by resolving any potential (explicit or resource-configured) {@link Ini}
065     * configuration and calling {@link #configure() configure} for actual instance configuration.
066     */
067    public void init() {
068
069        setIni(parseConfig());
070
071        configure();
072    }
073
074    /**
075     * Loads configuration {@link Ini} from {@link #getConfigLocations()} if set, otherwise falling back
076     * to the {@link #getDefaultConfigLocations()}. Finally any Ini objects will be merged with the value returned
077     * from {@link #getFrameworkIni()}
078     * @return Ini configuration to be used by this Environment.
079     * @since 1.4
080     */
081    protected Ini parseConfig() {
082        Ini ini = getIni();
083
084        String[] configLocations = getConfigLocations();
085
086        if (log.isWarnEnabled() && !CollectionUtils.isEmpty(ini) &&
087                configLocations != null && configLocations.length > 0) {
088            log.warn("Explicit INI instance has been provided, but configuration locations have also been " +
089                    "specified.  The {} implementation does not currently support multiple Ini config, but this may " +
090                    "be supported in the future. Only the INI instance will be used for configuration.",
091                    IniWebEnvironment.class.getName());
092        }
093
094        if (CollectionUtils.isEmpty(ini)) {
095            log.debug("Checking any specified config locations.");
096            ini = getSpecifiedIni(configLocations);
097        }
098
099        if (CollectionUtils.isEmpty(ini)) {
100            log.debug("No INI instance or config locations specified.  Trying default config locations.");
101            ini = getDefaultIni();
102        }
103
104        // Allow for integrations to provide default that will be merged other configuration.
105        // to retain backwards compatibility this must be a different method then 'getDefaultIni()'
106        ini = mergeIni(getFrameworkIni(), ini);
107
108        if (CollectionUtils.isEmpty(ini)) {
109            String msg = "Shiro INI configuration was either not found or discovered to be empty/unconfigured.";
110            throw new ConfigurationException(msg);
111        }
112        return ini;
113    }
114
115    protected void configure() {
116
117        this.objects.clear();
118
119        WebSecurityManager securityManager = createWebSecurityManager();
120        setWebSecurityManager(securityManager);
121
122        FilterChainResolver resolver = createFilterChainResolver();
123        if (resolver != null) {
124            setFilterChainResolver(resolver);
125        }
126    }
127
128    /**
129     * Extension point to allow subclasses to provide an {@link Ini} configuration that will be merged into the
130     * users configuration.  The users configuration will override anything set here.
131     * <p>
132     * <strong>NOTE:</strong> Framework developers should use with caution. It is possible a user could provide
133     * configuration that would conflict with the frameworks configuration.  For example: if this method returns an
134     * Ini object with the following configuration:
135     * <pre><code>
136     *     [main]
137     *     realm = com.myco.FoobarRealm
138     *     realm.foobarSpecificField = A string
139     * </code></pre>
140     * And the user provides a similar configuration:
141     * <pre><code>
142     *     [main]
143     *     realm = net.differentco.MyCustomRealm
144     * </code></pre>
145     *
146     * This would merge into:
147     * <pre><code>
148     *     [main]
149     *     realm = net.differentco.MyCustomRealm
150     *     realm.foobarSpecificField = A string
151     * </code></pre>
152     *
153     * This may cause a configuration error if <code>MyCustomRealm</code> does not contain the field <code>foobarSpecificField</code>.
154     * This can be avoided if the Framework Ini uses more unique names, such as <code>foobarRealm</code>. which would result
155     * in a merged configuration that looks like:
156     * <pre><code>
157     *     [main]
158     *     foobarRealm = com.myco.FoobarRealm
159     *     foobarRealm.foobarSpecificField = A string
160     *     realm = net.differentco.MyCustomRealm
161     * </code></pre>
162     *
163     * </p>
164     *
165     * @return Ini configuration used by the framework integrations.
166     * @since 1.4
167     */
168    protected Ini getFrameworkIni() {
169        return null;
170    }
171
172    protected Ini getSpecifiedIni(String[] configLocations) throws ConfigurationException {
173
174        Ini ini = null;
175
176        if (configLocations != null && configLocations.length > 0) {
177
178            if (configLocations.length > 1) {
179                log.warn("More than one Shiro .ini config location has been specified.  Only the first will be " +
180                        "used for configuration as the {} implementation does not currently support multiple " +
181                        "files.  This may be supported in the future however.", IniWebEnvironment.class.getName());
182            }
183
184            //required, as it is user specified:
185            ini = createIni(configLocations[0], true);
186        }
187
188        return ini;
189    }
190
191    protected Ini mergeIni(Ini ini1, Ini ini2) {
192
193        if (ini1 == null) {
194            return ini2;
195        }
196
197        if (ini2 == null) {
198            return ini1;
199        }
200
201        // at this point we have two valid ini objects, create a new one and merge the contents of 2 into 1
202        Ini iniResult = new Ini(ini1);
203        iniResult.merge(ini2);
204
205        return iniResult;
206    }
207
208    protected Ini getDefaultIni() {
209
210        Ini ini = null;
211
212        String[] configLocations = getDefaultConfigLocations();
213        if (configLocations != null) {
214            for (String location : configLocations) {
215                ini = createIni(location, false);
216                if (!CollectionUtils.isEmpty(ini)) {
217                    log.debug("Discovered non-empty INI configuration at location '{}'.  Using for configuration.",
218                            location);
219                    break;
220                }
221            }
222        }
223
224        return ini;
225    }
226
227    /**
228     * Creates an {@link Ini} instance reflecting the specified path, or {@code null} if the path does not exist and
229     * is not required.
230     * <p/>
231     * If the path is required and does not exist or is empty, a {@link ConfigurationException} will be thrown.
232     *
233     * @param configLocation the resource path to load into an {@code Ini} instance.
234     * @param required       if the path must exist and be converted to a non-empty {@link Ini} instance.
235     * @return an {@link Ini} instance reflecting the specified path, or {@code null} if the path does not exist and
236     *         is not required.
237     * @throws ConfigurationException if the path is required but results in a null or empty Ini instance.
238     */
239    protected Ini createIni(String configLocation, boolean required) throws ConfigurationException {
240
241        Ini ini = null;
242
243        if (configLocation != null) {
244            ini = convertPathToIni(configLocation, required);
245        }
246        if (required && CollectionUtils.isEmpty(ini)) {
247            String msg = "Required configuration location '" + configLocation + "' does not exist or did not " +
248                    "contain any INI configuration.";
249            throw new ConfigurationException(msg);
250        }
251
252        return ini;
253    }
254
255    protected FilterChainResolver createFilterChainResolver() {
256
257        FilterChainResolver resolver = null;
258
259        Ini ini = getIni();
260
261        if (!CollectionUtils.isEmpty(ini)) {
262            Factory<FilterChainResolver> factory = (Factory<FilterChainResolver>) this.objects.get(FILTER_CHAIN_RESOLVER_NAME);
263            if (factory instanceof IniFactorySupport) {
264                IniFactorySupport iniFactory = (IniFactorySupport) factory;
265                iniFactory.setIni(ini);
266                iniFactory.setDefaults(this.objects);
267            }
268            resolver = factory.getInstance();
269        }
270
271        return resolver;
272    }
273
274    protected WebSecurityManager createWebSecurityManager() {
275
276        Ini ini = getIni();
277        if (!CollectionUtils.isEmpty(ini)) {
278            factory.setIni(ini);
279        }
280
281        Map<String, Object> defaults = getDefaults();
282        if (!CollectionUtils.isEmpty(defaults)) {
283            factory.setDefaults(defaults);
284        }
285
286        WebSecurityManager wsm = (WebSecurityManager)factory.getInstance();
287
288        //SHIRO-306 - get beans after they've been created (the call was before the factory.getInstance() call,
289        //which always returned null.
290        Map<String, ?> beans = factory.getBeans();
291        if (!CollectionUtils.isEmpty(beans)) {
292            this.objects.putAll(beans);
293        }
294
295        return wsm;
296    }
297
298    /**
299     * Returns an array with two elements, {@code /WEB-INF/shiro.ini} and {@code classpath:shiro.ini}.
300     *
301     * @return an array with two elements, {@code /WEB-INF/shiro.ini} and {@code classpath:shiro.ini}.
302     */
303    protected String[] getDefaultConfigLocations() {
304        return new String[]{
305                DEFAULT_WEB_INI_RESOURCE_PATH,
306                IniFactorySupport.DEFAULT_INI_RESOURCE_PATH
307        };
308    }
309
310    /**
311     * Converts the specified file path to an {@link Ini} instance.
312     * <p/>
313     * If the path does not have a resource prefix as defined by {@link org.apache.shiro.io.ResourceUtils#hasResourcePrefix(String)}, the
314     * path is expected to be resolvable by the {@code ServletContext} via
315     * {@link javax.servlet.ServletContext#getResourceAsStream(String)}.
316     *
317     * @param path     the path of the INI resource to load into an INI instance.
318     * @param required if the specified path must exist
319     * @return an INI instance populated based on the given INI resource path.
320     */
321    private Ini convertPathToIni(String path, boolean required) {
322
323        //TODO - this logic is ugly - it'd be ideal if we had a Resource API to polymorphically encaspulate this behavior
324
325        Ini ini = null;
326
327        if (StringUtils.hasText(path)) {
328            InputStream is = null;
329
330            //SHIRO-178: Check for servlet context resource and not only resource paths:
331            if (!ResourceUtils.hasResourcePrefix(path)) {
332                is = getServletContextResourceStream(path);
333            } else {
334                try {
335                    is = ResourceUtils.getInputStreamForPath(path);
336                } catch (IOException e) {
337                    if (required) {
338                        throw new ConfigurationException(e);
339                    } else {
340                        if (log.isDebugEnabled()) {
341                            log.debug("Unable to load optional path '" + path + "'.", e);
342                        }
343                    }
344                }
345            }
346            if (is != null) {
347                ini = new Ini();
348                ini.load(is);
349            } else {
350                if (required) {
351                    throw new ConfigurationException("Unable to load resource path '" + path + "'");
352                }
353            }
354        }
355
356        return ini;
357    }
358
359    //TODO - this logic is ugly - it'd be ideal if we had a Resource API to polymorphically encaspulate this behavior
360    private InputStream getServletContextResourceStream(String path) {
361        InputStream is = null;
362
363        path = WebUtils.normalize(path);
364        ServletContext sc = getServletContext();
365        if (sc != null) {
366            is = sc.getResourceAsStream(path);
367        }
368
369        return is;
370    }
371
372    /**
373     * Returns the {@code Ini} instance reflecting this WebEnvironment's configuration.
374     *
375     * @return the {@code Ini} instance reflecting this WebEnvironment's configuration.
376     */
377    public Ini getIni() {
378        return this.ini;
379    }
380
381    /**
382     * Allows for configuration via a direct {@link Ini} instance instead of via
383     * {@link #getConfigLocations() config locations}.
384     * <p/>
385     * If the specified instance is null or empty, the fallback/default resource-based configuration will be used.
386     *
387     * @param ini the ini instance to use for creation.
388     */
389    public void setIni(Ini ini) {
390        this.ini = ini;
391    }
392
393    protected Map<String, Object> getDefaults() {
394        Map<String, Object> defaults = new HashMap<String, Object>();
395        defaults.put(FILTER_CHAIN_RESOLVER_NAME, new IniFilterChainResolverFactory());
396        return defaults;
397    }
398
399    /**
400     * Returns the SecurityManager factory used by this WebEnvironment.
401     *
402     * @return the SecurityManager factory used by this WebEnvironment.
403     * @since 1.4
404     */
405    @SuppressWarnings("unused")
406    protected WebIniSecurityManagerFactory getSecurityManagerFactory() {
407        return factory;
408    }
409
410    /**
411     * Allows for setting the SecurityManager factory which will be used to create the SecurityManager.
412     *
413     * @param factory the SecurityManager factory to used.
414     * @since 1.4
415     */
416    protected void setSecurityManagerFactory(WebIniSecurityManagerFactory factory) {
417        this.factory = factory;
418    }
419}