001: /*
002: JSPWiki - a JSP-based WikiWiki clone.
003:
004: Copyright (C) 2001-2005 The JSPWiki Development Group
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;
021:
022: import java.security.AccessControlException;
023: import java.security.Principal;
024: import java.security.PrivilegedAction;
025: import java.util.*;
026:
027: import javax.security.auth.Subject;
028: import javax.security.auth.callback.CallbackHandler;
029: import javax.security.auth.login.LoginContext;
030: import javax.security.auth.login.LoginException;
031: import javax.servlet.http.HttpServletRequest;
032: import javax.servlet.http.HttpSession;
033:
034: import org.apache.log4j.Logger;
035:
036: import com.ecyrd.jspwiki.auth.*;
037: import com.ecyrd.jspwiki.auth.authorize.Group;
038: import com.ecyrd.jspwiki.auth.authorize.GroupManager;
039: import com.ecyrd.jspwiki.auth.authorize.Role;
040: import com.ecyrd.jspwiki.auth.login.CookieAssertionLoginModule;
041: import com.ecyrd.jspwiki.auth.login.PrincipalWrapper;
042: import com.ecyrd.jspwiki.auth.user.UserDatabase;
043: import com.ecyrd.jspwiki.auth.user.UserProfile;
044: import com.ecyrd.jspwiki.event.WikiEvent;
045: import com.ecyrd.jspwiki.event.WikiEventListener;
046: import com.ecyrd.jspwiki.event.WikiSecurityEvent;
047:
048: /**
049: * <p>Represents a long-running wiki session, with an associated user Principal,
050: * user Subject, and authentication status. This class is initialized with
051: * minimal, default-deny values: authentication is set to <code>false</code>,
052: * and the user principal is set to <code>null</code>.</p>
053: * <p>The WikiSession class allows callers to:</p>
054: * <ul>
055: * <li>Obtain the authentication status of the user via
056: * {@link #isAnonymous()} and {@link #isAuthenticated()}</li>
057: * <li>Query the session for Principals representing the
058: * user's identity via {@link #getLoginPrincipal()},
059: * {@link #getUserPrincipal()} and {@link #getPrincipals()}</li>
060: * <li>Store, retrieve and clear UI messages via
061: * {@link #addMessage(String)}, {@link #getMessages(String)}
062: * and {@link #clearMessages(String)}</li>
063: * </ul>
064: * <p>To keep track of the Principals each user posseses, each WikiSession
065: * stores a JAAS Subject. Various login processes add or remove Principals
066: * when users authenticate or log out.</p>
067: * <p>WikiSession implements the {@link com.ecyrd.jspwiki.event.WikiEventListener}
068: * interface and listens for group add/change/delete events fired by
069: * event sources the WikiSession is registered with. Normally,
070: * {@link com.ecyrd.jspwiki.auth.AuthenticationManager} registers each WikiSession
071: * with the {@link com.ecyrd.jspwiki.auth.authorize.GroupManager}
072: * so it can catch group events. Thus, when a user is added to a
073: * {@link com.ecyrd.jspwiki.auth.authorize.Group}, a corresponding
074: * {@link com.ecyrd.jspwiki.auth.GroupPrincipal} is injected into
075: * the Subject's Principal set. Likewise, when the user is removed from
076: * the Group or the Group is deleted, the GroupPrincipal is removed
077: * from the Subject. The effect that this strategy produces is extremely
078: * beneficial: when someone adds a user to a wiki group, that user
079: * <em>immediately</em> gains the privileges associated with that
080: * group; he or she does not need to re-authenticate.
081: * </p>
082: * <p>In addition to methods for examining individual <code>WikiSession</code>
083: * objects, this class also contains a number of static methods for
084: * managing WikiSessions for an entire wiki. These methods allow callers
085: * to find, query and remove WikiSession objects, and
086: * to obtain a list of the current wiki session users.</p>
087: * <p>WikiSession encloses a protected static class, {@link SessionMonitor},
088: * to keep track of WikiSessions registered with each wiki.</p>
089: * @author Andrew R. Jaquith
090: */
091: public final class WikiSession implements WikiEventListener {
092:
093: /** An anonymous user's session status. */
094: public static final String ANONYMOUS = "anonymous";
095:
096: /** An asserted user's session status. */
097: public static final String ASSERTED = "asserted";
098:
099: /** An authenticated user's session status. */
100: public static final String AUTHENTICATED = "authenticated";
101:
102: private static final int ONE = 48;
103:
104: private static final int NINE = 57;
105:
106: private static final int DOT = 46;
107:
108: private static final Logger log = Logger
109: .getLogger(WikiSession.class);
110:
111: private static final String ALL = "*";
112:
113: private static ThreadLocal c_guestSession = new ThreadLocal();
114:
115: private final Subject m_subject = new Subject();
116:
117: private final Map m_messages = new HashMap();
118:
119: private String m_cachedCookieIdentity = null;
120:
121: private String m_cachedRemoteUser = null;
122:
123: private Principal m_cachedUserPrincipal = null;
124:
125: /** The WikiEngine that created this session. */
126: private WikiEngine m_engine = null;
127:
128: private boolean m_isNew = true;
129:
130: private String m_status = ANONYMOUS;
131:
132: private Principal m_userPrincipal = WikiPrincipal.GUEST;
133:
134: private Principal m_loginPrincipal = WikiPrincipal.GUEST;
135:
136: private Locale m_cachedLocale = Locale.getDefault();
137:
138: /**
139: * Returns <code>true</code> if one of this WikiSession's user Principals
140: * can be shown to belong to a particular wiki group. If the user is
141: * not authenticated, this method will always return <code>false</code>.
142: * @param group the group to test
143: * @return the result
144: */
145: protected final boolean isInGroup(Group group) {
146: Principal[] principals = getPrincipals();
147: for (int i = 0; i < principals.length; i++) {
148: if (isAuthenticated() && group.isMember(principals[i])) {
149: return true;
150: }
151: }
152: return false;
153: }
154:
155: /**
156: * Returns <code>true</code> if the wiki session is newly initialized.
157: *
158: * @return True, if this is a new session.
159: */
160: protected final boolean isNew() {
161: return m_isNew;
162: }
163:
164: /**
165: * Sets the status of this wiki session.
166: * @param isNew whether this session should be considered "new".
167: */
168: protected final void setNew(boolean isNew) {
169: m_isNew = isNew;
170: }
171:
172: /**
173: * Private constructor to prevent WikiSession from being instantiated
174: * directly.
175: */
176: private WikiSession() {
177: }
178:
179: /**
180: * Returns <code>true</code> if the user is considered asserted via
181: * a session cookie; that is, the Subject contains the Principal
182: * Role.ASSERTED.
183: * @return Returns <code>true</code> if the user is asserted
184: */
185: public final boolean isAsserted() {
186: return m_subject.getPrincipals().contains(Role.ASSERTED);
187: }
188:
189: /**
190: * Returns the authentication status of the user's session. The user is
191: * considered authenticated if the Subject contains the Principal
192: * Role.AUTHENTICATED. If this method determines that an earlier
193: * LoginModule did not inject Role.AUTHENTICATED, it will inject one
194: * if the user is not anonymous <em>and</em> not asserted.
195: * @return Returns <code>true</code> if the user is authenticated
196: */
197: public final boolean isAuthenticated() {
198: // If Role.AUTHENTICATED is in principals set, always return true.
199: if (m_subject.getPrincipals().contains(Role.AUTHENTICATED)) {
200: return true;
201: }
202:
203: // With non-JSPWiki LoginModules, the role may not be there, so
204: // we need to add it if the user really is authenticated.
205: if (!isAnonymous() && !isAsserted()) {
206: // Inject AUTHENTICATED role
207: m_subject.getPrincipals().add(Role.AUTHENTICATED);
208: return true;
209: }
210:
211: return false;
212: }
213:
214: /**
215: * <p>Determines whether the current session is anonymous. This will be
216: * true if any of these conditions are true:</p>
217: * <ul>
218: * <li>The session's Principal set contains
219: * {@link com.ecyrd.jspwiki.auth.authorize.Role#ANONYMOUS}</li>
220: * <li>The session's Principal set contains
221: * {@link com.ecyrd.jspwiki.auth.WikiPrincipal#GUEST}</li>
222: * <li>The Principal returned by {@link #getUserPrincipal()} evaluates
223: * to an IP address.</li>
224: * </ul>
225: * <p>The criteria above are listed in the order in which they are
226: * evaluated.</p>
227: * @return whether the current user's identity is equivalent to an IP
228: * address
229: */
230: public final boolean isAnonymous() {
231: Set principals = m_subject.getPrincipals();
232: return principals.contains(Role.ANONYMOUS)
233: || principals.contains(WikiPrincipal.GUEST)
234: || isIPV4Address(getUserPrincipal().getName());
235: }
236:
237: /**
238: * Creates and returns a new login context for this wiki session.
239: * This method is called by
240: * {@link com.ecyrd.jspwiki.auth.AuthenticationManager}.
241: * @param application the name of the application
242: * @param handler the callback handler
243: * @return A new login context
244: * @throws LoginException If the login context cannot be created
245: */
246: public final LoginContext getLoginContext(String application,
247: CallbackHandler handler) throws LoginException {
248: return new LoginContext(application, m_subject, handler);
249: }
250:
251: /**
252: * <p> Returns the Principal used to log in to an authenticated session. The
253: * login principal is determined by examining the Subject's Principal set
254: * for PrincipalWrappers or WikiPrincipals with type designator
255: * <code>LOGIN_NAME</code>; the first one found is the login principal.
256: * If one is not found, this method returns the first principal that isn't
257: * of type Role or GroupPrincipal. If neither of these conditions hold, this method returns
258: * {@link com.ecyrd.jspwiki.auth.WikiPrincipal#GUEST}.
259: * @return the login Principal. If it is a PrincipalWrapper containing an
260: * externally-provided Principal, the object returned is the Principal, not
261: * the wrapper around it.
262: */
263: public final Principal getLoginPrincipal() {
264: if (m_loginPrincipal instanceof PrincipalWrapper) {
265: return ((PrincipalWrapper) m_loginPrincipal).getPrincipal();
266: }
267: return m_loginPrincipal;
268: }
269:
270: /**
271: * <p>Returns the primary user Principal associated with this session. The
272: * primary user principal is determined as follows:</p> <ol> <li>If the
273: * Subject's Principal set contains WikiPrincipals, the first WikiPrincipal
274: * with type designator <code>WIKI_NAME</code> or (alternatively)
275: * <code>FULL_NAME</code> is the primary Principal.</li>
276: * <li>For all other cases, the first Principal in the Subject's principal
277: * collection that that isn't of type Role or GroupPrincipal is the primary.</li>
278: * </ol>
279: * If no primary user Principal is found, this method returns
280: * {@link com.ecyrd.jspwiki.auth.WikiPrincipal#GUEST}.
281: * @return the primary user Principal
282: */
283: public final Principal getUserPrincipal() {
284: if (m_loginPrincipal instanceof PrincipalWrapper) {
285: return ((PrincipalWrapper) m_userPrincipal).getPrincipal();
286: }
287: return m_userPrincipal;
288: }
289:
290: /**
291: * Returns a cached Locale object for this user. It's better to use
292: * WikiContext's corresponding getBundle() method, since that will actually
293: * react if the user changes the locale in the middle, but if that's not
294: * available (or, for some reason, you need the speed), this method can
295: * also be used. The Locale expires when the WikiSession expires, and
296: * currently there is no way to reset the Locale.
297: *
298: * @return A cached Locale object
299: * @since 2.5.96
300: */
301: public final Locale getLocale() {
302: return m_cachedLocale;
303: }
304:
305: /**
306: * Adds a message to the generic list of messages associated with the
307: * session. These messages retain their order of insertion and remain until
308: * the {@link #clearMessages()} method is called.
309: * @param message the message to add; if <code>null</code> it is ignored.
310: */
311: public final void addMessage(String message) {
312: addMessage(ALL, message);
313: }
314:
315: /**
316: * Adds a message to the specific set of messages associated with the
317: * session. These messages retain their order of insertion and remain until
318: * the {@link #clearMessages()} method is called.
319: * @param topic the topic to associate the message to;
320: * @param message the message to add
321: */
322: public final void addMessage(String topic, String message) {
323: if (topic == null) {
324: throw new IllegalArgumentException(
325: "addMessage: topic cannot be null.");
326: }
327: if (message == null) {
328: message = "";
329: }
330: Set messages = (Set) m_messages.get(topic);
331: if (messages == null) {
332: messages = new LinkedHashSet();
333: m_messages.put(topic, messages);
334: }
335: messages.add(message);
336: }
337:
338: /**
339: * Clears all messages associated with this session.
340: */
341: public final void clearMessages() {
342: m_messages.clear();
343: }
344:
345: /**
346: * Clears all messages associated with a session topic.
347: * @param topic the topic whose messages should be cleared.
348: */
349: public final void clearMessages(String topic) {
350: Set messages = (Set) m_messages.get(topic);
351: if (messages != null) {
352: m_messages.clear();
353: }
354: }
355:
356: /**
357: * Returns all generic messages associated with this session.
358: * The messages stored with the session persist throughout the
359: * session unless they have been reset with {@link #clearMessages()}.
360: * @return the current messsages.
361: */
362: public final String[] getMessages() {
363: return getMessages(ALL);
364: }
365:
366: /**
367: * Returns all messages associated with a session topic.
368: * The messages stored with the session persist throughout the
369: * session unless they have been reset with {@link #clearMessages(String)}.
370: * @return the current messsages.
371: * @param topic The topic
372: */
373: public final String[] getMessages(String topic) {
374: Set messages = (Set) m_messages.get(topic);
375: if (messages == null || messages.size() == 0) {
376: return new String[0];
377: }
378: return (String[]) messages.toArray(new String[messages.size()]);
379: }
380:
381: /**
382: * Returns all user Principals associated with this session. User principals
383: * are those in the Subject's principal collection that aren't of type Role or
384: * of type GroupPrincipal. This is a defensive copy.
385: * @return Returns the user principal
386: * @see com.ecyrd.jspwiki.auth.AuthenticationManager#isUserPrincipal(Principal)
387: */
388: public final Principal[] getPrincipals() {
389: ArrayList principals = new ArrayList();
390:
391: // Take the first non Role as the main Principal
392: for (Iterator it = m_subject.getPrincipals().iterator(); it
393: .hasNext();) {
394: Principal principal = (Principal) it.next();
395: if (AuthenticationManager.isUserPrincipal(principal)) {
396: principals.add(principal);
397: }
398: }
399:
400: return (Principal[]) principals
401: .toArray(new Principal[principals.size()]);
402: }
403:
404: /**
405: * Returns an array of Principal objects that represents the groups and
406: * roles that the user associated with a WikiSession possesses. The array is
407: * built by iterating through the Subject's Principal set and extracting all
408: * Role and GroupPrincipal objects into a list. The list is returned as an
409: * array sorted in the natural order implied by each Principal's
410: * <code>getName</code> method. Note that this method does <em>not</em>
411: * consult the external Authorizer or GroupManager; it relies on the
412: * Principals that have been injected into the user's Subject at login time,
413: * or after group creation/modification/deletion.
414: * @return an array of Principal objects corresponding to the roles the
415: * Subject possesses
416: */
417: public final Principal[] getRoles() {
418: Set roles = new HashSet();
419:
420: // Add all of the Roles possessed by the Subject directly
421: roles.addAll(m_subject.getPrincipals(Role.class));
422:
423: // Add all of the GroupPrincipals possessed by the Subject directly
424: roles.addAll(m_subject.getPrincipals(GroupPrincipal.class));
425:
426: // Return a defensive copy
427: Principal[] roleArray = (Principal[]) roles
428: .toArray(new Principal[roles.size()]);
429: Arrays.sort(roleArray, WikiPrincipal.COMPARATOR);
430: return roleArray;
431: }
432:
433: /**
434: * Removes the wiki session associated with the user's HTTP request
435: * from the cache of wiki sessions, typically as part of a logout
436: * process.
437: * @param engine the wiki engine
438: * @param request the users's HTTP request
439: */
440: public static final void removeWikiSession(WikiEngine engine,
441: HttpServletRequest request) {
442: if (engine == null || request == null) {
443: throw new IllegalArgumentException(
444: "Request or engine cannot be null.");
445: }
446: SessionMonitor monitor = SessionMonitor.getInstance(engine);
447: monitor.remove(request.getSession());
448: }
449:
450: /**
451: * Returns <code>true</code> if the WikiSession's Subject
452: * possess a supplied Principal. This method eliminates the need
453: * to externally request and inspect the JAAS subject.
454: * @param principal the Principal to test
455: * @return the result
456: */
457: public final boolean hasPrincipal(Principal principal) {
458: return m_subject.getPrincipals().contains(principal);
459:
460: }
461:
462: /**
463: * Listens for WikiEvents generated by source objects such as the
464: * GroupManager. This method adds Principals to the private Subject managed
465: * by the WikiSession.
466: * @see com.ecyrd.jspwiki.event.WikiEventListener#actionPerformed(com.ecyrd.jspwiki.event.WikiEvent)
467: * {@inheritDoc}
468: */
469: public final void actionPerformed(WikiEvent event) {
470: if (event instanceof WikiSecurityEvent) {
471: WikiSecurityEvent e = (WikiSecurityEvent) event;
472: if (e.getTarget() != null) {
473: switch (e.getType()) {
474: case WikiSecurityEvent.GROUP_ADD: {
475: Group group = (Group) e.getTarget();
476: if (isInGroup(group)) {
477: m_subject.getPrincipals().add(
478: group.getPrincipal());
479: }
480: break;
481: }
482: case WikiSecurityEvent.GROUP_REMOVE: {
483: Group group = (Group) e.getTarget();
484: if (m_subject.getPrincipals().contains(
485: group.getPrincipal())) {
486: m_subject.getPrincipals().remove(
487: group.getPrincipal());
488: }
489: break;
490: }
491: case WikiSecurityEvent.GROUP_CLEAR_GROUPS: {
492: m_subject
493: .getPrincipals()
494: .removeAll(
495: m_subject
496: .getPrincipals(GroupPrincipal.class));
497: break;
498: }
499: case WikiSecurityEvent.LOGIN_INITIATED: {
500: WikiSession target = (WikiSession) e.getTarget();
501: if (this .equals(target)) {
502: updatePrincipals();
503: }
504: break;
505: }
506: case WikiSecurityEvent.LOGIN_ASSERTED: {
507: WikiSession target = (WikiSession) e.getTarget();
508: if (this .equals(target)) {
509: m_status = ASSERTED;
510: }
511: break;
512: }
513: case WikiSecurityEvent.LOGIN_AUTHENTICATED: {
514: WikiSession target = (WikiSession) e.getTarget();
515: if (this .equals(target)) {
516: m_status = AUTHENTICATED;
517: injectUserProfilePrincipals(); // Add principals for the user profile
518: injectRolePrincipals(); // Inject role and group principals
519: updatePrincipals();
520: }
521: break;
522: }
523: case WikiSecurityEvent.PROFILE_SAVE: {
524: WikiSession source = (WikiSession) e.getSource();
525: if (this .equals(source)) {
526: injectUserProfilePrincipals(); // Add principals for the user profile
527: updatePrincipals();
528: }
529: break;
530: }
531: case WikiSecurityEvent.PROFILE_NAME_CHANGED: {
532: // Refresh user principals based on new user profile
533: WikiSession source = (WikiSession) e.getSource();
534: if (this .equals(source)) {
535: // To prepare for refresh, set the new full name as the primary principal
536: UserProfile[] profiles = (UserProfile[]) e
537: .getTarget();
538: UserProfile newProfile = profiles[1];
539: if (newProfile.getFullname() == null) {
540: throw new IllegalStateException(
541: "User profile FullName cannot be null.");
542: }
543: m_userPrincipal = new WikiPrincipal(newProfile
544: .getFullname());
545:
546: // Refresh principals for the user profile
547: injectUserProfilePrincipals();
548: updatePrincipals();
549: }
550: break;
551: }
552:
553: //
554: // No action, if the event is not recognized.
555: //
556: default:
557: break;
558: }
559: }
560: }
561: }
562:
563: /**
564: * Invalidates the WikiSession and resets its Subject's
565: * Principals to the equivalent of a "guest session".
566: */
567: public final void invalidate() {
568: m_subject.getPrincipals().clear();
569: m_subject.getPrincipals().add(WikiPrincipal.GUEST);
570: m_subject.getPrincipals().add(Role.ANONYMOUS);
571: m_subject.getPrincipals().add(Role.ALL);
572: m_cachedCookieIdentity = null;
573: m_cachedRemoteUser = null;
574: m_cachedUserPrincipal = null;
575: }
576:
577: /**
578: * Returns whether the HTTP servlet container's authentication status has
579: * changed. Used to detect whether the container has logged in a user since
580: * the last call to this function. This method is stateful. After calling
581: * this function, the cached values are set to those in the current request.
582: * If the servlet request is <code>null</code>, this method always returns
583: * <code>false</code>. Note that once a user authenticates, the container
584: * status for the session will <em>never</em> change again, unless the
585: * session is invalidated by timeout or logout.
586: * @param request the servlet request
587: * @return <code>true</code> if the status has changed, <code>false</code>
588: * otherwise
589: */
590: protected final boolean isContainerStatusChanged(
591: HttpServletRequest request) {
592: if (request == null || m_status.equals(AUTHENTICATED)) {
593: return false;
594: }
595:
596: String remoteUser = request.getRemoteUser();
597: Principal userPrincipal = request.getUserPrincipal();
598: String cookieIdentity = CookieAssertionLoginModule
599: .getUserCookie(request);
600: boolean changed = false;
601:
602: // If request contains non-null remote user, update cached value if changed
603: if (remoteUser != null
604: && !remoteUser.equals(m_cachedRemoteUser)) {
605: m_cachedRemoteUser = remoteUser;
606: log.info("Remote user changed to " + remoteUser);
607: changed = true;
608: }
609:
610: // If request contains non-null user principal, updated cached value if changed
611: if (userPrincipal != null
612: && !userPrincipal.equals(m_cachedUserPrincipal)) {
613: m_cachedUserPrincipal = userPrincipal;
614: log.info("User principal changed to "
615: + userPrincipal.getName());
616: changed = true;
617: }
618:
619: // If cookie identity changed (to a different value or back to null), update cache
620: if ((cookieIdentity != null && !cookieIdentity
621: .equals(m_cachedCookieIdentity))
622: || (cookieIdentity == null && m_cachedCookieIdentity != null)) {
623: m_cachedCookieIdentity = cookieIdentity;
624: log.info("Cookie changed to " + cookieIdentity);
625: changed = true;
626: }
627: return changed;
628: }
629:
630: /**
631: * Injects GroupPrincipal and Role objects into the user's Principal set
632: * based on the groups and roles the user belongs to.
633: * For Roles, the algorithm first calls the
634: * {@link Authorizer#getRoles()} to obtain the array of
635: * Principals the authorizer knows about. Then, the method
636: * {@link Authorizer#isUserInRole(WikiSession, Principal)} is
637: * called for each Principal. If the user possesses the role,
638: * an equivalent role Principal is injected into the user's
639: * principal set.
640: * Reloads user Principals into the suppplied WikiSession's Subject.
641: * Existing Role principals are preserved; all other Principal
642: * types are flushed and replaced by those returned by
643: * {@link com.ecyrd.jspwiki.auth.user.UserDatabase#getPrincipals(String)}.
644: * This method should generally be called after a user's {@link com.ecyrd.jspwiki.auth.user.UserProfile}
645: * is saved. If the wiki session is null, or there is no matching user profile, the
646: * method returns silently.
647: */
648: protected final void injectRolePrincipals() {
649: // Get the GroupManager and test for each Group
650: GroupManager manager = m_engine.getGroupManager();
651: Principal[] groups = manager.getRoles();
652: for (int i = 0; i < groups.length; i++) {
653: if (manager.isUserInRole(this , groups[i])) {
654: m_subject.getPrincipals().add(groups[i]);
655: }
656: }
657:
658: // Get the authorizer's known roles, then test for each
659: try {
660: Authorizer authorizer = m_engine.getAuthorizationManager()
661: .getAuthorizer();
662: Principal[] roles = authorizer.getRoles();
663: for (int i = 0; i < roles.length; i++) {
664: Principal role = roles[i];
665: if (authorizer.isUserInRole(this , role)) {
666: String roleName = role.getName();
667: if (!Role.isReservedName(roleName)) {
668: m_subject.getPrincipals().add(
669: new Role(roleName));
670: }
671: }
672: }
673: } catch (WikiException e) {
674: log.error("Could not refresh role principals: "
675: + e.getMessage());
676: }
677: }
678:
679: /**
680: * Adds Principal objects to the Subject that correspond to the
681: * logged-in user's profile attributes for the wiki name, full name
682: * and login name. These Principals will be WikiPrincipals, and they
683: * will replace all other WikiPrincipals in the Subject. <em>Note:
684: * this method is never called during anonymous or asserted sessions.</em>
685: */
686: protected final void injectUserProfilePrincipals() {
687: // Copy all Role and GroupPrincipal principals into a temporary cache
688: Set oldPrincipals = m_subject.getPrincipals();
689: Set newPrincipals = new HashSet();
690: for (Iterator it = oldPrincipals.iterator(); it.hasNext();) {
691: Principal principal = (Principal) it.next();
692: if (AuthenticationManager.isRolePrincipal(principal)) {
693: newPrincipals.add(principal);
694: }
695: }
696: String searchId = getUserPrincipal().getName();
697: if (searchId == null) {
698: // Oh dear, this wasn't an authenticated user after all
699: log
700: .info("Refresh principals failed because WikiSession had no user Principal; maybe not logged in?");
701: return;
702: }
703:
704: // Look up the user and go get the new Principals
705: UserDatabase database = m_engine.getUserManager()
706: .getUserDatabase();
707: if (database == null) {
708: throw new IllegalStateException(
709: "User database cannot be null.");
710: }
711: try {
712: UserProfile profile = database.find(searchId);
713: Principal[] principals = database.getPrincipals(profile
714: .getLoginName());
715: for (int i = 0; i < principals.length; i++) {
716: Principal principal = principals[i];
717: newPrincipals.add(principal);
718: }
719:
720: // Replace the Subject's old Principals with the new ones
721: oldPrincipals.clear();
722: oldPrincipals.addAll(newPrincipals);
723: } catch (NoSuchPrincipalException e) {
724: // We will get here if the user has a principal but not a profile
725: // For example, it's a container-managed user who hasn't set up a profile yet
726: log
727: .warn("User profile '"
728: + searchId
729: + "' not found. This is normal for container-auth users who haven't set up a profile yet.");
730: }
731: }
732:
733: /**
734: * Updates the internally cached principals returned by {@link #getUserPrincipal()} and
735: * {@link #getLoginPrincipal()}. This method is called when the WikiSession receives
736: * the {@link com.ecyrd.jspwiki.event.WikiSecurityEvent#LOGIN_INITIATED} message.
737: */
738: protected final void updatePrincipals() {
739: Set principals = m_subject.getPrincipals();
740: m_loginPrincipal = null;
741: m_userPrincipal = null;
742: Principal wikinamePrincipal = null;
743: Principal fullnamePrincipal = null;
744: Principal otherPrincipal = null;
745:
746: for (Iterator it = principals.iterator(); it.hasNext();) {
747: Principal currentPrincipal = (Principal) it.next();
748: if (!(currentPrincipal instanceof Role || currentPrincipal instanceof GroupPrincipal)) {
749: // For login principal, take the first PrincipalWrapper or WikiPrincipal of type LOGIN_NAME
750: // For user principal take the first WikiPrincipal of type WIKI_NAME
751: if (currentPrincipal instanceof PrincipalWrapper) {
752: m_loginPrincipal = currentPrincipal;
753: } else if (currentPrincipal instanceof WikiPrincipal) {
754: WikiPrincipal wp = (WikiPrincipal) currentPrincipal;
755: if (wp.getType().equals(WikiPrincipal.LOGIN_NAME)) {
756: m_loginPrincipal = wp;
757: } else if (wp.getType().equals(
758: WikiPrincipal.WIKI_NAME)) {
759: wikinamePrincipal = currentPrincipal;
760: m_userPrincipal = wp;
761: } else if (wp.getType().equals(
762: WikiPrincipal.FULL_NAME)) {
763: fullnamePrincipal = currentPrincipal;
764: } else {
765: otherPrincipal = currentPrincipal;
766: }
767: } else if (otherPrincipal == null) {
768: otherPrincipal = currentPrincipal;
769: }
770: }
771: }
772: if (otherPrincipal == null) {
773: otherPrincipal = WikiPrincipal.GUEST;
774: }
775:
776: // If no login principal, use wiki name, full name or other principal (in that order)
777: if (m_loginPrincipal == null) {
778: m_loginPrincipal = wikinamePrincipal;
779: if (m_loginPrincipal == null) {
780: m_loginPrincipal = fullnamePrincipal;
781: if (m_loginPrincipal == null) {
782: m_loginPrincipal = otherPrincipal;
783: }
784: }
785: }
786:
787: // If no user principal, use wiki name, full name or login principal (in that order)
788: if (m_userPrincipal == null) {
789: m_userPrincipal = wikinamePrincipal;
790: if (m_userPrincipal == null) {
791: m_userPrincipal = fullnamePrincipal;
792: if (m_userPrincipal == null) {
793: m_userPrincipal = m_loginPrincipal;
794: }
795: }
796: }
797: }
798:
799: /**
800: * <p>Returns the status of the wiki session as a text string. Valid values are:</p>
801: * <ul>
802: * <li>{@link #AUTHENTICATED}</li>
803: * <li>{@link #ASSERTED}</li>
804: * <li>{@link #ANONYMOUS}</li>
805: * </ul>
806: * @return the user's session status
807: */
808: public final String getStatus() {
809: return m_status;
810: }
811:
812: /**
813: * <p>Static factory method that returns the WikiSession object associated with
814: * the current HTTP request. This method looks up the associated HttpSession
815: * in an internal WeakHashMap and attempts to retrieve the WikiSession. If
816: * not found, one is created. This method is guaranteed to always return a
817: * WikiSession, although the authentication status is unpredictable until
818: * the user attempts to log in. If the servlet request parameter is
819: * <code>null</code>, a synthetic {@link #guestSession(WikiEngine)}is returned.</p>
820: * <p>When a session is created, this method attaches a WikiEventListener
821: * to the GroupManager so that changes to groups are detected automatically.</p>
822: * @param engine the wiki engine
823: * @param request the servlet request object
824: * @return the existing (or newly created) wiki session
825: */
826: public static final WikiSession getWikiSession(WikiEngine engine,
827: HttpServletRequest request) {
828: // If request is null, return guest session
829: if (request == null) {
830: if (log.isDebugEnabled()) {
831: log
832: .debug("Looking up WikiSession for NULL HttpRequest: returning guestSession()");
833: }
834: return staticGuestSession(engine);
835: }
836:
837: // Look for a WikiSession associated with the user's Http Session
838: // and create one if it isn't there yet.
839: HttpSession session = request.getSession();
840: SessionMonitor monitor = SessionMonitor.getInstance(engine);
841: WikiSession wikiSession = monitor.find(session);
842:
843: // Attach reference to wiki engine
844: wikiSession.m_engine = engine;
845:
846: wikiSession.m_cachedLocale = request.getLocale();
847:
848: return wikiSession;
849: }
850:
851: /**
852: * Static factory method that creates a new "guest" session containing a single
853: * user Principal {@link com.ecyrd.jspwiki.auth.WikiPrincipal#GUEST},
854: * plus the role principals {@link Role#ALL} and
855: * {@link Role#ANONYMOUS}. This method also adds the session as a listener
856: * for GroupManager, AuthenticationManager and UserManager events.
857: * @param engine the wiki engine
858: * @return the guest wiki session
859: */
860: public static final WikiSession guestSession(WikiEngine engine) {
861: WikiSession session = new WikiSession();
862: session.m_engine = engine;
863: session.invalidate();
864:
865: // Add the session as listener for GroupManager, AuthManager, UserManager events
866: GroupManager groupMgr = engine.getGroupManager();
867: AuthenticationManager authMgr = engine
868: .getAuthenticationManager();
869: UserManager userMgr = engine.getUserManager();
870: groupMgr.addWikiEventListener(session);
871: authMgr.addWikiEventListener(session);
872: userMgr.addWikiEventListener(session);
873:
874: return session;
875: }
876:
877: /**
878: * Returns a static guest session, which is available for this
879: * thread only. This guest session is used internally whenever
880: * there is no HttpServletRequest involved, but the request is
881: * done e.g. when embedding JSPWiki code.
882: *
883: * @param engine WikiEngine for this session
884: * @return A static WikiSession which is shared by all in this
885: * same Thread.
886: */
887: // FIXME: Should really use WeakReferences to clean away unused sessions.
888: private static WikiSession staticGuestSession(WikiEngine engine) {
889: WikiSession session = (WikiSession) c_guestSession.get();
890:
891: if (session == null) {
892: session = guestSession(engine);
893:
894: c_guestSession.set(session);
895: }
896:
897: return session;
898: }
899:
900: /**
901: * Returns the total number of active wiki sessions for a
902: * particular wiki. This method delegates to the wiki's
903: * {@link SessionMonitor#sessions()} method.
904: * @param engine the wiki session
905: * @return the number of sessions
906: */
907: public static final int sessions(WikiEngine engine) {
908: SessionMonitor monitor = SessionMonitor.getInstance(engine);
909: return monitor.sessions();
910: }
911:
912: /**
913: * Returns Principals representing the current users known
914: * to a particular wiki. Each Principal will correspond to the
915: * value returned by each WikiSession's {@link #getUserPrincipal()}
916: * method. This method delegates to {@link SessionMonitor#userPrincipals()}.
917: * @param engine the wiki engine
918: * @return an array of Principal objects, sorted by name
919: */
920: public static final Principal[] userPrincipals(WikiEngine engine) {
921: SessionMonitor monitor = SessionMonitor.getInstance(engine);
922: return monitor.userPrincipals();
923: }
924:
925: /**
926: * Wrapper for
927: * {@link javax.security.auth.Subject#doAsPrivileged(Subject, java.security.PrivilegedExceptionAction, java.security.AccessControlContext)}
928: * that executes an action with the privileges posssessed by a
929: * WikiSession's Subject. The action executes with a <code>null</code>
930: * AccessControlContext, which has the effect of running it "cleanly"
931: * without the AccessControlContexts of the caller.
932: * @param session the wiki session
933: * @param action the privileged action
934: * @return the result of the privileged action; may be <code>null</code>
935: * @throws java.security.AccessControlException if the action is not permitted
936: * by the security policy
937: */
938: public static final Object doPrivileged(WikiSession session,
939: PrivilegedAction action) throws AccessControlException {
940: return Subject.doAsPrivileged(session.m_subject, action, null);
941: }
942:
943: /**
944: * Verifies whether a String represents an IPv4 address. The algorithm is
945: * extremely efficient and does not allocate any objects.
946: * @param name the address to test
947: * @return the result
948: */
949: protected static final boolean isIPV4Address(String name) {
950: if (name.charAt(0) == DOT
951: || name.charAt(name.length() - 1) == DOT) {
952: return false;
953: }
954:
955: int[] addr = new int[] { 0, 0, 0, 0 };
956: int currentOctet = 0;
957: for (int i = 0; i < name.length(); i++) {
958: int ch = name.charAt(i);
959: boolean isDigit = ch >= ONE && ch <= NINE;
960: boolean isDot = ch == DOT;
961: if (!isDigit && !isDot) {
962: return false;
963: }
964: if (isDigit) {
965: addr[currentOctet] = 10 * addr[currentOctet]
966: + (ch - ONE);
967: if (addr[currentOctet] > 255) {
968: return false;
969: }
970: } else if (name.charAt(i - 1) == DOT) {
971: return false;
972: } else {
973: currentOctet++;
974: }
975: }
976: return currentOctet == 3;
977: }
978:
979: }
|