001: /*
002: * JSPWiki - a JSP-based WikiWiki clone. Copyright (C) 2001-2003 Janne Jalkanen
003: * (Janne.Jalkanen@iki.fi) This program is free software; you can redistribute
004: * it and/or modify it under the terms of the GNU Lesser General Public License
005: * as published by the Free Software Foundation; either version 2.1 of the
006: * License, or (at your option) any later version. This program is distributed
007: * in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
008: * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
009: * See the GNU Lesser General Public License for more details. You should have
010: * received a copy of the GNU Lesser General Public License along with this
011: * program; if not, write to the Free Software Foundation, Inc., 59 Temple
012: * Place, Suite 330, Boston, MA 02111-1307 USA
013: */
014: package com.ecyrd.jspwiki.auth;
015:
016: import java.io.File;
017: import java.net.MalformedURLException;
018: import java.net.URL;
019: import java.security.AccessController;
020: import java.security.Principal;
021: import java.security.PrivilegedAction;
022: import java.util.Properties;
023:
024: import javax.security.auth.callback.CallbackHandler;
025: import javax.security.auth.login.AccountExpiredException;
026: import javax.security.auth.login.AppConfigurationEntry;
027: import javax.security.auth.login.Configuration;
028: import javax.security.auth.login.CredentialExpiredException;
029: import javax.security.auth.login.FailedLoginException;
030: import javax.security.auth.login.LoginContext;
031: import javax.security.auth.login.LoginException;
032: import javax.servlet.http.HttpServletRequest;
033: import javax.servlet.http.HttpSession;
034:
035: import org.apache.log4j.Logger;
036:
037: import com.ecyrd.jspwiki.TextUtil;
038: import com.ecyrd.jspwiki.WikiEngine;
039: import com.ecyrd.jspwiki.WikiException;
040: import com.ecyrd.jspwiki.WikiSession;
041: import com.ecyrd.jspwiki.auth.authorize.Role;
042: import com.ecyrd.jspwiki.auth.authorize.WebContainerAuthorizer;
043: import com.ecyrd.jspwiki.auth.login.CookieAssertionLoginModule;
044: import com.ecyrd.jspwiki.auth.login.CookieAuthenticationLoginModule;
045: import com.ecyrd.jspwiki.auth.login.WebContainerCallbackHandler;
046: import com.ecyrd.jspwiki.auth.login.WikiCallbackHandler;
047: import com.ecyrd.jspwiki.event.WikiEventListener;
048: import com.ecyrd.jspwiki.event.WikiEventManager;
049: import com.ecyrd.jspwiki.event.WikiSecurityEvent;
050:
051: /**
052: * Manages authentication activities for a WikiEngine: user login, logout, and
053: * credential refreshes. This class uses JAAS to determine how users log in.
054: * @author Andrew Jaquith
055: * @author Janne Jalkanen
056: * @author Erik Bunn
057: * @since 2.3
058: */
059: public final class AuthenticationManager {
060:
061: /** The name of the built-in cookie assertion module */
062: public static final String COOKIE_MODULE = CookieAssertionLoginModule.class
063: .getName();
064:
065: /** The name of the built-in cookie authentication module */
066: public static final String COOKIE_AUTHENTICATION_MODULE = CookieAuthenticationLoginModule.class
067: .getName();
068:
069: /** The JAAS application name for the web container authentication stack. */
070: public static final String LOGIN_CONTAINER = "JSPWiki-container";
071:
072: /** The JAAS application name for the JSPWiki custom authentication stack. */
073: public static final String LOGIN_CUSTOM = "JSPWiki-custom";
074:
075: /** If this jspwiki.properties property is <code>true</code>, logs the IP address of the editor on saving. */
076: public static final String PROP_STOREIPADDRESS = "jspwiki.storeIPAddress";
077:
078: protected static final Logger log = Logger
079: .getLogger(AuthenticationManager.class);
080:
081: /** Was JAAS login config already set before we startd up? */
082: protected boolean m_isJaasConfiguredAtStartup = false;
083:
084: /** Static Boolean for lazily-initializing the "allows assertions" flag */
085: private static Boolean c_allowsAssertions = null;
086:
087: /** Static Boolean for lazily-initializing the "allows cookie authentication" flag */
088: private static Boolean c_allowsAuthentication = null;
089:
090: private WikiEngine m_engine = null;
091:
092: /** If true, logs the IP address of the editor */
093: private boolean m_storeIPAddress = true;
094:
095: /** Value specifying that the user wants to use the container-managed security, just like
096: * in JSPWiki 2.2.
097: */
098: public static final String SECURITY_OFF = "off";
099:
100: /** Just to provide compatibility with the old versions. The same
101: * as SECURITY_OFF.
102: *
103: * @deprecated
104: */
105: protected static final String SECURITY_CONTAINER = "container";
106:
107: /** Value specifying that the user wants to use the built-in JAAS-based system */
108: public static final String SECURITY_JAAS = "jaas";
109:
110: /**
111: * This property determines whether we use JSPWiki authentication or not.
112: * Possible values are AUTH_JAAS or AUTH_CONTAINER.
113: *
114: */
115:
116: public static final String PROP_SECURITY = "jspwiki.security";
117: private static final String PROP_JAAS_CONFIG = "java.security.auth.login.config";
118: private static final String DEFAULT_JAAS_CONFIG = "jspwiki.jaas";
119:
120: private static boolean c_useJAAS = true;
121:
122: /**
123: * Creates an AuthenticationManager instance for the given WikiEngine and
124: * the specified set of properties. All initialization for the modules is
125: * done here.
126: * @param engine the wiki engine
127: * @param props the properties used to initialize the wiki engine
128: * @throws WikiException if the AuthenticationManager cannot be initialized
129: */
130: public final void initialize(WikiEngine engine, Properties props)
131: throws WikiException {
132: m_engine = engine;
133: m_storeIPAddress = TextUtil.getBooleanProperty(props,
134: PROP_STOREIPADDRESS, m_storeIPAddress);
135: m_isJaasConfiguredAtStartup = PolicyLoader.isJaasConfigured();
136:
137: // Yes, writing to a static field is done here on purpose.
138: c_useJAAS = SECURITY_JAAS.equals(props.getProperty(
139: PROP_SECURITY, SECURITY_JAAS));
140:
141: if (!c_useJAAS)
142: return;
143:
144: //
145: // The rest is JAAS implementation
146: //
147:
148: log.info("Checking JAAS configuration...");
149:
150: if (!m_isJaasConfiguredAtStartup) {
151: URL config = findConfigFile(engine, DEFAULT_JAAS_CONFIG);
152: log
153: .info("JAAS not configured. Installing default configuration: "
154: + config
155: + ". You can set the "
156: + PROP_JAAS_CONFIG
157: + " system property to point to your "
158: + "jspwiki.jaas file, or add the entries from jspwiki.jaas to your own "
159: + "JAAS configuration file.");
160: try {
161: PolicyLoader.setJaasConfiguration(config);
162: } catch (SecurityException e) {
163: log
164: .error("Could not configure JAAS: "
165: + e.getMessage());
166: }
167: } else {
168: log
169: .info("JAAS already configured by some other application (leaving it alone...)");
170: }
171: }
172:
173: /**
174: * Returns true if this WikiEngine uses container-managed authentication.
175: * This method is used primarily for cosmetic purposes in the JSP tier, and
176: * performs no meaningful security function per se. Delegates to
177: * {@link com.ecyrd.jspwiki.auth.authorize.WebContainerAuthorizer#isContainerAuthorized()},
178: * if used as the external authorizer; otherwise, returns <code>false</code>.
179: * @return <code>true</code> if the wiki's authentication is managed by
180: * the container, <code>false</code> otherwise
181: */
182: public final boolean isContainerAuthenticated() {
183: if (!c_useJAAS)
184: return true;
185:
186: try {
187: Authorizer authorizer = m_engine.getAuthorizationManager()
188: .getAuthorizer();
189: if (authorizer instanceof WebContainerAuthorizer) {
190: return ((WebContainerAuthorizer) authorizer)
191: .isContainerAuthorized();
192: }
193: } catch (WikiException e) {
194: // It's probably ok to fail silently...
195: }
196: return false;
197: }
198:
199: /**
200: * <p>Logs in the user by attempting to populate a WikiSession Subject from
201: * a web servlet request. This method leverages container-managed authentication.
202: * This method logs in the user if the user's status is "unknown" to the
203: * WikiSession, or if the Http servlet container's authentication status has
204: * changed. This method assumes that the HttpServletRequest is not null; otherwise,
205: * an IllegalStateException is thrown. This method is a <em>privileged</em> action;
206: * the caller must posess the (name here) permission.</p>
207: * <p>If <code>request</code> is <code>null</code>, or the WikiSession
208: * cannot be located for this request, this method throws an {@link IllegalStateException}.</p>
209: * methods return null
210: * @param request servlet request for this user
211: * @return the result of the login operation: <code>true</code> if the user logged in
212: * successfully; <code>false</code> otherwise
213: * @throws com.ecyrd.jspwiki.auth.WikiSecurityException if the Authorizer or UserManager cannot be obtained
214: * @since 2.3
215: */
216: public final boolean login(HttpServletRequest request)
217: throws WikiSecurityException {
218: if (request == null) {
219: throw new IllegalStateException(
220: "Wiki context's HttpRequest may not be null");
221: }
222:
223: WikiSession wikiSession = WikiSession.getWikiSession(m_engine,
224: request);
225: if (wikiSession == null) {
226: throw new IllegalStateException(
227: "Wiki context's WikiSession may not be null");
228: }
229:
230: // If using JAAS, try to log in; otherwise logins "always" succeed
231: boolean login = true;
232: if (c_useJAAS) {
233: AuthorizationManager authMgr = m_engine
234: .getAuthorizationManager();
235: CallbackHandler handler = new WebContainerCallbackHandler(
236: m_engine, request, authMgr.getAuthorizer());
237: login = doLogin(wikiSession, handler, LOGIN_CONTAINER);
238: }
239: return login;
240: }
241:
242: /**
243: * Attempts to perform a WikiSession login for the given username/password
244: * combination. This is custom authentication.
245: * @param session the current wiki session; may not be null.
246: * @param username The user name. This is a login name, not a WikiName. In
247: * most cases they are the same, but in some cases, they might
248: * not be.
249: * @param password The password
250: * @return true, if the username/password is valid
251: * @throws com.ecyrd.jspwiki.auth.WikiSecurityException if the Authorizer or UserManager cannot be obtained
252: */
253: public final boolean login(WikiSession session, String username,
254: String password) throws WikiSecurityException {
255: if (session == null) {
256: log.error("No wiki session provided, cannot log in.");
257: return false;
258: }
259:
260: UserManager userMgr = m_engine.getUserManager();
261: CallbackHandler handler = new WikiCallbackHandler(userMgr
262: .getUserDatabase(), username, password);
263: return doLogin(session, handler, LOGIN_CUSTOM);
264: }
265:
266: /**
267: * Logs the user out by retrieving the WikiSession associated with the
268: * HttpServletRequest and unbinding all of the Subject's Principals,
269: * except for {@link Role#ALL}, {@link Role#ANONYMOUS}.
270: * is a cheap-and-cheerful way to do it without invoking JAAS LoginModules.
271: * The logout operation will also flush the JSESSIONID cookie from
272: * the user's browser session, if it was set.
273: * @param request the current HTTP request
274: */
275: public final void logout(HttpServletRequest request) {
276: if (request == null) {
277: log.error("No HTTP reqest provided; cannot log out.");
278: return;
279: }
280:
281: HttpSession session = request.getSession();
282: String sid = (session == null) ? "(null)" : session.getId();
283: if (log.isDebugEnabled()) {
284: log.debug("Invalidating WikiSession for session ID=" + sid);
285: }
286: // Retrieve the associated WikiSession and clear the Principal set
287: WikiSession wikiSession = WikiSession.getWikiSession(m_engine,
288: request);
289: Principal originalPrincipal = wikiSession.getLoginPrincipal();
290: wikiSession.invalidate();
291:
292: // Remove the wikiSession from the WikiSession cache
293: WikiSession.removeWikiSession(m_engine, request);
294:
295: // We need to flush the HTTP session too
296: if (session != null) {
297: session.invalidate();
298: }
299:
300: // Log the event
301: fireEvent(WikiSecurityEvent.LOGOUT, originalPrincipal, null);
302: }
303:
304: /**
305: * Determines whether this WikiEngine allows users to assert identities using
306: * cookies instead of passwords. This is determined by inspecting
307: * the LoginConfiguration for application <code>JSPWiki-container</code>.
308: * @return <code>true</code> if cookies are allowed
309: */
310: public static final boolean allowsCookieAssertions() {
311: if (!c_useJAAS)
312: return true;
313:
314: // Lazily initialize
315: if (c_allowsAssertions == null) {
316: c_allowsAssertions = Boolean.FALSE;
317:
318: // Figure out whether cookie assertions are allowed
319: Configuration loginConfig = (Configuration) AccessController
320: .doPrivileged(new PrivilegedAction() {
321: public Object run() {
322: return Configuration.getConfiguration();
323: }
324: });
325:
326: if (loginConfig != null) {
327: AppConfigurationEntry[] configs = loginConfig
328: .getAppConfigurationEntry(LOGIN_CONTAINER);
329: if (configs != null) {
330: for (int i = 0; i < configs.length; i++) {
331: AppConfigurationEntry config = configs[i];
332: if (COOKIE_MODULE.equals(config
333: .getLoginModuleName())) {
334: c_allowsAssertions = Boolean.TRUE;
335: }
336: }
337: }
338: }
339: }
340:
341: return c_allowsAssertions.booleanValue();
342: }
343:
344: /**
345: * Determines whether this WikiEngine allows users to authenticate using
346: * cookies instead of passwords. This is determined by inspecting
347: * the LoginConfiguration for application <code>JSPWiki-container</code>.
348: * @return <code>true</code> if cookies are allowed for authentication
349: * @since 2.5.62
350: */
351: public static final boolean allowsCookieAuthentication() {
352: if (!c_useJAAS)
353: return true;
354:
355: // Lazily initialize
356: if (c_allowsAuthentication == null) {
357: c_allowsAuthentication = Boolean.FALSE;
358:
359: // Figure out whether cookie assertions are allowed
360: Configuration loginConfig = (Configuration) AccessController
361: .doPrivileged(new PrivilegedAction() {
362: public Object run() {
363: return Configuration.getConfiguration();
364: }
365: });
366:
367: if (loginConfig != null) {
368: AppConfigurationEntry[] configs = loginConfig
369: .getAppConfigurationEntry(LOGIN_CONTAINER);
370:
371: if (configs != null) {
372: for (int i = 0; i < configs.length; i++) {
373: AppConfigurationEntry config = configs[i];
374: if (COOKIE_AUTHENTICATION_MODULE.equals(config
375: .getLoginModuleName())) {
376: c_allowsAuthentication = Boolean.TRUE;
377: }
378: }
379: }
380: }
381: }
382:
383: return c_allowsAuthentication.booleanValue();
384: }
385:
386: /**
387: * Determines whether the supplied Principal is a "role principal".
388: * @param principal the principal to test
389: * @return <code>true</code> if the Principal is of type
390: * {@link GroupPrincipal} or
391: * {@link com.ecyrd.jspwiki.auth.authorize.Role},
392: * <code>false</code> otherwise
393: */
394: public static final boolean isRolePrincipal(Principal principal) {
395: return principal instanceof Role
396: || principal instanceof GroupPrincipal;
397: }
398:
399: /**
400: * Determines whether the supplied Principal is a "user principal".
401: * @param principal the principal to test
402: * @return <code>false</code> if the Principal is of type
403: * {@link GroupPrincipal} or
404: * {@link com.ecyrd.jspwiki.auth.authorize.Role},
405: * <code>true</code> otherwise
406: */
407: public static final boolean isUserPrincipal(Principal principal) {
408: return !isRolePrincipal(principal);
409: }
410:
411: /**
412: * Log in to the application using a given JAAS LoginConfiguration. Any
413: * configuration error
414: * @param wikiSession the current wiki session, to which the Subject will be associated
415: * @param handler handles callbacks sent by the LoginModules in the configuration
416: * @param application the name of the application whose LoginConfiguration should be used
417: * @return the result of the login
418: * @throws WikiSecurityException
419: */
420: private final boolean doLogin(final WikiSession wikiSession,
421: final CallbackHandler handler, final String application)
422: throws WikiSecurityException {
423: try {
424: LoginContext loginContext = (LoginContext) AccessController
425: .doPrivileged(new PrivilegedAction() {
426: public Object run() {
427: try {
428: return wikiSession.getLoginContext(
429: application, handler);
430: } catch (LoginException e) {
431: log
432: .error("Couldn't retrieve login configuration.\nMessage="
433: + e
434: .getLocalizedMessage());
435: return null;
436: }
437: }
438: });
439:
440: if (loginContext != null) {
441: loginContext.login();
442: fireEvent(WikiSecurityEvent.LOGIN_INITIATED, null,
443: wikiSession);
444: } else {
445: log
446: .error("No login context. Please double-check that JSPWiki found your 'jspwiki.jaas' file or the contents have been appended to your regular JAAS file.");
447: return false;
448: }
449:
450: // Fire event for the correct authentication event
451: if (wikiSession.isAnonymous()) {
452: fireEvent(WikiSecurityEvent.LOGIN_ANONYMOUS,
453: wikiSession.getLoginPrincipal(), wikiSession);
454: } else if (wikiSession.isAsserted()) {
455: fireEvent(WikiSecurityEvent.LOGIN_ASSERTED, wikiSession
456: .getLoginPrincipal(), wikiSession);
457: } else if (wikiSession.isAuthenticated()) {
458: fireEvent(WikiSecurityEvent.LOGIN_AUTHENTICATED,
459: wikiSession.getLoginPrincipal(), wikiSession);
460: }
461:
462: return true;
463: } catch (FailedLoginException e) {
464: //
465: // Just a mistyped password or a cracking attempt. No need to worry
466: // and alert the admin
467: //
468: log.info("Failed login: " + e.getLocalizedMessage());
469: fireEvent(WikiSecurityEvent.LOGIN_FAILED, wikiSession
470: .getLoginPrincipal(), wikiSession);
471: return false;
472: } catch (AccountExpiredException e) {
473: log.info("Expired account: " + e.getLocalizedMessage());
474: fireEvent(WikiSecurityEvent.LOGIN_ACCOUNT_EXPIRED,
475: wikiSession.getLoginPrincipal(), wikiSession);
476: return false;
477: } catch (CredentialExpiredException e) {
478: log.info("Credentials expired: " + e.getLocalizedMessage());
479: fireEvent(WikiSecurityEvent.LOGIN_CREDENTIAL_EXPIRED,
480: wikiSession.getLoginPrincipal(), wikiSession);
481: return false;
482: } catch (LoginException e) {
483: //
484: // This should only be caught if something unforeseen happens,
485: // so therefore we can log it as an error.
486: //
487: log.error("Couldn't log in.\nMessage="
488: + e.getLocalizedMessage());
489: return false;
490: } catch (SecurityException e) {
491: log
492: .error(
493: "Could not log in. Please check that your jaas.config file is found.",
494: e);
495: return false;
496: }
497: }
498:
499: /**
500: * Looks up and obtains a configuration file inside the WEB-INF folder of a
501: * wiki webapp.
502: * @param engine the wiki engine
503: * @param name the file to obtain, <em>e.g.</em>, <code>jspwiki.policy</code>
504: * @return the URL to the file
505: */
506: protected static final URL findConfigFile(WikiEngine engine,
507: String name) {
508: // Try creating an absolute path first
509: File defaultFile = null;
510: if (engine.getRootPath() != null) {
511: defaultFile = new File(engine.getRootPath() + "/WEB-INF/"
512: + name);
513: }
514: if (defaultFile != null && defaultFile.exists()) {
515: try {
516: return defaultFile.toURL();
517: } catch (MalformedURLException e) {
518: // Shouldn't happen, but log it if it does
519: log.warn("Malformed URL: " + e.getMessage());
520: }
521:
522: }
523:
524: // Ok, the absolute path didn't work; try other methods
525: ClassLoader cl = AuthenticationManager.class.getClassLoader();
526:
527: URL path = cl.getResource("/WEB-INF/" + name);
528:
529: if (path == null)
530: path = cl.getResource("/" + name);
531:
532: if (path == null)
533: path = cl.getResource(name);
534:
535: if (path == null && engine.getServletContext() != null) {
536: try {
537: path = engine.getServletContext().getResource(
538: "/WEB-INF/" + name);
539: } catch (MalformedURLException e) {
540: // This should never happen unless I screw up
541: log
542: .fatal("Your code is b0rked. You are a bad person.");
543: }
544: }
545:
546: return path;
547: }
548:
549: // events processing .......................................................
550:
551: /**
552: * Registers a WikiEventListener with this instance.
553: * This is a convenience method.
554: * @param listener the event listener
555: */
556: public final synchronized void addWikiEventListener(
557: WikiEventListener listener) {
558: WikiEventManager.addWikiEventListener(this , listener);
559: }
560:
561: /**
562: * Un-registers a WikiEventListener with this instance.
563: * This is a convenience method.
564: * @param listener the event listener
565: */
566: public final synchronized void removeWikiEventListener(
567: WikiEventListener listener) {
568: WikiEventManager.removeWikiEventListener(this , listener);
569: }
570:
571: /**
572: * Fires a WikiSecurityEvent of the provided type, Principal and target Object
573: * to all registered listeners.
574: *
575: * @see com.ecyrd.jspwiki.event.WikiSecurityEvent
576: * @param type the event type to be fired
577: * @param principal the subject of the event, which may be <code>null</code>
578: * @param target the changed Object, which may be <code>null</code>
579: */
580: protected final void fireEvent(int type, Principal principal,
581: Object target) {
582: if (WikiEventManager.isListening(this )) {
583: WikiEventManager.fireEvent(this , new WikiSecurityEvent(
584: this, type, principal, target));
585: }
586: }
587:
588: }
|