001: /*
002: JSPWiki - a JSP-based WikiWiki clone.
003:
004: Copyright (C) 2001-2003 Janne Jalkanen (Janne.Jalkanen@iki.fi)
005:
006: This program is free software; you can redistribute it and/or modify
007: it under the terms of the GNU Lesser General Public License as published by
008: the Free Software Foundation; either version 2.1 of the License, or
009: (at your option) any later version.
010:
011: This program is distributed in the hope that it will be useful,
012: but WITHOUT ANY WARRANTY; without even the implied warranty of
013: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014: GNU Lesser General Public License for more details.
015:
016: You should have received a copy of the GNU Lesser General Public License
017: along with this program; if not, write to the Free Software
018: Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
019: */
020: package com.ecyrd.jspwiki.auth;
021:
022: import java.util.*;
023: import java.security.Principal;
024: import javax.servlet.http.HttpSession;
025: import javax.servlet.http.HttpServletRequest;
026: import javax.servlet.http.HttpServletResponse;
027: import javax.servlet.http.Cookie;
028: import org.apache.log4j.Logger;
029: import com.ecyrd.jspwiki.WikiEngine;
030: import com.ecyrd.jspwiki.TextUtil;
031: import com.ecyrd.jspwiki.WikiException;
032: import com.ecyrd.jspwiki.util.ClassUtil;
033: import com.ecyrd.jspwiki.util.HttpUtil;
034:
035: import java.security.AccessController;
036: import com.sun.identity.security.*;
037: import com.iplanet.sso.*;
038: import com.iplanet.am.sdk.*;
039:
040: /**
041: * Manages user accounts, logins/logouts, passwords, etc.
042: *
043: * @author Janne Jalkanen
044: * @author Erik Bunn
045: */
046: public class UserManager {
047: static Logger log = Logger.getLogger(UserManager.class);
048:
049: /** The name the UserProfile is stored in a Session by. */
050: public static final String WIKIUSER = "currentUser";
051:
052: /** If true, logs the IP address of the editor on saving. */
053: public static final String PROP_STOREIPADDRESS = "jspwiki.storeIPAddress";
054:
055: public static final String PROP_AUTHENTICATOR = "jspwiki.authenticator";
056: public static final String PROP_USERDATABASE = "jspwiki.userdatabase";
057:
058: public static final String PROP_ADMINISTRATOR = "jspwiki.auth.administrator";
059:
060: /** If true, logs the IP address of the editor */
061: private boolean m_storeIPAddress = true;
062:
063: private HashMap m_groups = new HashMap();
064:
065: // FIXME: These should probably be localized.
066: // FIXME: All is used as a catch-all.
067:
068: public static final String GROUP_GUEST = "Guest";
069: public static final String GROUP_NAMEDGUEST = "NamedGuest";
070: public static final String GROUP_KNOWNPERSON = "KnownPerson";
071:
072: private static final String DEFAULT_DATABASE = "com.ecyrd.jspwiki.auth.modules.WikiDatabase";
073:
074: /**
075: * The default administrator group is called "AdminGroup"
076: */
077: private static final String DEFAULT_ADMINISTRATOR = "AdminGroup";
078:
079: private WikiAuthenticator m_authenticator;
080: private UserDatabase m_database;
081:
082: private WikiEngine m_engine;
083:
084: private String m_administrator;
085:
086: private boolean m_useAuth = false;
087:
088: private SSOToken m_adminSSOToken;
089: private AMStoreConnection m_adminStoreConnection;
090:
091: /**
092: * Creates an UserManager instance for the given WikiEngine and
093: * the specified set of properties. All initialization for the
094: * modules is done here.
095: */
096: public UserManager(WikiEngine engine, Properties props)
097: throws WikiException {
098: m_engine = engine;
099:
100: m_storeIPAddress = TextUtil.getBooleanProperty(props,
101: PROP_STOREIPADDRESS, m_storeIPAddress);
102:
103: m_administrator = props.getProperty(PROP_ADMINISTRATOR,
104: DEFAULT_ADMINISTRATOR);
105:
106: m_useAuth = TextUtil.getBooleanProperty(props,
107: AuthorizationManager.PROP_USEOLDAUTH, false);
108:
109: try {
110: m_adminSSOToken = (SSOToken) AccessController
111: .doPrivileged(AdminTokenAction.getInstance());
112: m_adminStoreConnection = new AMStoreConnection(
113: m_adminSSOToken);
114: } catch (SSOException ssoe) {
115: log
116: .warn(
117: "UserManager: Access Manager connection cannot be made",
118: ssoe);
119: }
120:
121: if (!m_useAuth)
122: return;
123:
124: WikiGroup all = new AllGroup();
125: all.setName("All");
126: m_groups.put(GROUP_GUEST, new AllGroup());
127: // m_groups.put( "All", all );
128: m_groups.put(GROUP_NAMEDGUEST, new NamedGroup());
129: m_groups.put(GROUP_KNOWNPERSON, new KnownGroup());
130:
131: String authClassName = props.getProperty(PROP_AUTHENTICATOR);
132:
133: if (authClassName != null) {
134: try {
135: Class authenticatorClass = ClassUtil
136: .findClass("com.ecyrd.jspwiki.auth.modules",
137: authClassName);
138:
139: m_authenticator = (WikiAuthenticator) authenticatorClass
140: .newInstance();
141: m_authenticator.initialize(props);
142:
143: log.info("Initialized " + authClassName
144: + " for authentication.");
145: } catch (ClassNotFoundException e) {
146: log.fatal("Authenticator " + authClassName
147: + " cannot be found", e);
148: throw new WikiException("Authenticator cannot be found");
149: } catch (InstantiationException e) {
150: log.fatal("Authenticator " + authClassName
151: + " cannot be created", e);
152: throw new WikiException(
153: "Authenticator cannot be created");
154: } catch (IllegalAccessException e) {
155: log
156: .fatal(
157: "You are not allowed to access this authenticator class",
158: e);
159: throw new WikiException(
160: "You are not allowed to access this authenticator class");
161: }
162: }
163:
164: String dbClassName = props.getProperty(PROP_USERDATABASE,
165: DEFAULT_DATABASE);
166:
167: try {
168: Class dbClass = ClassUtil.findClass(
169: "com.ecyrd.jspwiki.auth.modules", dbClassName);
170: m_database = (UserDatabase) dbClass.newInstance();
171: m_database.initialize(m_engine, props);
172: } catch (ClassNotFoundException e) {
173: log.fatal("UserDatabase " + dbClassName
174: + " cannot be found", e);
175: throw new WikiException("UserDatabase cannot be found");
176: } catch (InstantiationException e) {
177: log.fatal("UserDatabase " + dbClassName
178: + " cannot be created", e);
179: throw new WikiException("UserDatabase cannot be created");
180: } catch (IllegalAccessException e) {
181: log
182: .fatal(
183: "You are not allowed to access this user database class",
184: e);
185: throw new WikiException(
186: "You are not allowed to access this user database class");
187: }
188:
189: }
190:
191: /**
192: * Convenience shortcut to UserDatabase.getUserProfile().
193: */
194: public UserProfile getUserProfile(String name) {
195: if (m_database == null) {
196: // No user database, so return a dummy profile
197: UserProfile wup = new UserProfile();
198: wup.setName(name);
199: wup.setLoginName(name);
200: wup.setLoginStatus(UserProfile.COOKIE);
201:
202: return wup;
203: }
204:
205: WikiPrincipal up = m_database.getPrincipal(name);
206:
207: if (!(up instanceof UserProfile)) {
208: log.info(name + " is not a user!");
209: up = null;
210: }
211:
212: return ((UserProfile) up);
213: }
214:
215: /**
216: * Returns the UserDatabase employed by this UserManager.
217: */
218: public UserDatabase getUserDatabase() {
219: return (m_database);
220: }
221:
222: /**
223: * Returns the WikiAuthenticator object employed by this UserManager.
224: */
225: public WikiAuthenticator getAuthenticator() {
226: return (m_authenticator);
227: }
228:
229: /**
230: * Returns true, if the user or the group represents a super user,
231: * which should be allowed access to everything.
232: *
233: * @param p Principal to check for administrator access.
234: * @return true, if the principal is an administrator.
235: */
236: public boolean isAdministrator(WikiPrincipal p) {
237: //
238: // Direct name matches are returned always.
239: //
240: if (p.getName().equals(m_administrator)) {
241: return true;
242: }
243:
244: // XXX not suuporting admin groups for now (avoids errors from group system)
245: if (true)
246: return false;
247:
248: //
249: // Try to get the super group and check if the user is a part
250: // of it.
251: //
252: WikiGroup super Principal = getWikiGroup(m_administrator);
253:
254: if (super Principal == null) {
255: // log.warn("No supergroup '"+m_administrator+"' exists; you should create one.");
256:
257: return false;
258: }
259:
260: return super Principal.isMember(p);
261: }
262:
263: /**
264: * Returns a WikiGroup instance for a given name. WikiGroups are cached,
265: * so there is basically a singleton across the Wiki for a group.
266: * The reason why this class caches them instead of the WikiGroup
267: * class itself is that it is the business of the User Manager to
268: * handle such issues.
269: *
270: * @param name Name of the group. This is case-sensitive.
271: * @return A WikiGroup instance.
272: */
273: // FIXME: Someone should really check when groups cease to be used,
274: // and release groups that are not being used.
275: // FIXME: Error handling is still deficient.
276: public WikiGroup getWikiGroup(String name) {
277: WikiGroup group;
278:
279: synchronized (m_groups) {
280: group = (WikiGroup) m_groups.get(name);
281:
282: if (group == null) {
283: WikiPrincipal p = m_database.getPrincipal(name);
284:
285: if (!(p instanceof WikiGroup)) {
286: log.info(name + " is not a group!");
287: } else {
288: group = (WikiGroup) p;
289: }
290: }
291: }
292:
293: return group;
294: }
295:
296: /**
297: * Returns a list of all WikiGroups this Principal is a member
298: * of.
299: */
300: // FIXME: This is not a very good solution; UserProfile
301: // should really cache the information.
302: // FIXME: Should really query the page manager.
303: public List getGroupsForPrincipal(Principal user)
304: throws NoSuchPrincipalException {
305: List list = null;
306:
307: //
308: // Add the groups ONLY if the user has been authenticated.
309: //
310: // FIXME: This is probably the wrong place, since this prevents
311: // us from querying stuff later on.
312:
313: if (user instanceof UserProfile
314: && ((UserProfile) user).isAuthenticated()) {
315: if (m_database != null)
316: list = m_database.getGroupsForPrincipal(user);
317: }
318:
319: if (list == null)
320: list = new ArrayList();
321:
322: //
323: // Add the default groups.
324: //
325:
326: synchronized (m_groups) {
327: for (Iterator i = m_groups.values().iterator(); i.hasNext();) {
328: WikiGroup g = (WikiGroup) i.next();
329:
330: if (g.isMember(user)) {
331: log.debug("User " + user.getName()
332: + " is a member of " + g.getName());
333: list.add(g);
334: }
335: }
336: }
337:
338: return list;
339: }
340:
341: /**
342: * Attempts to find a Principal from the list of known principals.
343: */
344:
345: public Principal getPrincipal(String name) {
346: Principal p = getWikiGroup(name);
347:
348: if (p == null) {
349: p = getUserProfile(name);
350:
351: if (p == null) {
352: log.debug("No such principal defined: " + name
353: + ", using UndefinedPrincipal");
354: p = new UndefinedPrincipal(name);
355: }
356: }
357:
358: return p;
359: }
360:
361: /**
362: * Attempts to perform a login for the given username/password
363: * combination. Also sets the attribute UserManager.WIKIUSER in the current session,
364: * which can then be used to fetch the current UserProfile. Or you can be lazy and
365: * just call getUserProfile()...
366: *
367: * @param username The user name. This is an user name, not a WikiName. In most cases
368: * they are the same, but in some cases, they might not be.
369: * @param password The password.
370: * @return true, if the username/password is valid.
371: * @throws PasswordException, if password has expired
372: */
373: public boolean login(String username, String password,
374: HttpSession session) throws WikiSecurityException {
375: if (m_authenticator == null)
376: return false;
377:
378: if (session == null) {
379: log.error("No session provided, cannot log in.");
380: return false;
381: }
382:
383: UserProfile wup = getUserProfile(username);
384: if (wup != null) {
385: wup.setPassword(password);
386:
387: boolean isValid = false;
388: boolean expired = false;
389:
390: try {
391: isValid = m_authenticator.authenticate(wup);
392: } catch (PasswordExpiredException e) {
393: isValid = true;
394: expired = true;
395: }
396:
397: if (isValid) {
398: wup.setLoginStatus(UserProfile.PASSWORD);
399: session.setAttribute(WIKIUSER, wup);
400: log.info("Logged in user " + username);
401:
402: if (expired)
403: throw new PasswordExpiredException(""); //FIXME!
404: } else {
405: log
406: .info("Username "
407: + username
408: + " attempted to log in with the wrong password.");
409: }
410:
411: return isValid;
412: }
413:
414: return false;
415: }
416:
417: /**
418: * Logs a web user out, clearing the session.
419: *
420: * @param session The current HTTP session for this user.
421: */
422: public void logout(HttpSession session) {
423: if (session != null) {
424: UserProfile wup = (UserProfile) session
425: .getAttribute(WIKIUSER);
426: if (wup != null) {
427: log.info("logged out user " + wup.getName());
428: wup.setLoginStatus(UserProfile.NONE);
429: }
430: session.invalidate();
431: }
432: }
433:
434: /**
435: * Gets a UserProfile, either from the request (presumably
436: * authenticated and with auth information) or a new one
437: * (with default permissions).
438: *
439: * @param request The servlet request for this user.
440: * @return A valid UserProfile. Can also return null in case it is not possible
441: * to get an UserProfile.
442: * @since 2.1.10.
443: */
444: public UserProfile getUserProfile(HttpServletRequest request) {
445: // First, see if we already have a user profile.
446: HttpSession session = request.getSession(true);
447: UserProfile wup = (UserProfile) session
448: .getAttribute(UserManager.WIKIUSER);
449:
450: if (wup != null) {
451: return wup;
452: }
453:
454: // Try to get a limited login. This will be inserted into the request.
455:
456: wup = limitedLogin(request);
457: if (wup != null) {
458: return wup;
459: }
460:
461: log.error("Unable to get a default UserProfile!");
462:
463: return null;
464: }
465:
466: /**
467: * Performs a "limited" login: sniffs for a user name from a cookie or the
468: * client, and creates a limited user profile based on it.
469: */
470: protected UserProfile limitedLogin(HttpServletRequest request) {
471: UserProfile wup = null;
472: String role = null;
473:
474: //
475: // First, checks whether container has done authentication for us.
476: //
477: String uid = request.getRemoteUser();
478:
479: if (uid != null) {
480: wup = getUserProfile(uid);
481: if (wup != null) {
482: wup.setLoginStatus(UserProfile.CONTAINER);
483: HttpSession session = request.getSession(true);
484: session.setAttribute(WIKIUSER, wup);
485: }
486: } else {
487: //
488: // See if a cookie exists, and create a default account.
489: //
490: uid = HttpUtil.retrieveCookieValue(request,
491: WikiEngine.PREFS_COOKIE_NAME);
492:
493: log.debug("Stored username=" + uid);
494:
495: if (uid != null) {
496: try {
497: wup = UserProfile.parseStringRepresentation(uid);
498: if (wup != null) {
499: wup.setLoginStatus(UserProfile.COOKIE);
500: }
501: } catch (NoSuchElementException e) {
502: // We fail silently, as the cookie is invalid.
503: }
504: }
505: }
506:
507: // If the UserDatabase declined to give us a UserPrincipal,
508: // we manufacture one here explicitly.
509: if (wup == null) {
510: wup = new UserProfile();
511: wup.setLoginName(GROUP_GUEST);
512: wup.setLoginStatus(UserProfile.NONE);
513:
514: //
515: // No username either, so fall back to the IP address.
516: //
517: if (m_storeIPAddress) {
518: wup.setName(request.getRemoteHost());
519: } else {
520: wup.setName(wup.getLoginName());
521: }
522: }
523:
524: //
525: // FIXME:
526: //
527: // We cannot store the UserProfile into the session, because of the following:
528: // Assume that Edit.jsp is protected through container auth.
529: //
530: // User without a cookie arrives through Wiki.jsp. A
531: // UserProfile is created, which essentially contains his IP
532: // address. If this is stored in the session, then, when the user
533: // tries to access the Edit.jsp page and container does auth, he will
534: // always be then known by his IP address, regardless of what the
535: // request.getRemoteUser() says.
536:
537: // So, until this is solved, we create a new UserProfile on each
538: // access. Ouch.
539:
540: // Limited login hasn't been authenticated. Just to emphasize the point:
541: // wup.setPassword( null );
542:
543: // HttpSession session = request.getSession( true );
544: // session.setAttribute( WIKIUSER, wup );
545:
546: return wup;
547: }
548:
549: /**
550: * Sets the username cookie.
551: *
552: * @since 2.1.47.
553: */
554: public void setUserCookie(HttpServletResponse response, String name) {
555: UserProfile profile = getUserProfile(name);
556: String uname = null;
557: if (profile != null) {
558: Cookie prefs = new Cookie(WikiEngine.PREFS_COOKIE_NAME,
559: profile.getStringRepresentation());
560: prefs.setMaxAge(1001 * 24 * 60 * 60); // 1001 days is default.
561: response.addCookie(prefs);
562: }
563: }
564:
565: public String getUserCommonName(String userid) {
566: String commonname = null;
567: try {
568: if (m_adminStoreConnection != null
569: && m_adminStoreConnection.isValidEntry(userid)) {
570: AMUser user = m_adminStoreConnection.getUser(userid);
571: commonname = user.getStringAttribute("cn");
572: }
573: } catch (Exception e) {
574: log.warn(
575: "UserManager:getUserCommonName() User name lookup failed for "
576: + userid, e);
577: }
578: if (commonname == null || commonname.length() == 0)
579: commonname = userid;
580: return commonname;
581: }
582:
583: public SSOToken getAdminSSOToken() {
584: return m_adminSSOToken;
585: }
586:
587: }
|