blob: b6aa863867e297673db7e7e109ff59d7b31ed121 [file] [log] [blame]
J. Duke319a3b92007-12-01 00:00:00 +00001/*
2 * Copyright 2004-2006 Sun Microsystems, Inc. All Rights Reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Sun designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Sun in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
22 * CA 95054 USA or visit www.sun.com if you need additional information or
23 * have any questions.
24 */
25
26package com.sun.jmx.remote.security;
27
28import java.io.BufferedInputStream;
29import java.io.File;
30import java.io.FileInputStream;
31import java.io.FilePermission;
32import java.io.IOException;
33import java.security.AccessControlException;
34import java.security.AccessController;
35import java.util.Arrays;
36import java.util.Hashtable;
37import java.util.Map;
38import java.util.Properties;
39
40import javax.security.auth.*;
41import javax.security.auth.callback.*;
42import javax.security.auth.login.*;
43import javax.security.auth.spi.*;
44import javax.management.remote.JMXPrincipal;
45
46import com.sun.jmx.remote.util.ClassLogger;
47import com.sun.jmx.remote.util.EnvHelp;
48import sun.management.jmxremote.ConnectorBootstrap;
49
50import sun.security.action.GetPropertyAction;
51
52/**
53 * This {@link LoginModule} performs file-based authentication.
54 *
55 * <p> A supplied username and password is verified against the
56 * corresponding user credentials stored in a designated password file.
57 * If successful then a new {@link JMXPrincipal} is created with the
58 * user's name and it is associated with the current {@link Subject}.
59 * Such principals may be identified and granted management privileges in
60 * the access control file for JMX remote management or in a Java security
61 * policy.
62 *
63 * <p> The password file comprises a list of key-value pairs as specified in
64 * {@link Properties}. The key represents a user's name and the value is its
65 * associated cleartext password. By default, the following password file is
66 * used:
67 * <pre>
68 * ${java.home}/lib/management/jmxremote.password
69 * </pre>
70 * A different password file can be specified via the <code>passwordFile</code>
71 * configuration option.
72 *
73 * <p> This module recognizes the following <code>Configuration</code> options:
74 * <dl>
75 * <dt> <code>passwordFile</code> </dt>
76 * <dd> the path to an alternative password file. It is used instead of
77 * the default password file.</dd>
78 *
79 * <dt> <code>useFirstPass</code> </dt>
80 * <dd> if <code>true</code>, this module retrieves the username and password
81 * from the module's shared state, using "javax.security.auth.login.name"
82 * and "javax.security.auth.login.password" as the respective keys. The
83 * retrieved values are used for authentication. If authentication fails,
84 * no attempt for a retry is made, and the failure is reported back to
85 * the calling application.</dd>
86 *
87 * <dt> <code>tryFirstPass</code> </dt>
88 * <dd> if <code>true</code>, this module retrieves the username and password
89 * from the module's shared state, using "javax.security.auth.login.name"
90 * and "javax.security.auth.login.password" as the respective keys. The
91 * retrieved values are used for authentication. If authentication fails,
92 * the module uses the CallbackHandler to retrieve a new username and
93 * password, and another attempt to authenticate is made. If the
94 * authentication fails, the failure is reported back to the calling
95 * application.</dd>
96 *
97 * <dt> <code>storePass</code> </dt>
98 * <dd> if <code>true</code>, this module stores the username and password
99 * obtained from the CallbackHandler in the module's shared state, using
100 * "javax.security.auth.login.name" and
101 * "javax.security.auth.login.password" as the respective keys. This is
102 * not performed if existing values already exist for the username and
103 * password in the shared state, or if authentication fails.</dd>
104 *
105 * <dt> <code>clearPass</code> </dt>
106 * <dd> if <code>true</code>, this module clears the username and password
107 * stored in the module's shared state after both phases of authentication
108 * (login and commit) have completed.</dd>
109 * </dl>
110 */
111public class FileLoginModule implements LoginModule {
112
113 // Location of the default password file
114 private static final String DEFAULT_PASSWORD_FILE_NAME =
115 AccessController.doPrivileged(new GetPropertyAction("java.home")) +
116 File.separatorChar + "lib" +
117 File.separatorChar + "management" + File.separatorChar +
118 ConnectorBootstrap.DefaultValues.PASSWORD_FILE_NAME;
119
120 // Key to retrieve the stored username
121 private static final String USERNAME_KEY =
122 "javax.security.auth.login.name";
123
124 // Key to retrieve the stored password
125 private static final String PASSWORD_KEY =
126 "javax.security.auth.login.password";
127
128 // Log messages
129 private static final ClassLogger logger =
130 new ClassLogger("javax.management.remote.misc", "FileLoginModule");
131
132 // Configurable options
133 private boolean useFirstPass = false;
134 private boolean tryFirstPass = false;
135 private boolean storePass = false;
136 private boolean clearPass = false;
137
138 // Authentication status
139 private boolean succeeded = false;
140 private boolean commitSucceeded = false;
141
142 // Supplied username and password
143 private String username;
144 private char[] password;
145 private JMXPrincipal user;
146
147 // Initial state
148 private Subject subject;
149 private CallbackHandler callbackHandler;
150 private Map<String, ?> sharedState;
151 private Map options;
152 private String passwordFile;
153 private String passwordFileDisplayName;
154 private boolean userSuppliedPasswordFile;
155 private boolean hasJavaHomePermission;
156 private Properties userCredentials;
157
158 /**
159 * Initialize this <code>LoginModule</code>.
160 *
161 * @param subject the <code>Subject</code> to be authenticated.
162 * @param callbackHandler a <code>CallbackHandler</code> to acquire the
163 * user's name and password.
164 * @param sharedState shared <code>LoginModule</code> state.
165 * @param options options specified in the login
166 * <code>Configuration</code> for this particular
167 * <code>LoginModule</code>.
168 */
169 public void initialize(Subject subject, CallbackHandler callbackHandler,
170 Map<String,?> sharedState,
171 Map<String,?> options)
172 {
173
174 this.subject = subject;
175 this.callbackHandler = callbackHandler;
176 this.sharedState = sharedState;
177 this.options = options;
178
179 // initialize any configured options
180 tryFirstPass =
181 "true".equalsIgnoreCase((String)options.get("tryFirstPass"));
182 useFirstPass =
183 "true".equalsIgnoreCase((String)options.get("useFirstPass"));
184 storePass =
185 "true".equalsIgnoreCase((String)options.get("storePass"));
186 clearPass =
187 "true".equalsIgnoreCase((String)options.get("clearPass"));
188
189 passwordFile = (String)options.get("passwordFile");
190 passwordFileDisplayName = passwordFile;
191 userSuppliedPasswordFile = true;
192
193 // set the location of the password file
194 if (passwordFile == null) {
195 passwordFile = DEFAULT_PASSWORD_FILE_NAME;
196 userSuppliedPasswordFile = false;
197 try {
198 System.getProperty("java.home");
199 hasJavaHomePermission = true;
200 passwordFileDisplayName = passwordFile;
201 } catch (SecurityException e) {
202 hasJavaHomePermission = false;
203 passwordFileDisplayName =
204 ConnectorBootstrap.DefaultValues.PASSWORD_FILE_NAME;
205 }
206 }
207 }
208
209 /**
210 * Begin user authentication (Authentication Phase 1).
211 *
212 * <p> Acquire the user's name and password and verify them against
213 * the corresponding credentials from the password file.
214 *
215 * @return true always, since this <code>LoginModule</code>
216 * should not be ignored.
217 * @exception FailedLoginException if the authentication fails.
218 * @exception LoginException if this <code>LoginModule</code>
219 * is unable to perform the authentication.
220 */
221 public boolean login() throws LoginException {
222
223 try {
224 loadPasswordFile();
225 } catch (IOException ioe) {
226 LoginException le = new LoginException(
227 "Error: unable to load the password file: " +
228 passwordFileDisplayName);
229 throw EnvHelp.initCause(le, ioe);
230 }
231
232 if (userCredentials == null) {
233 throw new LoginException
234 ("Error: unable to locate the users' credentials.");
235 }
236
237 if (logger.debugOn()) {
238 logger.debug("login",
239 "Using password file: " + passwordFileDisplayName);
240 }
241
242 // attempt the authentication
243 if (tryFirstPass) {
244
245 try {
246 // attempt the authentication by getting the
247 // username and password from shared state
248 attemptAuthentication(true);
249
250 // authentication succeeded
251 succeeded = true;
252 if (logger.debugOn()) {
253 logger.debug("login",
254 "Authentication using cached password has succeeded");
255 }
256 return true;
257
258 } catch (LoginException le) {
259 // authentication failed -- try again below by prompting
260 cleanState();
261 logger.debug("login",
262 "Authentication using cached password has failed");
263 }
264
265 } else if (useFirstPass) {
266
267 try {
268 // attempt the authentication by getting the
269 // username and password from shared state
270 attemptAuthentication(true);
271
272 // authentication succeeded
273 succeeded = true;
274 if (logger.debugOn()) {
275 logger.debug("login",
276 "Authentication using cached password has succeeded");
277 }
278 return true;
279
280 } catch (LoginException le) {
281 // authentication failed
282 cleanState();
283 logger.debug("login",
284 "Authentication using cached password has failed");
285
286 throw le;
287 }
288 }
289
290 if (logger.debugOn()) {
291 logger.debug("login", "Acquiring password");
292 }
293
294 // attempt the authentication using the supplied username and password
295 try {
296 attemptAuthentication(false);
297
298 // authentication succeeded
299 succeeded = true;
300 if (logger.debugOn()) {
301 logger.debug("login", "Authentication has succeeded");
302 }
303 return true;
304
305 } catch (LoginException le) {
306 cleanState();
307 logger.debug("login", "Authentication has failed");
308
309 throw le;
310 }
311 }
312
313 /**
314 * Complete user authentication (Authentication Phase 2).
315 *
316 * <p> This method is called if the LoginContext's
317 * overall authentication has succeeded
318 * (all the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL
319 * LoginModules have succeeded).
320 *
321 * <p> If this LoginModule's own authentication attempt
322 * succeeded (checked by retrieving the private state saved by the
323 * <code>login</code> method), then this method associates a
324 * <code>JMXPrincipal</code> with the <code>Subject</code> located in the
325 * <code>LoginModule</code>. If this LoginModule's own
326 * authentication attempted failed, then this method removes
327 * any state that was originally saved.
328 *
329 * @exception LoginException if the commit fails
330 * @return true if this LoginModule's own login and commit
331 * attempts succeeded, or false otherwise.
332 */
333 public boolean commit() throws LoginException {
334
335 if (succeeded == false) {
336 return false;
337 } else {
338 if (subject.isReadOnly()) {
339 cleanState();
340 throw new LoginException("Subject is read-only");
341 }
342 // add Principals to the Subject
343 if (!subject.getPrincipals().contains(user)) {
344 subject.getPrincipals().add(user);
345 }
346
347 if (logger.debugOn()) {
348 logger.debug("commit",
349 "Authentication has completed successfully");
350 }
351 }
352 // in any case, clean out state
353 cleanState();
354 commitSucceeded = true;
355 return true;
356 }
357
358 /**
359 * Abort user authentication (Authentication Phase 2).
360 *
361 * <p> This method is called if the LoginContext's overall authentication
362 * failed (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL
363 * LoginModules did not succeed).
364 *
365 * <p> If this LoginModule's own authentication attempt
366 * succeeded (checked by retrieving the private state saved by the
367 * <code>login</code> and <code>commit</code> methods),
368 * then this method cleans up any state that was originally saved.
369 *
370 * @exception LoginException if the abort fails.
371 * @return false if this LoginModule's own login and/or commit attempts
372 * failed, and true otherwise.
373 */
374 public boolean abort() throws LoginException {
375
376 if (logger.debugOn()) {
377 logger.debug("abort",
378 "Authentication has not completed successfully");
379 }
380
381 if (succeeded == false) {
382 return false;
383 } else if (succeeded == true && commitSucceeded == false) {
384
385 // Clean out state
386 succeeded = false;
387 cleanState();
388 user = null;
389 } else {
390 // overall authentication succeeded and commit succeeded,
391 // but someone else's commit failed
392 logout();
393 }
394 return true;
395 }
396
397 /**
398 * Logout a user.
399 *
400 * <p> This method removes the Principals
401 * that were added by the <code>commit</code> method.
402 *
403 * @exception LoginException if the logout fails.
404 * @return true in all cases since this <code>LoginModule</code>
405 * should not be ignored.
406 */
407 public boolean logout() throws LoginException {
408 if (subject.isReadOnly()) {
409 cleanState();
410 throw new LoginException ("Subject is read-only");
411 }
412 subject.getPrincipals().remove(user);
413
414 // clean out state
415 cleanState();
416 succeeded = false;
417 commitSucceeded = false;
418 user = null;
419
420 if (logger.debugOn()) {
421 logger.debug("logout", "Subject is being logged out");
422 }
423
424 return true;
425 }
426
427 /**
428 * Attempt authentication
429 *
430 * @param usePasswdFromSharedState a flag to tell this method whether
431 * to retrieve the password from the sharedState.
432 */
433 @SuppressWarnings("unchecked") // sharedState used as Map<String,Object>
434 private void attemptAuthentication(boolean usePasswdFromSharedState)
435 throws LoginException {
436
437 // get the username and password
438 getUsernamePassword(usePasswdFromSharedState);
439
440 String localPassword = null;
441
442 // userCredentials is initialized in login()
443 if (((localPassword = userCredentials.getProperty(username)) == null) ||
444 (! localPassword.equals(new String(password)))) {
445
446 // username not found or passwords do not match
447 if (logger.debugOn()) {
448 logger.debug("login", "Invalid username or password");
449 }
450 throw new FailedLoginException("Invalid username or password");
451 }
452
453 // Save the username and password in the shared state
454 // only if authentication succeeded
455 if (storePass &&
456 !sharedState.containsKey(USERNAME_KEY) &&
457 !sharedState.containsKey(PASSWORD_KEY)) {
458 ((Map) sharedState).put(USERNAME_KEY, username);
459 ((Map) sharedState).put(PASSWORD_KEY, password);
460 }
461
462 // Create a new user principal
463 user = new JMXPrincipal(username);
464
465 if (logger.debugOn()) {
466 logger.debug("login",
467 "User '" + username + "' successfully validated");
468 }
469 }
470
471 /*
472 * Read the password file.
473 */
474 private void loadPasswordFile() throws IOException {
475 FileInputStream fis;
476 try {
477 fis = new FileInputStream(passwordFile);
478 } catch (SecurityException e) {
479 if (userSuppliedPasswordFile || hasJavaHomePermission) {
480 throw e;
481 } else {
482 FilePermission fp =
483 new FilePermission(passwordFileDisplayName, "read");
484 AccessControlException ace = new AccessControlException(
485 "access denied " + fp.toString());
486 ace.setStackTrace(e.getStackTrace());
487 throw ace;
488 }
489 }
490 BufferedInputStream bis = new BufferedInputStream(fis);
491 userCredentials = new Properties();
492 userCredentials.load(bis);
493 bis.close();
494 }
495
496 /**
497 * Get the username and password.
498 * This method does not return any value.
499 * Instead, it sets global name and password variables.
500 *
501 * <p> Also note that this method will set the username and password
502 * values in the shared state in case subsequent LoginModules
503 * want to use them via use/tryFirstPass.
504 *
505 * @param usePasswdFromSharedState boolean that tells this method whether
506 * to retrieve the password from the sharedState.
507 */
508 private void getUsernamePassword(boolean usePasswdFromSharedState)
509 throws LoginException {
510
511 if (usePasswdFromSharedState) {
512 // use the password saved by the first module in the stack
513 username = (String)sharedState.get(USERNAME_KEY);
514 password = (char[])sharedState.get(PASSWORD_KEY);
515 return;
516 }
517
518 // acquire username and password
519 if (callbackHandler == null)
520 throw new LoginException("Error: no CallbackHandler available " +
521 "to garner authentication information from the user");
522
523 Callback[] callbacks = new Callback[2];
524 callbacks[0] = new NameCallback("username");
525 callbacks[1] = new PasswordCallback("password", false);
526
527 try {
528 callbackHandler.handle(callbacks);
529 username = ((NameCallback)callbacks[0]).getName();
530 char[] tmpPassword = ((PasswordCallback)callbacks[1]).getPassword();
531 password = new char[tmpPassword.length];
532 System.arraycopy(tmpPassword, 0,
533 password, 0, tmpPassword.length);
534 ((PasswordCallback)callbacks[1]).clearPassword();
535
536 } catch (IOException ioe) {
537 LoginException le = new LoginException(ioe.toString());
538 throw EnvHelp.initCause(le, ioe);
539 } catch (UnsupportedCallbackException uce) {
540 LoginException le = new LoginException(
541 "Error: " + uce.getCallback().toString() +
542 " not available to garner authentication " +
543 "information from the user");
544 throw EnvHelp.initCause(le, uce);
545 }
546 }
547
548 /**
549 * Clean out state because of a failed authentication attempt
550 */
551 private void cleanState() {
552 username = null;
553 if (password != null) {
554 Arrays.fill(password, ' ');
555 password = null;
556 }
557
558 if (clearPass) {
559 sharedState.remove(USERNAME_KEY);
560 sharedState.remove(PASSWORD_KEY);
561 }
562 }
563}