Coverage Report - org.apache.shiro.realm.text.PropertiesRealm
 
Classes in this File Line Coverage Branch Coverage Complexity
PropertiesRealm
53%
58/109
32%
18/56
2.619
 
 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.realm.text;
 20  
 
 21  
 import org.apache.shiro.ShiroException;
 22  
 import org.apache.shiro.io.ResourceUtils;
 23  
 import org.apache.shiro.util.Destroyable;
 24  
 import org.slf4j.Logger;
 25  
 import org.slf4j.LoggerFactory;
 26  
 
 27  
 import java.io.File;
 28  
 import java.io.IOException;
 29  
 import java.io.InputStream;
 30  
 import java.util.Enumeration;
 31  
 import java.util.Properties;
 32  
 import java.util.concurrent.ExecutorService;
 33  
 import java.util.concurrent.Executors;
 34  
 import java.util.concurrent.ScheduledExecutorService;
 35  
 import java.util.concurrent.TimeUnit;
 36  
 
 37  
 /**
 38  
  * A {@link TextConfigurationRealm} that defers all logic to the parent class, but just enables
 39  
  * {@link java.util.Properties Properties} based configuration in addition to the parent class's String configuration.
 40  
  * <p/>
 41  
  * This class allows processing of a single .properties file for user, role, and
 42  
  * permission configuration.
 43  
  * <p/>
 44  
  * The {@link #setResourcePath resourcePath} <em>MUST</em> be set before this realm can be initialized.  You
 45  
  * can specify any resource path supported by
 46  
  * {@link ResourceUtils#getInputStreamForPath(String) ResourceUtils.getInputStreamForPath} method.
 47  
  * <p/>
 48  
  * The Properties format understood by this implementation must be written as follows:
 49  
  * <p/>
 50  
  * Each line's key/value pair represents either a user-to-role(s) mapping <em>or</em> a role-to-permission(s)
 51  
  * mapping.
 52  
  * <p/>
 53  
  * The user-to-role(s) lines have this format:</p>
 54  
  * <p/>
 55  
  * <code><b>user.</b><em>username</em> = <em>password</em>,role1,role2,...</code></p>
 56  
  * <p/>
 57  
  * Note that each key is prefixed with the token <b>{@code user.}</b>  Each value must adhere to the
 58  
  * the {@link #setUserDefinitions(String) setUserDefinitions(String)} JavaDoc.
 59  
  * <p/>
 60  
  * The role-to-permission(s) lines have this format:</p>
 61  
  * <p/>
 62  
  * <code><b>role.</b><em>rolename</em> = <em>permissionDefinition1</em>, <em>permissionDefinition2</em>, ...</code>
 63  
  * <p/>
 64  
  * where each key is prefixed with the token <b>{@code role.}</b> and the value adheres to the format specified in
 65  
  * the {@link #setRoleDefinitions(String) setRoleDefinitions(String)} JavaDoc.
 66  
  * <p/>
 67  
  * Here is an example of a very simple properties definition that conforms to the above format rules and corresponding
 68  
  * method JavaDocs:
 69  
  * <p/>
 70  
  * <code>user.root = <em>rootPassword</em>,administrator<br/>
 71  
  * user.jsmith = <em>jsmithPassword</em>,manager,engineer,employee<br/>
 72  
  * user.abrown = <em>abrownPassword</em>,qa,employee<br/>
 73  
  * user.djones = <em>djonesPassword</em>,qa,contractor<br/>
 74  
  * <br/>
 75  
  * role.administrator = *<br/>
 76  
  * role.manager = &quot;user:read,write&quot;, file:execute:/usr/local/emailManagers.sh<br/>
 77  
  * role.engineer = &quot;file:read,execute:/usr/local/tomcat/bin/startup.sh&quot;<br/>
 78  
  * role.employee = application:use:wiki<br/>
 79  
  * role.qa = &quot;server:view,start,shutdown,restart:someQaServer&quot;, server:view:someProductionServer<br/>
 80  
  * role.contractor = application:use:timesheet</code>
 81  
  *
 82  
  * @since 0.2
 83  
  */
 84  
 public class PropertiesRealm extends TextConfigurationRealm implements Destroyable, Runnable {
 85  
 
 86  
     //TODO - complete JavaDoc
 87  
 
 88  
     /*-------------------------------------------
 89  
     |             C O N S T A N T S             |
 90  
     ============================================*/
 91  
     private static final int DEFAULT_RELOAD_INTERVAL_SECONDS = 10;
 92  
     private static final String USERNAME_PREFIX = "user.";
 93  
     private static final String ROLENAME_PREFIX = "role.";
 94  
     private static final String DEFAULT_RESOURCE_PATH = "classpath:shiro-users.properties";
 95  
 
 96  
     /*-------------------------------------------
 97  
     |    I N S T A N C E   V A R I A B L E S    |
 98  
     ============================================*/
 99  1
     private static final Logger log = LoggerFactory.getLogger(PropertiesRealm.class);
 100  
 
 101  1
     protected ExecutorService scheduler = null;
 102  1
     protected boolean useXmlFormat = false;
 103  1
     protected String resourcePath = DEFAULT_RESOURCE_PATH;
 104  
     protected long fileLastModified;
 105  1
     protected int reloadIntervalSeconds = DEFAULT_RELOAD_INTERVAL_SECONDS;
 106  
 
 107  
     public PropertiesRealm() {
 108  1
         super();
 109  1
     }
 110  
 
 111  
     /*--------------------------------------------
 112  
     |  A C C E S S O R S / M O D I F I E R S    |
 113  
     ============================================*/
 114  
 
 115  
     /**
 116  
      * Determines whether or not the properties XML format should be used.  For more information, see
 117  
      * {@link Properties#loadFromXML(java.io.InputStream)}
 118  
      *
 119  
      * @param useXmlFormat true to use XML or false to use the normal format.  Defaults to false.
 120  
      */
 121  
     public void setUseXmlFormat(boolean useXmlFormat) {
 122  0
         this.useXmlFormat = useXmlFormat;
 123  0
     }
 124  
 
 125  
     /**
 126  
      * Sets the path of the properties file to load user, role, and permission information from.  The properties
 127  
      * file will be loaded using {@link ResourceUtils#getInputStreamForPath(String)} so any convention recongized
 128  
      * by that method is accepted here.  For example, to load a file from the classpath use
 129  
      * {@code classpath:myfile.properties}; to load a file from disk simply specify the full path; to load
 130  
      * a file from a URL use {@code url:www.mysite.com/myfile.properties}.
 131  
      *
 132  
      * @param resourcePath the path to load the properties file from.  This is a required property.
 133  
      */
 134  
     public void setResourcePath(String resourcePath) {
 135  1
         this.resourcePath = resourcePath;
 136  1
     }
 137  
 
 138  
     /**
 139  
      * Sets the interval in seconds at which the property file will be checked for changes and reloaded.  If this is
 140  
      * set to zero or less, property file reloading will be disabled.  If it is set to 1 or greater, then a
 141  
      * separate thread will be created to monitor the propery file for changes and reload the file if it is updated.
 142  
      *
 143  
      * @param reloadIntervalSeconds the interval in seconds at which the property file should be examined for changes.
 144  
      *                              If set to zero or less, reloading is disabled.
 145  
      */
 146  
     public void setReloadIntervalSeconds(int reloadIntervalSeconds) {
 147  0
         this.reloadIntervalSeconds = reloadIntervalSeconds;
 148  0
     }
 149  
 
 150  
     /*--------------------------------------------
 151  
     |               M E T H O D S               |
 152  
     ============================================*/
 153  
 
 154  
     @Override
 155  
     public void onInit() {
 156  1
         super.onInit();
 157  
         //TODO - cleanup - this method shouldn't be necessary
 158  1
         afterRoleCacheSet();
 159  1
     }
 160  
 
 161  
     protected void afterRoleCacheSet() {
 162  1
         loadProperties();
 163  
         //we can only determine if files have been modified at runtime (not classpath entries or urls), so only
 164  
         //start the thread in this case:
 165  1
         if (this.resourcePath.startsWith(ResourceUtils.FILE_PREFIX) && scheduler == null) {
 166  0
             startReloadThread();
 167  
         }
 168  1
     }
 169  
 
 170  
     /**
 171  
      * Destroy reload scheduler if one exists.
 172  
      */
 173  
     public void destroy() {
 174  
         try {
 175  0
             if (scheduler != null) {
 176  0
                 scheduler.shutdown();
 177  
             }
 178  0
         } catch (Exception e) {
 179  0
             if (log.isInfoEnabled()) {
 180  0
                 log.info("Unable to cleanly shutdown Scheduler.  Ignoring (shutting down)...", e);
 181  
             }
 182  
         } finally {
 183  0
             scheduler = null;
 184  0
         }
 185  0
     }
 186  
 
 187  
     protected void startReloadThread() {
 188  0
         if (this.reloadIntervalSeconds > 0) {
 189  0
             this.scheduler = Executors.newSingleThreadScheduledExecutor();
 190  0
             ((ScheduledExecutorService) this.scheduler).scheduleAtFixedRate(this, reloadIntervalSeconds, reloadIntervalSeconds, TimeUnit.SECONDS);
 191  
         }
 192  0
     }
 193  
 
 194  
     public void run() {
 195  
         try {
 196  0
             reloadPropertiesIfNecessary();
 197  0
         } catch (Exception e) {
 198  0
             if (log.isErrorEnabled()) {
 199  0
                 log.error("Error while reloading property files for realm.", e);
 200  
             }
 201  0
         }
 202  0
     }
 203  
 
 204  
     private void loadProperties() {
 205  1
         if (resourcePath == null || resourcePath.length() == 0) {
 206  0
             throw new IllegalStateException("The resourcePath property is not set.  " +
 207  
                     "It must be set prior to this realm being initialized.");
 208  
         }
 209  
 
 210  1
         if (log.isDebugEnabled()) {
 211  1
             log.debug("Loading user security information from file [" + resourcePath + "]...");
 212  
         }
 213  
 
 214  1
         Properties properties = loadProperties(resourcePath);
 215  1
         createRealmEntitiesFromProperties(properties);
 216  1
     }
 217  
 
 218  
     private Properties loadProperties(String resourcePath) {
 219  1
         Properties props = new Properties();
 220  
 
 221  1
         InputStream is = null;
 222  
         try {
 223  
 
 224  1
             if (log.isDebugEnabled()) {
 225  1
                 log.debug("Opening input stream for path [" + resourcePath + "]...");
 226  
             }
 227  
 
 228  1
             is = ResourceUtils.getInputStreamForPath(resourcePath);
 229  1
             if (useXmlFormat) {
 230  
 
 231  0
                 if (log.isDebugEnabled()) {
 232  0
                     log.debug("Loading properties from path [" + resourcePath + "] in XML format...");
 233  
                 }
 234  
 
 235  0
                 props.loadFromXML(is);
 236  
             } else {
 237  
 
 238  1
                 if (log.isDebugEnabled()) {
 239  1
                     log.debug("Loading properties from path [" + resourcePath + "]...");
 240  
                 }
 241  
 
 242  1
                 props.load(is);
 243  
             }
 244  
 
 245  0
         } catch (IOException e) {
 246  0
             throw new ShiroException("Error reading properties path [" + resourcePath + "].  " +
 247  
                     "Initializing of the realm from this file failed.", e);
 248  
         } finally {
 249  1
             ResourceUtils.close(is);
 250  1
         }
 251  
 
 252  1
         return props;
 253  
     }
 254  
 
 255  
 
 256  
     private void reloadPropertiesIfNecessary() {
 257  0
         if (isSourceModified()) {
 258  0
             restart();
 259  
         }
 260  0
     }
 261  
 
 262  
     private boolean isSourceModified() {
 263  
         //we can only check last modified times on files - classpath and URL entries can't tell us modification times
 264  0
         return this.resourcePath.startsWith(ResourceUtils.FILE_PREFIX) && isFileModified();
 265  
     }
 266  
 
 267  
     private boolean isFileModified() {
 268  
         //SHIRO-394: strip file prefix before constructing the File instance:
 269  0
         String fileNameWithoutPrefix = this.resourcePath.substring(this.resourcePath.indexOf(":") + 1);
 270  0
         File propertyFile = new File(fileNameWithoutPrefix);
 271  0
         long currentLastModified = propertyFile.lastModified();
 272  0
         if (currentLastModified > this.fileLastModified) {
 273  0
             this.fileLastModified = currentLastModified;
 274  0
             return true;
 275  
         } else {
 276  0
             return false;
 277  
         }
 278  
     }
 279  
 
 280  
     @SuppressWarnings("unchecked")
 281  
     private void restart() {
 282  0
         if (resourcePath == null || resourcePath.length() == 0) {
 283  0
             throw new IllegalStateException("The resourcePath property is not set.  " +
 284  
                     "It must be set prior to this realm being initialized.");
 285  
         }
 286  
 
 287  0
         if (log.isDebugEnabled()) {
 288  0
             log.debug("Loading user security information from file [" + resourcePath + "]...");
 289  
         }
 290  
 
 291  
         try {
 292  0
             destroy();
 293  0
         } catch (Exception e) {
 294  
             //ignored
 295  0
         }
 296  0
         init();
 297  0
     }
 298  
 
 299  
     @SuppressWarnings("unchecked")
 300  
     private void createRealmEntitiesFromProperties(Properties properties) {
 301  
 
 302  1
         StringBuilder userDefs = new StringBuilder();
 303  1
         StringBuilder roleDefs = new StringBuilder();
 304  
 
 305  1
         Enumeration<String> propNames = (Enumeration<String>) properties.propertyNames();
 306  
 
 307  3
         while (propNames.hasMoreElements()) {
 308  
 
 309  2
             String key = propNames.nextElement().trim();
 310  2
             String value = properties.getProperty(key).trim();
 311  2
             if (log.isTraceEnabled()) {
 312  2
                 log.trace("Processing properties line - key: [" + key + "], value: [" + value + "].");
 313  
             }
 314  
 
 315  2
             if (isUsername(key)) {
 316  1
                 String username = getUsername(key);
 317  1
                 userDefs.append(username).append(" = ").append(value).append("\n");
 318  1
             } else if (isRolename(key)) {
 319  1
                 String rolename = getRolename(key);
 320  1
                 roleDefs.append(rolename).append(" = ").append(value).append("\n");
 321  1
             } else {
 322  0
                 String msg = "Encountered unexpected key/value pair.  All keys must be prefixed with either '" +
 323  
                         USERNAME_PREFIX + "' or '" + ROLENAME_PREFIX + "'.";
 324  0
                 throw new IllegalStateException(msg);
 325  
             }
 326  2
         }
 327  
 
 328  1
         setUserDefinitions(userDefs.toString());
 329  1
         setRoleDefinitions(roleDefs.toString());
 330  1
         processDefinitions();
 331  1
     }
 332  
 
 333  
     protected String getName(String key, String prefix) {
 334  2
         return key.substring(prefix.length(), key.length());
 335  
     }
 336  
 
 337  
     protected boolean isUsername(String key) {
 338  2
         return key != null && key.startsWith(USERNAME_PREFIX);
 339  
     }
 340  
 
 341  
     protected boolean isRolename(String key) {
 342  1
         return key != null && key.startsWith(ROLENAME_PREFIX);
 343  
     }
 344  
 
 345  
     protected String getUsername(String key) {
 346  1
         return getName(key, USERNAME_PREFIX);
 347  
     }
 348  
 
 349  
     protected String getRolename(String key) {
 350  1
         return getName(key, ROLENAME_PREFIX);
 351  
     }
 352  
 }