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 passwordFile) throws 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 callback) throws 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[] password) throws 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[] password) throws 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 principal) throws 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 < 2 || 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 < 2 || 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 }
|