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.realm.text;
020    
021    import org.apache.shiro.ShiroException;
022    import org.apache.shiro.io.ResourceUtils;
023    import org.apache.shiro.util.Destroyable;
024    import org.slf4j.Logger;
025    import org.slf4j.LoggerFactory;
026    
027    import java.io.File;
028    import java.io.IOException;
029    import java.io.InputStream;
030    import java.util.Enumeration;
031    import java.util.Properties;
032    import java.util.concurrent.ExecutorService;
033    import java.util.concurrent.Executors;
034    import java.util.concurrent.ScheduledExecutorService;
035    import java.util.concurrent.TimeUnit;
036    
037    
038    /**
039     * A {@link TextConfigurationRealm} that defers all logic to the parent class, but just enables
040     * {@link java.util.Properties Properties} based configuration in addition to the parent class's String configuration.
041     * <p/>
042     * This class allows processing of a single .properties file for user, role, and
043     * permission configuration.
044     * <p/>
045     * The {@link #setResourcePath resourcePath} <em>MUST</em> be set before this realm can be initialized.  You
046     * can specify any resource path supported by
047     * {@link ResourceUtils#getInputStreamForPath(String) ResourceUtils.getInputStreamForPath} method.
048     * <p/>
049     * The Properties format understood by this implementation must be written as follows:
050     * <p/>
051     * Each line's key/value pair represents either a user-to-role(s) mapping <em>or</em> a role-to-permission(s)
052     * mapping.
053     * <p/>
054     * The user-to-role(s) lines have this format:</p>
055     * <p/>
056     * <code><b>user.</b><em>username</em> = <em>password</em>,role1,role2,...</code></p>
057     * <p/>
058     * Note that each key is prefixed with the token <b>{@code user.}</b>  Each value must adhere to the
059     * the {@link #setUserDefinitions(String) setUserDefinitions(String)} JavaDoc.
060     * <p/>
061     * The role-to-permission(s) lines have this format:</p>
062     * <p/>
063     * <code><b>role.</b><em>rolename</em> = <em>permissionDefinition1</em>, <em>permissionDefinition2</em>, ...</code>
064     * <p/>
065     * where each key is prefixed with the token <b>{@code role.}</b> and the value adheres to the format specified in
066     * the {@link #setRoleDefinitions(String) setRoleDefinitions(String)} JavaDoc.
067     * <p/>
068     * Here is an example of a very simple properties definition that conforms to the above format rules and corresponding
069     * method JavaDocs:
070     * <p/>
071     * <code>user.root = <em>rootPassword</em>,administrator<br/>
072     * user.jsmith = <em>jsmithPassword</em>,manager,engineer,employee<br/>
073     * user.abrown = <em>abrownPassword</em>,qa,employee<br/>
074     * user.djones = <em>djonesPassword</em>,qa,contractor<br/>
075     * <br/>
076     * role.administrator = *<br/>
077     * role.manager = &quot;user:read,write&quot;, file:execute:/usr/local/emailManagers.sh<br/>
078     * role.engineer = &quot;file:read,execute:/usr/local/tomcat/bin/startup.sh&quot;<br/>
079     * role.employee = application:use:wiki<br/>
080     * role.qa = &quot;server:view,start,shutdown,restart:someQaServer&quot;, server:view:someProductionServer<br/>
081     * role.contractor = application:use:timesheet</code>
082     *
083     * @author Les Hazlewood
084     * @author Jeremy Haile
085     * @since 0.2
086     */
087    public class PropertiesRealm extends TextConfigurationRealm implements Destroyable, Runnable {
088    
089        //TODO - complete JavaDoc
090    
091        /*-------------------------------------------
092        |             C O N S T A N T S             |
093        ============================================*/
094        private static final int DEFAULT_RELOAD_INTERVAL_SECONDS = 10;
095        private static final String USERNAME_PREFIX = "user.";
096        private static final String ROLENAME_PREFIX = "role.";
097        private static final String DEFAULT_RESOURCE_PATH = "classpath:shiro-users.properties";
098    
099        /*-------------------------------------------
100        |    I N S T A N C E   V A R I A B L E S    |
101        ============================================*/
102        private static final Logger log = LoggerFactory.getLogger(PropertiesRealm.class);
103    
104        protected ExecutorService scheduler = null;
105        protected boolean useXmlFormat = false;
106        protected String resourcePath = DEFAULT_RESOURCE_PATH;
107        protected long fileLastModified;
108        protected int reloadIntervalSeconds = DEFAULT_RELOAD_INTERVAL_SECONDS;
109    
110        public PropertiesRealm() {
111            super();
112        }
113    
114        /*--------------------------------------------
115        |  A C C E S S O R S / M O D I F I E R S    |
116        ============================================*/
117    
118        /**
119         * Determines whether or not the properties XML format should be used.  For more information, see
120         * {@link Properties#loadFromXML(java.io.InputStream)}
121         *
122         * @param useXmlFormat true to use XML or false to use the normal format.  Defaults to false.
123         */
124        public void setUseXmlFormat(boolean useXmlFormat) {
125            this.useXmlFormat = useXmlFormat;
126        }
127    
128        /**
129         * Sets the path of the properties file to load user, role, and permission information from.  The properties
130         * file will be loaded using {@link ResourceUtils#getInputStreamForPath(String)} so any convention recongized
131         * by that method is accepted here.  For example, to load a file from the classpath use
132         * {@code classpath:myfile.properties}; to load a file from disk simply specify the full path; to load
133         * a file from a URL use {@code url:www.mysite.com/myfile.properties}.
134         *
135         * @param resourcePath the path to load the properties file from.  This is a required property.
136         */
137        public void setResourcePath(String resourcePath) {
138            this.resourcePath = resourcePath;
139        }
140    
141        /**
142         * Sets the interval in seconds at which the property file will be checked for changes and reloaded.  If this is
143         * set to zero or less, property file reloading will be disabled.  If it is set to 1 or greater, then a
144         * separate thread will be created to monitor the propery file for changes and reload the file if it is updated.
145         *
146         * @param reloadIntervalSeconds the interval in seconds at which the property file should be examined for changes.
147         *                              If set to zero or less, reloading is disabled.
148         */
149        public void setReloadIntervalSeconds(int reloadIntervalSeconds) {
150            this.reloadIntervalSeconds = reloadIntervalSeconds;
151        }
152    
153        /*--------------------------------------------
154        |               M E T H O D S               |
155        ============================================*/
156    
157        @Override
158        public void onInit() {
159            //TODO - cleanup - this method shouldn't be necessary
160            afterRoleCacheSet();
161        }
162    
163        protected void afterRoleCacheSet() {
164            loadProperties();
165            //we can only determine if files have been modified at runtime (not classpath entries or urls), so only
166            //start the thread in this case:
167            if (this.resourcePath.startsWith(ResourceUtils.FILE_PREFIX) && scheduler != null) {
168                startReloadThread();
169            }
170        }
171    
172        public void destroy() {
173            try {
174                if (scheduler != null) {
175                    scheduler.shutdown();
176                }
177            } catch (Exception e) {
178                if (log.isInfoEnabled()) {
179                    log.info("Unable to cleanly shutdown Scheduler.  Ignoring (shutting down)...", e);
180                }
181            }
182        }
183    
184        protected void startReloadThread() {
185            if (this.reloadIntervalSeconds > 0) {
186                this.scheduler = Executors.newSingleThreadScheduledExecutor();
187                ((ScheduledExecutorService) this.scheduler).scheduleAtFixedRate(this, reloadIntervalSeconds, reloadIntervalSeconds, TimeUnit.SECONDS);
188            }
189        }
190    
191        public void run() {
192            try {
193                reloadPropertiesIfNecessary();
194            } catch (Exception e) {
195                if (log.isErrorEnabled()) {
196                    log.error("Error while reloading property files for realm.", e);
197                }
198            }
199        }
200    
201        private void loadProperties() {
202            if (resourcePath == null || resourcePath.length() == 0) {
203                throw new IllegalStateException("The resourcePath property is not set.  " +
204                        "It must be set prior to this realm being initialized.");
205            }
206    
207            if (log.isDebugEnabled()) {
208                log.debug("Loading user security information from file [" + resourcePath + "]...");
209            }
210    
211            Properties properties = loadProperties(resourcePath);
212            createRealmEntitiesFromProperties(properties);
213        }
214    
215        private Properties loadProperties(String resourcePath) {
216            Properties props = new Properties();
217    
218            InputStream is = null;
219            try {
220    
221                if (log.isDebugEnabled()) {
222                    log.debug("Opening input stream for path [" + resourcePath + "]...");
223                }
224    
225                is = ResourceUtils.getInputStreamForPath(resourcePath);
226                if (useXmlFormat) {
227    
228                    if (log.isDebugEnabled()) {
229                        log.debug("Loading properties from path [" + resourcePath + "] in XML format...");
230                    }
231    
232                    props.loadFromXML(is);
233                } else {
234    
235                    if (log.isDebugEnabled()) {
236                        log.debug("Loading properties from path [" + resourcePath + "]...");
237                    }
238    
239                    props.load(is);
240                }
241    
242            } catch (IOException e) {
243                throw new ShiroException("Error reading properties path [" + resourcePath + "].  " +
244                        "Initializing of the realm from this file failed.", e);
245            } finally {
246                ResourceUtils.close(is);
247            }
248    
249            return props;
250        }
251    
252    
253        private void reloadPropertiesIfNecessary() {
254            if (isSourceModified()) {
255                restart();
256            }
257        }
258    
259        private boolean isSourceModified() {
260            //we can only check last modified times on files - classpath and URL entries can't tell us modification times
261            return this.resourcePath.startsWith(ResourceUtils.FILE_PREFIX) && isFileModified();
262        }
263    
264        private boolean isFileModified() {
265            File propertyFile = new File(this.resourcePath);
266            long currentLastModified = propertyFile.lastModified();
267            if (currentLastModified > this.fileLastModified) {
268                this.fileLastModified = currentLastModified;
269                return true;
270            } else {
271                return false;
272            }
273        }
274    
275        @SuppressWarnings("unchecked")
276        private void restart() {
277            if (resourcePath == null || resourcePath.length() == 0) {
278                throw new IllegalStateException("The resourcePath property is not set.  " +
279                        "It must be set prior to this realm being initialized.");
280            }
281    
282            if (log.isDebugEnabled()) {
283                log.debug("Loading user security information from file [" + resourcePath + "]...");
284            }
285    
286            try {
287                destroy();
288            } catch (Exception e) {
289                //ignored
290            }
291            init();
292        }
293    
294        @SuppressWarnings("unchecked")
295        private void createRealmEntitiesFromProperties(Properties properties) {
296    
297            StringBuffer userDefs = new StringBuffer();
298            StringBuffer roleDefs = new StringBuffer();
299    
300            Enumeration<String> propNames = (Enumeration<String>) properties.propertyNames();
301    
302            while (propNames.hasMoreElements()) {
303    
304                String key = propNames.nextElement().trim();
305                String value = properties.getProperty(key).trim();
306                if (log.isTraceEnabled()) {
307                    log.trace("Processing properties line - key: [" + key + "], value: [" + value + "].");
308                }
309    
310                if (isUsername(key)) {
311                    String username = getUsername(key);
312                    userDefs.append(username).append(" = ").append(value).append("\n");
313                } else if (isRolename(key)) {
314                    String rolename = getRolename(key);
315                    roleDefs.append(rolename).append(" = ").append(value).append("\n");
316                } else {
317                    String msg = "Encountered unexpected key/value pair.  All keys must be prefixed with either '" +
318                            USERNAME_PREFIX + "' or '" + ROLENAME_PREFIX + "'.";
319                    throw new IllegalStateException(msg);
320                }
321            }
322    
323            setUserDefinitions(userDefs.toString());
324            setRoleDefinitions(roleDefs.toString());
325            processDefinitions();
326        }
327    
328        protected String getName(String key, String prefix) {
329            return key.substring(prefix.length(), key.length());
330        }
331    
332        protected boolean isUsername(String key) {
333            return key != null && key.startsWith(USERNAME_PREFIX);
334        }
335    
336        protected boolean isRolename(String key) {
337            return key != null && key.startsWith(ROLENAME_PREFIX);
338        }
339    
340        protected String getUsername(String key) {
341            return getName(key, USERNAME_PREFIX);
342        }
343    
344        protected String getRolename(String key) {
345            return getName(key, ROLENAME_PREFIX);
346        }
347    }