Source code: com/RuntimeCollective/webapps/tag/CheckLogonTag.java
1 /* $Header: /home/CVS/rjp/src/com/RuntimeCollective/webapps/tag/CheckLogonTag.java,v 1.55 2003/10/13 15:42:44 fabrice Exp $
2 * $Revision: 1.55 $
3 * $Date: 2003/10/13 15:42:44 $
4 *
5 * ====================================================================
6 *
7 * Josephine : http://www.runtime-collective.com/josephine/index.html
8 *
9 * Copyright (C) 2003 Runtime Collective
10 *
11 * This product includes software developed by the
12 * Apache Software Foundation (http://www.apache.org/).
13 *
14 * This library is free software; you can redistribute it and/or
15 * modify it under the terms of the GNU Lesser General Public
16 * License as published by the Free Software Foundation; either
17 * version 2.1 of the License, or (at your option) any later version.
18 *
19 * This library is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
22 * Lesser General Public License for more details.
23 *
24 * You should have received a copy of the GNU Lesser General Public
25 * License along with this library; if not, write to the Free Software
26 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
27 *
28 */
29
30 package com.RuntimeCollective.webapps.tag;
31
32 import com.RuntimeCollective.webapps.bean.User;
33 import com.RuntimeCollective.webapps.bean.TrackedUser;
34 import com.RuntimeCollective.webapps.bean.UserGroup;
35 import com.RuntimeCollective.webapps.bean.UserGroupType;
36 import com.RuntimeCollective.webapps.RuntimeParameters;
37 import com.RuntimeCollective.webapps.bean.LoginCookie;
38 import com.RuntimeCollective.webapps.bean.SearchResult;
39 import com.RuntimeCollective.webapps.IndexedEntityBeanStore;
40 import com.RuntimeCollective.webapps.SaveRequestURLFilter;
41 import com.RuntimeCollective.permission.SecurityConstants;
42
43 import java.io.IOException;
44 import java.lang.reflect.Method;
45 import java.util.ArrayList;
46 import java.util.Date;
47 import java.util.Enumeration;
48 import java.util.Iterator;
49 import java.util.Set;
50 import java.util.StringTokenizer;
51 import javax.servlet.http.HttpSession;
52 import javax.servlet.http.HttpServletRequest;
53 import javax.servlet.jsp.JspException;
54 import javax.servlet.jsp.JspWriter;
55 import javax.servlet.jsp.PageContext;
56 import javax.servlet.jsp.tagext.TagSupport;
57 import javax.servlet.ServletContext;
58
59 import org.apache.struts.action.Action;
60 import org.apache.struts.action.ActionError;
61 import org.apache.struts.action.ActionErrors;
62 import org.apache.struts.util.MessageResources;
63
64
65 /**
66 * Check for a valid User logged on in the current session under
67 * the key <code>RuntimeParameters.getParam("logonUserKey")</code>.
68 * If there is no such user, forward control to the logon page.
69 * <p>
70 * <b>Important:</b> This tag must be placed on a jsp page before anything is written to the response. It's safest to always put it at the top.
71 * <p>This tag takes the following optional parameters:
72 * <ul>
73 * <li><code>page</code> - the page to go to if the user is not logged in (defaults to /logon.jsp)
74 * <li><code>group</code> - if set then the tag will only admit users if they are logged in, and are members of the UserGroup with this name.
75 * <li><code>groups</code> - if set then the tag will only admit users if they are logged in, and are members of any UserGroups with these names.
76 * <li><code>groupType</code> - if set then the tag will only admit users if they are logged in, and are members of any group which has this UserGroupType.
77 * <li><code>role</code> - if set then the tag will only allow users in if they are logged in, and have this role.You may seperate roles with commas e.g. "0,1,2" - this will allow users in if they have ANY of the specified roles.For example, inserting
78 * <pre><code><%@ taglib uri="/WEB-INF/runtime-struts.tld" prefix="rs" %>
79 * <rs:checkLogon role="0" page="home.jsp"/></code></pre>
80 * into a jsp page will check that the user is logged in, with role 0, and if not will forward them to the home page.
81 * <p>An example of how to use the <code>groups</code> parameter to allow only members of the Administrators or Editors group:</p>
82 * <p><code><rs:belongsTo groups='<%= new String[]{"Administrators", "Editors"} %>'></code></p>
83 * </ul>
84 *
85 * @see com.RuntimeCollective.webapps.bean.User
86 * @see com.RuntimeCollective.webapps.bean.UserGroup
87 * @see com.RuntimeCollective.webapps.bean.UserGroupType
88 *
89 * @version $Id: CheckLogonTag.java,v 1.55 2003/10/13 15:42:44 fabrice Exp $
90 */
91 public class CheckLogonTag extends TagSupport {
92
93 /** Where to put the list of group names on the request when the user is not in the groups specified. */
94 public static final String LIST_GROUP_NAMES_KEY = "ugpermrule_uncleared_group_name_list";
95
96 /** The key of the session-scope bean we look for. */
97 protected static String name = RuntimeParameters.getParam("logonUserKey");
98
99 /** The page to which we should forward for the user to log on. */
100 protected String page = "/logon.jsp";
101
102 /** The role(s) that the user must have in order to access this page. */
103 protected String role = null;
104
105 /** The name of the group (or names, seperated by commas) that the user must be a member of [Optional] */
106 private String iGroup;
107
108 /** The names of the groups, at least one of which the user must be a member of [Optional] */
109 private String[] iGroups;
110
111 /** The user must be a member of at least one group that has this group type [Optional] */
112 private String iGroupType;
113
114 /** The cookie name to look for. */
115 protected String cookie = LoginCookie.COOKIE_NAME;
116
117 /** Return the forward page. */
118 public String getPage() {
119 return (this.page);
120 }
121
122 /** Set the forward page.
123 * @param page The new forward page
124 */
125 public void setPage(String page) {
126 this.page = page;
127 }
128
129 /** Return the required role. */
130 public String getRole() { return this.role; }
131
132 /** Set the required role. */
133 public void setRole(String role) { this.role = role; }
134
135
136 /** Get the name (or names, seperated by commas) of the group that the user must be a member of [Optional] */
137 public String getGroup() { return iGroup; }
138
139 /** Set the name (or names, seperated by commas) of the group that the user must be a member of [Optional] */
140 public void setGroup(String group) { iGroup = group; }
141
142
143 /** Get the names of the groups, at least one of which the user must be a member of [Optional] */
144 public String[] getGroups() { return iGroups; }
145
146 /** Set the names of the groups, at least one of which the user must be a member of [Optional] */
147 public void setGroups(String[] groups) { iGroups = groups; }
148
149
150 /** Get the name of the group type containing a group that the user must be a member of [Optional] */
151 public String getGroupType() { return iGroupType; }
152
153 /** Set the name of the group type containing a group that the user must be a member of [Optional] */
154 public void setGroupType(String groupType) { iGroupType = groupType; }
155
156
157 /** Constructor **/
158 public CheckLogonTag() {
159 // initialised once and for all - actually this didn't seem to work, either
160 //name = RuntimeParameters.getParam("logonUserKey");
161 }
162
163
164 /** Defer our checking until the end of this tag is encountered.
165 * @exception JspException if a JSP exception has occurred
166 */
167 public int doStartTag() throws JspException {
168 return (SKIP_BODY);
169 }
170
171 /**
172 * Perform our logged-in user check by looking for the existence of
173 * a session scope bean under the specified name, using <code>getUser</code>.
174 * Control is forwarded using <code>forwardControl</code>, based on whether
175 * <code>getUser</code> returns a User or null.
176 * @exception JspException if a JSP exception has occurred
177 */
178 public int doEndTag() throws JspException {
179
180 // Get our user; will be null if there isn't one
181 User user = getLoggedOnUser((HttpServletRequest) pageContext.getRequest(), pageContext.getSession());
182
183 if (user!=null) {
184 RuntimeParameters.logDebug(this,"User found, id="+user.getId());
185 } else {
186 RuntimeParameters.logDebug(this,"No user found");
187 }
188
189 // Forward control based on whether this user is valid
190 return forwardControl( checkValid(user) );
191 }
192
193
194 /**
195 * CheckLogonTag (or a subclass)'s "getUser(HttpServletRequest request, HttpSession session)" method
196 */
197 private static Method getUserMethod = null;
198
199 /**
200 * Get the currently logged-on user from the given session.
201 * This uses the "getUser" method of CheckLogonTag, or a subclass defined in web.xml as "checkLogonTag".
202 */
203 public static User getLoggedOnUser(HttpServletRequest request, HttpSession session) throws JspException {
204
205 RuntimeParameters.logDebug("CheckLogonTag", "Getting logged on user");
206
207 // If "getUserMethod" is null, we need to find it
208 if (getUserMethod == null) {
209
210 // Find if we're using a subclass of CheckLogonTag, instead of this actual one
211 String logonTag = "";
212 // Get the subclass of CheckLogonTag we're using
213 if (RuntimeParameters.getParam("checkLogonTag") != null) {
214 logonTag = RuntimeParameters.getParam("checkLogonTag");
215 }
216
217 // param.checkLogonTag has not been set; use CheckLogonTag itself
218 if (logonTag.equals("")) {
219 return getUser(request, session);
220 }
221
222 // Call its "getUser" method, and store it in the static variable "getUserMethod"
223 try {
224 Class tagClass = Class.forName(logonTag);
225 getUserMethod = tagClass.getMethod("getUser", new Class[]{
226 Class.forName("javax.servlet.http.HttpServletRequest"),
227 Class.forName("javax.servlet.http.HttpSession")});
228
229 } catch (Exception e) {
230 throw new JspException("Problem finding the getUser method for "+logonTag+": "+e);
231 }
232 }
233
234 // Use the stored "getUserMethod" method
235 try {
236 User loggedUser = null;
237 Object obj = getUserMethod.invoke(null, new Object[]{ request, session });
238 if (obj != null) {
239 loggedUser = (User) obj;
240 loggedUser = (User) RuntimeParameters.getStore().get(User.class.getName(), loggedUser.getId());
241 }
242 return loggedUser;
243 } catch (Exception e) {
244 throw new JspException("Problem executing the getUser method: "+e);
245 }
246
247 }
248
249
250 /**
251 * Perform our logged-in user check by looking for the existence of
252 * a session scope bean under the specified name. If this bean is not
253 * present, check for a cookie called "LoginCookie.COOKIE_NAME".
254 * If no such cookie exists, the current page is stored on the Session with <code>PutReturnURLOnSession</code>
255 * and control is forwarded to the specified logon page.
256 * If we find a user from a cookie, set it on the session with <code>foundUserFromCookie</code>.
257 *
258 * @return Will return the logged-in User, if there is one, or <code>null</code> if not.
259 */
260 public static User getUser(HttpServletRequest request, HttpSession session) throws JspException {
261
262 // Is there a user logged on?
263 User user = null;
264
265 if (session!=null) {
266 // Check on the session
267 user = (User) session.getAttribute(name);
268 RuntimeParameters.logDebug("CheckLogonTag", "getUser: Session attribute '"+name+"' checked: "+user);
269 }
270
271 if (user == null) {
272 if (request != null) {
273 // Check the cookie
274 user = LoginCookie.getUser(request.getCookies());
275 RuntimeParameters.logDebug("CheckLogonTag", "getUser: LoginCookie checked: "+user);
276 // If we've found a user, put them on the session
277 if (user != null) {
278 foundUserFromCookie(user, request);
279 }
280 }
281 }
282
283 if (user == null) {
284 // last resort, try to get the AuthToken
285 // the session may have the AuthToken, but not the User object,
286 // for example after serialisation
287 Integer authTokenId = (Integer) session.getAttribute(SecurityConstants.AUTH_TOKEN);
288 RuntimeParameters.logDebug("CheckLogonTag", "getUser: AuthToken checked : "+authTokenId);
289 if (authTokenId != null) {
290 // get the user from the id
291 user = (User) RuntimeParameters.getStore().get(User.class.getName(), authTokenId.intValue());
292 if (user != null) {
293 foundUserFromAuthToken(user, request);
294 }
295 }
296 }
297
298 // Don't do that anymore, as this method is called from outside the Tag,
299 // and it ended up setting unwanted return urls
300 // // If still no user, we don't have one;
301 // // put the return url on the session
302 // if (user == null) {
303 // putReturnURLOnSession(request, session);
304 // }
305
306 // This may return null!
307 return user;
308 }
309
310 /**
311 * If the group attribute is set, check the user is in the specified group.
312 * <p>Otherwise, if the groupType attribute is set, check the user is in at least one
313 * group of the given type.
314 * <p>Otherwise, if the role attribute is set, then check the user has the required role(s)
315 * by calling <code>checkRole()</code>.
316 */
317 public boolean checkValid(User user) throws JspException {
318 // Is there a valid user logged on?
319 boolean valid = false;
320
321 RuntimeParameters.logDebug(this, "checkValid: user="+user);
322
323 // Do we have a user?
324 if (user != null) {
325
326 RuntimeParameters.logDebug(this, "checkValid: getGroup()="+getGroup());
327
328 // Is the user in the specified group(s)?
329 if (getGroup()!=null) {
330 UserGroup group = RuntimeParameters.getUserGroups().getForName( getGroup() );
331 valid = group.contains( user );
332
333 // if not valid, we put the group name (as a list) on the request,
334 // so the resulting JSP knows what to display
335 ArrayList groupNames = new ArrayList();
336 groupNames.add(getGroup());
337 if (!valid) {
338 pageContext.getRequest().setAttribute(LIST_GROUP_NAMES_KEY, groupNames);
339 }
340
341 } else if (getGroups() != null) {
342 for (int i=0; i<getGroups().length; i++) {
343 UserGroup group = RuntimeParameters.getUserGroups().getForName( getGroups()[i] );
344 valid = valid || group.contains( user );
345 }
346
347 // if not valid, we put the list of group names on the request,
348 // so the resulting JSP knows what to display
349 if (!valid) {
350 // put together the list of group names
351 ArrayList groupNames = new ArrayList();
352 for (int i=0; i<getGroups().length; i++) {
353 groupNames.add(getGroups()[i]);
354 }
355 pageContext.getRequest().setAttribute(LIST_GROUP_NAMES_KEY, groupNames);
356 }
357
358 } else if (getGroupType()!=null) {
359 UserGroupType type = UserGroupType.getForName( getGroupType() );
360 if (type != null) {
361 Iterator allGroups = RuntimeParameters.getUserGroups().getAllGroups( type ).iterator();
362 UserGroup group;
363 SearchResult result;
364 while (valid == false && allGroups.hasNext()) {
365 result = (SearchResult) allGroups.next();
366 group = (UserGroup) RuntimeParameters.getStore().get(UserGroup.class.getName(), result.getId());
367 valid = group.contains( user );
368 }
369
370 // if the user is being bumped out of the "Admin and edit"
371 // user group type, raise a detailed error.
372 // we could do the same for the common groups (admins, editors)
373 // and other group types?
374 if (!valid && ("Administrators and Editors".equals(getGroupType()))) {
375 ActionErrors errors = new ActionErrors();
376 errors.add(ActionErrors.GLOBAL_ERROR, new ActionError("error.logon.editor"));
377 pageContext.getRequest().setAttribute(Action.ERROR_KEY, errors);
378 }
379 }
380
381 } else {
382 valid = checkRole(role, user);
383 }
384 }
385
386 // If NOT valid, then set the return URL on the session
387 if (!valid) {
388 putReturnURLOnSession((HttpServletRequest) pageContext.getRequest(), pageContext.getSession());
389 }
390
391 return valid;
392 }
393
394
395 /**
396 * Put the return URL on the session
397 * under "LoginCookie.RETURN_URL_NAME",
398 */
399 public static void putReturnURLOnSession(HttpServletRequest request, HttpSession session) {
400
401 String returnUrl;
402
403 // Try getting the current URI from the SaveRequestURLFilter
404 String sruf = SaveRequestURLFilter.getRequestURI(request);
405
406 if ((sruf != null) && (!sruf.equals(""))) {
407 returnUrl = sruf;
408 } else {
409 returnUrl = request.getRequestURI();
410 if (request.getQueryString() != null) {
411 returnUrl = returnUrl.concat("?").concat(request.getQueryString());
412 }
413 }
414
415 // Strip the context from this URL path
416 String contextPath = request.getContextPath();
417 if ((!"".equals(contextPath)) && (returnUrl.startsWith(contextPath))) {
418 returnUrl = returnUrl.substring(contextPath.length());
419 }
420
421 RuntimeParameters.logDebug("CheckLogonTag", "ReturnUrl : "+returnUrl);
422 session.setAttribute(LoginCookie.RETURN_URL_NAME, returnUrl);
423 }
424
425 /**
426 * This method is called when a user is found from a cookie.
427 * <p>It adds the user to the session.
428 */
429 public static void foundUserFromCookie(User user, HttpServletRequest request) {
430 RuntimeParameters.logDebug("", "Setting attribute "+RuntimeParameters.getParam("logonUserKey")+" to : "+user);
431 request.getSession().setAttribute(RuntimeParameters.getParam("logonUserKey"), user);
432 request.getSession().setAttribute(SecurityConstants.AUTH_TOKEN, new Integer(user.getId()));
433
434 // also update the last login date and no logins (ticket #5770)
435 // FIXME : we could want to do this after the user has been validated
436 if (user instanceof TrackedUser) {
437 ((TrackedUser) user).increaseNoOfLogins();
438 ((TrackedUser) user).setLastLoginDate(new Date());
439 RuntimeParameters.getStore().save(user);
440 }
441 }
442
443 /**
444 * This method is called when a user is found from the auth token.
445 * <p>
446 * It adds the user to the session.
447 */
448 public static void foundUserFromAuthToken(User user, HttpServletRequest request) {
449 RuntimeParameters.logDebug("CheckLogonTag", "Found user from AuthToken, user object itself was absent.");
450
451 // FIXME: no need to reset the auth token
452 foundUserFromCookie(user, request);
453 }
454
455
456 /**
457 * Remove the user from the session.
458 */
459 public static void removeUserFromSession(User user, HttpServletRequest request) {
460 request.getSession().removeAttribute(RuntimeParameters.getParam("logonUserKey"));
461 request.getSession().removeAttribute(SecurityConstants.AUTH_TOKEN);
462 }
463
464
465 /**
466 * Continue to evaluate the page, or redirect to the login page
467 * @param valid If true, will continue evaluating the page; if false, will redirect
468 * @return EVAL_PAGE if evaluating the page, SKIP_PAGE if redirecting
469 */
470 public int forwardControl(boolean valid) throws JspException {
471 if (valid)
472 return (EVAL_PAGE);
473 else {
474 try {
475 // only set the generic "error.logon" error if there is nothing there already
476 // (the checkValid(user) method may have already put a more sensible error)
477 if ((pageContext.getRequest().getAttribute(LIST_GROUP_NAMES_KEY) == null) && ((pageContext.getRequest().getAttribute(Action.ERROR_KEY) == null) || ((ActionErrors) pageContext.getRequest().getAttribute(Action.ERROR_KEY)).empty())) {
478 ActionErrors errors = new ActionErrors();
479 errors.add(ActionErrors.GLOBAL_ERROR, new ActionError("error.logon"));
480 pageContext.getRequest().setAttribute(Action.ERROR_KEY, errors);
481 }
482
483 // Note. If the response is already committed then we cannot forward
484 // This may happen if, eg, the tag is in an included page
485 pageContext.include(getPage());
486
487 } catch (Exception e) {
488 // FIXME: the log never shows the real stack trace unless we do this...
489 // FIXME java 1.4 has getStackTrace()
490 e.printStackTrace();
491 throw new JspException(e.toString());
492 }
493
494 return (SKIP_PAGE);
495 }
496 }
497
498
499 /** Check roles.
500 * @param role Comma-delimited list of roles (e.g. 0,1), or <code>null</code>
501 * @return If role is not <code>null</code>, returns whether the user has the specified roles. Otherwise, returns <code>true</code>.
502 */
503 protected boolean checkRole(String role, User user) {
504 boolean valid = false;
505 if (role!=null) {
506 // If so, does the user have the required role(s)?
507 // (role may be a comma-seperated list (e.g. 1,2,3) -
508 // if so, accept if the user has ANY role)
509 boolean hasRole = false;
510 StringTokenizer st = new StringTokenizer(role, ",");
511 while (st.hasMoreTokens()) {
512 String nextRole = st.nextToken();
513 if ( user.hasRole( Integer.parseInt( nextRole ) ) ) valid = true;
514 }
515 } else {
516 valid = true;
517 }
518
519 return valid;
520 }
521
522
523
524 /** Release any acquired resources. */
525 public void release() {
526 super.release();
527 this.page = "/logon.jsp";
528 this.role = null;
529 setGroup(null);
530 setGroups(null);
531 setGroupType(null);
532 }
533 }