001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017:
018: package org.apache.catalina.realm;
019:
020: import java.security.Principal;
021: import java.util.ArrayList;
022: import java.util.Iterator;
023: import java.util.List;
024:
025: import javax.security.auth.Subject;
026: import javax.security.auth.login.AccountExpiredException;
027: import javax.security.auth.login.CredentialExpiredException;
028: import javax.security.auth.login.FailedLoginException;
029: import javax.security.auth.login.LoginContext;
030: import javax.security.auth.login.LoginException;
031:
032: import org.apache.catalina.Container;
033: import org.apache.catalina.LifecycleException;
034: import org.apache.catalina.util.StringManager;
035: import org.apache.juli.logging.Log;
036: import org.apache.juli.logging.LogFactory;
037:
038: /**
039: * <p>Implmentation of <b>Realm</b> that authenticates users via the <em>Java
040: * Authentication and Authorization Service</em> (JAAS). JAAS support requires
041: * either JDK 1.4 (which includes it as part of the standard platform) or
042: * JDK 1.3 (with the plug-in <code>jaas.jar</code> file).</p>
043: *
044: * <p>The value configured for the <code>appName</code> property is passed to
045: * the <code>javax.security.auth.login.LoginContext</code> constructor, to
046: * specify the <em>application name</em> used to select the set of relevant
047: * <code>LoginModules</code> required.</p>
048: *
049: * <p>The JAAS Specification describes the result of a successful login as a
050: * <code>javax.security.auth.Subject</code> instance, which can contain zero
051: * or more <code>java.security.Principal</code> objects in the return value
052: * of the <code>Subject.getPrincipals()</code> method. However, it provides
053: * no guidance on how to distinguish Principals that describe the individual
054: * user (and are thus appropriate to return as the value of
055: * request.getUserPrincipal() in a web application) from the Principal(s)
056: * that describe the authorized roles for this user. To maintain as much
057: * independence as possible from the underlying <code>LoginMethod</code>
058: * implementation executed by JAAS, the following policy is implemented by
059: * this Realm:</p>
060: * <ul>
061: * <li>The JAAS <code>LoginModule</code> is assumed to return a
062: * <code>Subject</code> with at least one <code>Principal</code> instance
063: * representing the user himself or herself, and zero or more separate
064: * <code>Principals</code> representing the security roles authorized
065: * for this user.</li>
066: * <li>On the <code>Principal</code> representing the user, the Principal
067: * name is an appropriate value to return via the Servlet API method
068: * <code>HttpServletRequest.getRemoteUser()</code>.</li>
069: * <li>On the <code>Principals</code> representing the security roles, the
070: * name is the name of the authorized security role.</li>
071: * <li>This Realm will be configured with two lists of fully qualified Java
072: * class names of classes that implement
073: * <code>java.security.Principal</code> - one that identifies class(es)
074: * representing a user, and one that identifies class(es) representing
075: * a security role.</li>
076: * <li>As this Realm iterates over the <code>Principals</code> returned by
077: * <code>Subject.getPrincipals()</code>, it will identify the first
078: * <code>Principal</code> that matches the "user classes" list as the
079: * <code>Principal</code> for this user.</li>
080: * <li>As this Realm iterates over the <code>Princpals</code> returned by
081: * <code>Subject.getPrincipals()</code>, it will accumulate the set of
082: * all <code>Principals</code> matching the "role classes" list as
083: * identifying the security roles for this user.</li>
084: * <li>It is a configuration error for the JAAS login method to return a
085: * validated <code>Subject</code> without a <code>Principal</code> that
086: * matches the "user classes" list.</li>
087: * <li>By default, the enclosing Container's name serves as the
088: * application name used to obtain the JAAS LoginContext ("Catalina" in
089: * a default installation). Tomcat must be able to find an application
090: * with this name in the JAAS configuration file. Here is a hypothetical
091: * JAAS configuration file entry for a database-oriented login module that uses
092: * a Tomcat-managed JNDI database resource:
093: * <blockquote><pre>Catalina {
094: org.foobar.auth.DatabaseLoginModule REQUIRED
095: JNDI_RESOURCE=jdbc/AuthDB
096: USER_TABLE=users
097: USER_ID_COLUMN=id
098: USER_NAME_COLUMN=name
099: USER_CREDENTIAL_COLUMN=password
100: ROLE_TABLE=roles
101: ROLE_NAME_COLUMN=name
102: PRINCIPAL_FACTORY=org.foobar.auth.impl.SimplePrincipalFactory;
103: };</pre></blockquote></li>
104: * <li>To set the JAAS configuration file
105: * location, set the <code>CATALINA_OPTS</code> environment variable
106: * similar to the following:
107: <blockquote><code>CATALINA_OPTS="-Djava.security.auth.login.config=$CATALINA_HOME/conf/jaas.config"</code></blockquote>
108: * </li>
109: * <li>As part of the login process, JAASRealm registers its own <code>CallbackHandler</code>,
110: * called (unsurprisingly) <code>JAASCallbackHandler</code>. This handler supplies the
111: * HTTP requests's username and credentials to the user-supplied <code>LoginModule</code></li>
112: * <li>As with other <code>Realm</code> implementations, digested passwords are supported if
113: * the <code><Realm></code> element in <code>server.xml</code> contains a
114: * <code>digest</code> attribute; <code>JAASCallbackHandler</code> will digest the password
115: * prior to passing it back to the <code>LoginModule</code></li>
116: * </ul>
117: *
118: * @author Craig R. McClanahan
119: * @author Yoav Shapira
120: * @version $Revision: 543691 $ $Date: 2007-06-02 03:37:08 +0200 (sam., 02 juin 2007) $
121: */
122:
123: public class JAASRealm extends RealmBase {
124: private static Log log = LogFactory.getLog(JAASRealm.class);
125:
126: // ----------------------------------------------------- Instance Variables
127:
128: /**
129: * The application name passed to the JAAS <code>LoginContext</code>,
130: * which uses it to select the set of relevant <code>LoginModule</code>s.
131: */
132: protected String appName = null;
133:
134: /**
135: * Descriptive information about this <code>Realm</code> implementation.
136: */
137: protected static final String info = "org.apache.catalina.realm.JAASRealm/1.0";
138:
139: /**
140: * Descriptive information about this <code>Realm</code> implementation.
141: */
142: protected static final String name = "JAASRealm";
143:
144: /**
145: * The list of role class names, split out for easy processing.
146: */
147: protected List<String> roleClasses = new ArrayList<String>();
148:
149: /**
150: * The string manager for this package.
151: */
152: protected static final StringManager sm = StringManager
153: .getManager(Constants.Package);
154:
155: /**
156: * The set of user class names, split out for easy processing.
157: */
158: protected List<String> userClasses = new ArrayList<String>();
159:
160: /**
161: * Whether to use context ClassLoader or default ClassLoader.
162: * True means use context ClassLoader, and True is the default
163: * value.
164: */
165: protected boolean useContextClassLoader = true;
166:
167: // ------------------------------------------------------------- Properties
168:
169: /**
170: * setter for the <code>appName</code> member variable
171: * @deprecated JAAS should use the <code>Engine</code> (domain) name and webpp/host overrides
172: */
173: public void setAppName(String name) {
174: appName = name;
175: }
176:
177: /**
178: * getter for the <code>appName</code> member variable
179: */
180: public String getAppName() {
181: return appName;
182: }
183:
184: /**
185: * Sets whether to use the context or default ClassLoader.
186: * True means use context ClassLoader.
187: *
188: * @param useContext True means use context ClassLoader
189: */
190: public void setUseContextClassLoader(boolean useContext) {
191: useContextClassLoader = useContext;
192: log.info("Setting useContextClassLoader = " + useContext);
193: }
194:
195: /**
196: * Returns whether to use the context or default ClassLoader.
197: * True means to use the context ClassLoader.
198: *
199: * @return The value of useContextClassLoader
200: */
201: public boolean isUseContextClassLoader() {
202: return useContextClassLoader;
203: }
204:
205: public void setContainer(Container container) {
206: super .setContainer(container);
207:
208: if (appName == null) {
209: String name = container.getName();
210: name = makeLegalForJAAS(name);
211:
212: appName = name;
213:
214: log.info("Set JAAS app name " + appName);
215: }
216: }
217:
218: /**
219: * Comma-delimited list of <code>java.security.Principal</code> classes
220: * that represent security roles.
221: */
222: protected String roleClassNames = null;
223:
224: public String getRoleClassNames() {
225: return (this .roleClassNames);
226: }
227:
228: /**
229: * Sets the list of comma-delimited classes that represent
230: * roles. The classes in the list must implement <code>java.security.Principal</code>.
231: * When this accessor is called (for example, by a <code>Digester</code>
232: * instance parsing the
233: * configuration file), it will parse the class names and store the resulting
234: * string(s) into the <code>ArrayList</code> field </code>roleClasses</code>.
235: */
236: public void setRoleClassNames(String roleClassNames) {
237: this .roleClassNames = roleClassNames;
238: parseClassNames(roleClassNames, roleClasses);
239: }
240:
241: /**
242: * Parses a comma-delimited list of class names, and store the class names
243: * in the provided List. Each class must implement <codejava.security.Principal</code>.
244: *
245: * @param classNamesString a comma-delimited list of fully qualified class names.
246: * @param classNamesList the list in which the class names will be stored.
247: * The list is cleared before being populated.
248: */
249: protected void parseClassNames(String classNamesString,
250: List<String> classNamesList) {
251: classNamesList.clear();
252: if (classNamesString == null)
253: return;
254:
255: String[] classNames = classNamesString.split("[ ]*,[ ]*");
256: for (int i = 0; i < classNames.length; i++) {
257: if (classNames[i].length() == 0)
258: continue;
259: try {
260: Class principalClass = Class.forName(classNames[i]);
261: if (Principal.class.isAssignableFrom(principalClass)) {
262: classNamesList.add(classNames[i]);
263: } else {
264: log
265: .error("Class "
266: + classNames[i]
267: + " is not implementing "
268: + "java.security.Principal! Class not added.");
269: }
270: } catch (ClassNotFoundException e) {
271: log.error("Class " + classNames[i]
272: + " not found! Class not added.");
273: }
274: }
275: }
276:
277: /**
278: * Comma-delimited list of <code>java.security.Principal</code> classes
279: * that represent individual users.
280: */
281: protected String userClassNames = null;
282:
283: public String getUserClassNames() {
284: return (this .userClassNames);
285: }
286:
287: /**
288: * Sets the list of comma-delimited classes that represent individual
289: * users. The classes in the list must implement <code>java.security.Principal</code>.
290: * When this accessor is called (for example, by a <code>Digester</code>
291: * instance parsing the
292: * configuration file), it will parse the class names and store the resulting
293: * string(s) into the <code>ArrayList</code> field </code>userClasses</code>.
294: */
295: public void setUserClassNames(String userClassNames) {
296: this .userClassNames = userClassNames;
297: parseClassNames(userClassNames, userClasses);
298: }
299:
300: // --------------------------------------------------------- Public Methods
301:
302: /**
303: * Return the <code>Principal</code> associated with the specified username and
304: * credentials, if there is one; otherwise return <code>null</code>.
305: *
306: * If there are any errors with the JDBC connection, executing
307: * the query or anything we return null (don't authenticate). This
308: * event is also logged, and the connection will be closed so that
309: * a subsequent request will automatically re-open it.
310: *
311: * @param username Username of the <code>Principal</code> to look up
312: * @param credentials Password or other credentials to use in
313: * authenticating this username
314: */
315: public Principal authenticate(String username, String credentials) {
316:
317: // Establish a LoginContext to use for authentication
318: try {
319: LoginContext loginContext = null;
320: if (appName == null)
321: appName = "Tomcat";
322:
323: if (log.isDebugEnabled())
324: log.debug(sm.getString("jaasRealm.beginLogin",
325: username, appName));
326:
327: // What if the LoginModule is in the container class loader ?
328: ClassLoader ocl = null;
329:
330: if (isUseContextClassLoader()) {
331: ocl = Thread.currentThread().getContextClassLoader();
332: Thread.currentThread().setContextClassLoader(
333: this .getClass().getClassLoader());
334: }
335:
336: try {
337: loginContext = new LoginContext(appName,
338: new JAASCallbackHandler(this , username,
339: credentials));
340: } catch (Throwable e) {
341: log.error(sm.getString("jaasRealm.unexpectedError"), e);
342: return (null);
343: } finally {
344: if (isUseContextClassLoader()) {
345: Thread.currentThread().setContextClassLoader(ocl);
346: }
347: }
348:
349: if (log.isDebugEnabled())
350: log.debug("Login context created " + username);
351:
352: // Negotiate a login via this LoginContext
353: Subject subject = null;
354: try {
355: loginContext.login();
356: subject = loginContext.getSubject();
357: if (subject == null) {
358: if (log.isDebugEnabled())
359: log.debug(sm.getString("jaasRealm.failedLogin",
360: username));
361: return (null);
362: }
363: } catch (AccountExpiredException e) {
364: if (log.isDebugEnabled())
365: log.debug(sm.getString("jaasRealm.accountExpired",
366: username));
367: return (null);
368: } catch (CredentialExpiredException e) {
369: if (log.isDebugEnabled())
370: log.debug(sm.getString(
371: "jaasRealm.credentialExpired", username));
372: return (null);
373: } catch (FailedLoginException e) {
374: if (log.isDebugEnabled())
375: log.debug(sm.getString("jaasRealm.failedLogin",
376: username));
377: return (null);
378: } catch (LoginException e) {
379: log.warn(sm.getString("jaasRealm.loginException",
380: username), e);
381: return (null);
382: } catch (Throwable e) {
383: log.error(sm.getString("jaasRealm.unexpectedError"), e);
384: return (null);
385: }
386:
387: if (log.isDebugEnabled())
388: log.debug(sm.getString("jaasRealm.loginContextCreated",
389: username));
390:
391: // Return the appropriate Principal for this authenticated Subject
392: Principal principal = createPrincipal(username, subject);
393: if (principal == null) {
394: log.debug(sm.getString("jaasRealm.authenticateFailure",
395: username));
396: return (null);
397: }
398: if (log.isDebugEnabled()) {
399: log.debug(sm.getString("jaasRealm.authenticateSuccess",
400: username));
401: }
402:
403: return (principal);
404: } catch (Throwable t) {
405: log.error("error ", t);
406: return null;
407: }
408: }
409:
410: // -------------------------------------------------------- Package Methods
411:
412: // ------------------------------------------------------ Protected Methods
413:
414: /**
415: * Return a short name for this <code>Realm</code> implementation.
416: */
417: protected String getName() {
418:
419: return (name);
420:
421: }
422:
423: /**
424: * Return the password associated with the given principal's user name.
425: */
426: protected String getPassword(String username) {
427:
428: return (null);
429:
430: }
431:
432: /**
433: * Return the <code>Principal</code> associated with the given user name.
434: */
435: protected Principal getPrincipal(String username) {
436:
437: return (null);
438:
439: }
440:
441: /**
442: * Identify and return a <code>java.security.Principal</code> instance
443: * representing the authenticated user for the specified <code>Subject</code>.
444: * The Principal is constructed by scanning the list of Principals returned
445: * by the JAASLoginModule. The first <code>Principal</code> object that matches
446: * one of the class names supplied as a "user class" is the user Principal.
447: * This object is returned to tha caller.
448: * Any remaining principal objects returned by the LoginModules are mapped to
449: * roles, but only if their respective classes match one of the "role class" classes.
450: * If a user Principal cannot be constructed, return <code>null</code>.
451: * @param subject The <code>Subject</code> representing the logged-in user
452: */
453: protected Principal createPrincipal(String username, Subject subject) {
454: // Prepare to scan the Principals for this Subject
455:
456: List<String> roles = new ArrayList<String>();
457: Principal userPrincipal = null;
458:
459: // Scan the Principals for this Subject
460: Iterator principals = subject.getPrincipals().iterator();
461: while (principals.hasNext()) {
462: Principal principal = (Principal) principals.next();
463:
464: String principalClass = principal.getClass().getName();
465:
466: if (log.isDebugEnabled()) {
467: log.debug(sm.getString("jaasRealm.checkPrincipal",
468: principal, principalClass));
469: }
470:
471: if (userPrincipal == null
472: && userClasses.contains(principalClass)) {
473: userPrincipal = principal;
474: if (log.isDebugEnabled()) {
475: log.debug(sm.getString(
476: "jaasRealm.userPrincipalSuccess", principal
477: .getName()));
478: }
479: }
480:
481: if (roleClasses.contains(principalClass)) {
482: roles.add(principal.getName());
483: if (log.isDebugEnabled()) {
484: log.debug(sm.getString(
485: "jaasRealm.rolePrincipalAdd", principal
486: .getName()));
487: }
488: }
489: }
490:
491: // Print failure message if needed
492: if (userPrincipal == null) {
493: if (log.isDebugEnabled()) {
494: log.debug(sm
495: .getString("jaasRealm.userPrincipalFailure"));
496: log.debug(sm
497: .getString("jaasRealm.rolePrincipalFailure"));
498: }
499: } else {
500: if (roles.size() == 0) {
501: if (log.isDebugEnabled()) {
502: log
503: .debug(sm
504: .getString("jaasRealm.rolePrincipalFailure"));
505: }
506: }
507: }
508:
509: // Return the resulting Principal for our authenticated user
510: return new GenericPrincipal(this , username, null, roles,
511: userPrincipal);
512: }
513:
514: /**
515: * Ensure the given name is legal for JAAS configuration.
516: * Added for Bugzilla 30869, made protected for easy customization
517: * in case my implementation is insufficient, which I think is
518: * very likely.
519: *
520: * @param src The name to validate
521: * @return A string that's a valid JAAS realm name
522: */
523: protected String makeLegalForJAAS(final String src) {
524: String result = src;
525:
526: // Default name is "other" per JAAS spec
527: if (result == null) {
528: result = "other";
529: }
530:
531: // Strip leading slash if present, as Sun JAAS impl
532: // barfs on it (see Bugzilla 30869 bug report).
533: if (result.startsWith("/")) {
534: result = result.substring(1);
535: }
536:
537: return result;
538: }
539:
540: // ------------------------------------------------------ Lifecycle Methods
541:
542: /**
543: *
544: * Prepare for active use of the public methods of this <code>Component</code>.
545: *
546: * @exception LifecycleException if this component detects a fatal error
547: * that prevents it from being started
548: */
549: public void start() throws LifecycleException {
550:
551: // Perform normal superclass initialization
552: super .start();
553:
554: }
555:
556: /**
557: * Gracefully shut down active use of the public methods of this <code>Component</code>.
558: *
559: * @exception LifecycleException if this component detects a fatal error
560: * that needs to be reported
561: */
562: public void stop() throws LifecycleException {
563:
564: // Perform normal superclass finalization
565: super.stop();
566:
567: }
568:
569: }
|