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