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     */
019    package org.apache.shiro.web.env;
020    
021    import org.apache.shiro.config.ConfigurationException;
022    import org.apache.shiro.config.Ini;
023    import org.apache.shiro.config.IniFactorySupport;
024    import org.apache.shiro.io.ResourceUtils;
025    import org.apache.shiro.util.CollectionUtils;
026    import org.apache.shiro.util.Destroyable;
027    import org.apache.shiro.util.Initializable;
028    import org.apache.shiro.util.StringUtils;
029    import org.apache.shiro.web.config.IniFilterChainResolverFactory;
030    import org.apache.shiro.web.config.WebIniSecurityManagerFactory;
031    import org.apache.shiro.web.filter.mgt.FilterChainResolver;
032    import org.apache.shiro.web.mgt.WebSecurityManager;
033    import org.apache.shiro.web.util.WebUtils;
034    import org.slf4j.Logger;
035    import org.slf4j.LoggerFactory;
036    
037    import javax.servlet.ServletContext;
038    import java.io.IOException;
039    import java.io.InputStream;
040    import 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     */
047    public 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    }