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.FilenameFilter;
23  import java.io.IOException;
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.Scanner;
40  import org.eclipse.jetty.util.Scanner.BulkListener;
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.Resource;
46  import org.eclipse.jetty.util.security.Credential;
47  
48  /**
49   * PropertyUserStore
50   *
51   * This class monitors a property file of the format mentioned below and notifies registered listeners of the changes to the the given file.
52   *
53   * <PRE>
54   *  username: password [,rolename ...]
55   * </PRE>
56   *
57   * Passwords may be clear text, obfuscated or checksummed. The class com.eclipse.Util.Password should be used to generate obfuscated passwords or password
58   * checksums.
59   *
60   * If DIGEST Authentication is used, the password must be in a recoverable format, either plain text or OBF:.
61   */
62  public class PropertyUserStore extends AbstractLifeCycle
63  {
64      private static final Logger LOG = Log.getLogger(PropertyUserStore.class);
65  
66      private String _config;
67      private Resource _configResource;
68      private Scanner _scanner;
69      private int _refreshInterval = 0;// default is not to reload
70  
71      private IdentityService _identityService = new DefaultIdentityService();
72      private boolean _firstLoad = true; // true if first load, false from that point on
73      private final List<String> _knownUsers = new ArrayList<String>();
74      private final Map<String, UserIdentity> _knownUserIdentities = new HashMap<String, UserIdentity>();
75      private List<UserListener> _listeners;
76  
77      /* ------------------------------------------------------------ */
78      public String getConfig()
79      {
80          return _config;
81      }
82  
83      /* ------------------------------------------------------------ */
84      public void setConfig(String config)
85      {
86          _config = config;
87      }
88  
89      /* ------------------------------------------------------------ */
90          public UserIdentity getUserIdentity(String userName)
91          {
92              return _knownUserIdentities.get(userName);
93          }
94  
95      /* ------------------------------------------------------------ */
96      /**
97       * returns the resource associated with the configured properties file, creating it if necessary
98       */
99      public Resource getConfigResource() throws IOException
100     {
101         if (_configResource == null)
102         {
103             _configResource = Resource.newResource(_config);
104         }
105 
106         return _configResource;
107     }
108 
109     /* ------------------------------------------------------------ */
110     /**
111      * sets the refresh interval (in seconds)
112      */
113     public void setRefreshInterval(int msec)
114     {
115         _refreshInterval = msec;
116     }
117 
118     /* ------------------------------------------------------------ */
119     /**
120      * refresh interval in seconds for how often the properties file should be checked for changes
121      */
122     public int getRefreshInterval()
123     {
124         return _refreshInterval;
125     }
126 
127     /* ------------------------------------------------------------ */
128     private void loadUsers() throws IOException
129     {
130         if (_config == null)
131             return;
132 
133         if (LOG.isDebugEnabled())
134             LOG.debug("Load " + this + " from " + _config);
135         Properties properties = new Properties();
136         if (getConfigResource().exists())
137             properties.load(getConfigResource().getInputStream());
138         Set<String> known = new HashSet<String>();
139 
140         for (Map.Entry<Object, Object> entry : properties.entrySet())
141         {
142             String username = ((String)entry.getKey()).trim();
143             String credentials = ((String)entry.getValue()).trim();
144             String roles = null;
145             int c = credentials.indexOf(',');
146             if (c > 0)
147             {
148                 roles = credentials.substring(c + 1).trim();
149                 credentials = credentials.substring(0,c).trim();
150             }
151 
152             if (username != null && username.length() > 0 && credentials != null && credentials.length() > 0)
153             {
154                 String[] roleArray = IdentityService.NO_ROLES;
155                 if (roles != null && roles.length() > 0)
156                 {
157                     roleArray = StringUtil.csvSplit(roles);
158                 }
159                 known.add(username);
160                 Credential credential = Credential.getCredential(credentials);
161 
162                 Principal userPrincipal = new KnownUser(username,credential);
163                 Subject subject = new Subject();
164                 subject.getPrincipals().add(userPrincipal);
165                 subject.getPrivateCredentials().add(credential);
166 
167                 if (roles != null)
168                 {
169                     for (String role : roleArray)
170                     {
171                         subject.getPrincipals().add(new RolePrincipal(role));
172                     }
173                 }
174 
175                 subject.setReadOnly();
176 
177                 _knownUserIdentities.put(username,_identityService.newUserIdentity(subject,userPrincipal,roleArray));
178                 notifyUpdate(username,credential,roleArray);
179             }
180         }
181 
182         synchronized (_knownUsers)
183         {
184             /*
185              * if its not the initial load then we want to process removed users
186              */
187             if (!_firstLoad)
188             {
189                 Iterator<String> users = _knownUsers.iterator();
190                 while (users.hasNext())
191                 {
192                     String user = users.next();
193                     if (!known.contains(user))
194                     {
195                         _knownUserIdentities.remove(user);
196                         notifyRemove(user);
197                     }
198                 }
199             }
200 
201             /*
202              * reset the tracked _users list to the known users we just processed
203              */
204 
205             _knownUsers.clear();
206             _knownUsers.addAll(known);
207 
208         }
209 
210         /*
211          * set initial load to false as there should be no more initial loads
212          */
213         _firstLoad = false;
214     }
215 
216     /* ------------------------------------------------------------ */
217     /**
218      * 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
219      * 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.
220      *
221      *
222      * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStart()
223      */
224     protected void doStart() throws Exception
225     {
226         super.doStart();
227 
228         if (getRefreshInterval() > 0)
229         {
230             _scanner = new Scanner();
231             _scanner.setScanInterval(getRefreshInterval());
232             List<File> dirList = new ArrayList<File>(1);
233             dirList.add(getConfigResource().getFile().getParentFile());
234             _scanner.setScanDirs(dirList);
235             _scanner.setFilenameFilter(new FilenameFilter()
236             {
237                 public boolean accept(File dir, String name)
238                 {
239                     File f = new File(dir,name);
240                     try
241                     {
242                         if (f.compareTo(getConfigResource().getFile()) == 0)
243                         {
244                             return true;
245                         }
246                     }
247                     catch (IOException e)
248                     {
249                         return false;
250                     }
251 
252                     return false;
253                 }
254 
255             });
256 
257             _scanner.addListener(new BulkListener()
258             {
259                 public void filesChanged(List<String> filenames) throws Exception
260                 {
261                     if (filenames == null)
262                         return;
263                     if (filenames.isEmpty())
264                         return;
265                     if (filenames.size() == 1)
266                     {
267                         Resource r = Resource.newResource(filenames.get(0));
268                         if (r.getFile().equals(_configResource.getFile()))
269                             loadUsers();
270                     }
271                 }
272 
273                 public String toString()
274                 {
275                     return "PropertyUserStore$Scanner";
276                 }
277 
278             });
279 
280             _scanner.setReportExistingFilesOnStartup(true);
281             _scanner.setRecursive(false);
282             _scanner.start();
283         }
284         else
285         {
286             loadUsers();
287         }
288     }
289 
290     /* ------------------------------------------------------------ */
291     /**
292      * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStop()
293      */
294     protected void doStop() throws Exception
295     {
296         super.doStop();
297         if (_scanner != null)
298             _scanner.stop();
299         _scanner = null;
300     }
301 
302     /**
303      * Notifies the registered listeners of potential updates to a user
304      *
305      * @param username
306      * @param credential
307      * @param roleArray
308      */
309     private void notifyUpdate(String username, Credential credential, String[] roleArray)
310     {
311         if (_listeners != null)
312         {
313             for (Iterator<UserListener> i = _listeners.iterator(); i.hasNext();)
314             {
315                 i.next().update(username,credential,roleArray);
316             }
317         }
318     }
319 
320     /**
321      * notifies the registered listeners that a user has been removed.
322      *
323      * @param username
324      */
325     private void notifyRemove(String username)
326     {
327         if (_listeners != null)
328         {
329             for (Iterator<UserListener> i = _listeners.iterator(); i.hasNext();)
330             {
331                 i.next().remove(username);
332             }
333         }
334     }
335 
336     /**
337      * registers a listener to be notified of the contents of the property file
338      */
339     public void registerUserListener(UserListener listener)
340     {
341         if (_listeners == null)
342         {
343             _listeners = new ArrayList<UserListener>();
344         }
345         _listeners.add(listener);
346     }
347 
348     /**
349      * UserListener
350      */
351     public interface UserListener
352     {
353         public void update(String username, Credential credential, String[] roleArray);
354 
355         public void remove(String username);
356     }
357 }