From 1ae07779a2f8a5ef73fb2346f72f9745700cdb95 Mon Sep 17 00:00:00 2001 From: Ben Alex Date: Sat, 22 Oct 2005 01:53:03 +0000 Subject: [PATCH] SEC-710: Refactor concurrent session handling support. --- .../ConcurrentLoginException.java | 13 +- .../ConcurrentSessionController.java | 65 ++++ .../ConcurrentSessionControllerImpl.java | 155 +++++++++ .../concurrent/ConcurrentSessionFilter.java | 128 +++++++ .../NullConcurrentSessionController.java} | 19 +- .../SessionAlreadyUsedException.java | 32 ++ .../concurrent/SessionInformation.java | 94 ++++++ .../concurrent/SessionRegistry.java | 83 +++++ .../concurrent/SessionRegistryImpl.java | 147 +++++++++ .../concurrent/SessionRegistryUtils.java | 57 ++++ .../ConcurrentSessionControllerImpl.java | 312 ------------------ .../ConcurrentSessionViolationEvent.java | 42 --- .../NullConcurrentSessionController.java | 41 --- .../providers/ProviderManager.java | 6 +- .../ConcurrentSessionControllerImplTests.java | 124 +++++++ .../ConcurrentSessionFilterTests.java | 173 ++++++++++ .../concurrent/SessionInformationTests.java | 49 +++ .../concurrent/SessionRegistryImplTests.java | 155 +++++++++ .../ConcurrentSessionControllerImplTests.java | 280 ---------------- .../providers/ProviderManagerTests.java | 2 + 20 files changed, 1284 insertions(+), 693 deletions(-) rename core/src/main/java/org/acegisecurity/{providers => concurrent}/ConcurrentLoginException.java (76%) create mode 100644 core/src/main/java/org/acegisecurity/concurrent/ConcurrentSessionController.java create mode 100644 core/src/main/java/org/acegisecurity/concurrent/ConcurrentSessionControllerImpl.java create mode 100644 core/src/main/java/org/acegisecurity/concurrent/ConcurrentSessionFilter.java rename core/src/main/java/org/acegisecurity/{providers/ConcurrentSessionController.java => concurrent/NullConcurrentSessionController.java} (63%) create mode 100644 core/src/main/java/org/acegisecurity/concurrent/SessionAlreadyUsedException.java create mode 100644 core/src/main/java/org/acegisecurity/concurrent/SessionInformation.java create mode 100644 core/src/main/java/org/acegisecurity/concurrent/SessionRegistry.java create mode 100644 core/src/main/java/org/acegisecurity/concurrent/SessionRegistryImpl.java create mode 100644 core/src/main/java/org/acegisecurity/concurrent/SessionRegistryUtils.java delete mode 100644 core/src/main/java/org/acegisecurity/providers/ConcurrentSessionControllerImpl.java delete mode 100644 core/src/main/java/org/acegisecurity/providers/ConcurrentSessionViolationEvent.java delete mode 100644 core/src/main/java/org/acegisecurity/providers/NullConcurrentSessionController.java create mode 100644 core/src/test/java/org/acegisecurity/concurrent/ConcurrentSessionControllerImplTests.java create mode 100644 core/src/test/java/org/acegisecurity/concurrent/ConcurrentSessionFilterTests.java create mode 100644 core/src/test/java/org/acegisecurity/concurrent/SessionInformationTests.java create mode 100644 core/src/test/java/org/acegisecurity/concurrent/SessionRegistryImplTests.java delete mode 100644 core/src/test/java/org/acegisecurity/providers/ConcurrentSessionControllerImplTests.java diff --git a/core/src/main/java/org/acegisecurity/providers/ConcurrentLoginException.java b/core/src/main/java/org/acegisecurity/concurrent/ConcurrentLoginException.java similarity index 76% rename from core/src/main/java/org/acegisecurity/providers/ConcurrentLoginException.java rename to core/src/main/java/org/acegisecurity/concurrent/ConcurrentLoginException.java index f7a2381616..6a9121389c 100644 --- a/core/src/main/java/org/acegisecurity/providers/ConcurrentLoginException.java +++ b/core/src/main/java/org/acegisecurity/concurrent/ConcurrentLoginException.java @@ -12,21 +12,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -package net.sf.acegisecurity.providers; +package net.sf.acegisecurity.concurrent; import net.sf.acegisecurity.AuthenticationException; /** - * Thrown by the ConcurrentSessionController when the number of sessions - * allowed is attempting to be exceeded. + * Thrown by ConcurrentSessionControllerImpl if + * an attempt is made to login and the user has already exceeded + * their maxmimum allowed sessions. * - * @author Ray Krueger + * @author Ben Alex + * @version $Id$ */ public class ConcurrentLoginException extends AuthenticationException { - //~ Constructors =========================================================== - public ConcurrentLoginException(String msg) { super(msg); } diff --git a/core/src/main/java/org/acegisecurity/concurrent/ConcurrentSessionController.java b/core/src/main/java/org/acegisecurity/concurrent/ConcurrentSessionController.java new file mode 100644 index 0000000000..db94ca4694 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/concurrent/ConcurrentSessionController.java @@ -0,0 +1,65 @@ +/* Copyright 2004, 2005 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity.concurrent; + +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.AuthenticationException; + + +/** + * Provides two methods that can be called by an {@link + * net.sf.acegisecurity.AuthenticationManager} to integrate with the + * concurrent session handling infrastructure. + * + * @author Ben Alex + * @version $Id$ + */ +public interface ConcurrentSessionController { + //~ Methods ================================================================ + + /** + * Called by any class that wishes to know whether the current + * authentication request should be permitted. Generally callers will be + * AuthenticationManagers before they authenticate, but could + * equally include Filters or other interceptors that wish to + * confirm the ongoing validity of a previously authenticated + * Authentication. + * + *

+ * The implementation should throw a suitable exception if the user has + * exceeded their maximum allowed concurrent sessions. + *

+ * + * @param request the authentication request (never null) + * + * @throws AuthenticationException if the user has exceeded their maximum + * allowed current sessions + */ + public void checkAuthenticationAllowed(Authentication request) + throws AuthenticationException; + + /** + * Called by an AuthenticationManager when the authentication + * was successful. An implementation is expected to register the + * authenticated user in some sort of registry, for future concurrent + * tracking via the {@link #checkConcurrentAuthentication(Authentication)} + * method. + * + * @param authentication the successfully authenticated user (never + * null) + */ + public void registerSuccessfulAuthentication(Authentication authentication); +} diff --git a/core/src/main/java/org/acegisecurity/concurrent/ConcurrentSessionControllerImpl.java b/core/src/main/java/org/acegisecurity/concurrent/ConcurrentSessionControllerImpl.java new file mode 100644 index 0000000000..d598e1cac6 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/concurrent/ConcurrentSessionControllerImpl.java @@ -0,0 +1,155 @@ +/* Copyright 2004, 2005 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity.concurrent; + +import java.util.Date; + +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.AuthenticationException; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; + + +/** + * Base implementation of {@link ConcurrentSessionControllerImpl} which + * prohibits simultaneous logins. + * + *

+ * By default uses {@link net.sf.acegisecurity.concurrent.SessionRegistryImpl}, + * although any SessionRegistry may be used. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public class ConcurrentSessionControllerImpl + implements ConcurrentSessionController, InitializingBean { + //~ Instance fields ======================================================== + + private SessionRegistry sessionRegistry = new SessionRegistryImpl(); + private int maximumSessions = 1; + private boolean exceptionIfMaximumExceeded = false; + + //~ Methods ================================================================ + + public void setMaximumSessions(int maximumSessions) { + this.maximumSessions = maximumSessions; + } + + public void setSessionRegistry(SessionRegistry sessionRegistry) { + this.sessionRegistry = sessionRegistry; + } + + public void setExceptionIfMaximumExceeded(boolean exceptionIfMaximumExceeded) { + this.exceptionIfMaximumExceeded = exceptionIfMaximumExceeded; + } + + public void afterPropertiesSet() throws Exception { + Assert.notNull(sessionRegistry, "SessionRegistry required"); + Assert.isTrue(maximumSessions != 0, + "MaximumLogins must be either -1 to allow unlimited logins, or a positive integer to specify a maximum"); + } + + public void checkAuthenticationAllowed(Authentication request) + throws AuthenticationException { + Assert.notNull(request, + "Authentication request cannot be null (violation of interface contract)"); + + Object principal = SessionRegistryUtils + .obtainPrincipalFromAuthentication(request); + String sessionId = SessionRegistryUtils + .obtainSessionIdFromAuthentication(request); + + SessionInformation[] sessions = sessionRegistry.getAllSessions(principal); + + int sessionCount = 0; + + if (sessions != null) { + sessionCount = sessions.length; + } + + int allowableSessions = getMaximumSessionsForThisUser(request); + Assert.isTrue(allowableSessions != 0, + "getMaximumSessionsForThisUser() must return either -1 to allow unlimited logins, or a positive integer to specify a maximum"); + + if (sessionCount < allowableSessions) { + return; + } else if (sessionCount == allowableSessions) { + // Only permit it though if this request is associated with one of the sessions + for (int i = 0; i < sessionCount; i++) { + if (sessions[i].getSessionId().equals(sessionId)) { + return; + } + } + } + + allowableSessionsExceeded(sessionId, sessions, allowableSessions, sessionRegistry); + } + + /** + * Allows subclasses to customise behaviour when too many sessions are + * detected. + * + * @param sessionId the session ID of the present request + * @param sessions either null or all unexpired sessions associated with the principal + * @param registry an instance of the SessionRegistry for subclass use + */ + protected void allowableSessionsExceeded(String sessionId, SessionInformation[] sessions, int allowableSessions, SessionRegistry registry) { + if (exceptionIfMaximumExceeded || sessions == null) { + throw new ConcurrentLoginException("Maximum sessions of " + + allowableSessions + " for this principal exceeded"); + } + + // Determine least recently used session, and mark it for invalidation + SessionInformation leastRecentlyUsed = null; + for (int i = 0; i < sessions.length; i++) { + if (leastRecentlyUsed == null || sessions[i].getLastRequest().before(leastRecentlyUsed.getLastRequest())) { + leastRecentlyUsed = sessions[i]; + } + } + + leastRecentlyUsed.expireNow(); + } + + public void registerSuccessfulAuthentication(Authentication authentication) { + Assert.notNull(authentication, + "Authentication cannot be null (violation of interface contract)"); + + Object principal = SessionRegistryUtils + .obtainPrincipalFromAuthentication(authentication); + String sessionId = SessionRegistryUtils + .obtainSessionIdFromAuthentication(authentication); + + sessionRegistry.removeSessionInformation(sessionId); + sessionRegistry.registerNewSession(sessionId, principal); + } + + /** + * Method intended for use by subclasses to override the maximum number of + * sessions that are permitted for a particular authentication. The + * default implementation simply returns the maximumSessions + * value for the bean. + * + * @param authentication to determine the maximum sessions for + * + * @return either -1 meaning unlimited, or a positive integer to limit + * (never zero) + */ + protected int getMaximumSessionsForThisUser(Authentication authentication) { + return maximumSessions; + } +} diff --git a/core/src/main/java/org/acegisecurity/concurrent/ConcurrentSessionFilter.java b/core/src/main/java/org/acegisecurity/concurrent/ConcurrentSessionFilter.java new file mode 100644 index 0000000000..3f07e1b60b --- /dev/null +++ b/core/src/main/java/org/acegisecurity/concurrent/ConcurrentSessionFilter.java @@ -0,0 +1,128 @@ +/* Copyright 2004, 2005 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity.concurrent; + +import org.springframework.beans.factory.InitializingBean; + +import org.springframework.util.Assert; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + + +/** + * Filter required by concurrent session handling package. + * + *

+ * This filter performs two functions. First, it calls {@link + * net.sf.acegisecurity.concurrent.SessionRegistry#refreshLastRequest(String)} + * for each request. That way, registered sessions always have a correct "last + * update" date/time. Second, it retrieves {@link + * net.sf.acegisecurity.concurrent.SessionInformation} from the + * SessionRegistry for each request and checks if the session has + * been marked as expired. If it has been marked as expired, the session is + * invalidated. The invalidation of the session will also cause the request to + * redirect to the URL specified, and a {@link + * net.sf.acegisecurity.ui.session.HttpSessionDestroyedEvent} to be published + * via the {@link net.sf.acegisecurity.ui.session.HttpSessionEventPublisher} + * registered in web.xml. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public class ConcurrentSessionFilter implements Filter, + InitializingBean { + //~ Instance fields ======================================================== + + private SessionRegistry sessionRegistry; + private String expiredUrl; + + //~ Methods ================================================================ + + public void setExpiredUrl(String expiredUrl) { + this.expiredUrl = expiredUrl; + } + + public void setSessionRegistry(SessionRegistry sessionRegistry) { + this.sessionRegistry = sessionRegistry; + } + + public void afterPropertiesSet() throws Exception { + Assert.notNull(sessionRegistry, "SessionRegistry required"); + Assert.hasText(expiredUrl, "ExpiredUrl required"); + } + + /** + * Does nothing. We use IoC container lifecycle services instead. + */ + public void destroy() {} + + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + Assert.isInstanceOf(HttpServletRequest.class, request, + "Can only process HttpServletRequest"); + Assert.isInstanceOf(HttpServletResponse.class, response, + "Can only process HttpServletResponse"); + + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + + HttpSession session = httpRequest.getSession(false); + + if (session != null) { + SessionInformation info = sessionRegistry.getSessionInformation(session + .getId()); + + if (info != null) { + if (info.isExpired()) { + // Expired - abort processing + session.invalidate(); + + String targetUrl = httpRequest.getContextPath() + + expiredUrl; + httpResponse.sendRedirect(httpResponse.encodeRedirectURL( + targetUrl)); + + return; + } else { + // Non-expired - update last request date/time + info.refreshLastRequest(); + } + } + } + + chain.doFilter(request, response); + } + + /** + * Does nothing. We use IoC container lifecycle services instead. + * + * @param arg0 ignored + * + * @throws ServletException ignored + */ + public void init(FilterConfig arg0) throws ServletException {} +} diff --git a/core/src/main/java/org/acegisecurity/providers/ConcurrentSessionController.java b/core/src/main/java/org/acegisecurity/concurrent/NullConcurrentSessionController.java similarity index 63% rename from core/src/main/java/org/acegisecurity/providers/ConcurrentSessionController.java rename to core/src/main/java/org/acegisecurity/concurrent/NullConcurrentSessionController.java index 0bdcd86e7c..0af8d36d38 100644 --- a/core/src/main/java/org/acegisecurity/providers/ConcurrentSessionController.java +++ b/core/src/main/java/org/acegisecurity/concurrent/NullConcurrentSessionController.java @@ -13,24 +13,25 @@ * limitations under the License. */ -package net.sf.acegisecurity.providers; +package net.sf.acegisecurity.concurrent; import net.sf.acegisecurity.Authentication; import net.sf.acegisecurity.AuthenticationException; /** - * See: {@link ConcurrentSessionControllerImpl} + * No-op implementation of {@link + * net.sf.acegisecurity.concurrent.ConcurrentSessionController}. * - * @author Ray Krueger - * @see ConcurrentSessionControllerImpl + * @author Ben Alex + * @version $Id$ */ -public interface ConcurrentSessionController { +public class NullConcurrentSessionController + implements ConcurrentSessionController { //~ Methods ================================================================ - void afterAuthentication(Authentication initialAuth, Authentication result) - throws AuthenticationException; + public void checkAuthenticationAllowed(Authentication request) + throws AuthenticationException {} - void beforeAuthentication(Authentication initialAuth) - throws AuthenticationException; + public void registerSuccessfulAuthentication(Authentication authentication) {} } diff --git a/core/src/main/java/org/acegisecurity/concurrent/SessionAlreadyUsedException.java b/core/src/main/java/org/acegisecurity/concurrent/SessionAlreadyUsedException.java new file mode 100644 index 0000000000..91cd85f6a6 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/concurrent/SessionAlreadyUsedException.java @@ -0,0 +1,32 @@ +/* Copyright 2004, 2005 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.sf.acegisecurity.concurrent; + +import net.sf.acegisecurity.AuthenticationException; + + +/** + * Thrown by a SessionRegistry implementation if + * an attempt is made to create new session information for an existing + * sessionId. The user should firstly clear the existing session from the + * ConcurrentSessionRegistry. + * + * @author Ben Alex + */ +public class SessionAlreadyUsedException extends AuthenticationException { + public SessionAlreadyUsedException(String msg) { + super(msg); + } +} diff --git a/core/src/main/java/org/acegisecurity/concurrent/SessionInformation.java b/core/src/main/java/org/acegisecurity/concurrent/SessionInformation.java new file mode 100644 index 0000000000..f50273db5f --- /dev/null +++ b/core/src/main/java/org/acegisecurity/concurrent/SessionInformation.java @@ -0,0 +1,94 @@ +/* Copyright 2004, 2005 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity.concurrent; + +import org.springframework.util.Assert; + +import java.util.Date; + + +/** + * Represents a record of a session within the Acegi Security framework. + * + *

+ * This is primarily used for concurrent session support. + *

+ * + *

+ * Sessions have three states: active, expired, and destroyed. A session can + * that is invalidated by session.invalidate() or via Servlet + * Container management is considered "destroyed". An "expired" session, on + * the other hand, is a session that Acegi Security wants to end because it + * was selected for removal for some reason (generally as it was the least + * recently used session and the maximum sessions for the user were reached). + * An "expired" session is removed as soon as possible by a + * Filter. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public class SessionInformation { + //~ Instance fields ======================================================== + + private Date lastRequest; + private Object principal; + private String sessionId; + private boolean expired = false; + + //~ Constructors =========================================================== + + public SessionInformation(Object principal, String sessionId, + Date lastRequest) { + Assert.notNull(principal, "Principal required"); + Assert.hasText(sessionId, "SessionId required"); + Assert.notNull(lastRequest, "LastRequest required"); + this.principal = principal; + this.sessionId = sessionId; + this.lastRequest = lastRequest; + } + + private SessionInformation() {} + + //~ Methods ================================================================ + + public boolean isExpired() { + return expired; + } + + public Date getLastRequest() { + return lastRequest; + } + + public Object getPrincipal() { + return principal; + } + + public String getSessionId() { + return sessionId; + } + + public void expireNow() { + this.expired = true; + } + + /** + * Refreshes the internal lastRequest to the current date and time. + */ + public void refreshLastRequest() { + this.lastRequest = new Date(); + } +} diff --git a/core/src/main/java/org/acegisecurity/concurrent/SessionRegistry.java b/core/src/main/java/org/acegisecurity/concurrent/SessionRegistry.java new file mode 100644 index 0000000000..57eebc3e42 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/concurrent/SessionRegistry.java @@ -0,0 +1,83 @@ +/* Copyright 2004, 2005 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity.concurrent; + +/** + * Maintains a registry of SessionInformation instances. + * + * @author Ben Alex + * @version $Id$ + */ +public interface SessionRegistry { + //~ Methods ================================================================ + + /** + * Obtains all the known sessions for the specified principal. Sessions + * that have expired or destroyed are not returned. + * + * @param principal to locate sessions for (should never be + * null) + * + * @return the unexpired and undestroyed sessions for this principal, or + * null if none were found + */ + public SessionInformation[] getAllSessions(Object principal); + + /** + * Obtains the session information for the specified + * sessionId. Even expired sessions are returned (although + * destroyed sessions are never returned). + * + * @param sessionId to lookup (should never be null) + * + * @return the session information, or null if not found + */ + public SessionInformation getSessionInformation(String sessionId); + + /** + * Updates the given sessionId so its last request time is + * equal to the present date and time. Silently returns if the given + * sessionId cannot be found or the session is marked to expire. + * + * @param sessionId for which to update the date and time of the last + * request (should never be null) + */ + public void refreshLastRequest(String sessionId); + + /** + * Registers a new session for the specified principal. The newly + * registered session will not be marked for expiration. + * + * @param sessionId to associate with the principal (should never be + * null) + * @param principal to associate with the session (should never be + * null) + * + * @throws SessionAlreadyUsedException DOCUMENT ME! + */ + public void registerNewSession(String sessionId, Object principal) + throws SessionAlreadyUsedException; + + /** + * Deletes all the session information being maintained for the specified + * sessionId. If the sessionId is not found, the + * method gracefully returns. + * + * @param sessionId to delete information for (should never be + * null) + */ + public void removeSessionInformation(String sessionId); +} diff --git a/core/src/main/java/org/acegisecurity/concurrent/SessionRegistryImpl.java b/core/src/main/java/org/acegisecurity/concurrent/SessionRegistryImpl.java new file mode 100644 index 0000000000..022533619d --- /dev/null +++ b/core/src/main/java/org/acegisecurity/concurrent/SessionRegistryImpl.java @@ -0,0 +1,147 @@ +/* Copyright 2004, 2005 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity.concurrent; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.servlet.http.HttpSession; + +import net.sf.acegisecurity.ui.session.HttpSessionDestroyedEvent; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.util.Assert; + + +/** + * Base implementation of {@link + * net.sf.acegisecurity.concurrent.SessionRegistry} which also listens for + * {@link net.sf.acegisecurity.ui.session.HttpSessionDestroyedEvent}s + * published in the Spring application context. + * + *

+ * NB: It is important that you register the {@link + * net.sf.acegisecurity.ui.session.HttpSessionEventPublisher} in + * web.xml so that this class is notified of sessions that + * expire. + *

+ * + * @author Ben Alex + * @version $Id${date} + */ +public class SessionRegistryImpl implements SessionRegistry, + ApplicationListener { + //~ Instance fields ======================================================== + + private Map principals = Collections.synchronizedMap(new HashMap()); // + private Map sessionIds = Collections.synchronizedMap(new HashMap()); // + + //~ Methods ================================================================ + + public SessionInformation[] getAllSessions(Object principal) { + Set sessionsUsedByPrincipal = (Set) principals.get(principal); + + if (sessionsUsedByPrincipal == null) { + return null; + } + + List list = new ArrayList(); + Iterator iter = sessionsUsedByPrincipal.iterator(); + while (iter.hasNext()) { + String sessionId = (String) iter.next(); + list.add(getSessionInformation(sessionId)); + } + + return (SessionInformation[]) list.toArray(new SessionInformation[] {}); + } + + public SessionInformation getSessionInformation(String sessionId) { + Assert.hasText(sessionId, "SessionId required as per inerface contract"); + + return (SessionInformation) sessionIds.get(sessionId); + } + + public void onApplicationEvent(ApplicationEvent event) { + if (event instanceof HttpSessionDestroyedEvent) { + String sessionId = ((HttpSession) event.getSource()).getId(); + removeSessionInformation(sessionId); + } + } + + public void refreshLastRequest(String sessionId) { + Assert.hasText(sessionId, "SessionId required as per inerface contract"); + + SessionInformation info = getSessionInformation(sessionId); + + if (info != null) { + info.refreshLastRequest(); + } + } + + public void registerNewSession(String sessionId, Object principal) + throws SessionAlreadyUsedException { + Assert.hasText(sessionId, "SessionId required as per inerface contract"); + Assert.notNull(principal, "Principal required as per inerface contract"); + + if (getSessionInformation(sessionId) != null) { + throw new SessionAlreadyUsedException("Session " + sessionId + + " is already is use"); + } + + sessionIds.put(sessionId, + new SessionInformation(principal, sessionId, new Date())); + + Set sessionsUsedByPrincipal = (Set) principals.get(principal); + + if (sessionsUsedByPrincipal == null) { + sessionsUsedByPrincipal = Collections.synchronizedSet(new HashSet()); + } + + sessionsUsedByPrincipal.add(sessionId); + + principals.put(principal, sessionsUsedByPrincipal); + } + + public void removeSessionInformation(String sessionId) { + Assert.hasText(sessionId, "SessionId required as per inerface contract"); + + SessionInformation info = getSessionInformation(sessionId); + + if (info != null) { + sessionIds.remove(sessionId); + + Set sessionsUsedByPrincipal = (Set) principals.get(info + .getPrincipal()); + + if (sessionsUsedByPrincipal != null) { + sessionsUsedByPrincipal.remove(sessionId); + + if (sessionsUsedByPrincipal.size() == 0) { + // No need to keep pbject in principals Map anymore + principals.remove(info.getPrincipal()); + } + } + } + } +} diff --git a/core/src/main/java/org/acegisecurity/concurrent/SessionRegistryUtils.java b/core/src/main/java/org/acegisecurity/concurrent/SessionRegistryUtils.java new file mode 100644 index 0000000000..f04a54581d --- /dev/null +++ b/core/src/main/java/org/acegisecurity/concurrent/SessionRegistryUtils.java @@ -0,0 +1,57 @@ +/* Copyright 2004, 2005 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity.concurrent; + +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.UserDetails; +import net.sf.acegisecurity.ui.WebAuthenticationDetails; + +import org.springframework.util.Assert; + + +/** + * Utility methods to assist with concurrent session management. + * + * @author Ben Alex + * @version $Id$ + */ +public class SessionRegistryUtils { + //~ Methods ================================================================ + + public static Object obtainPrincipalFromAuthentication(Authentication auth) { + Assert.notNull(auth, "Authentication required"); + Assert.notNull(auth.getPrincipal(), + "Authentication.getPrincipal() required"); + + if (auth.getPrincipal() instanceof UserDetails) { + return ((UserDetails) auth.getPrincipal()).getUsername(); + } else { + return auth.getPrincipal(); + } + } + + public static String obtainSessionIdFromAuthentication(Authentication auth) { + Assert.notNull(auth, "Authentication required"); + Assert.notNull(auth.getDetails(), "Authentication.getDetails() required"); + Assert.isInstanceOf(WebAuthenticationDetails.class, auth.getDetails()); + + String sessionId = ((WebAuthenticationDetails) auth.getDetails()) + .getSessionId(); + Assert.hasText(sessionId, "WebAuthenticationDetails missing SessionId"); + + return sessionId; + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/ConcurrentSessionControllerImpl.java b/core/src/main/java/org/acegisecurity/providers/ConcurrentSessionControllerImpl.java deleted file mode 100644 index 1244ef6bee..0000000000 --- a/core/src/main/java/org/acegisecurity/providers/ConcurrentSessionControllerImpl.java +++ /dev/null @@ -1,312 +0,0 @@ -/* Copyright 2004, 2005 Acegi Technology Pty Limited - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.sf.acegisecurity.providers; - -import net.sf.acegisecurity.Authentication; -import net.sf.acegisecurity.AuthenticationTrustResolver; -import net.sf.acegisecurity.AuthenticationTrustResolverImpl; -import net.sf.acegisecurity.UserDetails; -import net.sf.acegisecurity.ui.WebAuthenticationDetails; -import net.sf.acegisecurity.ui.session.HttpSessionDestroyedEvent; -import org.springframework.beans.BeansException; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.ApplicationEvent; -import org.springframework.context.ApplicationListener; - -import javax.servlet.http.HttpSession; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - - -/** - * Used by the {@link ProviderManager} to track Authentications and their - * respective sessions. A given user is allowed {@link #setMaxSessions(int)} - * sessions. If they attempt to exceed that ammount a {@link - * ConcurrentLoginException} will be thrown. The - * ConcurrentSessionControllerImpl class will listen for {@link - * HttpSessionDestroyedEvent}s in the ApplicationContext to remove a session - * from the internal tracking. This class will not function properly - * without a {@link net.sf.acegisecurity.ui.session.HttpSessionEventPublisher} - * configured in web.xml. - * - * @author Ray Krueger - * @author Ben Alex - */ -public class ConcurrentSessionControllerImpl - implements ConcurrentSessionController, ApplicationListener, - ApplicationContextAware { - //~ Instance fields ======================================================== - - protected Map principalsToSessions = new HashMap(); - protected Map sessionsToPrincipals = new HashMap(); - protected Set sessionSet = new HashSet(); - private ApplicationContext applicationContext; - private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); - private int maxSessions = 1; - - //~ Methods ================================================================ - - public void setApplicationContext(ApplicationContext applicationContext) - throws BeansException { - this.applicationContext = applicationContext; - } - - public ApplicationContext getApplicationContext() { - return applicationContext; - } - - /** - * Set the maximum number of sessions a user is allowed to have, defaults - * to 1. Setting this to anything less than 1 will allow unlimited - * sessions - * - * @param maxSessions - */ - public void setMaxSessions(int maxSessions) { - this.maxSessions = maxSessions; - } - - /** - * The maximum sessions per user. - * - * @return int - */ - public int getMaxSessions() { - return maxSessions; - } - - /** - * The trustResolver to use for determining Anonymous users and ignoring - * them. Defaults to {@link AuthenticationTrustResolverImpl} - * - * @param trustResolver - */ - public void setTrustResolver(AuthenticationTrustResolver trustResolver) { - this.trustResolver = trustResolver; - } - - /** - * Get the configured AuthenticationTrustResolver - * - * @return The configured AuthenticationTrustResolver or {@link - * AuthenticationTrustResolverImpl} by default. - */ - public AuthenticationTrustResolver getTrustResolver() { - return trustResolver; - } - - /** - * Called by the {@link ProviderManager} after receiving a response from a - * configured AuthenticationProvider. - * - * @param request Used to retieve the {@link WebAuthenticationDetails} - * @param response Used to store the sessionId for the current Principal - * @throws ConcurrentLoginException If the user is already logged in the - * maximum number of times - * @see #determineSessionPrincipal(net.sf.acegisecurity.Authentication) - */ - public void afterAuthentication(Authentication request, - Authentication response) throws ConcurrentLoginException { - enforceConcurrentLogins(response); - - if (request.getDetails() instanceof WebAuthenticationDetails) { - String sessionId = ((WebAuthenticationDetails) request.getDetails()) - .getSessionId(); - addSession(determineSessionPrincipal(response), sessionId); - } - } - - /** - * Called by the {@link ProviderManager} before iterating the configured - * {@link AuthenticationProvider}s - * - * @param request The Authentication in question - * @throws ConcurrentLoginException If the user is already logged in the - * maximum number of times #setMaxSessions(int)} - */ - public void beforeAuthentication(Authentication request) - throws ConcurrentLoginException { - enforceConcurrentLogins(request); - } - - /** - * Checks for {@link HttpSessionDestroyedEvent}s and calls {@link - * #removeSession(String)} for the destoyed HttpSessions id. - * - * @param event - */ - public void onApplicationEvent(ApplicationEvent event) { - if (event instanceof HttpSessionDestroyedEvent) { - String sessionId = ((HttpSession) event.getSource()).getId(); - removeSession(sessionId); - } - } - - /** - * Compares the sessionIds stored for the given principal to determine if - * the given sessionId is new or existing. - * - * @param principal The principal in question - * @param sessionId The new or existing sessionId - * @return true if it's the same as a session already in use, false if it - * is a new session - */ - protected boolean isActiveSession(Object principal, String sessionId) { - Set sessions = (Set) principalsToSessions.get(principal); - - if (sessions == null) { - return false; - } - - return sessions.contains(sessionId); - } - - /** - * Updates internal maps with the sessionId for the given principal. Can be - * overridden by subclasses to provide a specialized means of principal - * -> session tracking. - * - * @param principal - * @param sessionId - */ - protected void addSession(Object principal, String sessionId) { - Set sessions = (Set) principalsToSessions.get(principal); - - if (sessions == null) { - sessions = new HashSet(); - principalsToSessions.put(principal, sessions); - } - - sessions.add(sessionId); - sessionsToPrincipals.put(sessionId, principal); - } - - /** - * Counts the number of sessions in use by the given principal - * - * @param principal The principal object - * @return 0 if there are no sessions, > if there are any - */ - protected int countSessions(Object principal) { - Set set = (Set) principalsToSessions.get(principal); - - if (set == null) { - return 0; - } - - return set.size(); - } - - /** - * Checks to see if the Authentication principal is of type UserDetails. If - * it is then the {@link net.sf.acegisecurity.UserDetails#getUsername()} - * is returned. Otherwise Authentication.getPrincipal().toString() is - * returned. Subclasses can override this method to provide a more - * specific implementation. - * - * @param auth The Authentication in question - * @return The principal to be used as the key against sessions - */ - protected Object determineSessionPrincipal(Authentication auth) { - if (auth.getPrincipal() instanceof UserDetails) { - return ((UserDetails) auth.getPrincipal()).getUsername(); - } else { - return auth.getPrincipal().toString(); - } - } - - /** - * Called by both the beforeAuthentication and afterAuthentication methods. - * Anonymous requests as determined by the configured {@link - * AuthenticationTrustResolver} are ignored. If the details are - * WebAuthenticationDetails, get the sessionId and and the principal off - * of the authentication using the {@link - * #determineSessionPrincipal(net.sf.acegisecurity.Authentication)} - * method. Uses the sessionId and principal to determine if the session - * is new, and if the user is already at the maxSessions value. Subclasses - * may override for more specific functionality - * - * @param request Authentication being evaluated - * @throws ConcurrentLoginException If the session is new, and the user is - * already at maxSessions - */ - protected void enforceConcurrentLogins(Authentication request) - throws ConcurrentLoginException { - //If the max is less than 1, sessions are unlimited - if (maxSessions < 1) { - return; - } - - //If it is an anonymous user, ignore them - if (trustResolver.isAnonymous(request)) { - return; - } - - if (request.getDetails() instanceof WebAuthenticationDetails) { - String sessionId = ((WebAuthenticationDetails) request.getDetails()) - .getSessionId(); - - Object principal = determineSessionPrincipal(request); - - if (!isActiveSession(principal, sessionId)) { - if (maxSessions == countSessions(principal)) { - //Publish the event - publishViolationEvent(request); - - //The user is AT their max, toss them out - throw new ConcurrentLoginException(principal - + " has reached the maximum concurrent logins"); - } - } - } - } - - /** - * Publish the even to the application context. - * The default action is to publish a new {@link ConcurrentSessionViolationEvent} - * - * @param auth The authentication object that caused the violation - */ - protected void publishViolationEvent(Authentication auth) { - getApplicationContext().publishEvent(new ConcurrentSessionViolationEvent(auth)); - } - - /** - * Remove the given sessionId from storage. Used by {@link - * #onApplicationEvent(org.springframework.context.ApplicationEvent)} for - * HttpSessionDestroyedEvent - * - * @param sessionId - */ - protected void removeSession(String sessionId) { - // find out which principal is associated with this sessionId - Object associatedPrincipal = sessionsToPrincipals.get(sessionId); - - if (associatedPrincipal != null) { - Set sessions = (Set) principalsToSessions.get(associatedPrincipal); - sessions.remove(sessionId); - - if (sessions.isEmpty()) { - principalsToSessions.remove(associatedPrincipal); - } - - sessionsToPrincipals.remove(sessionId); - } - } -} diff --git a/core/src/main/java/org/acegisecurity/providers/ConcurrentSessionViolationEvent.java b/core/src/main/java/org/acegisecurity/providers/ConcurrentSessionViolationEvent.java deleted file mode 100644 index dd55dc3bb0..0000000000 --- a/core/src/main/java/org/acegisecurity/providers/ConcurrentSessionViolationEvent.java +++ /dev/null @@ -1,42 +0,0 @@ -/* Copyright 2004, 2005 Acegi Technology Pty Limited - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.sf.acegisecurity.providers; - -import net.sf.acegisecurity.Authentication; - -import org.springframework.context.ApplicationEvent; - - -/** - * Published by the ConcurrentSessionControllerImpl to notify the application - * context that a user has attempted to login more than the maximum times - * allowed by the {@link ConcurrentSessionControllerImpl#setMaxSessions(int)} - * - * @author Ray Krueger - */ -public class ConcurrentSessionViolationEvent extends ApplicationEvent { - //~ Constructors =========================================================== - - public ConcurrentSessionViolationEvent(Authentication auth) { - super(auth); - } - - //~ Methods ================================================================ - - public Authentication getAuthentication() { - return (Authentication) getSource(); - } -} diff --git a/core/src/main/java/org/acegisecurity/providers/NullConcurrentSessionController.java b/core/src/main/java/org/acegisecurity/providers/NullConcurrentSessionController.java deleted file mode 100644 index e84e8b6f4c..0000000000 --- a/core/src/main/java/org/acegisecurity/providers/NullConcurrentSessionController.java +++ /dev/null @@ -1,41 +0,0 @@ -/* Copyright 2004, 2005 Acegi Technology Pty Limited - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.sf.acegisecurity.providers; - -import net.sf.acegisecurity.Authentication; -import net.sf.acegisecurity.AuthenticationException; - - -/** - * Do nothing implementation of ConcurrentSessionController. This is the {@link ProviderManager} default - * - * @author Ray Krueger - * @see ConcurrentSessionControllerImpl - */ -public class NullConcurrentSessionController - implements ConcurrentSessionController { - //~ Methods ================================================================ - - public void afterAuthentication(Authentication initialAuth, - Authentication result) throws AuthenticationException { - //Do nothing - } - - public void beforeAuthentication(Authentication initialAuth) - throws AuthenticationException { - //Do nothing - } -} diff --git a/core/src/main/java/org/acegisecurity/providers/ProviderManager.java b/core/src/main/java/org/acegisecurity/providers/ProviderManager.java index ef01b7445b..6dcf70bd8d 100644 --- a/core/src/main/java/org/acegisecurity/providers/ProviderManager.java +++ b/core/src/main/java/org/acegisecurity/providers/ProviderManager.java @@ -18,6 +18,8 @@ package net.sf.acegisecurity.providers; import net.sf.acegisecurity.AbstractAuthenticationManager; import net.sf.acegisecurity.Authentication; import net.sf.acegisecurity.AuthenticationException; +import net.sf.acegisecurity.concurrent.ConcurrentSessionController; +import net.sf.acegisecurity.concurrent.NullConcurrentSessionController; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -146,7 +148,7 @@ public class ProviderManager extends AbstractAuthenticationManager Class toTest = authentication.getClass(); - sessionController.beforeAuthentication(authentication); + sessionController.checkAuthenticationAllowed(authentication); while (iter.hasNext()) { AuthenticationProvider provider = (AuthenticationProvider) iter @@ -159,7 +161,7 @@ public class ProviderManager extends AbstractAuthenticationManager Authentication result = provider.authenticate(authentication); if (result != null) { - sessionController.afterAuthentication(authentication, result); + sessionController.registerSuccessfulAuthentication(result); return result; } diff --git a/core/src/test/java/org/acegisecurity/concurrent/ConcurrentSessionControllerImplTests.java b/core/src/test/java/org/acegisecurity/concurrent/ConcurrentSessionControllerImplTests.java new file mode 100644 index 0000000000..4ed6f2de97 --- /dev/null +++ b/core/src/test/java/org/acegisecurity/concurrent/ConcurrentSessionControllerImplTests.java @@ -0,0 +1,124 @@ +/* Copyright 2004, 2005 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity.concurrent; + +import junit.framework.TestCase; + +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken; +import net.sf.acegisecurity.ui.WebAuthenticationDetails; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpSession; + + +/** + * Tests {@link ConcurrentSessionControllerImpl}. + * + * @author Ben Alex + * @version $Id$ + */ +public class ConcurrentSessionControllerImplTests extends TestCase { + //~ Methods ================================================================ + + public void testLifecycle() throws Exception { + // Build a test fixture + ConcurrentSessionControllerImpl sc = new ConcurrentSessionControllerImpl(); + SessionRegistry registry = new SessionRegistryImpl(); + sc.setSessionRegistry(registry); + + // Attempt to authenticate - it should be successful + Authentication auth = createAuthentication("bob", "1212"); + sc.checkAuthenticationAllowed(auth); + sc.registerSuccessfulAuthentication(auth); + + String sessionId1 = ((WebAuthenticationDetails) auth.getDetails()) + .getSessionId(); + assertFalse(registry.getSessionInformation(sessionId1).isExpired()); + + // Attempt to authenticate again - it should still be successful + sc.checkAuthenticationAllowed(auth); + sc.registerSuccessfulAuthentication(auth); + + // Attempt to authenticate with a different session for same principal - should fail + sc.setExceptionIfMaximumExceeded(true); + + Authentication auth2 = createAuthentication("bob", "1212"); + assertFalse(registry.getSessionInformation(sessionId1).isExpired()); + + try { + sc.checkAuthenticationAllowed(auth2); + fail("Should have thrown ConcurrentLoginException"); + } catch (ConcurrentLoginException expected) { + assertTrue(true); + } + + // Attempt to authenticate with a different session for same principal - should expire first session + sc.setExceptionIfMaximumExceeded(false); + + Authentication auth3 = createAuthentication("bob", "1212"); + sc.checkAuthenticationAllowed(auth3); + sc.registerSuccessfulAuthentication(auth3); + + String sessionId3 = ((WebAuthenticationDetails) auth3.getDetails()) + .getSessionId(); + assertTrue(registry.getSessionInformation(sessionId1).isExpired()); + assertFalse(registry.getSessionInformation(sessionId3).isExpired()); + } + + public void testStartupDetectsInvalidMaximumSessions() + throws Exception { + ConcurrentSessionControllerImpl sc = new ConcurrentSessionControllerImpl(); + sc.setMaximumSessions(0); + + try { + sc.afterPropertiesSet(); + fail("Should have thrown IAE"); + } catch (IllegalArgumentException expected) { + assertTrue(true); + } + } + + public void testStartupDetectsInvalidSessionRegistry() + throws Exception { + ConcurrentSessionControllerImpl sc = new ConcurrentSessionControllerImpl(); + sc.setSessionRegistry(null); + + try { + sc.afterPropertiesSet(); + fail("Should have thrown IAE"); + } catch (IllegalArgumentException expected) { + assertTrue(true); + } + } + + private Authentication createAuthentication(String user, String password) { + UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, + password); + auth.setDetails(createWebDetails(auth)); + + return auth; + } + + private WebAuthenticationDetails createWebDetails(Authentication auth) { + MockHttpSession session = new MockHttpSession(); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setSession(session); + request.setUserPrincipal(auth); + + return new WebAuthenticationDetails(request); + } +} diff --git a/core/src/test/java/org/acegisecurity/concurrent/ConcurrentSessionFilterTests.java b/core/src/test/java/org/acegisecurity/concurrent/ConcurrentSessionFilterTests.java new file mode 100644 index 0000000000..79c66e0268 --- /dev/null +++ b/core/src/test/java/org/acegisecurity/concurrent/ConcurrentSessionFilterTests.java @@ -0,0 +1,173 @@ +/* Copyright 2004, 2005 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity.concurrent; + +import junit.framework.TestCase; + +import org.springframework.mock.web.MockFilterConfig; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; + +import java.io.IOException; + +import java.util.Date; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + + +/** + * Tests {@link ConcurrentSessionFilter}. + * + * @author Ben Alex + * @version $Id$ + */ +public class ConcurrentSessionFilterTests extends TestCase { + //~ Constructors =========================================================== + + public ConcurrentSessionFilterTests() { + super(); + } + + public ConcurrentSessionFilterTests(String arg0) { + super(arg0); + } + + //~ Methods ================================================================ + + public static void main(String[] args) { + junit.textui.TestRunner.run(ConcurrentSessionFilterTests.class); + } + + public void testDetectsExpiredSessions() throws Exception { + // Setup our HTTP request + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpSession session = new MockHttpSession(); + request.setSession(session); + + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterConfig config = new MockFilterConfig(null); + + // Setup our expectation that the filter chain will not be invoked, as we redirect to expiredUrl + MockFilterChain chain = new MockFilterChain(false); + + // Setup our test fixture and registry to want this session to be expired + ConcurrentSessionFilter filter = new ConcurrentSessionFilter(); + SessionRegistry registry = new SessionRegistryImpl(); + registry.registerNewSession(session.getId(), "principal"); + registry.getSessionInformation(session.getId()).expireNow(); + filter.setSessionRegistry(registry); + filter.setExpiredUrl("/expired.jsp"); + + // Test + executeFilterInContainerSimulator(config, filter, request, response, + chain); + + assertEquals("/expired.jsp", response.getRedirectedUrl()); + } + + public void testDetectsMissingExpiredUrl() throws Exception { + ConcurrentSessionFilter filter = new ConcurrentSessionFilter(); + filter.setSessionRegistry(new SessionRegistryImpl()); + + try { + filter.afterPropertiesSet(); + fail("Should have thrown IAE"); + } catch (IllegalArgumentException expected) { + assertTrue(true); + } + } + + public void testDetectsMissingSessionRegistry() throws Exception { + ConcurrentSessionFilter filter = new ConcurrentSessionFilter(); + filter.setExpiredUrl("xcx"); + + try { + filter.afterPropertiesSet(); + fail("Should have thrown IAE"); + } catch (IllegalArgumentException expected) { + assertTrue(true); + } + } + + public void testUpdatesLastRequestTime() throws Exception { + // Setup our HTTP request + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpSession session = new MockHttpSession(); + request.setSession(session); + + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterConfig config = new MockFilterConfig(null); + + // Setup our expectation that the filter chain will be invoked, as our session hasn't expired + MockFilterChain chain = new MockFilterChain(true); + + // Setup our test fixture + ConcurrentSessionFilter filter = new ConcurrentSessionFilter(); + SessionRegistry registry = new SessionRegistryImpl(); + registry.registerNewSession(session.getId(), "principal"); + + Date lastRequest = registry.getSessionInformation(session.getId()) + .getLastRequest(); + filter.setSessionRegistry(registry); + filter.setExpiredUrl("/expired.jsp"); + + Thread.sleep(1000); + + // Test + executeFilterInContainerSimulator(config, filter, request, response, + chain); + + assertTrue(registry.getSessionInformation(session.getId()) + .getLastRequest().after(lastRequest)); + } + + private void executeFilterInContainerSimulator(FilterConfig filterConfig, + Filter filter, ServletRequest request, ServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + filter.init(filterConfig); + filter.doFilter(request, response, filterChain); + filter.destroy(); + } + + //~ Inner Classes ========================================================== + + private class MockFilterChain implements FilterChain { + private boolean expectToProceed; + + public MockFilterChain(boolean expectToProceed) { + this.expectToProceed = expectToProceed; + } + + private MockFilterChain() { + super(); + } + + public void doFilter(ServletRequest request, ServletResponse response) + throws IOException, ServletException { + if (expectToProceed) { + assertTrue(true); + } else { + fail("Did not expect filter chain to proceed"); + } + } + } +} diff --git a/core/src/test/java/org/acegisecurity/concurrent/SessionInformationTests.java b/core/src/test/java/org/acegisecurity/concurrent/SessionInformationTests.java new file mode 100644 index 0000000000..31791f82bd --- /dev/null +++ b/core/src/test/java/org/acegisecurity/concurrent/SessionInformationTests.java @@ -0,0 +1,49 @@ +/* Copyright 2004, 2005 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity.concurrent; + +import junit.framework.TestCase; + +import java.util.Date; + + +/** + * Tests {@link SessionInformation}. + * + * @author Ben Alex + * @version $Id$ + */ +public class SessionInformationTests extends TestCase { + //~ Methods ================================================================ + + public void testObject() throws Exception { + Object principal = "Some principal object"; + String sessionId = "1234567890"; + Date currentDate = new Date(); + + SessionInformation info = new SessionInformation(principal, sessionId, + currentDate); + assertEquals(principal, info.getPrincipal()); + assertEquals(sessionId, info.getSessionId()); + assertEquals(currentDate, info.getLastRequest()); + + Thread.sleep(1000); + + info.refreshLastRequest(); + + assertTrue(info.getLastRequest().after(currentDate)); + } +} diff --git a/core/src/test/java/org/acegisecurity/concurrent/SessionRegistryImplTests.java b/core/src/test/java/org/acegisecurity/concurrent/SessionRegistryImplTests.java new file mode 100644 index 0000000000..545c82c338 --- /dev/null +++ b/core/src/test/java/org/acegisecurity/concurrent/SessionRegistryImplTests.java @@ -0,0 +1,155 @@ +/* Copyright 2004, 2005 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity.concurrent; + +import junit.framework.TestCase; + +import net.sf.acegisecurity.ui.session.HttpSessionDestroyedEvent; + +import org.springframework.mock.web.MockHttpSession; + +import java.util.Date; + + +/** + * Tests {@link SessionRegistryImpl}. + * + * @author Ben Alex + * @version $Id$ + */ +public class SessionRegistryImplTests extends TestCase { + //~ Methods ================================================================ + + public void testEventPublishing() { + MockHttpSession httpSession = new MockHttpSession(); + Object principal = "Some principal object"; + String sessionId = httpSession.getId(); + assertNotNull(sessionId); + + SessionRegistryImpl sessionRegistry = new SessionRegistryImpl(); + + // Register new Session + sessionRegistry.registerNewSession(sessionId, principal); + + // Deregister session via an ApplicationEvent + sessionRegistry.onApplicationEvent(new HttpSessionDestroyedEvent( + httpSession)); + + // Check attempts to retrieve cleared session return null + assertNull(sessionRegistry.getSessionInformation(sessionId)); + } + + public void testSessionInformationLifecycle() throws Exception { + Object principal = "Some principal object"; + String sessionId = "1234567890"; + SessionRegistryImpl sessionRegistry = new SessionRegistryImpl(); + + // Register new Session + sessionRegistry.registerNewSession(sessionId, principal); + + // Retrieve existing session by session ID + Date currentDateTime = sessionRegistry.getSessionInformation(sessionId) + .getLastRequest(); + assertEquals(principal, + sessionRegistry.getSessionInformation(sessionId).getPrincipal()); + assertEquals(sessionId, + sessionRegistry.getSessionInformation(sessionId).getSessionId()); + assertNotNull(sessionRegistry.getSessionInformation(sessionId) + .getLastRequest()); + + // Retrieve existing session by principal + assertEquals(1, sessionRegistry.getAllSessions(principal).length); + + // Sleep to ensure SessionRegistryImpl will update time + Thread.sleep(1000); + + // Update request date/time + sessionRegistry.refreshLastRequest(sessionId); + + Date retrieved = sessionRegistry.getSessionInformation(sessionId) + .getLastRequest(); + assertTrue(retrieved.after(currentDateTime)); + + // Check it retrieves correctly when looked up via principal + assertEquals(retrieved, + sessionRegistry.getAllSessions(principal)[0].getLastRequest()); + + // Clear session information + sessionRegistry.removeSessionInformation(sessionId); + + // Check attempts to retrieve cleared session return null + assertNull(sessionRegistry.getSessionInformation(sessionId)); + assertNull(sessionRegistry.getAllSessions(principal)); + } + + public void testTwoSessionsOnePrincipalHandling() throws Exception { + Object principal = "Some principal object"; + String sessionId1 = "1234567890"; + String sessionId2 = "9876543210"; + SessionRegistryImpl sessionRegistry = new SessionRegistryImpl(); + + // Register new Session + sessionRegistry.registerNewSession(sessionId1, principal); + assertEquals(1, sessionRegistry.getAllSessions(principal).length); + assertEquals(sessionId1, + sessionRegistry.getAllSessions(principal)[0].getSessionId()); + + // Register new Session + sessionRegistry.registerNewSession(sessionId2, principal); + assertEquals(2, sessionRegistry.getAllSessions(principal).length); + assertEquals(sessionId2, + sessionRegistry.getAllSessions(principal)[1].getSessionId()); + + // Clear session information + sessionRegistry.removeSessionInformation(sessionId1); + assertEquals(1, sessionRegistry.getAllSessions(principal).length); + assertEquals(sessionId2, + sessionRegistry.getAllSessions(principal)[0].getSessionId()); + + // Clear final session + sessionRegistry.removeSessionInformation(sessionId2); + assertNull(sessionRegistry.getSessionInformation(sessionId2)); + assertNull(sessionRegistry.getAllSessions(principal)); + } + + public void testTwoSessionsOnePrincipalExpiring() throws Exception { + Object principal = "Some principal object"; + String sessionId1 = "1234567890"; + String sessionId2 = "9876543210"; + SessionRegistryImpl sessionRegistry = new SessionRegistryImpl(); + + // Register new Session + sessionRegistry.registerNewSession(sessionId1, principal); + assertEquals(1, sessionRegistry.getAllSessions(principal).length); + assertEquals(sessionId1, + sessionRegistry.getAllSessions(principal)[0].getSessionId()); + + // Register new Session + sessionRegistry.registerNewSession(sessionId2, principal); + assertEquals(2, sessionRegistry.getAllSessions(principal).length); + assertEquals(sessionId2, + sessionRegistry.getAllSessions(principal)[1].getSessionId()); + + // Expire one session + SessionInformation session = sessionRegistry.getSessionInformation(sessionId2); + session.expireNow(); + + // Check retrieval still correct + assertTrue(sessionRegistry.getSessionInformation(sessionId2).isExpired()); + assertFalse(sessionRegistry.getSessionInformation(sessionId1).isExpired()); + } + +} diff --git a/core/src/test/java/org/acegisecurity/providers/ConcurrentSessionControllerImplTests.java b/core/src/test/java/org/acegisecurity/providers/ConcurrentSessionControllerImplTests.java deleted file mode 100644 index 3bb9d64af4..0000000000 --- a/core/src/test/java/org/acegisecurity/providers/ConcurrentSessionControllerImplTests.java +++ /dev/null @@ -1,280 +0,0 @@ -/* Copyright 2004, 2005 Acegi Technology Pty Limited - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.sf.acegisecurity.providers; - -import junit.framework.TestCase; - - -import net.sf.acegisecurity.providers.anonymous.AnonymousAuthenticationToken; -import net.sf.acegisecurity.providers.dao.User; -import net.sf.acegisecurity.ui.WebAuthenticationDetails; -import net.sf.acegisecurity.ui.session.HttpSessionCreatedEvent; -import net.sf.acegisecurity.ui.session.HttpSessionDestroyedEvent; -import net.sf.acegisecurity.GrantedAuthority; -import net.sf.acegisecurity.GrantedAuthorityImpl; -import net.sf.acegisecurity.Authentication; -import net.sf.acegisecurity.UserDetails; -import net.sf.acegisecurity.AuthenticationTrustResolverImpl; -import net.sf.acegisecurity.MockApplicationContext; - -import org.springframework.context.ApplicationListener; -import org.springframework.mock.web.MockHttpSession; -import org.springframework.mock.web.MockHttpServletRequest; - -import java.security.Principal; - - -/** - * Tests for {@link ConcurrentSessionControllerImpl} - * - * @author Ray Krueger - * @author Luke Taylor - * @version $Id$ - */ -public class ConcurrentSessionControllerImplTests extends TestCase { - //~ Instance fields ======================================================== - - ConcurrentSessionControllerImpl target; - - //~ Methods ================================================================ - - public void testAnonymous() throws Exception { - AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken("blah", - "anon", - new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ANON")}); - target.beforeAuthentication(auth); - target.afterAuthentication(auth, auth); - } - - public void testBumpCoverage() throws Exception { - target.onApplicationEvent(new HttpSessionCreatedEvent( - new MockHttpSession())); - } - - public void testEnforcementKnownGood() throws Exception { - Authentication auth = createAuthentication("user", "password"); - target.beforeAuthentication(auth); - target.afterAuthentication(auth, auth); - } - - public void testEnforcementMultipleSessions() throws Exception { - target.setMaxSessions(5); - - Authentication auth = null; - - for (int i = 0; i < 5; i++) { // creates 5 sessions - auth = createAuthentication("user", "password"); - target.beforeAuthentication(auth); - target.afterAuthentication(auth, auth); - } - - try { - auth = createAuthentication("user", "password"); - target.beforeAuthentication(auth); - fail( - "Only allowed 5 sessions, this should have thrown a ConcurrentLoginException"); - } catch (ConcurrentLoginException e) { - assertTrue(e.getMessage().startsWith(auth.getPrincipal().toString())); - } - } - - public void testEnforcementSingleSession() throws Exception { - target.setMaxSessions(1); - - Authentication auth = createAuthentication("user", "password"); - - target.beforeAuthentication(auth); - target.afterAuthentication(auth, auth); - - try { - target.beforeAuthentication(createAuthentication("user", "password")); - fail( - "Only allowed 1 session, this should have thrown a ConcurrentLoginException"); - } catch (ConcurrentLoginException e) {} - } - - public void testEnforcementUnlimitedSameSession() throws Exception { - target.setMaxSessions(1); - MockHttpSession session = new MockHttpSession(); // all requests are within this session - - for (int i = 0; i < 100; i++) { - UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("user", - "password"); - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setSession(session); - request.setUserPrincipal(auth); - auth.setDetails(new WebAuthenticationDetails(request)); - target.beforeAuthentication(auth); - target.afterAuthentication(auth, auth); - } - } - - public void testEnforcementUnlimitedSessions() throws Exception { - target.setMaxSessions(0); - - for (int i = 0; i < 100; i++) { - Authentication auth = createAuthentication("user", "password"); - target.beforeAuthentication(auth); - target.afterAuthentication(auth, auth); - } - } - - public void testEventHandler() throws Exception { - target.setMaxSessions(1); - - UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("user", - "password"); - MockHttpSession session = new MockHttpSession(); - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setSession(session); - request.setUserPrincipal(auth); - auth.setDetails(new WebAuthenticationDetails(request)); - - target.beforeAuthentication(auth); - target.afterAuthentication(auth, auth); - - target.onApplicationEvent(new HttpSessionDestroyedEvent(session)); - - Authentication different = createAuthentication("user", "password"); - target.beforeAuthentication(different); - target.afterAuthentication(different, different); - } - - public void testEventObject() { - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("user", - "password"); - ConcurrentSessionViolationEvent ev = new ConcurrentSessionViolationEvent(token); - assertEquals("The token that went in should be the token that comes out", - token, ev.getAuthentication()); - } - - public void testImplementsApplicationListener() throws Exception { - assertTrue("This class must implement ApplicationListener, and at one point it didn't.", - target instanceof ApplicationListener); - } - - public void testNonWebDetails() throws Exception { - UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("asdf", - "asdf"); - auth.setDetails("Hi there"); - target.beforeAuthentication(auth); - target.afterAuthentication(auth, auth); - } - - public void testPrincipals() throws Exception { - target.setMaxSessions(1); - - final UserDetails user = new User("user", "password", true, true, true, - true, new GrantedAuthority[0]); - final UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, - "password", user.getAuthorities()); - auth.setDetails(createWebDetails(auth)); - - target.beforeAuthentication(auth); - target.afterAuthentication(auth, auth); - - try { - UsernamePasswordAuthenticationToken otherAuth = new UsernamePasswordAuthenticationToken(new Principal() { - public String getName() { - return "user"; - } - - public String toString() { - return getName(); - } - }, "password"); - - otherAuth.setDetails(createWebDetails(otherAuth)); - target.beforeAuthentication(otherAuth); - fail( - "Same principal, different principal type, different session should have thrown ConcurrentLoginException"); - } catch (ConcurrentLoginException e) {} - } - - public void testSetMax() throws Exception { - target.setMaxSessions(1); - assertEquals(1, target.getMaxSessions()); - - target.setMaxSessions(2); - assertEquals(2, target.getMaxSessions()); - } - - public void testSetTrustManager() throws Exception { - assertNotNull("There is supposed to be a default AuthenticationTrustResolver", - target.getTrustResolver()); - - AuthenticationTrustResolverImpl impl = new AuthenticationTrustResolverImpl(); - target.setTrustResolver(impl); - assertEquals(impl, target.getTrustResolver()); - } - - public void testUtilityMethods() throws Exception { - Object key = new Object(); - - target.addSession(key, "1"); - target.addSession(key, "2"); - target.addSession(key, "3"); - - target.removeSession("2"); - - assertFalse(target.isActiveSession(key, "2")); - assertTrue(target.isActiveSession(key, "1")); - assertTrue(target.isActiveSession(key, "3")); - - assertNull(target.sessionsToPrincipals.get("2")); - - assertEquals(2, target.countSessions(key)); - target.addSession(key, "2"); - assertEquals(3, target.countSessions(key)); - - target.addSession(key, "2"); - target.addSession(key, "2"); - assertEquals(3, target.countSessions(key)); - - assertTrue(target.isActiveSession(key, "1")); - assertTrue(target.isActiveSession(key, "2")); - assertTrue(target.isActiveSession(key, "3")); - - assertFalse(target.isActiveSession(key, "nope")); - - assertFalse(target.isActiveSession(new Object(), "1")); - assertFalse(target.isActiveSession(new Object(), "1")); - - target.removeSession("nothing to see here"); - } - - protected void setUp() throws Exception { - target = new ConcurrentSessionControllerImpl(); - target.setApplicationContext(MockApplicationContext.getContext()); - } - - private Authentication createAuthentication(String user, String password) { - UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, - password); - auth.setDetails(createWebDetails(auth)); - - return auth; - } - - private WebAuthenticationDetails createWebDetails(Authentication auth) { - MockHttpSession session = new MockHttpSession(); - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setSession(session); - request.setUserPrincipal(auth); - - return new WebAuthenticationDetails(request); - } -} diff --git a/core/src/test/java/org/acegisecurity/providers/ProviderManagerTests.java b/core/src/test/java/org/acegisecurity/providers/ProviderManagerTests.java index 2b7fcdf13f..4d535d4f2b 100644 --- a/core/src/test/java/org/acegisecurity/providers/ProviderManagerTests.java +++ b/core/src/test/java/org/acegisecurity/providers/ProviderManagerTests.java @@ -18,6 +18,8 @@ package net.sf.acegisecurity.providers; import junit.framework.TestCase; import net.sf.acegisecurity.*; +import net.sf.acegisecurity.concurrent.ConcurrentSessionControllerImpl; +import net.sf.acegisecurity.concurrent.NullConcurrentSessionController; import java.util.List; import java.util.Vector;