001: /*
002: * Copyright 2005-2007 the original author or authors.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package org.acegisecurity.context;
018:
019: import java.lang.reflect.Method;
020:
021: import javax.portlet.ActionRequest;
022: import javax.portlet.ActionResponse;
023: import javax.portlet.PortletException;
024: import javax.portlet.PortletRequest;
025: import javax.portlet.PortletResponse;
026: import javax.portlet.PortletSession;
027: import javax.portlet.RenderRequest;
028: import javax.portlet.RenderResponse;
029:
030: import org.apache.commons.logging.Log;
031: import org.apache.commons.logging.LogFactory;
032: import org.springframework.beans.factory.InitializingBean;
033: import org.springframework.util.Assert;
034: import org.springframework.util.ReflectionUtils;
035: import org.springframework.web.portlet.HandlerInterceptor;
036: import org.springframework.web.portlet.ModelAndView;
037:
038: /**
039: * <p>This interceptor populates the {@link SecurityContextHolder} with information obtained from the
040: * <code>PortletSession</code>. It is applied to both <code>ActionRequest</code>s and
041: * <code>RenderRequest</code>s</p>
042: *
043: * <p>The <code>PortletSession</code> will be queried to retrieve the <code>SecurityContext</code> that should
044: * be stored against the <code>SecurityContextHolder</code> for the duration of the portlet request. At the
045: * end of the request, any updates made to the <code>SecurityContextHolder</code> will be persisted back to the
046: * <code>PortletSession</code> by this interceptor.</p>
047: *
048: * <p> If a valid <code>SecurityContext</code> cannot be obtained from the <code>PortletSession</code> for
049: * whatever reason, a fresh <code>SecurityContext</code> will be created and used instead. The created object
050: * will be of the instance defined by the {@link #setContext(Class)} method (which defaults to
051: * {@link org.acegisecurity.context.SecurityContextImpl}. </p>
052: *
053: * <p>A <code>PortletSession</code> may be created by this interceptor if one does not already exist. If at the
054: * end of the portlet request the <code>PortletSession</code> does not exist, one will <b>only</b> be created if
055: * the current contents of the <code>SecurityContextHolder</code> are not the {@link java.lang.Object#equals}
056: * to a <code>new</code> instance of {@link #context}. This avoids needless <code>PortletSession</code> creation,
057: * and automates the storage of changes made to the <code>SecurityContextHolder</code>. There is one exception to
058: * this rule, that is if the {@link #forceEagerSessionCreation} property is <code>true</code>, in which case
059: * sessions will always be created irrespective of normal session-minimization logic (the default is
060: * <code>false</code>, as this is resource intensive and not recommended).</p>
061: *
062: * <p>If for whatever reason no <code>PortletSession</code> should <b>ever</b> be created, the
063: * {@link #allowSessionCreation} property should be set to <code>false</code>. Only do this if you really need
064: * to conserve server memory and ensure all classes using the <code>SecurityContextHolder</code> are designed to
065: * have no persistence of the <code>SecurityContext</code> between web requests. Please note that if
066: * {@link #forceEagerSessionCreation} is <code>true</code>, the <code>allowSessionCreation</code> must also be
067: * <code>true</code> (setting it to <code>false</code> will cause a startup-time error).</p>
068:
069: * <p>This interceptor <b>must</b> be executed <b>before</p> any authentication processing mechanisms. These
070: * mechanisms (specifically {@link org.acegisecurity.ui.portlet.PortletProcessingInterceptor}) expect the
071: * <code>SecurityContextHolder</code> to contain a valid <code>SecurityContext</code> by the time they execute.</p>
072: *
073: * <p>An important nuance to this interceptor is that (by default) the <code>SecurityContext</code> is stored
074: * into the <code>APPLICATION_SCOPE</code> of the <code>PortletSession</code>. This doesn't just mean you will be
075: * sharing it with all the other portlets in your webapp (which is generally a good idea). It also means that (if
076: * you have done all the other appropriate magic), you will share this <code>SecurityContext</code> with servlets in
077: * your webapp. This is very useful if you have servlets serving images or processing AJAX calls from your portlets
078: * since they can now use the {@link HttpSessionContextIntegrationFilter} to access the same <code>SecurityContext<code>
079: * object from the session. This allows these calls to be secured as well as the portlet calls.</p>
080: *
081: * Much of the logic of this interceptor comes from the {@link HttpSessionContextIntegrationFilter} class which
082: * fills the same purpose on the servlet side. Ben Alex and Patrick Burlson are listed as authors here because they
083: * are the authors of that class and there are blocks of code that essentially identical between the two. (Making this
084: * a good candidate for refactoring someday.)
085: *
086: * <p>Unlike <code>HttpSessionContextIntegrationFilter</code>, this interceptor does not check to see if it is
087: * getting applied multiple times. This shouldn't be a problem since the application of interceptors is under the
088: * control of the Spring Portlet MVC framework and tends to be more explicit and more predictable than the application
089: * of filters. However, you should still be careful to only apply this inteceptor to your request once.</p>
090: *
091: * @author John A. Lewis
092: * @author Ben Alex
093: * @author Patrick Burleson
094: * @since 2.0
095: * @version $Id$
096: */
097: public class PortletSessionContextIntegrationInterceptor implements
098: InitializingBean, HandlerInterceptor {
099:
100: //~ Static fields/initializers =====================================================================================
101:
102: protected static final Log logger = LogFactory
103: .getLog(PortletSessionContextIntegrationInterceptor.class);
104:
105: public static final String ACEGI_SECURITY_CONTEXT_KEY = HttpSessionContextIntegrationFilter.ACEGI_SECURITY_CONTEXT_KEY;
106:
107: private static final String SESSION_EXISTED = PortletSessionContextIntegrationInterceptor.class
108: .getName()
109: + ".SESSION_EXISTED";
110: private static final String CONTEXT_HASHCODE = PortletSessionContextIntegrationInterceptor.class
111: .getName()
112: + ".CONTEXT_HASHCODE";
113:
114: //~ Instance fields ================================================================================================
115:
116: private Class context = SecurityContextImpl.class;
117:
118: private Object contextObject;
119:
120: /**
121: * Indicates if this interceptor can create a <code>PortletSession</code> if
122: * needed (sessions are always created sparingly, but setting this value to
123: * <code>false</code> will prohibit sessions from ever being created).
124: * Defaults to <code>true</code>. Do not set to <code>false</code> if
125: * you are have set {@link #forceEagerSessionCreation} to <code>true</code>,
126: * as the properties would be in conflict.
127: */
128: private boolean allowSessionCreation = true;
129:
130: /**
131: * Indicates if this interceptor is required to create a <code>PortletSession</code>
132: * for every request before proceeding through the request process, even if the
133: * <code>PortletSession</code> would not ordinarily have been created. By
134: * default this is <code>false</code>, which is entirely appropriate for
135: * most circumstances as you do not want a <code>PortletSession</code>
136: * created unless the interceptor actually needs one. It is envisaged the main
137: * situation in which this property would be set to <code>true</code> is
138: * if using other interceptors that depend on a <code>PortletSession</code>
139: * already existing. This is only required in specialized cases, so leave it set to
140: * <code>false</code> unless you have an actual requirement and aware of the
141: * session creation overhead.
142: */
143: private boolean forceEagerSessionCreation = false;
144:
145: /**
146: * Indicates whether the <code>SecurityContext</code> will be cloned from
147: * the <code>PortletSession</code>. The default is to simply reference
148: * (the default is <code>false</code>). The default may cause issues if
149: * concurrent threads need to have a different security identity from other
150: * threads being concurrently processed that share the same
151: * <code>PortletSession</code>. In most normal environments this does not
152: * represent an issue, as changes to the security identity in one thread is
153: * allowed to affect the security identity in other threads associated with
154: * the same <code>PortletSession</code>. For unusual cases where this is not
155: * permitted, change this value to <code>true</code> and ensure the
156: * {@link #context} is set to a <code>SecurityContext</code> that
157: * implements {@link Cloneable} and overrides the <code>clone()</code>
158: * method.
159: */
160: private boolean cloneFromPortletSession = false;
161:
162: /**
163: * Indicates wether the <code>APPLICATION_SCOPE</code> mode of the
164: * <code>PortletSession</code> should be used for storing the
165: * <code>SecurityContext</code>. The default is </code>true</code>.
166: * This allows it to be shared between the portlets in the webapp and
167: * potentially with servlets in the webapp as well. If this is set to
168: * <code>false</code>, then the <code>PORTLET_SCOPE</code> will be used
169: * instead.
170: */
171: private boolean useApplicationScopePortletSession = true;
172:
173: //~ Constructors ===================================================================================================
174:
175: public PortletSessionContextIntegrationInterceptor()
176: throws PortletException {
177: this .contextObject = generateNewContext();
178: }
179:
180: //~ Methods ========================================================================================================
181:
182: public void afterPropertiesSet() throws Exception {
183:
184: // check that the value of context is legal
185: if ((this .context == null)
186: || (!SecurityContext.class
187: .isAssignableFrom(this .context))) {
188: throw new IllegalArgumentException(
189: "context must be defined and implement SecurityContext "
190: + "(typically use org.acegisecurity.context.SecurityContextImpl; existing class is "
191: + this .context + ")");
192: }
193:
194: // check that session creation options make sense
195: if ((forceEagerSessionCreation == true)
196: && (allowSessionCreation == false)) {
197: throw new IllegalArgumentException(
198: "If using forceEagerSessionCreation, you must set allowSessionCreation to also be true");
199: }
200: }
201:
202: public boolean preHandleAction(ActionRequest request,
203: ActionResponse response, Object handler) throws Exception {
204: // call to common preHandle method
205: return preHandle(request, response, handler);
206: }
207:
208: public boolean preHandleRender(RenderRequest request,
209: RenderResponse response, Object handler) throws Exception {
210: // call to common preHandle method
211: return preHandle(request, response, handler);
212: }
213:
214: public void postHandleRender(RenderRequest request,
215: RenderResponse response, Object handler,
216: ModelAndView modelAndView) throws Exception {
217: // no-op
218: }
219:
220: public void afterActionCompletion(ActionRequest request,
221: ActionResponse response, Object handler, Exception ex)
222: throws Exception {
223: // call to common afterCompletion method
224: afterCompletion(request, response, handler, ex);
225: }
226:
227: public void afterRenderCompletion(RenderRequest request,
228: RenderResponse response, Object handler, Exception ex)
229: throws Exception {
230: // call to common afterCompletion method
231: afterCompletion(request, response, handler, ex);
232: }
233:
234: private boolean preHandle(PortletRequest request,
235: PortletResponse response, Object handler) throws Exception {
236:
237: // make sure the holder is clear
238: if (SecurityContextHolder.getContext() != null) {
239: if (logger.isWarnEnabled())
240: logger
241: .warn("SecurityContextHolder should have been null but contained: '"
242: + SecurityContextHolder.getContext()
243: + "'; setting to null now");
244: SecurityContextHolder.clearContext();
245: }
246:
247: PortletSession portletSession = null;
248: boolean portletSessionExistedAtStartOfRequest = false;
249:
250: // see if the portlet session already exists (or should be eagerly created)
251: try {
252: portletSession = request
253: .getPortletSession(forceEagerSessionCreation);
254: } catch (IllegalStateException ignored) {
255: }
256:
257: // if there is a session, then see if there is a context to bring in
258: if (portletSession != null) {
259:
260: // remember that the session already existed
261: portletSessionExistedAtStartOfRequest = true;
262:
263: // attempt to retrieve the context from the session
264: Object contextFromSessionObject = portletSession
265: .getAttribute(ACEGI_SECURITY_CONTEXT_KEY,
266: portletSessionScope());
267:
268: // if we got a context then place it into the holder
269: if (contextFromSessionObject != null) {
270:
271: // if we are supposed to clone it, then do so
272: if (cloneFromPortletSession) {
273: Assert
274: .isInstanceOf(Cloneable.class,
275: contextFromSessionObject,
276: "Context must implement Clonable and provide a Object.clone() method");
277: try {
278: Method m = contextFromSessionObject.getClass()
279: .getMethod("clone", new Class[] {});
280: if (!m.isAccessible()) {
281: m.setAccessible(true);
282: }
283: contextFromSessionObject = m.invoke(
284: contextFromSessionObject,
285: new Object[] {});
286: } catch (Exception ex) {
287: ReflectionUtils.handleReflectionException(ex);
288: }
289: }
290:
291: // if what we got is a valid context then place it into the holder, otherwise create a new one
292: if (contextFromSessionObject instanceof SecurityContext) {
293: if (logger.isDebugEnabled())
294: logger
295: .debug("Obtained from ACEGI_SECURITY_CONTEXT a valid SecurityContext and "
296: + "set to SecurityContextHolder: '"
297: + contextFromSessionObject
298: + "'");
299: SecurityContextHolder
300: .setContext((SecurityContext) contextFromSessionObject);
301: } else {
302: if (logger.isWarnEnabled())
303: logger
304: .warn("ACEGI_SECURITY_CONTEXT did not contain a SecurityContext but contained: '"
305: + contextFromSessionObject
306: + "'; are you improperly modifying the PortletSession directly "
307: + "(you should always use SecurityContextHolder) or using the PortletSession attribute "
308: + "reserved for this class? - new SecurityContext instance associated with "
309: + "SecurityContextHolder");
310: SecurityContextHolder
311: .setContext(generateNewContext());
312: }
313:
314: } else {
315:
316: // there was no context in the session, so create a new context and put it in the holder
317: if (logger.isDebugEnabled())
318: logger
319: .debug("PortletSession returned null object for ACEGI_SECURITY_CONTEXT - new "
320: + "SecurityContext instance associated with SecurityContextHolder");
321: SecurityContextHolder.setContext(generateNewContext());
322: }
323:
324: } else {
325:
326: // there was no session, so create a new context and place it in the holder
327: if (logger.isDebugEnabled())
328: logger
329: .debug("No PortletSession currently exists - new SecurityContext instance "
330: + "associated with SecurityContextHolder");
331: SecurityContextHolder.setContext(generateNewContext());
332:
333: }
334:
335: // place attributes onto the request to remember if the session existed and the hashcode of the context
336: request.setAttribute(SESSION_EXISTED, new Boolean(
337: portletSessionExistedAtStartOfRequest));
338: request.setAttribute(CONTEXT_HASHCODE, new Integer(
339: SecurityContextHolder.getContext().hashCode()));
340:
341: return true;
342: }
343:
344: private void afterCompletion(PortletRequest request,
345: PortletResponse response, Object handler, Exception ex)
346: throws Exception {
347:
348: PortletSession portletSession = null;
349:
350: // retrieve the attributes that remember if the session existed and the hashcode of the context
351: boolean portletSessionExistedAtStartOfRequest = ((Boolean) request
352: .getAttribute(SESSION_EXISTED)).booleanValue();
353: int oldContextHashCode = ((Integer) request
354: .getAttribute(CONTEXT_HASHCODE)).intValue();
355:
356: // try to retrieve an existing portlet session
357: try {
358: portletSession = request.getPortletSession(false);
359: } catch (IllegalStateException ignored) {
360: }
361:
362: // if there is now no session but there was one at the beginning then it must have been invalidated
363: if ((portletSession == null)
364: && portletSessionExistedAtStartOfRequest) {
365: if (logger.isDebugEnabled())
366: logger
367: .debug("PortletSession is now null, but was not null at start of request; "
368: + "session was invalidated, so do not create a new session");
369: }
370:
371: // create a new portlet session if we need to
372: if ((portletSession == null)
373: && !portletSessionExistedAtStartOfRequest) {
374:
375: // if we're not allowed to create a new session, then report that
376: if (!allowSessionCreation) {
377: if (logger.isDebugEnabled())
378: logger
379: .debug("The PortletSession is currently null, and the "
380: + "PortletSessionContextIntegrationInterceptor is prohibited from creating a PortletSession "
381: + "(because the allowSessionCreation property is false) - SecurityContext thus not "
382: + "stored for next request");
383: }
384: // if the context was changed during the request, then go ahead and create a session
385: else if (!contextObject.equals(SecurityContextHolder
386: .getContext())) {
387: if (logger.isDebugEnabled())
388: logger
389: .debug("PortletSession being created as SecurityContextHolder contents are non-default");
390: try {
391: portletSession = request.getPortletSession(true);
392: } catch (IllegalStateException ignored) {
393: }
394: }
395: // if nothing in the context changed, then don't bother to create a session
396: else {
397: if (logger.isDebugEnabled())
398: logger
399: .debug("PortletSession is null, but SecurityContextHolder has not changed from default: ' "
400: + SecurityContextHolder
401: .getContext()
402: + "'; not creating PortletSession or storing SecurityContextHolder contents");
403: }
404: }
405:
406: // if the session exists and the context has changes, then store the context back into the session
407: if ((portletSession != null)
408: && (SecurityContextHolder.getContext().hashCode() != oldContextHashCode)) {
409: portletSession.setAttribute(ACEGI_SECURITY_CONTEXT_KEY,
410: SecurityContextHolder.getContext(),
411: portletSessionScope());
412: if (logger.isDebugEnabled())
413: logger
414: .debug("SecurityContext stored to PortletSession: '"
415: + SecurityContextHolder.getContext()
416: + "'");
417: }
418:
419: // remove the contents of the holder
420: SecurityContextHolder.clearContext();
421: if (logger.isDebugEnabled())
422: logger
423: .debug("SecurityContextHolder set to new context, as request processing completed");
424:
425: }
426:
427: /**
428: * Creates a new <code>SecurityContext</code> object. The specific class is
429: * determined by the setting of the {@link #context} property.
430: * @return the new <code>SecurityContext</code>
431: * @throws PortletException if the creation throws an <code>InstantiationException</code> or
432: * an <code>IllegalAccessException</code>, then this method will wrap them in a
433: * <code>PortletException</code>
434: */
435: public SecurityContext generateNewContext() throws PortletException {
436: try {
437: return (SecurityContext) this .context.newInstance();
438: } catch (InstantiationException ie) {
439: throw new PortletException(ie);
440: } catch (IllegalAccessException iae) {
441: throw new PortletException(iae);
442: }
443: }
444:
445: private int portletSessionScope() {
446: // return the appropriate scope setting based on our property value
447: return (this .useApplicationScopePortletSession ? PortletSession.APPLICATION_SCOPE
448: : PortletSession.PORTLET_SCOPE);
449: }
450:
451: public Class getContext() {
452: return context;
453: }
454:
455: public void setContext(Class secureContext) {
456: this .context = secureContext;
457: }
458:
459: public boolean isAllowSessionCreation() {
460: return allowSessionCreation;
461: }
462:
463: public void setAllowSessionCreation(boolean allowSessionCreation) {
464: this .allowSessionCreation = allowSessionCreation;
465: }
466:
467: public boolean isForceEagerSessionCreation() {
468: return forceEagerSessionCreation;
469: }
470:
471: public void setForceEagerSessionCreation(
472: boolean forceEagerSessionCreation) {
473: this .forceEagerSessionCreation = forceEagerSessionCreation;
474: }
475:
476: public boolean isCloneFromPortletSession() {
477: return cloneFromPortletSession;
478: }
479:
480: public void setCloneFromPortletSession(
481: boolean cloneFromPortletSession) {
482: this .cloneFromPortletSession = cloneFromPortletSession;
483: }
484:
485: public boolean isUseApplicationScopePortletSession() {
486: return useApplicationScopePortletSession;
487: }
488:
489: public void setUseApplicationScopePortletSession(
490: boolean useApplicationScopePortletSession) {
491: this.useApplicationScopePortletSession = useApplicationScopePortletSession;
492: }
493:
494: }
|