blob: 87d165c37ac73ffd75c7c646eeb949a6c322cf37 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.jsecurity.mgt;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jsecurity.authc.*;
import org.jsecurity.authz.AuthorizationException;
import org.jsecurity.authz.Authorizer;
import org.jsecurity.crypto.Cipher;
import org.jsecurity.realm.Realm;
import org.jsecurity.session.InvalidSessionException;
import org.jsecurity.session.Session;
import org.jsecurity.session.mgt.DelegatingSession;
import org.jsecurity.subject.PrincipalCollection;
import org.jsecurity.subject.Subject;
import org.jsecurity.util.ThreadContext;
import java.io.Serializable;
import java.net.InetAddress;
import java.util.Collection;
/**
* <p>The JSecurity framework's default concrete implementation of the {@link SecurityManager} interface,
* based around a collection of {@link org.jsecurity.realm.Realm}s. This implementation delegates its
* authentication, authorization, and session operations to wrapped {@link Authenticator}, {@link Authorizer}, and
* {@link org.jsecurity.session.mgt.SessionManager SessionManager} instances respectively via superclass
* implementation.</p>
*
* <p>To greatly reduce and simplify configuration, this implementation (and its superclasses) will
* create suitable defaults for all of its required dependencies, <em>except</em> the required one or more
* {@link Realm Realm}s. Because <code>Realm</code> implementations usually interact with an application's data model,
* they are almost always application specific; you will want to specify at least one custom
* <tt>Realm</tt> implementation that 'knows' about your application's data/security model
* (via {@link #setRealm} or one of the overloaded constructors). All other attributes in this class hierarchy
* will have suitable defaults for most enterprise applications.</p>
*
* <p><b>RememberMe notice</b>: This class supports the ability to configure a
* {@link #setRememberMeManager RememberMeManager}
* for <tt>RememberMe</tt> identity services for login/logout, BUT, a default instance <em>will not</em> be created
* for this attribute at startup.
*
* <p>Because RememberMe services are inherently client tier-specific and
* therefore aplication-dependent, if you want <tt>RememberMe</tt> services enabled, you will have to specify an
* instance yourself via the {@link #setRememberMeManager(RememberMeManager) setRememberMeManager}
* mutator. However if you're reading this JavaDoc with the
* expectation of operating in a Web environment, take a look at the
* {@link org.jsecurity.web.DefaultWebSecurityManager DefaultWebSecurityManager} implementation, which
* <em>does</em> support <tt>RememberMe</tt> services by default at startup.
*
* @author Les Hazlewood
* @author Jeremy Haile
* @see org.jsecurity.web.DefaultWebSecurityManager
* @since 0.2
*/
public class DefaultSecurityManager extends SessionsSecurityManager {
//TODO - complete JavaDoc
private static final Log log = LogFactory.getLog(DefaultSecurityManager.class);
protected RememberMeManager rememberMeManager;
protected SubjectFactory subjectFactory;
protected SubjectBinder subjectBinder;
/**
* Default no-arg constructor.
*/
public DefaultSecurityManager() {
super();
this.subjectFactory = new DefaultSubjectFactory(this);
this.subjectBinder = new SessionSubjectBinder();
}
/**
* Supporting constructor for a single-realm application.
*
* @param singleRealm the single realm used by this SecurityManager.
*/
public DefaultSecurityManager(Realm singleRealm) {
this();
setRealm(singleRealm);
}
/**
* Supporting constructor for multiple {@link #setRealms realms}.
*
* @param realms the realm instances backing this SecurityManager.
*/
public DefaultSecurityManager(Collection<Realm> realms) {
this();
setRealms(realms);
}
public SubjectFactory getSubjectFactory() {
return subjectFactory;
}
public void setSubjectFactory(SubjectFactory subjectFactory) {
this.subjectFactory = subjectFactory;
if (this.subjectFactory instanceof SecurityManagerAware) {
((SecurityManagerAware) this.subjectFactory).setSecurityManager(this);
}
}
public SubjectBinder getSubjectBinder() {
return subjectBinder;
}
public void setSubjectBinder(SubjectBinder subjectBinder) {
this.subjectBinder = subjectBinder;
}
public RememberMeManager getRememberMeManager() {
return rememberMeManager;
}
public void setRememberMeManager(RememberMeManager rememberMeManager) {
this.rememberMeManager = rememberMeManager;
}
private AbstractRememberMeManager getRememberMeManagerForCipherAttributes() {
if (!(this.rememberMeManager instanceof AbstractRememberMeManager)) {
String msg = "The convenience passthrough methods for setting remember me cipher attributes " +
"are only available when the underlying RememberMeManager implementation is a subclass of " +
AbstractRememberMeManager.class.getName() + ".";
throw new IllegalStateException(msg);
}
return (AbstractRememberMeManager) this.rememberMeManager;
}
public void setRememberMeCipher(Cipher cipher) {
getRememberMeManagerForCipherAttributes().setCipher(cipher);
}
public void setRememberMeCipherKey(byte[] bytes) {
getRememberMeManagerForCipherAttributes().setCipherKey(bytes);
}
public void setRememberMeCipherKeyHex(String hex) {
getRememberMeManagerForCipherAttributes().setCipherKeyHex(hex);
}
public void setRememberMeCipherKeyBase64(String base64) {
getRememberMeManagerForCipherAttributes().setCipherKeyBase64(base64);
}
public void setRememberMeEncryptionCipherKey(byte[] bytes) {
getRememberMeManagerForCipherAttributes().setEncryptionCipherKey(bytes);
}
public void setRememberMeEncryptionCipherKeyHex(String hex) {
getRememberMeManagerForCipherAttributes().setEncryptionCipherKeyHex(hex);
}
public void setRememberMeEncryptionCipherKeyBase64(String base64) {
getRememberMeManagerForCipherAttributes().setEncryptionCipherKeyBase64(base64);
}
public void setRememberMeDecryptionCipherKey(byte[] bytes) {
getRememberMeManagerForCipherAttributes().setDecryptionCipherKey(bytes);
}
public void setRememberMeDecryptionCipherKeyHex(String hex) {
getRememberMeManagerForCipherAttributes().setDecryptionCipherKeyHex(hex);
}
public void setRememberMeDecryptionCipherKeyBase64(String base64) {
getRememberMeManagerForCipherAttributes().setDecryptionCipherKeyBase64(base64);
}
protected Subject createSubject() {
Subject subject = null;
Serializable sessionId = ThreadContext.getSessionId();
if (sessionId != null) {
try {
subject = getSubjectBySessionId(sessionId);
} catch (InvalidSessionException e) {
if (log.isDebugEnabled()) {
log.debug("Session id referenced on the current thread [" + sessionId + "] is invalid. " +
"Ignoring and creating a new Subject instance to continue. This message can be " +
"safely ignored.", e);
}
} catch (AuthorizationException e) {
if (log.isWarnEnabled()) {
log.warn("Session id referenced on the current thread [" + sessionId + "] is not allowed to be " +
"referenced. Ignoring and creating a Subject instance without a session to continue.", e);
}
}
}
if (subject == null) {
PrincipalCollection principals = getRememberedIdentity();
return getSubjectFactory().createSubject(principals, null, false, null);
}
return subject;
}
/**
* Creates a <tt>Subject</tt> instance for the user represented by the given method arguments.
*
* @param token the <tt>AuthenticationToken</tt> submitted for the successful authentication.
* @param info the <tt>AuthenticationInfo</tt> of a newly authenticated user.
* @return the <tt>Subject</tt> instance that represents the user and session data for the newly
* authenticated user.
*/
protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info) {
return getSubjectFactory().createSubject(token, info, getSubject(false));
}
/**
* Binds a <tt>Subject</tt> instance created after authentication to the application for later use.
*
* <p>The default implementation merely binds the argument to the thread local via the {@link ThreadContext}.
* Should be overridden by subclasses for environment-specific binding (e.g. web environment, etc).
*
* @param subject the <tt>Subject</tt> instance created after authentication to be bound to the application
* for later use.
*/
protected void bind(Subject subject) {
getSubjectBinder().bind(subject);
}
private void assertCreation(Subject subject) throws IllegalStateException {
if (subject == null) {
String msg = "Programming error - please verify that you have overridden the " +
getClass().getName() + ".createSubject( AuthenticationInfo info ) method to return " +
"a non-null Subject instance";
throw new IllegalStateException(msg);
}
}
protected void rememberMeSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info) {
RememberMeManager rmm = getRememberMeManager();
if (rmm != null) {
try {
rmm.onSuccessfulLogin(token, info);
} catch (Exception e) {
if (log.isWarnEnabled()) {
String msg = "Delegate RememberMeManager instance of type [" + rmm.getClass().getName() +
"] threw an exception during onSuccessfulLogin. RememberMe services will not be " +
"performed for account [" + info + "].";
log.warn(msg, e);
}
}
} else {
if (log.isDebugEnabled()) {
log.debug("This " + getClass().getName() + " instance does not have a " +
"[" + RememberMeManager.class.getName() + "] instance configured. RememberMe services " +
"will not be performed for account [" + info + "].");
}
}
}
protected void rememberMeFailedLogin(AuthenticationToken token, AuthenticationException ex) {
RememberMeManager rmm = getRememberMeManager();
if (rmm != null) {
try {
rmm.onFailedLogin(token, ex);
} catch (Exception e) {
if (log.isWarnEnabled()) {
String msg = "Delegate RememberMeManager instance of type [" + rmm.getClass().getName() +
"] threw an exception during onFailedLogin for AuthenticationToken [" +
token + "].";
log.warn(msg, e);
}
}
}
}
protected void rememberMeLogout(PrincipalCollection subjectPrincipals) {
RememberMeManager rmm = getRememberMeManager();
if (rmm != null) {
try {
rmm.onLogout(subjectPrincipals);
} catch (Exception e) {
if (log.isWarnEnabled()) {
String msg = "Delegate RememberMeManager instance of type [" + rmm.getClass().getName() +
"] threw an exception during onLogout for subject with principals [" +
subjectPrincipals + "]";
log.warn(msg, e);
}
}
}
}
/**
* First authenticates the <tt>AuthenticationToken</tt> argument, and if successful, constructs a
* <tt>Subject</tt> instance representing the authenticated account's identity.
*
* <p>Once constructed, the <tt>Subject</tt> instance is then {@link #bind bound} to the application for
* subsequent access before being returned to the caller.
*
* @param token the authenticationToken to process for the login attempt.
* @return a Subject representing the authenticated user.
* @throws AuthenticationException if there is a problem authenticating the specified <tt>token</tt>.
*/
public Subject login(AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = authenticate(token);
onSuccessfulLogin(token, info);
} catch (AuthenticationException ae) {
try {
onFailedLogin(token, ae);
} catch (Exception e) {
if (log.isInfoEnabled()) {
log.info("onFailedLogin(AuthenticationToken,AuthenticationException) method threw an " +
"exception. Logging and propagating original AuthenticationException.", e);
}
}
throw ae; //propagate
}
Subject subject = createSubject(token, info);
assertCreation(subject);
bind(subject);
return subject;
}
protected void onSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info) {
rememberMeSuccessfulLogin(token, info);
}
protected void onFailedLogin(AuthenticationToken token, AuthenticationException ae) {
rememberMeFailedLogin(token, ae);
}
protected void beforeLogout(PrincipalCollection subjectIdentifier) {
rememberMeLogout(subjectIdentifier);
}
public void logout(PrincipalCollection principals) {
if (principals != null) {
beforeLogout(principals);
Authenticator authc = getAuthenticator();
if (authc instanceof LogoutAware) {
((LogoutAware) authc).onLogout(principals);
}
}
//Method arg is ignored - get the Subject from the environment if it exists:
Subject subject = getSubject(false);
if (subject != null) {
try {
unbind(subject);
try {
stopSession(subject);
} catch (Exception e) {
if (log.isDebugEnabled()) {
String msg = "Unable to cleanly stop Session for Subject [" + subject.getPrincipal() + "] " +
"Ignoring (logging out).";
log.debug(msg, e);
}
}
} catch (Exception e) {
if (log.isDebugEnabled()) {
String msg = "Unable to cleanly unbind Subject. Ignoring (logging out).";
log.debug(msg, e);
}
}
}
}
protected void stopSession(Subject subject) {
Session s = subject.getSession(false);
if (s != null) {
try {
s.stop();
} catch (InvalidSessionException ise) {
//ignored - we're invalidating, and have no further need of the session anyway
//log just in case someone wants to know:
if (log.isTraceEnabled()) {
log.trace("Session has already been invalidated for subject [" +
subject.getPrincipal() + "]. Ignoring and continuing logout ...", ise);
}
}
}
}
protected void unbind(Subject subject) {
getSubjectBinder().unbind(subject);
}
protected PrincipalCollection getRememberedIdentity() {
RememberMeManager rmm = getRememberMeManager();
if (rmm != null) {
try {
return rmm.getRememberedPrincipals();
} catch (Exception e) {
if (log.isWarnEnabled()) {
String msg = "Delegate RememberMeManager instance of type [" + rmm.getClass().getName() +
"] threw an exception during getRememberedPrincipals().";
log.warn(msg, e);
}
}
}
return null;
}
protected Subject getSubject(boolean create) {
Subject subject = getSubjectBinder().getSubject();
if (subject == null && create) {
subject = createSubject();
bind(subject);
}
return subject;
}
public Subject getSubject() {
return getSubject(true);
}
protected PrincipalCollection getPrincipals(Session session) {
return (PrincipalCollection) session.getAttribute(SessionSubjectBinder.PRINCIPALS_SESSION_KEY);
}
protected boolean isAuthenticated(Session session, PrincipalCollection principals) {
if (principals != null) {
Boolean authc = (Boolean) session.getAttribute(SessionSubjectBinder.AUTHENTICATED_SESSION_KEY);
return authc != null && authc;
}
return false;
}
/**
* Acquires the {@link Subject Subject} that owns the {@link Session Session} with the specified {@code sessionId}.
*
* <p><b>Although simple in concept, this method provides incredibly powerful functionality:</b>
*
* <p>The ability to reference a {@code Subject} and their server-side session
* <em>across clients of different mediums</em> such as web applications, Java applets,
* standalone C# clients over XMLRPC and/or SOAP, and many others. This is a <em>huge</em>
* benefit in heterogeneous enterprise applications.
*
* <p>To maintain session integrity across client mediums, the {@code sessionId} <b>must</b> be transmitted
* to all client mediums securely (e.g. over SSL) to prevent man-in-the-middle attacks. This
* is nothing new - all web applications are susceptible to the same problem when transmitting
* {@link javax.servlet.http.Cookie Cookie}s or when using URL rewriting. As long as the
* {@code sessionId} is transmitted securely, session integrity can be maintained.
*
* @param sessionId the id of the session that backs the desired Subject being acquired.
* @return the {@code Subject} that owns the {@code Session Session} with the specified {@code sessionId}
* @throws org.jsecurity.session.InvalidSessionException
* if the session identified by <tt>sessionId</tt> has
* been stopped, expired, or doesn't exist.
* @throws org.jsecurity.authz.AuthorizationException
* if the executor of this method is not allowed to acquire the owning {@code Subject}. The reason
* for the exception is implementation-specific and could be for any number of reasons. A common
* reason in many systems would be if one host tried to acquire a {@code Subject} based on a
* {@code Session} that originated on an entirely different host (although it is not a JSecurity
* requirement this scenario is disallowed - its just an example that <em>may</em> throw an Exception in
* some systems).
* @see org.jsecurity.authz.HostUnauthorizedException
* @since 1.0
*/
private Subject getSubjectBySessionId(Serializable sessionId) throws InvalidSessionException, AuthorizationException {
if (!isValid(sessionId)) {
String msg = "Specified id [" + sessionId + "] does not correspond to a valid Session It either " +
"does not exist or the corresponding session has been stopped or expired.";
throw new InvalidSessionException(msg, sessionId);
}
Session existing = new DelegatingSession(this, sessionId);
PrincipalCollection principals = getPrincipals(existing);
boolean authenticated = isAuthenticated(existing, principals);
InetAddress host = existing.getHostAddress();
return getSubjectFactory().createSubject(principals, existing, authenticated, host);
}
}