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}