Base64MD5PasswordFilePrincipalDatabase.java
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  
020  */
021 package org.apache.qpid.server.security.auth.database;
022 
023 import org.apache.log4j.Logger;
024 import org.apache.qpid.server.security.access.management.AMQUserManagementMBean;
025 import org.apache.qpid.server.security.auth.sasl.AuthenticationProviderInitialiser;
026 import org.apache.qpid.server.security.auth.sasl.UsernamePrincipal;
027 import org.apache.qpid.server.security.auth.sasl.crammd5.CRAMMD5HashedInitialiser;
028 
029 import javax.security.auth.callback.PasswordCallback;
030 import javax.security.auth.login.AccountNotFoundException;
031 import java.io.BufferedReader;
032 import java.io.File;
033 import java.io.FileNotFoundException;
034 import java.io.FileReader;
035 import java.io.IOException;
036 import java.io.PrintStream;
037 import java.security.Principal;
038 import java.util.HashMap;
039 import java.util.LinkedList;
040 import java.util.List;
041 import java.util.Map;
042 import java.util.concurrent.locks.ReentrantLock;
043 import java.util.regex.Pattern;
044 
045 /**
046  * Represents a user database where the account information is stored in a simple flat file.
047  *
048  * The file is expected to be in the form: username:password username1:password1 ... usernamen:passwordn
049  *
050  * where a carriage return separates each username/password pair. Passwords are assumed to be in plain text.
051  */
052 public class Base64MD5PasswordFilePrincipalDatabase implements PrincipalDatabase
053 {
054     private static final Logger _logger = Logger.getLogger(Base64MD5PasswordFilePrincipalDatabase.class);
055 
056     private File _passwordFile;
057 
058     private Pattern _regexp = Pattern.compile(":");
059 
060     private Map<String, AuthenticationProviderInitialiser> _saslServers;
061 
062     AMQUserManagementMBean _mbean;
063     public static final String DEFAULT_ENCODING = "utf-8";
064     private Map<String, HashedUser> _users = new HashMap<String, HashedUser>();
065     private ReentrantLock _userUpdate = new ReentrantLock();
066 
067     public Base64MD5PasswordFilePrincipalDatabase()
068     {
069         _saslServers = new HashMap<String, AuthenticationProviderInitialiser>();
070 
071         /**
072          *  Create Authenticators for MD5 Password file.
073          */
074 
075         // Accept Plain incomming and hash it for comparison to the file.
076         CRAMMD5HashedInitialiser cram = new CRAMMD5HashedInitialiser();
077         cram.initialise(this);
078         _saslServers.put(cram.getMechanismName(), cram);
079 
080         //fixme The PDs should setup a PD Mangement MBean
081 //        try
082 //        {
083 //            _mbean = new AMQUserManagementMBean();
084 //            _mbean.setPrincipalDatabase(this);
085 //        }
086 //        catch (JMException e)
087 //        {
088 //            _logger.warn("User management disabled as unable to create MBean:" + e);
089 //        }
090     }
091 
092     public void setPasswordFile(String passwordFilethrows IOException
093     {
094         File f = new File(passwordFile);
095         _logger.info("PasswordFilePrincipalDatabase using file " + f.getAbsolutePath());
096         _passwordFile = f;
097         if (!f.exists())
098         {
099             throw new FileNotFoundException("Cannot find password file " + f);
100         }
101         if (!f.canRead())
102         {
103             throw new FileNotFoundException("Cannot read password file " + f +
104                                             ". Check permissions.");
105         }
106 
107         loadPasswordFile();
108     }
109 
110     /**
111      * SASL Callback Mechanism - sets the Password in the PasswordCallback based on the value in the PasswordFile
112      * If you want to change the password for a user, use updatePassword instead.
113      *
114      @param principal The Principal to set the password for
115      @param callback  The PasswordCallback to call setPassword on
116      *
117      @throws AccountNotFoundException If the Principal cannont be found in this Database
118      */
119     public void setPassword(Principal principal, PasswordCallback callbackthrows AccountNotFoundException
120     {
121         if (_passwordFile == null)
122         {
123             throw new AccountNotFoundException("Unable to locate principal since no password file was specified during initialisation");
124         }
125         if (principal == null)
126         {
127             throw new IllegalArgumentException("principal must not be null");
128         }
129 
130         char[] pwd = lookupPassword(principal.getName());
131 
132         if (pwd != null)
133         {
134             callback.setPassword(pwd);
135         }
136         else
137         {
138             throw new AccountNotFoundException("No account found for principal " + principal);
139         }
140     }
141 
142     /**
143      * Used to verify that the presented Password is correct. Currently only used by Management Console
144      *
145      @param principal The principal to authenticate
146      @param password  The password to check
147      *
148      @return true if password is correct
149      *
150      @throws AccountNotFoundException if the principal cannot be found
151      */
152     public boolean verifyPassword(String principal, char[] passwordthrows AccountNotFoundException
153     {
154         char[] pwd = lookupPassword(principal);
155 
156         return compareCharArray(pwd, password);
157     }
158     
159     private boolean compareCharArray(char[] a, char[] b)
160     {
161         boolean equal = false;
162         if (a.length == b.length)
163         {
164             equal = true;
165             int index = 0;
166             while (equal && index < a.length)
167             {
168                 equal = a[index== b[index];
169                 index++;
170             }
171         }
172         return equal;
173     }
174 
175     /**
176      * Changes the password for the specified user
177      
178      @param principal to change the password for
179      @param password plaintext password to set the password too
180      */
181     public boolean updatePassword(Principal principal, char[] passwordthrows AccountNotFoundException
182     {
183         HashedUser user = _users.get(principal.getName());
184 
185         if (user == null)
186         {
187             throw new AccountNotFoundException(principal.getName());
188         }
189 
190         try
191         {
192             try
193             {
194                 _userUpdate.lock();
195                 char[] orig = user.getPassword();
196                 user.setPassword(password);
197 
198                 try
199                 {
200                     savePasswordFile();
201                 }
202                 catch (IOException e)
203                 {
204                     _logger.error("Unable to save password file, password change for user'"
205                                   + principal + "' will revert at restart");
206                     //revert the password change
207                     user.setPassword(orig);
208                     return false;
209                 }
210                 return true;
211             }
212             finally
213             {
214                 if (_userUpdate.isHeldByCurrentThread())
215                 {
216                     _userUpdate.unlock();
217                 }
218             }
219         }
220         catch (Exception e)
221         {
222             return false;
223         }
224     }
225 
226     public boolean createPrincipal(Principal principal, char[] password)
227     {
228         if (_users.get(principal.getName()) != null)
229         {
230             return false;
231         }
232 
233         HashedUser user = new HashedUser(principal.getName(), password);
234 
235         try
236         {
237             _userUpdate.lock();
238             _users.put(user.getName(), user);
239 
240             try
241             {
242                 savePasswordFile();
243                 return true;
244             }
245             catch (IOException e)
246             {
247                 //remove the use on failure.
248                 _users.remove(user.getName());
249                 return false;
250             }
251         }
252         finally
253         {
254             if (_userUpdate.isHeldByCurrentThread())
255             {
256                 _userUpdate.unlock();
257             }
258         }
259     }
260 
261     public boolean deletePrincipal(Principal principalthrows AccountNotFoundException
262     {
263         HashedUser user = _users.get(principal.getName());
264 
265         if (user == null)
266         {
267             throw new AccountNotFoundException(principal.getName());
268         }
269 
270         try
271         {
272             _userUpdate.lock();
273             user.delete();
274 
275             try
276             {
277                 savePasswordFile();
278             }
279             catch (IOException e)
280             {
281                 _logger.warn("Unable to remove user '" + user.getName() "' from password file.");
282                 return false;
283             }
284 
285             _users.remove(user.getName());
286         }
287         finally
288         {
289             if (_userUpdate.isHeldByCurrentThread())
290             {
291                 _userUpdate.unlock();
292             }
293         }
294 
295         return true;
296     }
297 
298     public Map<String, AuthenticationProviderInitialiser> getMechanisms()
299     {
300         return _saslServers;
301     }
302 
303     public List<Principal> getUsers()
304     {
305         return new LinkedList<Principal>(_users.values());
306     }
307 
308     public Principal getUser(String username)
309     {
310         if (_users.containsKey(username))
311         {
312             return new UsernamePrincipal(username);
313         }
314         return null;
315     }
316 
317     /**
318      * Looks up the password for a specified user in the password file. Note this code is <b>not</b> secure since it
319      * creates strings of passwords. It should be modified to create only char arrays which get nulled out.
320      *
321      @param name The principal name to lookup
322      *
323      @return a char[] for use in SASL.
324      */
325     private char[] lookupPassword(String name)
326     {
327         HashedUser user = _users.get(name);
328         if (user == null)
329         {
330             return null;
331         }
332         else
333         {
334             return user.getPassword();
335         }
336     }
337 
338     private void loadPasswordFile() throws IOException
339     {
340         try
341         {
342             _userUpdate.lock();
343             _users.clear();
344 
345             BufferedReader reader = null;
346             try
347             {
348                 reader = new BufferedReader(new FileReader(_passwordFile));
349                 String line;
350 
351                 while ((line = reader.readLine()) != null)
352                 {
353                     String[] result = _regexp.split(line);
354                     if (result == null || result.length < || result[0].startsWith("#"))
355                     {
356                         continue;
357                     }
358 
359                     HashedUser user = new HashedUser(result);
360                     _logger.info("Created user:" + user);
361                     _users.put(user.getName(), user);
362                 }
363             }
364             finally
365             {
366                 if (reader != null)
367                 {
368                     reader.close();
369                 }
370             }
371         }
372         finally
373         {
374             if (_userUpdate.isHeldByCurrentThread())
375             {
376                 _userUpdate.unlock();
377             }
378         }
379     }
380 
381     private void savePasswordFile() throws IOException
382     {
383         try
384         {
385             _userUpdate.lock();
386 
387             BufferedReader reader = null;
388             PrintStream writer = null;
389             File tmp = File.createTempFile(_passwordFile.getName()".tmp");
390 
391             try
392             {
393                 writer = new PrintStream(tmp);
394                 reader = new BufferedReader(new FileReader(_passwordFile));
395                 String line;
396 
397                 while ((line = reader.readLine()) != null)
398                 {
399                     String[] result = _regexp.split(line);
400                     if (result == null || result.length < || result[0].startsWith("#"))
401                     {
402                         writer.write(line.getBytes(DEFAULT_ENCODING));
403                         writer.println();
404                         continue;
405                     }
406 
407                     HashedUser user = _users.get(result[0]);
408 
409                     if (user == null)
410                     {
411                         writer.write(line.getBytes(DEFAULT_ENCODING));
412                         writer.println();
413                     }
414                     else if (!user.isDeleted())
415                     {
416                         if (!user.isModified())
417                         {
418                             writer.write(line.getBytes(DEFAULT_ENCODING));
419                             writer.println();
420                         }
421                         else
422                         {
423                             try
424                             {
425                                 byte[] encodedPassword = user.getEncodedPassword();
426 
427                                 writer.write((user.getName() ":").getBytes(DEFAULT_ENCODING));
428                                 writer.write(encodedPassword);
429                                 writer.println();
430 
431                                 user.saved();
432                             }
433                             catch (Exception e)
434                             {
435                                 _logger.warn("Unable to encode new password reverting to old password.");
436                                 writer.write(line.getBytes(DEFAULT_ENCODING));
437                                 writer.println();
438                             }
439                         }
440                     }
441                 }
442 
443                 for (HashedUser user : _users.values())
444                 {
445                     if (user.isModified())
446                     {
447                         byte[] encodedPassword;
448                         try
449                         {
450                             encodedPassword = user.getEncodedPassword();
451                             writer.write((user.getName() ":").getBytes(DEFAULT_ENCODING));
452                             writer.write(encodedPassword);
453                             writer.println();
454                             user.saved();
455                         }
456                         catch (Exception e)
457                         {
458                             _logger.warn("Unable to get Encoded password for user'" + user.getName() "' password not saved");
459                         }
460                     }
461                 }
462             }
463             finally
464             {
465                 if (reader != null)
466                 {
467                     reader.close();
468                 }
469 
470                 if (writer != null)
471                 {
472                     writer.close();
473                 }
474 
475                 // Swap temp file to main password file.
476                 File old = new File(_passwordFile.getAbsoluteFile() ".old");
477                 if (old.exists())
478                 {
479                     old.delete();
480                 }
481                 _passwordFile.renameTo(old);
482                 tmp.renameTo(_passwordFile);
483                 tmp.delete();
484             }
485         }
486         finally
487         {
488             if (_userUpdate.isHeldByCurrentThread())
489             {
490                 _userUpdate.unlock();
491             }
492         }
493     }
494 
495     public void reload() throws IOException
496     {
497         loadPasswordFile();
498     }
499 
500 }