blob: e687da5a414da48b830b528c31ec369e4219995b [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.apache.shiro.subject.support;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.HostAuthenticationToken;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.Permission;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.InvalidSessionException;
import org.apache.shiro.session.ProxiedSession;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionException;
import org.apache.shiro.session.mgt.DefaultSessionContext;
import org.apache.shiro.session.mgt.Flushable;
import org.apache.shiro.session.mgt.SessionContext;
import org.apache.shiro.subject.ExecutionException;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.CollectionUtils;
import org.apache.shiro.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Callable;
/**
* Implementation of the {@code Subject} interface that delegates
* method calls to an underlying {@link org.apache.shiro.mgt.SecurityManager SecurityManager} instance for security checks.
* It is essentially a {@code SecurityManager} proxy.
* <p/>
* This implementation does not maintain state such as roles and permissions (only {@code Subject}
* {@link #getPrincipals() principals}, such as usernames or user primary keys) for better performance in a stateless
* architecture. It instead asks the underlying {@code SecurityManager} every time to perform
* the authorization check.
* <p/>
* A common misconception in using this implementation is that an EIS resource (RDBMS, etc) would
* be &quot;hit&quot; every time a method is called. This is not necessarily the case and is
* up to the implementation of the underlying {@code SecurityManager} instance. If caching of authorization
* data is desired (to eliminate EIS round trips and therefore improve database performance), it is considered
* much more elegant to let the underlying {@code SecurityManager} implementation or its delegate components
* manage caching, not this class. A {@code SecurityManager} is considered a business-tier component,
* where caching strategies are better suited.
* <p/>
* Applications from large and clustered to simple and JVM-local all benefit from
* stateless architectures. This implementation plays a part in the stateless programming
* paradigm and should be used whenever possible.
*
* @since 0.1
*/
public class DelegatingSubject implements Subject {
private static final Logger log = LoggerFactory.getLogger(DelegatingSubject.class);
private static final String RUN_AS_PRINCIPALS_SESSION_KEY =
DelegatingSubject.class.getName() + ".RUN_AS_PRINCIPALS_SESSION_KEY";
protected PrincipalCollection principals;
protected boolean authenticated;
protected String host;
protected Session session;
protected boolean sessionCreationEnabled; //since 1.2
protected boolean sessionUpdateDeferred; //since 1.3
private List<PrincipalCollection> runAsPrincipals; //supports assumed identities (aka 'run as')
protected transient SecurityManager securityManager;
public DelegatingSubject(SecurityManager securityManager) {
this(null, false, null, null, securityManager);
}
public DelegatingSubject(PrincipalCollection principals, boolean authenticated, String host,
Session session, SecurityManager securityManager) {
this(principals, authenticated, host, session, true, securityManager);
}
//since 1.2
public DelegatingSubject(PrincipalCollection principals, boolean authenticated, String host,
Session session, boolean sessionCreationEnabled, SecurityManager securityManager) {
this(principals, authenticated, host, session, sessionCreationEnabled, false, securityManager);
}
//since 1.3
public DelegatingSubject(PrincipalCollection principals, boolean authenticated, String host,
Session session, boolean sessionCreationEnabled, boolean sessionUpdateDeferred,
SecurityManager securityManager) {
if (securityManager == null) {
throw new IllegalArgumentException("SecurityManager argument cannot be null.");
}
this.securityManager = securityManager;
this.principals = principals;
this.authenticated = authenticated;
this.host = host;
if (session != null) {
this.session = decorate(session);
this.runAsPrincipals = getRunAsPrincipals(this.session);
}
this.sessionCreationEnabled = sessionCreationEnabled;
this.sessionUpdateDeferred = sessionUpdateDeferred;
}
protected Session decorate(Session session) {
if (session == null) {
throw new IllegalArgumentException("session cannot be null");
}
if (session instanceof Flushable) {
return new FlushableStoppingAwareProxiedSession(session, this);
} else {
return new StoppingAwareProxiedSession(session, this);
}
}
public SecurityManager getSecurityManager() {
return securityManager;
}
protected boolean hasPrincipals() {
return !CollectionUtils.isEmpty(getPrincipals());
}
/**
* Returns the host name or IP associated with the client who created/is interacting with this Subject.
*
* @return the host name or IP associated with the client who created/is interacting with this Subject.
*/
public String getHost() {
return this.host;
}
private Object getPrimaryPrincipal(PrincipalCollection principals) {
if (!CollectionUtils.isEmpty(principals)) {
return principals.getPrimaryPrincipal();
}
return null;
}
/**
* @see Subject#getPrincipal()
*/
public Object getPrincipal() {
return getPrimaryPrincipal(getPrincipals());
}
public PrincipalCollection getPrincipals() {
return CollectionUtils.isEmpty(this.runAsPrincipals) ? this.principals : this.runAsPrincipals.get(0);
}
public boolean isPermitted(String permission) {
return hasPrincipals() && securityManager.isPermitted(getPrincipals(), permission);
}
public boolean isPermitted(Permission permission) {
return hasPrincipals() && securityManager.isPermitted(getPrincipals(), permission);
}
public boolean[] isPermitted(String... permissions) {
if (hasPrincipals()) {
return securityManager.isPermitted(getPrincipals(), permissions);
} else {
return new boolean[permissions.length];
}
}
public boolean[] isPermitted(List<Permission> permissions) {
if (hasPrincipals()) {
return securityManager.isPermitted(getPrincipals(), permissions);
} else {
return new boolean[permissions.size()];
}
}
public boolean isPermittedAll(String... permissions) {
return hasPrincipals() && securityManager.isPermittedAll(getPrincipals(), permissions);
}
public boolean isPermittedAll(Collection<Permission> permissions) {
return hasPrincipals() && securityManager.isPermittedAll(getPrincipals(), permissions);
}
protected void assertAuthzCheckPossible() throws AuthorizationException {
if (!hasPrincipals()) {
String msg = "This subject is anonymous - it does not have any identifying principals and " +
"authorization operations require an identity to check against. A Subject instance will " +
"acquire these identifying principals automatically after a successful login is performed " +
"be executing " + Subject.class.getName() + ".login(AuthenticationToken) or when 'Remember Me' " +
"functionality is enabled by the SecurityManager. This exception can also occur when a " +
"previously logged-in Subject has logged out which " +
"makes it anonymous again. Because an identity is currently not known due to any of these " +
"conditions, authorization is denied.";
throw new UnauthenticatedException(msg);
}
}
public void checkPermission(String permission) throws AuthorizationException {
assertAuthzCheckPossible();
securityManager.checkPermission(getPrincipals(), permission);
}
public void checkPermission(Permission permission) throws AuthorizationException {
assertAuthzCheckPossible();
securityManager.checkPermission(getPrincipals(), permission);
}
public void checkPermissions(String... permissions) throws AuthorizationException {
assertAuthzCheckPossible();
securityManager.checkPermissions(getPrincipals(), permissions);
}
public void checkPermissions(Collection<Permission> permissions) throws AuthorizationException {
assertAuthzCheckPossible();
securityManager.checkPermissions(getPrincipals(), permissions);
}
public boolean hasRole(String roleIdentifier) {
return hasPrincipals() && securityManager.hasRole(getPrincipals(), roleIdentifier);
}
public boolean[] hasRoles(List<String> roleIdentifiers) {
if (hasPrincipals()) {
return securityManager.hasRoles(getPrincipals(), roleIdentifiers);
} else {
return new boolean[roleIdentifiers.size()];
}
}
public boolean hasAllRoles(Collection<String> roleIdentifiers) {
return hasPrincipals() && securityManager.hasAllRoles(getPrincipals(), roleIdentifiers);
}
public void checkRole(String role) throws AuthorizationException {
assertAuthzCheckPossible();
securityManager.checkRole(getPrincipals(), role);
}
public void checkRoles(String... roleIdentifiers) throws AuthorizationException {
assertAuthzCheckPossible();
securityManager.checkRoles(getPrincipals(), roleIdentifiers);
}
public void checkRoles(Collection<String> roles) throws AuthorizationException {
assertAuthzCheckPossible();
securityManager.checkRoles(getPrincipals(), roles);
}
public void login(AuthenticationToken token) throws AuthenticationException {
clearRunAsIdentities();
Subject subject = securityManager.login(this, token);
PrincipalCollection principals;
String host = null;
if (subject instanceof DelegatingSubject) {
DelegatingSubject delegating = (DelegatingSubject) subject;
//we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
principals = delegating.principals;
host = delegating.host;
} else {
principals = subject.getPrincipals();
}
if (principals == null || principals.isEmpty()) {
String msg = "Principals returned from securityManager.login( token ) returned a null or " +
"empty value. This value must be non null and populated with one or more elements.";
throw new IllegalStateException(msg);
}
this.principals = principals;
this.authenticated = true;
if (token instanceof HostAuthenticationToken) {
host = ((HostAuthenticationToken) token).getHost();
}
if (host != null) {
this.host = host;
}
Session session = subject.getSession(false);
if (session != null) {
this.session = decorate(session);
this.runAsPrincipals = getRunAsPrincipals(this.session);
} else {
this.session = null;
}
}
public boolean isAuthenticated() {
return authenticated;
}
public boolean isRemembered() {
PrincipalCollection principals = getPrincipals();
return principals != null && !principals.isEmpty() && !isAuthenticated();
}
/**
* Returns {@code true} if this Subject is allowed to create sessions, {@code false} otherwise.
*
* @return {@code true} if this Subject is allowed to create sessions, {@code false} otherwise.
* @since 1.2
*/
protected boolean isSessionCreationEnabled() {
return this.sessionCreationEnabled;
}
protected boolean isSessionUpdateDeferred() {
return this.sessionUpdateDeferred;
}
public Session getSession() {
return getSession(true);
}
public Session getSession(boolean create) {
if (log.isTraceEnabled()) {
log.trace("attempting to get session; create = " + create + "; session is null = " + (this.session == null) + "; session has id = " + (this.session != null && session.getId() != null));
}
if (this.session == null && create) {
//added in 1.2:
if (!isSessionCreationEnabled()) {
String msg = "Session creation has been disabled for the current subject. This exception indicates " +
"that there is either a programming error (using a session when it should never be " +
"used) or that Shiro's configuration needs to be adjusted to allow Sessions to be created " +
"for the current Subject. See the " + DisabledSessionException.class.getName() + " JavaDoc " +
"for more.";
throw new DisabledSessionException(msg);
}
log.trace("Starting session for host {}", getHost());
SessionContext sessionContext = createSessionContext();
Session session = this.securityManager.start(sessionContext);
this.session = decorate(session);
}
return this.session;
}
protected SessionContext createSessionContext() {
DefaultSessionContext sessionContext = new DefaultSessionContext();
if (StringUtils.hasText(host)) {
sessionContext.setHost(host);
}
//added for 1.3 (see SHIRO-317):
boolean updateDeferred = isSessionUpdateDeferred();
if (updateDeferred) {
sessionContext.setUpdateDeferred(updateDeferred);
}
return sessionContext;
}
public void logout() {
try {
//try/catch added for SHIRO-298
try {
clearRunAsIdentities();
} catch (SessionException se) {
log.debug("Encountered session exception trying to clear 'runAs' identities during logout. This " +
"can generally safely be ignored.", se);
}
this.securityManager.logout(this);
} finally {
this.session = null;
this.principals = null;
this.authenticated = false;
this.runAsPrincipals = null;
//Don't set securityManager to null here - the Subject can still be
//used, it is just considered anonymous at this point. The SecurityManager instance is
//necessary if the subject would log in again or acquire a new session. This is in response to
//https://issues.apache.org/jira/browse/JSEC-22
//this.securityManager = null;
}
}
private void sessionStopped() {
this.session = null;
}
public <V> V execute(Callable<V> callable) throws ExecutionException {
Callable<V> associated = associateWith(callable);
try {
return associated.call();
} catch (Throwable t) {
throw new ExecutionException(t);
}
}
public void execute(Runnable runnable) {
Runnable associated = associateWith(runnable);
associated.run();
}
public <V> Callable<V> associateWith(Callable<V> callable) {
return new SubjectCallable<V>(this, callable);
}
public Runnable associateWith(Runnable runnable) {
if (runnable instanceof Thread) {
String msg = "This implementation does not support Thread arguments because of JDK ThreadLocal " +
"inheritance mechanisms required by Shiro. Instead, the method argument should be a non-Thread " +
"Runnable and the return value from this method can then be given to an ExecutorService or " +
"another Thread.";
throw new UnsupportedOperationException(msg);
}
return new SubjectRunnable(this, runnable);
}
private class StoppingAwareProxiedSession extends ProxiedSession {
private final DelegatingSubject owner;
private StoppingAwareProxiedSession(Session target, DelegatingSubject owningSubject) {
super(target);
owner = owningSubject;
}
public void stop() throws InvalidSessionException {
super.stop();
owner.sessionStopped();
}
}
private class FlushableStoppingAwareProxiedSession extends StoppingAwareProxiedSession implements Flushable {
private final DelegatingSubject owner;
private FlushableStoppingAwareProxiedSession(Session target, DelegatingSubject owningSubject) {
super(target, owningSubject);
if (!(target instanceof Flushable)) {
throw new IllegalStateException("Target session must be Flushable");
}
owner = owningSubject;
}
public void stop() throws InvalidSessionException {
super.stop();
owner.sessionStopped();
}
public void flush() {
((Flushable)delegate).flush();
}
}
// ======================================
// 'Run As' support implementations
// ======================================
public void runAs(PrincipalCollection principals) {
if (!hasPrincipals()) {
String msg = "This subject does not yet have an identity. Assuming the identity of another " +
"Subject is only allowed for Subjects with an existing identity. Try logging this subject in " +
"first, or using the " + Subject.Builder.class.getName() + " to build ad hoc Subject instances " +
"with identities as necessary.";
throw new IllegalStateException(msg);
}
pushIdentity(principals);
}
public boolean isRunAs() {
return !CollectionUtils.isEmpty(this.runAsPrincipals);
}
public PrincipalCollection getPreviousPrincipals() {
return isRunAs() ? this.principals : null;
}
public PrincipalCollection releaseRunAs() {
return popIdentity();
}
@SuppressWarnings({"unchecked"})
private List<PrincipalCollection> getRunAsPrincipals(Session session) {
if (session != null) {
return (List<PrincipalCollection>) session.getAttribute(RUN_AS_PRINCIPALS_SESSION_KEY);
}
return null;
}
private void clearRunAsIdentities() {
//setting to null must occur before interacting with the session in case it throws an exception (SHIRO-298)
this.runAsPrincipals = null;
Session session = getSession(false);
if (session != null) {
session.removeAttribute(RUN_AS_PRINCIPALS_SESSION_KEY);
}
}
private void pushIdentity(PrincipalCollection principals) throws NullPointerException {
if (CollectionUtils.isEmpty(principals)) {
String msg = "Specified Subject principals cannot be null or empty for 'run as' functionality.";
throw new NullPointerException(msg);
}
if (this.runAsPrincipals == null) {
this.runAsPrincipals = new ArrayList<PrincipalCollection>();
}
this.runAsPrincipals.add(0, principals);
Session session = getSession();
session.setAttribute(RUN_AS_PRINCIPALS_SESSION_KEY, this.runAsPrincipals);
}
private PrincipalCollection popIdentity() {
PrincipalCollection popped = null;
if (!CollectionUtils.isEmpty(this.runAsPrincipals)) {
popped = this.runAsPrincipals.remove(0);
Session session;
if (!CollectionUtils.isEmpty(this.runAsPrincipals)) {
//persist the changed deque to the session
session = getSession();
session.setAttribute(RUN_AS_PRINCIPALS_SESSION_KEY, this.runAsPrincipals);
} else {
//deque is empty, remove it from the session:
session = getSession(false);
if (session != null) {
session.removeAttribute(RUN_AS_PRINCIPALS_SESSION_KEY);
}
}
}
return popped;
}
}