Merge pull request #1079 from jcabrerizo/feature/session-timeout
Adding logic to manage a max session age
diff --git a/karaf/jetty-config/src/main/resources/jetty.xml b/karaf/jetty-config/src/main/resources/jetty.xml
index 42677ac..90f87ea 100644
--- a/karaf/jetty-config/src/main/resources/jetty.xml
+++ b/karaf/jetty-config/src/main/resources/jetty.xml
@@ -21,18 +21,6 @@
<Configure id="Server" class="org.eclipse.jetty.server.Server">
- <!--Config Jetty HouseKeeper scavenge interval for invalidate session to one hour to avoid losing authentication-->
- <!--token -->
- <Set name="sessionIdManager">
- <New id="idMgr" class="org.eclipse.jetty.server.session.DefaultSessionIdManager">
- <Arg><Ref refid="Server"/></Arg>
- <Set name="sessionHouseKeeper">
- <New class="org.eclipse.jetty.server.session.HouseKeeper">
- <Set name="intervalSec"><Property name="jetty.sessionScavengeInterval.seconds" default="3600"/></Set>
- </New>
- </Set>
- </New>
- </Set>
</Configure>
diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/BrooklynSecurityProviderFilterHelper.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/BrooklynSecurityProviderFilterHelper.java
index 9570f79..6f34ca7 100644
--- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/BrooklynSecurityProviderFilterHelper.java
+++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/BrooklynSecurityProviderFilterHelper.java
@@ -22,6 +22,7 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
+import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
@@ -89,7 +90,14 @@
public void run(HttpServletRequest webRequest, ManagementContext mgmt) throws SecurityProviderDeniedAuthentication {
SecurityProvider provider = getProvider(mgmt);
- MultiSessionAttributeAdapter preferredSessionWrapper = MultiSessionAttributeAdapter.of(webRequest, false);
+ MultiSessionAttributeAdapter preferredSessionWrapper = null;
+ try{
+ preferredSessionWrapper = MultiSessionAttributeAdapter.of(webRequest, false);
+ }catch (WebApplicationException e){
+ // there is no valid session
+
+ abort(e.getResponse());
+ }
final HttpSession preferredSession1 = preferredSessionWrapper==null ? null : preferredSessionWrapper.getPreferredSession();
if (log.isTraceEnabled()) {
@@ -126,15 +134,23 @@
}
}
- Supplier<HttpSession> sessionSupplier = () -> preferredSession1!=null ? preferredSession1 : MultiSessionAttributeAdapter.of(webRequest, true).getPreferredSession();
- if (provider.authenticate(webRequest, sessionSupplier, user, pass)) {
- HttpSession preferredSession2 = sessionSupplier.get();
- log.trace("{} authentication successful - {}", this, preferredSession2);
- preferredSession2.setAttribute(BrooklynWebConfig.REMOTE_ADDRESS_SESSION_ATTRIBUTE, webRequest.getRemoteAddr());
- if (user != null) {
- preferredSession2.setAttribute(AUTHENTICATED_USER_SESSION_ATTRIBUTE, user);
+ Supplier<HttpSession> sessionSupplier = () -> {
+ return preferredSession1 != null ? preferredSession1 : MultiSessionAttributeAdapter.of(webRequest, true).getPreferredSession();
+ };
+
+ try{
+ if (provider.authenticate(webRequest, sessionSupplier, user, pass)) {
+ // gets new session created after authentication
+ HttpSession preferredSession2 = sessionSupplier.get();
+ log.trace("{} authentication successful - {}", this, preferredSession2);
+ preferredSession2.setAttribute(BrooklynWebConfig.REMOTE_ADDRESS_SESSION_ATTRIBUTE, webRequest.getRemoteAddr());
+ if (user != null) {
+ preferredSession2.setAttribute(AUTHENTICATED_USER_SESSION_ATTRIBUTE, user);
+ }
+ return;
}
- return;
+ } catch (WebApplicationException e) {
+ abort(e.getResponse());
}
throw abort("Authentication failed", provider.requiresUserPass());
@@ -150,6 +166,10 @@
throw new SecurityProviderDeniedAuthentication(response.build());
}
+ void abort(Response response) throws SecurityProviderDeniedAuthentication {
+ throw new SecurityProviderDeniedAuthentication(response);
+ }
+
SecurityProviderDeniedAuthentication redirect(String path, String msg) throws SecurityProviderDeniedAuthentication {
ResponseBuilder response = Response.status(Status.FOUND);
response.header(HttpHeader.LOCATION.asString(), path);
diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/MultiSessionAttributeAdapter.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/MultiSessionAttributeAdapter.java
index 70e9c23..3be897c 100644
--- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/MultiSessionAttributeAdapter.java
+++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/MultiSessionAttributeAdapter.java
@@ -18,13 +18,16 @@
*/
package org.apache.brooklyn.rest.util;
-import java.lang.reflect.Field;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpSession;
-
+import com.google.common.collect.ImmutableSet;
+import com.google.gson.JsonObject;
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.text.Strings;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.collections.EnumerationUtils;
+import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandler;
@@ -34,6 +37,20 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
/**
* Convenience to assist working with multiple sessions, ensuring requests in different bundles can
* get a consistent shared view of data.
@@ -80,15 +97,21 @@
private static final String KEY_PREFERRED_SESSION_HANDLER_INSTANCE = "org.apache.brooklyn.server.PreferredSessionHandlerInstance";
private static final String KEY_IS_PREFERRED = "org.apache.brooklyn.server.IsPreferred";
- private static final int MAX_INACTIVE_INTERVAL = 3601;
+ public final static ConfigKey<Long> MAX_SESSION_AGE = ConfigKeys.newLongConfigKey(
+ "org.apache.brooklyn.server.maxSessionAge", "Max session age in seconds");
- private static final Object PREFERRED_SYMBOLIC_NAME =
+ public final static ConfigKey<Integer> MAX_INACTIVE_INTERVAL = ConfigKeys.newIntegerConfigKey(
+ "org.apache.brooklyn.server.maxInactiveInterval", "Max inactive interval in seconds",
+ 3600);
+
+ private static final Object PREFERRED_SYMBOLIC_NAME =
"org.apache.cxf.cxf-rt-transports-http";
//// our bundle here doesn't have a session handler; sessions to the REST API get the handler from CXF
//"org.apache.brooklyn.rest.rest-resources";
private final HttpSession preferredSession;
private final HttpSession localSession;
+ private final ManagementContext mgmt;
private boolean silentlyAcceptLocalOnlyValues = false;
private boolean setLocalValuesAlso = false;
@@ -96,12 +119,23 @@
private static final Factory FACTORY = new Factory();
- protected MultiSessionAttributeAdapter(HttpSession preferredSession, HttpSession localSession) {
+ protected MultiSessionAttributeAdapter(HttpSession preferredSession, HttpSession localSession, HttpServletRequest request) {
this.preferredSession = preferredSession;
this.localSession = localSession;
+
+ ServletContext servletContext = request!=null ? request.getServletContext() :
+ localSession!=null ? localSession.getServletContext() :
+ preferredSession!=null ? preferredSession.getServletContext() :
+ null;
+
+ this.mgmt = servletContext != null ? new ManagementContextProvider(servletContext).getManagementContext() : null;
resetExpiration();
}
-
+
+ public MultiSessionAttributeAdapter(HttpSession preferredSession, HttpSession session) {
+ this(preferredSession, session, null);
+ }
+
public static MultiSessionAttributeAdapter of(HttpServletRequest r) {
return of(r, true);
}
@@ -109,7 +143,7 @@
public static MultiSessionAttributeAdapter of(HttpServletRequest r, boolean create) {
HttpSession session = r.getSession(create);
if (session==null) return null;
- return new MultiSessionAttributeAdapter(FACTORY.findPreferredSession(r), session);
+ return new MultiSessionAttributeAdapter(FACTORY.findPreferredSession(r), session, r);
}
/** Where the request isn't available, and the preferred session is expected to exist.
@@ -118,9 +152,9 @@
return new MultiSessionAttributeAdapter(FACTORY.findPreferredSession(session, null), session);
}
-
+
protected static class Factory {
-
+
private HttpSession findPreferredSession(HttpServletRequest r) {
if (r.getSession(false)==null) {
log.warn("Creating session", new Exception("source of created session"));
@@ -128,8 +162,59 @@
}
return findPreferredSession(r.getSession(), r);
}
-
+
private HttpSession findPreferredSession(HttpSession localSession, HttpServletRequest optionalRequest) {
+ HttpSession preferredSession = findValidPreferredSession(localSession, optionalRequest);
+
+ //TODO just check this the first time preferred session is accessed on a given request (when it is looked up)
+
+ ManagementContext mgmt = null;
+ ServletContext servletContext = optionalRequest!=null ? optionalRequest.getServletContext() : localSession!=null ? localSession.getServletContext() : preferredSession!=null ? preferredSession.getServletContext() : null;
+ if(servletContext != null){
+ mgmt = new ManagementContextProvider(servletContext).getManagementContext();
+ }
+
+ boolean isValid = ((Session)preferredSession).isValid();
+ if (!isValid) {
+ throw new SessionExpiredException("Session invalidated", SessionErrors.SESSION_INVALIDATED, optionalRequest);
+ }
+
+ if(mgmt !=null){
+ Long maxSessionAge = mgmt.getConfig().getConfig(MAX_SESSION_AGE);
+ if (maxSessionAge!=null) {
+ if (isAgeExceeded(preferredSession, maxSessionAge)) {
+ invalidateAllSession(preferredSession, localSession);
+ throw new SessionExpiredException("Max session age exceeded", SessionErrors.SESSION_AGE_EXCEEDED, optionalRequest);
+ }
+ }
+ }
+
+ return preferredSession;
+ }
+
+ private boolean isAgeExceeded(HttpSession preferredSession, Long maxSessionAge) {
+ return preferredSession.getCreationTime() + maxSessionAge*1000 < System.currentTimeMillis();
+ }
+
+ private void invalidateAllSession(HttpSession preferredSession, HttpSession localSession) {
+ Server server = ((Session)preferredSession).getSessionHandler().getServer();
+ final Handler[] handlers = server.getChildHandlersByClass(SessionHandler.class);
+ List<String> invalidatedSessions = new ArrayList<>();
+ if (handlers!=null) {
+ for (Handler h: handlers) {
+ Session session = ((SessionHandler)h).getSession(preferredSession.getId());
+ if (session!=null) {
+ invalidatedSessions.add(session.getId());
+ session.invalidate();
+ }
+ }
+ }
+ if(!invalidatedSessions.contains(localSession.getId())){
+ localSession.invalidate();
+ }
+ }
+
+ private HttpSession findValidPreferredSession(HttpSession localSession, HttpServletRequest optionalRequest) {
if (localSession instanceof Session) {
SessionHandler preferredHandler = getPreferredJettyHandler((Session)localSession, true, true);
HttpSession preferredSession = preferredHandler==null ? null : preferredHandler.getHttpSession(localSession.getId());
@@ -180,21 +265,21 @@
return preferredServerGlobalSessionHandler;
}
}
-
+
Handler[] handlers = server.getChildHandlersByClass(SessionHandler.class);
-
+
// if there is a session marked, use it, unless the server has a preferred session handler and it has an equivalent session
// this way if a session is marked (from use in a context where we don't have a web request) it will be used
SessionHandler preferredHandlerForMarkedSession = findPeerSessionMarkedPreferred(localSession.getId(), handlers);
if (preferredHandlerForMarkedSession!=null) return preferredHandlerForMarkedSession;
-
+
// nothing marked as preferred; if server global handler has a session, mark it as preferred
// this way it will get found quickly on subsequent requests
if (sessionAtServerGlobalPreferredHandler!=null) {
sessionAtServerGlobalPreferredHandler.setAttribute(KEY_IS_PREFERRED, true);
- return preferredServerGlobalSessionHandler;
+ return preferredServerGlobalSessionHandler;
}
-
+
if (allowHandlerThatDoesntHaveSession && preferredServerGlobalSessionHandler!=null) {
return preferredServerGlobalSessionHandler;
}
@@ -205,31 +290,31 @@
return getPreferredJettyHandler(localSession, allowHandlerThatDoesntHaveSession, markAndReturnThisIfNoneFound);
}
}
-
+
if (markAndReturnThisIfNoneFound) {
// nothing detected as preferred ... let's mark this session as the preferred one
markSessionAsPreferred(localSession, " (this is the handler that the request came in on)");
- return localHandler;
+ return localHandler;
}
-
+
} else {
log.warn("Could not find server for "+info(localSession));
}
return null;
}
-
+
protected void markSessionAsPreferred(HttpSession localSession, String msg) {
if (log.isTraceEnabled()) {
log.trace("Recording on "+info(localSession)+" that it is the preferred session"+msg);
}
localSession.setAttribute(KEY_IS_PREFERRED, true);
}
-
+
protected SessionHandler findPreferredBundleHandler(Session localSession, Server server, Handler[] handlers) {
if (PREFERRED_SYMBOLIC_NAME==null) return null;
-
+
SessionHandler preferredHandler = null;
-
+
if (handlers != null) {
for (Handler handler: handlers) {
SessionHandler sh = (SessionHandler) handler;
@@ -255,7 +340,7 @@
}
return preferredHandler;
}
-
+
protected SessionHandler findPeerSessionMarkedPreferred(String localSessionId, Handler[] handlers) {
SessionHandler preferredHandler = null;
// are any sessions themselves marked as primary
@@ -278,7 +363,7 @@
}
return preferredHandler;
}
-
+
protected SessionHandler getServerGlobalPreferredHandler(Server server) {
SessionHandler preferredHandler = (SessionHandler) server.getAttribute(KEY_PREFERRED_SESSION_HANDLER_INSTANCE);
if (preferredHandler!=null) {
@@ -292,6 +377,57 @@
}
return null;
}
+
+ enum SessionErrors {
+ SESSION_INVALIDATED, SESSION_AGE_EXCEEDED
+ }
+
+ private class SessionExpiredException extends WebApplicationException {
+ public SessionExpiredException(String message, SessionErrors error_status, HttpServletRequest optionalRequest) {
+ super(message, buildExceptionResponse(error_status, optionalRequest, message));
+ }
+ }
+
+ private static Response buildExceptionResponse(SessionErrors error_status, HttpServletRequest optionalRequest, String message) {
+ String mediaType;
+ String responseData;
+
+ if(requestIsHtml(optionalRequest)){
+ mediaType = MediaType.TEXT_HTML;
+ StringBuilder sb = new StringBuilder("<p>")
+ .append(message)
+ .append("</p>\n")
+ .append("<p>")
+ .append("Please go <a href=\"")
+ .append(optionalRequest.getRequestURL())
+ .append("\">here</a> to refresh.")
+ .append("</p>");
+ responseData = sb.toString();
+ }else{
+ mediaType = MediaType.APPLICATION_JSON;
+ JsonObject jsonEntity = new JsonObject();
+ jsonEntity.addProperty(error_status.toString(), true);
+ responseData = jsonEntity.toString();
+ }
+ return Response.status(Response.Status.FORBIDDEN)
+ .header(HttpHeader.CONTENT_TYPE.asString(), mediaType)
+ .entity(responseData).build();
+ }
+
+ private static boolean requestIsHtml(HttpServletRequest optionalRequest) {
+ Set headerList = separateOneLineMediaTypes(EnumerationUtils.toList(optionalRequest.getHeaders(HttpHeaders.ACCEPT)));
+ Set defaultMediaTypes = ImmutableSet.of(MediaType.TEXT_HTML, MediaType.APPLICATION_XHTML_XML, MediaType.APPLICATION_XML);
+ if(CollectionUtils.containsAny(headerList,defaultMediaTypes)){
+ return true;
+ }
+ return false;
+ }
+
+ private static Set separateOneLineMediaTypes(List<String> toList) {
+ Set<String> mediatypes = new HashSet<>();
+ toList.stream().forEach(headerLine -> mediatypes.addAll(Arrays.asList(headerLine.split(",|,\\s"))));
+ return mediatypes;
+ }
}
private static String getContextPath(Handler h) {
@@ -483,12 +619,16 @@
// force all sessions with this ID to be marked used so they are not expired
// (if _any_ session with this ID is expired, then they all are, even if another
// with the same ID is in use or has a later expiry)
+ Integer maxInativeInterval = MAX_INACTIVE_INTERVAL.getDefaultValue();
+ if(this.mgmt != null){
+ maxInativeInterval = mgmt.getConfig().getConfig(MAX_INACTIVE_INTERVAL);
+ }
Handler[] hh = getSessionHandlers();
if (hh!=null) {
for (Handler h: hh) {
Session ss = ((SessionHandler)h).getSession(getId());
if (ss!=null) {
- ss.setMaxInactiveInterval(MAX_INACTIVE_INTERVAL);
+ ss.setMaxInactiveInterval(maxInativeInterval);
}
}
}