View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.security;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.nio.file.Path;
24  import java.security.Principal;
25  import java.util.ArrayList;
26  import java.util.HashMap;
27  import java.util.HashSet;
28  import java.util.Iterator;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.Properties;
32  import java.util.Set;
33  
34  import javax.security.auth.Subject;
35  
36  import org.eclipse.jetty.security.MappedLoginService.KnownUser;
37  import org.eclipse.jetty.security.MappedLoginService.RolePrincipal;
38  import org.eclipse.jetty.server.UserIdentity;
39  import org.eclipse.jetty.util.PathWatcher;
40  import org.eclipse.jetty.util.PathWatcher.PathWatchEvent;
41  import org.eclipse.jetty.util.StringUtil;
42  import org.eclipse.jetty.util.component.AbstractLifeCycle;
43  import org.eclipse.jetty.util.log.Log;
44  import org.eclipse.jetty.util.log.Logger;
45  import org.eclipse.jetty.util.resource.PathResource;
46  import org.eclipse.jetty.util.resource.Resource;
47  import org.eclipse.jetty.util.security.Credential;
48  
49  /**
50   * PropertyUserStore
51   * <p>
52   * This class monitors a property file of the format mentioned below and notifies registered listeners of the changes to the the given file.
53   *
54   * <pre>
55   *  username: password [,rolename ...]
56   * </pre>
57   *
58   * Passwords may be clear text, obfuscated or checksummed. The class com.eclipse.Util.Password should be used to generate obfuscated passwords or password
59   * checksums.
60   *
61   * If DIGEST Authentication is used, the password must be in a recoverable format, either plain text or OBF:.
62   */
63  public class PropertyUserStore extends AbstractLifeCycle implements PathWatcher.Listener
64  {
65      private static final Logger LOG = Log.getLogger(PropertyUserStore.class);
66  
67      private Path _configPath;
68      private Resource _configResource;
69      
70      private PathWatcher pathWatcher;
71      private boolean hotReload = false; // default is not to reload
72  
73      private IdentityService _identityService = new DefaultIdentityService();
74      private boolean _firstLoad = true; // true if first load, false from that point on
75      private final List<String> _knownUsers = new ArrayList<String>();
76      private final Map<String, UserIdentity> _knownUserIdentities = new HashMap<String, UserIdentity>();
77      private List<UserListener> _listeners;
78  
79      /**
80       * Get the config (as a string)
81       * @return the config path as a string
82       * @deprecated use {@link #getConfigPath()} instead
83       */
84      @Deprecated
85      public String getConfig()
86      {
87          return _configPath.toString();
88      }
89  
90      /**
91       * Set the Config Path from a String reference to a file
92       * @param configFile the config file
93       * @deprecated use {@link #setConfigPath(String)} instead
94       */
95      @Deprecated
96      public void setConfig(String configFile)
97      {
98          setConfigPath(configFile);
99      }
100     
101     /**
102      * Get the Config {@link Path} reference.
103      * @return the config path
104      */
105     public Path getConfigPath()
106     {
107         return _configPath;
108     }
109 
110     /**
111      * Set the Config Path from a String reference to a file
112      * @param configFile the config file
113      */
114     public void setConfigPath(String configFile)
115     {
116         if (configFile == null)
117         {
118             _configPath = null;
119         }
120         else
121         {
122             _configPath = new File(configFile).toPath();
123         }
124     }
125 
126     /**
127      * Set the Config Path from a {@link File} reference
128      * @param configFile the config file
129      */
130     public void setConfigPath(File configFile)
131     {
132         _configPath = configFile.toPath();
133     }
134 
135     /**
136      * Set the Config Path
137      * @param configPath the config path
138      */
139     public void setConfigPath(Path configPath)
140     {
141         _configPath = configPath;
142     }
143     
144     /* ------------------------------------------------------------ */
145     public UserIdentity getUserIdentity(String userName)
146     {
147         return _knownUserIdentities.get(userName);
148     }
149 
150     /* ------------------------------------------------------------ */
151     /**
152      * @return the resource associated with the configured properties file, creating it if necessary
153      * @throws IOException if unable to get the resource
154      */
155     public Resource getConfigResource() throws IOException
156     {
157         if (_configResource == null)
158         {
159             _configResource = new PathResource(_configPath);
160         }
161 
162         return _configResource;
163     }
164     
165     /**
166      * Is hot reload enabled on this user store
167      * 
168      * @return true if hot reload was enabled before startup
169      */
170     public boolean isHotReload()
171     {
172         return hotReload;
173     }
174 
175     /**
176      * Enable Hot Reload of the Property File
177      * 
178      * @param enable true to enable, false to disable
179      */
180     public void setHotReload(boolean enable)
181     {
182         if (isRunning())
183         {
184             throw new IllegalStateException("Cannot set hot reload while user store is running");
185         }
186         this.hotReload = enable;
187     }
188 
189     /* ------------------------------------------------------------ */
190     /**
191      * sets the refresh interval (in seconds)
192      * @param sec the refresh interval
193      * @deprecated use {@link #setHotReload(boolean)} instead
194      */
195     @Deprecated
196     public void setRefreshInterval(int sec)
197     {
198     }
199 
200     /* ------------------------------------------------------------ */
201     /**
202      * @return refresh interval in seconds for how often the properties file should be checked for changes
203      * @deprecated use {@link #isHotReload()} instead
204      */
205     @Deprecated
206     public int getRefreshInterval()
207     {
208         return (hotReload)?1:0;
209     }
210     
211     @Override
212     public String toString()
213     {
214         StringBuilder s = new StringBuilder();
215         s.append(this.getClass().getName());
216         s.append("[");
217         s.append("users.count=").append(this._knownUsers.size());
218         s.append("identityService=").append(this._identityService);
219         s.append("]");
220         return s.toString();
221     }
222 
223     /* ------------------------------------------------------------ */
224     private void loadUsers() throws IOException
225     {
226         if (_configPath == null)
227             return;
228 
229         if (LOG.isDebugEnabled())
230         {
231             LOG.debug("Loading " + this + " from " + _configPath);
232         }
233         
234         Properties properties = new Properties();
235         if (getConfigResource().exists())
236             properties.load(getConfigResource().getInputStream());
237         
238         Set<String> known = new HashSet<String>();
239 
240         for (Map.Entry<Object, Object> entry : properties.entrySet())
241         {
242             String username = ((String)entry.getKey()).trim();
243             String credentials = ((String)entry.getValue()).trim();
244             String roles = null;
245             int c = credentials.indexOf(',');
246             if (c > 0)
247             {
248                 roles = credentials.substring(c + 1).trim();
249                 credentials = credentials.substring(0,c).trim();
250             }
251 
252             if (username != null && username.length() > 0 && credentials != null && credentials.length() > 0)
253             {
254                 String[] roleArray = IdentityService.NO_ROLES;
255                 if (roles != null && roles.length() > 0)
256                 {
257                     roleArray = StringUtil.csvSplit(roles);
258                 }
259                 known.add(username);
260                 Credential credential = Credential.getCredential(credentials);
261 
262                 Principal userPrincipal = new KnownUser(username,credential);
263                 Subject subject = new Subject();
264                 subject.getPrincipals().add(userPrincipal);
265                 subject.getPrivateCredentials().add(credential);
266 
267                 if (roles != null)
268                 {
269                     for (String role : roleArray)
270                     {
271                         subject.getPrincipals().add(new RolePrincipal(role));
272                     }
273                 }
274 
275                 subject.setReadOnly();
276 
277                 _knownUserIdentities.put(username,_identityService.newUserIdentity(subject,userPrincipal,roleArray));
278                 notifyUpdate(username,credential,roleArray);
279             }
280         }
281 
282         synchronized (_knownUsers)
283         {
284             /*
285              * if its not the initial load then we want to process removed users
286              */
287             if (!_firstLoad)
288             {
289                 Iterator<String> users = _knownUsers.iterator();
290                 while (users.hasNext())
291                 {
292                     String user = users.next();
293                     if (!known.contains(user))
294                     {
295                         _knownUserIdentities.remove(user);
296                         notifyRemove(user);
297                     }
298                 }
299             }
300 
301             /*
302              * reset the tracked _users list to the known users we just processed
303              */
304 
305             _knownUsers.clear();
306             _knownUsers.addAll(known);
307 
308         }
309 
310         /*
311          * set initial load to false as there should be no more initial loads
312          */
313         _firstLoad = false;
314         
315         if (LOG.isDebugEnabled())
316         {
317             LOG.debug("Loaded " + this + " from " + _configPath);
318         }
319     }
320     
321     /* ------------------------------------------------------------ */
322     /**
323      * Depending on the value of the refresh interval, this method will either start up a scanner thread that will monitor the properties file for changes after
324      * it has initially loaded it. Otherwise the users will be loaded and there will be no active monitoring thread so changes will not be detected.
325      *
326      *
327      * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStart()
328      */
329     protected void doStart() throws Exception
330     {
331         super.doStart();
332 
333         loadUsers();
334         if ( isHotReload() && (_configPath != null) )
335         {
336             this.pathWatcher = new PathWatcher();
337             this.pathWatcher.watch(_configPath);
338             this.pathWatcher.addListener(this);
339             this.pathWatcher.setNotifyExistingOnStart(false);
340             this.pathWatcher.start();
341         }
342        
343     }
344     
345     @Override
346     public void onPathWatchEvent(PathWatchEvent event)
347     {
348         try
349         {
350             loadUsers();
351         }
352         catch (IOException e)
353         {
354             LOG.warn(e);
355         }
356     }
357 
358     /* ------------------------------------------------------------ */
359     /**
360      * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStop()
361      */
362     protected void doStop() throws Exception
363     {
364         super.doStop();
365         if (this.pathWatcher != null)
366             this.pathWatcher.stop();
367         this.pathWatcher = null;
368     }
369 
370     /**
371      * Notifies the registered listeners of potential updates to a user
372      *
373      * @param username
374      * @param credential
375      * @param roleArray
376      */
377     private void notifyUpdate(String username, Credential credential, String[] roleArray)
378     {
379         if (_listeners != null)
380         {
381             for (Iterator<UserListener> i = _listeners.iterator(); i.hasNext();)
382             {
383                 i.next().update(username,credential,roleArray);
384             }
385         }
386     }
387 
388     /**
389      * notifies the registered listeners that a user has been removed.
390      *
391      * @param username
392      */
393     private void notifyRemove(String username)
394     {
395         if (_listeners != null)
396         {
397             for (Iterator<UserListener> i = _listeners.iterator(); i.hasNext();)
398             {
399                 i.next().remove(username);
400             }
401         }
402     }
403 
404     /**
405      * registers a listener to be notified of the contents of the property file
406      * @param listener the user listener
407      */
408     public void registerUserListener(UserListener listener)
409     {
410         if (_listeners == null)
411         {
412             _listeners = new ArrayList<UserListener>();
413         }
414         _listeners.add(listener);
415     }
416 
417     /**
418      * UserListener
419      */
420     public interface UserListener
421     {
422         public void update(String username, Credential credential, String[] roleArray);
423 
424         public void remove(String username);
425     }
426 }