| /* |
| * 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.apache.ki.mgt; |
| |
| import java.io.Serializable; |
| import java.net.InetAddress; |
| import java.util.Collection; |
| |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import org.apache.ki.authc.AuthenticationException; |
| import org.apache.ki.authc.AuthenticationInfo; |
| import org.apache.ki.authc.AuthenticationToken; |
| import org.apache.ki.authc.Authenticator; |
| import org.apache.ki.authc.LogoutAware; |
| import org.apache.ki.authz.AuthorizationException; |
| import org.apache.ki.authz.Authorizer; |
| import org.apache.ki.crypto.Cipher; |
| import org.apache.ki.realm.Realm; |
| import org.apache.ki.session.InvalidSessionException; |
| import org.apache.ki.session.Session; |
| import org.apache.ki.session.mgt.DelegatingSession; |
| import org.apache.ki.subject.PrincipalCollection; |
| import org.apache.ki.subject.Subject; |
| import org.apache.ki.util.ThreadContext; |
| |
| |
| /** |
| * <p>The Ki framework's default concrete implementation of the {@link SecurityManager} interface, |
| * based around a collection of {@link org.apache.ki.realm.Realm}s. This implementation delegates its |
| * authentication, authorization, and session operations to wrapped {@link Authenticator}, {@link Authorizer}, and |
| * {@link org.apache.ki.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.apache.ki.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.apache.ki.web.DefaultWebSecurityManager |
| * @since 0.2 |
| */ |
| public class DefaultSecurityManager extends SessionsSecurityManager { |
| |
| //TODO - complete JavaDoc |
| |
| private static final Logger log = LoggerFactory.getLogger(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 Serializable getCurrentSessionId() { |
| return ThreadContext.getSessionId(); |
| } |
| |
| protected Subject createSubject() { |
| Subject subject = null; |
| |
| Serializable sessionId = getCurrentSessionId(); |
| 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); |
| } |
| |
| protected Subject getSubject(PrincipalCollection principals) { |
| //Method arg is ignored at the moment - retrieve from the environment if it exists: |
| return getSubject(false); |
| } |
| |
| public void logout(PrincipalCollection principals) { |
| |
| Subject subject; |
| |
| if (principals != null) { |
| beforeLogout(principals); |
| Authenticator authc = getAuthenticator(); |
| if (authc instanceof LogoutAware) { |
| ((LogoutAware) authc).onLogout(principals); |
| } |
| subject = getSubject(principals); |
| } else { |
| subject = getSubject(false); |
| } |
| |
| try { |
| unbind(subject); |
| } catch (Exception e) { |
| if (log.isDebugEnabled()) { |
| String msg = "Unable to cleanly unbind Subject. Ignoring (logging out)."; |
| log.debug(msg, e); |
| } |
| } finally { |
| 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); |
| } |
| } |
| |
| } |
| } |
| |
| 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 |
| * {@code 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.apache.ki.session.InvalidSessionException |
| * if the session identified by <tt>sessionId</tt> has |
| * been stopped, expired, or doesn't exist. |
| * @throws org.apache.ki.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 Ki |
| * requirement this scenario is disallowed - its just an example that <em>may</em> throw an Exception in |
| * some systems). |
| * @see org.apache.ki.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); |
| } |
| } |