/*
* RHQ Management Platform
* Copyright (C) 2005-2008 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 2 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.rhq.enterprise.server.auth;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import org.rhq.core.domain.auth.Subject;
import org.rhq.enterprise.server.AllowRhqServerInternalsAccessPermission;
import org.rhq.enterprise.server.util.LookupUtil;
/**
* This is the JON Server's own session ID generator. It is outside any container-provided session mechanism. Its sole
* purpose is to provide session IDs to logged in {@link Subject}s. It will timeout those sessions regardless of any
* container-provided session-timeout mechanism.
* <p>
* Because this is a very security-sensitive class, any public method requires the caller to
* have the {@link AllowEjbAccessPermission} as any other calls to the EJB layer. This is so that the
* malicious users can't trick the EJB layer into thinking that some users are logged in or log out other
* users.
* <p>
* Also, for security reasons, this class is final so that malicious code can't subclass it and modify its
* behavior.
*
* <p>This object is a {@link #getInstance() singleton}.</p>
*/
public final class SessionManager {
private static final AllowRhqServerInternalsAccessPermission ACCESS_PERMISSION = new AllowRhqServerInternalsAccessPermission();
/**
* Our source for random session IDs.
*/
private static final Random _random = new Random();
/**
* Our session cache that is keyed on the session ID.
*/
private static final Map<Integer, AuthSession> _cache = new HashMap<Integer, AuthSession>();
/**
* The singleton instance
*/
private static final SessionManager _manager = new SessionManager();
/**
* The timeout for all user sessions.
*/
private static final long DEFAULT_TIMEOUT = 1000 * 60 * 90;
/**
* The timeout for overlord sessions.
*/
private static final long OVERLORD_TIMEOUT = 1000 * 60 * 2;
/**
* We know that our overlord user always has a subject ID of this value.
*/
private static final int OVERLORD_SUBJECT_ID = 1;
/**
* The overlord never, ever gets updated - it is static and the same forever, so we cache it here.
*/
private static Subject overlordSubject = null;
/**
* Default private constructor to prevent instantiation.
*/
private SessionManager() {
}
/**
* Return the singleton object.
*
* @return the {@link SessionManager}
*/
public static SessionManager getInstance() {
return _manager;
}
/**
* Returns the number of sessions that are currently held by this manager.
* This count includes those sessions that may have already timed out but not yet invalidated and purged.
*
* @return total number of sessions
*/
public synchronized int getSessionCount() {
return _cache.size();
}
/**
* Associates a {@link Subject} with a new session id. The new session will use the
* {@link #DEFAULT_TIMEOUT default timeout}.
*
* @param subject
*
* @return the Subject associated with session. Note, this may be a copy of the Subject passed into the method. The
* sessionId will be assigned.
*/
public Subject put(Subject subject) {
checkPermission();
return put(subject, DEFAULT_TIMEOUT);
}
/**
* Associates a {@link Subject} with a new session id with the given session timeout.
*
* @param subject
* @param timeout the timeout for the session, in milliseconds
*
* @return the Subject associated with session. This will be a copy of the Subject passed into the method (unless
* that Subject is overlord). The sessionId will be assigned.
*/
public synchronized Subject put(Subject subject, long timeout) {
checkPermission();
Integer key;
do {
key = new Integer(_random.nextInt());
} while (_cache.containsKey(key));
// Each session should have its own POJO Subject so that each can store a separate sessionId. The exception
// is our special-case shared singleton for overlord.
Subject sessionSubject;
if (subject.equals(overlordSubject)) {
sessionSubject = overlordSubject;
sessionSubject.setSessionId(key);
} else {
sessionSubject = getSessionSubject(subject, key);
}
_cache.put(key, new AuthSession(sessionSubject, timeout));
return sessionSubject;
}
/**
* Returns the {@link Subject} associated with the given session id.
*
* @param sessionId The session id
*
* @return the {@link Subject} associated with the session id
*
* @throws SessionNotFoundException
* @throws SessionTimeoutException
*/
public synchronized Subject getSubject(int sessionId) throws SessionNotFoundException, SessionTimeoutException {
checkPermission();
Integer id = new Integer(sessionId);
AuthSession session = _cache.get(id);
if (session == null) {
throw new SessionNotFoundException();
}
if (session.isExpired()) {
invalidate(sessionId);
throw new SessionTimeoutException();
}
return session.getSubject(true);
}
/**
* Invalidates the session associated with the given session ID.
*
* @param sessionId session id to invalidate
*/
public synchronized void invalidate(int sessionId) {
checkPermission();
Integer sessionIdObj = new Integer(sessionId);
// we currently use a shared session for overlord. don't log it out as it could affect another caller's use
// of the overlord user. The session will expire if unused by any caller for the overlord timeout period.
AuthSession session = _cache.get(sessionIdObj);
if (session != null) {
Subject doomedSubject = session.getSubject(false);
if (doomedSubject.getId() == OVERLORD_SUBJECT_ID) {
return;
}
}
_cache.remove(sessionIdObj);
// while we are here, let's go through the entire session cache and remove expired sessions
purgeTimedOutSessions();
return;
}
/**
* Asks the session manager to examine all sessions and invalidate those sessions that have timed out.
*/
public synchronized void purgeTimedOutSessions() {
checkPermission();
Iterator<Entry<Integer, AuthSession>> iterator = _cache.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Integer, AuthSession> map_entry = iterator.next();
AuthSession session = map_entry.getValue();
if (session.isExpired()) {
iterator.remove();
}
}
return;
}
/**
* Invalidates all sessions for the given username. This is for testing purposes ONLY.
*
* @param username username for the sessions to be invalidated
*/
public synchronized void invalidate(String username) {
checkPermission();
Iterator<Entry<Integer, AuthSession>> iterator = _cache.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Integer, AuthSession> map_entry = iterator.next();
AuthSession session = map_entry.getValue();
if (username.equals(session.getSubject(false).getName())) {
iterator.remove();
}
}
return;
}
public synchronized long getLastAccess(int sessionId) {
checkPermission();
AuthSession session = _cache.get(sessionId);
if (session == null) {
return -1;
}
return session.getLastAccess();
}
public synchronized Subject getOverlord() {
checkPermission();
if (overlordSubject == null) {
overlordSubject = LookupUtil.getSubjectManager().getSubjectById(OVERLORD_SUBJECT_ID);
if (overlordSubject == null) {
String err = "Cannot find the system's superuser - the database might be corrupted";
throw new IllegalStateException(err);
}
put(overlordSubject, OVERLORD_TIMEOUT);
}
int session_id = overlordSubject.getSessionId().intValue();
try {
// validate that the superuser session is still valid and update its LAT
getSubject(session_id);
} catch (SessionException e) {
// its been a while since the overlord has been needed - its session has expired.
// We need to create a new session and assign that new session ID to the instance this singleton holds internally
// no need to synchronize here - its OK if we concurrently create more than one session, they will eventually expire
session_id = put(overlordSubject, OVERLORD_TIMEOUT).getSessionId();
overlordSubject.setSessionId(session_id);
}
// we create a separate and detached Subject for each caller - do not share the copy this singleton holds internally
Subject copy = getSessionSubject(overlordSubject, session_id);
return copy;
}
private Subject getSessionSubject(Subject subject, Integer sessionId) {
Subject copy = new Subject(subject.getName(), subject.getFactive(), subject.getFsystem());
copy.setId(subject.getId());
copy.setSessionId(sessionId);
copy.setDepartment(subject.getDepartment());
copy.setEmailAddress(subject.getEmailAddress());
copy.setFirstName(subject.getFirstName());
copy.setLastName(subject.getLastName());
copy.setLdapRoles(subject.getLdapRoles());
copy.setOwnedGroups(subject.getOwnedGroups());
copy.setPhoneNumber(subject.getPhoneNumber());
copy.setRoles(subject.getRoles());
copy.setUserConfiguration(subject.getUserConfiguration());
return copy;
}
private static void checkPermission() {
SecurityManager sm = System.getSecurityManager();
if (sm != null)
sm.checkPermission(ACCESS_PERMISSION);
}
}