View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.shiro.web.env;
20  
21  import org.apache.shiro.config.ConfigurationException;
22  import org.apache.shiro.config.Ini;
23  import org.apache.shiro.config.IniFactorySupport;
24  import org.apache.shiro.io.ResourceUtils;
25  import org.apache.shiro.util.*;
26  import org.apache.shiro.web.config.IniFilterChainResolverFactory;
27  import org.apache.shiro.web.config.WebIniSecurityManagerFactory;
28  import org.apache.shiro.web.filter.mgt.FilterChainResolver;
29  import org.apache.shiro.web.mgt.WebSecurityManager;
30  import org.apache.shiro.web.util.WebUtils;
31  import org.slf4j.Logger;
32  import org.slf4j.LoggerFactory;
33  
34  import javax.servlet.ServletContext;
35  import java.io.IOException;
36  import java.io.InputStream;
37  import java.util.HashMap;
38  import java.util.Map;
39  
40  /**
41   * {@link WebEnvironment} implementation configured by an {@link Ini} instance or {@code Ini} resource locations.
42   *
43   * @since 1.2
44   */
45  public class IniWebEnvironment extends ResourceBasedWebEnvironment implements Initializable, Destroyable {
46  
47      public static final String DEFAULT_WEB_INI_RESOURCE_PATH = "/WEB-INF/shiro.ini";
48      public static final String FILTER_CHAIN_RESOLVER_NAME = "filterChainResolver";
49  
50      private static final Logger log = LoggerFactory.getLogger(IniWebEnvironment.class);
51  
52      /**
53       * The Ini that configures this WebEnvironment instance.
54       */
55      private Ini ini;
56  
57      private WebIniSecurityManagerFactory factory;
58  
59      public IniWebEnvironment() {
60          factory = new WebIniSecurityManagerFactory();
61      }
62  
63      /**
64       * Initializes this instance by resolving any potential (explicit or resource-configured) {@link Ini}
65       * configuration and calling {@link #configure() configure} for actual instance configuration.
66       */
67      public void init() {
68  
69          setIni(parseConfig());
70  
71          configure();
72      }
73  
74      /**
75       * Loads configuration {@link Ini} from {@link #getConfigLocations()} if set, otherwise falling back
76       * to the {@link #getDefaultConfigLocations()}. Finally any Ini objects will be merged with the value returned
77       * from {@link #getFrameworkIni()}
78       * @return Ini configuration to be used by this Environment.
79       * @since 1.4
80       */
81      protected Ini parseConfig() {
82          Ini ini = getIni();
83  
84          String[] configLocations = getConfigLocations();
85  
86          if (log.isWarnEnabled() && !CollectionUtils.isEmpty(ini) &&
87                  configLocations != null && configLocations.length > 0) {
88              log.warn("Explicit INI instance has been provided, but configuration locations have also been " +
89                      "specified.  The {} implementation does not currently support multiple Ini config, but this may " +
90                      "be supported in the future. Only the INI instance will be used for configuration.",
91                      IniWebEnvironment.class.getName());
92          }
93  
94          if (CollectionUtils.isEmpty(ini)) {
95              log.debug("Checking any specified config locations.");
96              ini = getSpecifiedIni(configLocations);
97          }
98  
99          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             //only create a resolver if the 'filters' or 'urls' sections are defined:
263             Ini.Section urls = ini.getSection(IniFilterChainResolverFactory.URLS);
264             Ini.Section filters = ini.getSection(IniFilterChainResolverFactory.FILTERS);
265             if (!CollectionUtils.isEmpty(urls) || !CollectionUtils.isEmpty(filters)) {
266                 //either the urls section or the filters section was defined.  Go ahead and create the resolver:
267 
268                 Factory<FilterChainResolver> factory = (Factory<FilterChainResolver>) this.objects.get(FILTER_CHAIN_RESOLVER_NAME);
269                 if (factory instanceof IniFactorySupport) {
270                     IniFactorySupport iniFactory = (IniFactorySupport) factory;
271                     iniFactory.setIni(ini);
272                     iniFactory.setDefaults(this.objects);
273                 }
274                 resolver = factory.getInstance();
275             }
276         }
277 
278         return resolver;
279     }
280 
281     protected WebSecurityManager createWebSecurityManager() {
282 
283         Ini ini = getIni();
284         if (!CollectionUtils.isEmpty(ini)) {
285             factory.setIni(ini);
286         }
287 
288         Map<String, Object> defaults = getDefaults();
289         if (!CollectionUtils.isEmpty(defaults)) {
290             factory.setDefaults(defaults);
291         }
292 
293         WebSecurityManager../../org/apache/shiro/web/mgt/WebSecurityManager.html#WebSecurityManager">WebSecurityManager wsm = (WebSecurityManager)factory.getInstance();
294 
295         //SHIRO-306 - get beans after they've been created (the call was before the factory.getInstance() call,
296         //which always returned null.
297         Map<String, ?> beans = factory.getBeans();
298         if (!CollectionUtils.isEmpty(beans)) {
299             this.objects.putAll(beans);
300         }
301 
302         return wsm;
303     }
304 
305     /**
306      * Returns an array with two elements, {@code /WEB-INF/shiro.ini} and {@code classpath:shiro.ini}.
307      *
308      * @return an array with two elements, {@code /WEB-INF/shiro.ini} and {@code classpath:shiro.ini}.
309      */
310     protected String[] getDefaultConfigLocations() {
311         return new String[]{
312                 DEFAULT_WEB_INI_RESOURCE_PATH,
313                 IniFactorySupport.DEFAULT_INI_RESOURCE_PATH
314         };
315     }
316 
317     /**
318      * Converts the specified file path to an {@link Ini} instance.
319      * <p/>
320      * If the path does not have a resource prefix as defined by {@link org.apache.shiro.io.ResourceUtils#hasResourcePrefix(String)}, the
321      * path is expected to be resolvable by the {@code ServletContext} via
322      * {@link javax.servlet.ServletContext#getResourceAsStream(String)}.
323      *
324      * @param path     the path of the INI resource to load into an INI instance.
325      * @param required if the specified path must exist
326      * @return an INI instance populated based on the given INI resource path.
327      */
328     private Ini convertPathToIni(String path, boolean required) {
329 
330         //TODO - this logic is ugly - it'd be ideal if we had a Resource API to polymorphically encaspulate this behavior
331 
332         Ini ini = null;
333 
334         if (StringUtils.hasText(path)) {
335             InputStream is = null;
336 
337             //SHIRO-178: Check for servlet context resource and not only resource paths:
338             if (!ResourceUtils.hasResourcePrefix(path)) {
339                 is = getServletContextResourceStream(path);
340             } else {
341                 try {
342                     is = ResourceUtils.getInputStreamForPath(path);
343                 } catch (IOException e) {
344                     if (required) {
345                         throw new ConfigurationException(e);
346                     } else {
347                         if (log.isDebugEnabled()) {
348                             log.debug("Unable to load optional path '" + path + "'.", e);
349                         }
350                     }
351                 }
352             }
353             if (is != null) {
354                 ini = new Ini();
355                 ini.load(is);
356             } else {
357                 if (required) {
358                     throw new ConfigurationException("Unable to load resource path '" + path + "'");
359                 }
360             }
361         }
362 
363         return ini;
364     }
365 
366     //TODO - this logic is ugly - it'd be ideal if we had a Resource API to polymorphically encaspulate this behavior
367     private InputStream getServletContextResourceStream(String path) {
368         InputStream is = null;
369 
370         path = WebUtils.normalize(path);
371         ServletContext sc = getServletContext();
372         if (sc != null) {
373             is = sc.getResourceAsStream(path);
374         }
375 
376         return is;
377     }
378 
379     /**
380      * Returns the {@code Ini} instance reflecting this WebEnvironment's configuration.
381      *
382      * @return the {@code Ini} instance reflecting this WebEnvironment's configuration.
383      */
384     public Ini getIni() {
385         return this.ini;
386     }
387 
388     /**
389      * Allows for configuration via a direct {@link Ini} instance instead of via
390      * {@link #getConfigLocations() config locations}.
391      * <p/>
392      * If the specified instance is null or empty, the fallback/default resource-based configuration will be used.
393      *
394      * @param ini the ini instance to use for creation.
395      */
396     public void setIni(Ini ini) {
397         this.ini = ini;
398     }
399 
400     protected Map<String, Object> getDefaults() {
401         Map<String, Object> defaults = new HashMap<String, Object>();
402         defaults.put(FILTER_CHAIN_RESOLVER_NAME, new IniFilterChainResolverFactory());
403         return defaults;
404     }
405 
406     /**
407      * Returns the SecurityManager factory used by this WebEnvironment.
408      *
409      * @return the SecurityManager factory used by this WebEnvironment.
410      * @since 1.4
411      */
412     @SuppressWarnings("unused")
413     protected WebIniSecurityManagerFactory getSecurityManagerFactory() {
414         return factory;
415     }
416 
417     /**
418      * Allows for setting the SecurityManager factory which will be used to create the SecurityManager.
419      *
420      * @param factory the SecurityManager factory to used.
421      * @since 1.4
422      */
423     protected void setSecurityManagerFactory(WebIniSecurityManagerFactory factory) {
424         this.factory = factory;
425     }
426 }