View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.shiro.subject.support;
20  
21  import org.apache.shiro.authc.AuthenticationException;
22  import org.apache.shiro.authc.AuthenticationToken;
23  import org.apache.shiro.authc.HostAuthenticationToken;
24  import org.apache.shiro.authz.AuthorizationException;
25  import org.apache.shiro.authz.Permission;
26  import org.apache.shiro.authz.UnauthenticatedException;
27  import org.apache.shiro.mgt.SecurityManager;
28  import org.apache.shiro.session.InvalidSessionException;
29  import org.apache.shiro.session.ProxiedSession;
30  import org.apache.shiro.session.Session;
31  import org.apache.shiro.session.SessionException;
32  import org.apache.shiro.session.mgt.DefaultSessionContext;
33  import org.apache.shiro.session.mgt.SessionContext;
34  import org.apache.shiro.subject.ExecutionException;
35  import org.apache.shiro.subject.PrincipalCollection;
36  import org.apache.shiro.subject.Subject;
37  import org.apache.shiro.util.CollectionUtils;
38  import org.apache.shiro.util.StringUtils;
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  
42  import java.util.Collection;
43  import java.util.List;
44  import java.util.concurrent.Callable;
45  import java.util.concurrent.CopyOnWriteArrayList;
46  
47  /**
48   * Implementation of the {@code Subject} interface that delegates
49   * method calls to an underlying {@link org.apache.shiro.mgt.SecurityManager SecurityManager} instance for security checks.
50   * It is essentially a {@code SecurityManager} proxy.
51   * <p/>
52   * This implementation does not maintain state such as roles and permissions (only {@code Subject}
53   * {@link #getPrincipals() principals}, such as usernames or user primary keys) for better performance in a stateless
54   * architecture.  It instead asks the underlying {@code SecurityManager} every time to perform
55   * the authorization check.
56   * <p/>
57   * A common misconception in using this implementation is that an EIS resource (RDBMS, etc) would
58   * be &quot;hit&quot; every time a method is called.  This is not necessarily the case and is
59   * up to the implementation of the underlying {@code SecurityManager} instance.  If caching of authorization
60   * data is desired (to eliminate EIS round trips and therefore improve database performance), it is considered
61   * much more elegant to let the underlying {@code SecurityManager} implementation or its delegate components
62   * manage caching, not this class.  A {@code SecurityManager} is considered a business-tier component,
63   * where caching strategies are better managed.
64   * <p/>
65   * Applications from large and clustered to simple and JVM-local all benefit from
66   * stateless architectures.  This implementation plays a part in the stateless programming
67   * paradigm and should be used whenever possible.
68   *
69   * @since 0.1
70   */
71  public class DelegatingSubject implements Subject {
72  
73      private static final Logger log = LoggerFactory.getLogger(DelegatingSubject.class);
74  
75      private static final String RUN_AS_PRINCIPALS_SESSION_KEY =
76              DelegatingSubject.class.getName() + ".RUN_AS_PRINCIPALS_SESSION_KEY";
77  
78      protected PrincipalCollection principals;
79      protected boolean authenticated;
80      protected String host;
81      protected Session session;
82      /**
83       * @since 1.2
84       */
85      protected boolean sessionCreationEnabled;
86  
87      protected transient SecurityManager securityManager;
88  
89      public DelegatingSubject(SecurityManager securityManager) {
90          this(null, false, null, null, securityManager);
91      }
92  
93      public DelegatingSubject(PrincipalCollection principals, boolean authenticated, String host,
94                               Session session, SecurityManager securityManager) {
95          this(principals, authenticated, host, session, true, securityManager);
96      }
97  
98      //since 1.2
99      public DelegatingSubject(PrincipalCollection principals, boolean authenticated, String host,
100                              Session session, boolean sessionCreationEnabled, SecurityManager securityManager) {
101         if (securityManager == null) {
102             throw new IllegalArgumentException("SecurityManager argument cannot be null.");
103         }
104         this.securityManager = securityManager;
105         this.principals = principals;
106         this.authenticated = authenticated;
107         this.host = host;
108         if (session != null) {
109             this.session = decorate(session);
110         }
111         this.sessionCreationEnabled = sessionCreationEnabled;
112     }
113 
114     protected Session../../../../../org/apache/shiro/session/Session.html#Session">Session decorate(Session session) {
115         if (session == null) {
116             throw new IllegalArgumentException("session cannot be null");
117         }
118         return new StoppingAwareProxiedSession(session, this);
119     }
120 
121     public SecurityManager getSecurityManager() {
122         return securityManager;
123     }
124 
125     private static boolean isEmpty(PrincipalCollection pc) {
126         return pc == null || pc.isEmpty();
127     }
128 
129     protected boolean hasPrincipals() {
130         return !isEmpty(getPrincipals());
131     }
132 
133     /**
134      * Returns the host name or IP associated with the client who created/is interacting with this Subject.
135      *
136      * @return the host name or IP associated with the client who created/is interacting with this Subject.
137      */
138     public String getHost() {
139         return this.host;
140     }
141 
142     private Object getPrimaryPrincipal(PrincipalCollection principals) {
143         if (!isEmpty(principals)) {
144             return principals.getPrimaryPrincipal();
145         }
146         return null;
147     }
148 
149     /**
150      * @see Subject#getPrincipal()
151      */
152     public Object getPrincipal() {
153         return getPrimaryPrincipal(getPrincipals());
154     }
155 
156     public PrincipalCollection getPrincipals() {
157         List<PrincipalCollection> runAsPrincipals = getRunAsPrincipalsStack();
158         return CollectionUtils.isEmpty(runAsPrincipals) ? this.principals : runAsPrincipals.get(0);
159     }
160 
161     public boolean isPermitted(String permission) {
162         return hasPrincipals() && securityManager.isPermitted(getPrincipals(), permission);
163     }
164 
165     public boolean isPermitted(Permission permission) {
166         return hasPrincipals() && securityManager.isPermitted(getPrincipals(), permission);
167     }
168 
169     public boolean[] isPermitted(String... permissions) {
170         if (hasPrincipals()) {
171             return securityManager.isPermitted(getPrincipals(), permissions);
172         } else {
173             return new boolean[permissions.length];
174         }
175     }
176 
177     public boolean[] isPermitted(List<Permission> permissions) {
178         if (hasPrincipals()) {
179             return securityManager.isPermitted(getPrincipals(), permissions);
180         } else {
181             return new boolean[permissions.size()];
182         }
183     }
184 
185     public boolean isPermittedAll(String... permissions) {
186         return hasPrincipals() && securityManager.isPermittedAll(getPrincipals(), permissions);
187     }
188 
189     public boolean isPermittedAll(Collection<Permission> permissions) {
190         return hasPrincipals() && securityManager.isPermittedAll(getPrincipals(), permissions);
191     }
192 
193     protected void assertAuthzCheckPossible() throws AuthorizationException {
194         if (!hasPrincipals()) {
195             String msg = "This subject is anonymous - it does not have any identifying principals and " +
196                     "authorization operations require an identity to check against.  A Subject instance will " +
197                     "acquire these identifying principals automatically after a successful login is performed " +
198                     "be executing " + Subject.class.getName() + ".login(AuthenticationToken) or when 'Remember Me' " +
199                     "functionality is enabled by the SecurityManager.  This exception can also occur when a " +
200                     "previously logged-in Subject has logged out which " +
201                     "makes it anonymous again.  Because an identity is currently not known due to any of these " +
202                     "conditions, authorization is denied.";
203             throw new UnauthenticatedException(msg);
204         }
205     }
206 
207     public void checkPermission(String permission) throws AuthorizationException {
208         assertAuthzCheckPossible();
209         securityManager.checkPermission(getPrincipals(), permission);
210     }
211 
212     public void checkPermission(Permission permission) throws AuthorizationException {
213         assertAuthzCheckPossible();
214         securityManager.checkPermission(getPrincipals(), permission);
215     }
216 
217     public void checkPermissions(String... permissions) throws AuthorizationException {
218         assertAuthzCheckPossible();
219         securityManager.checkPermissions(getPrincipals(), permissions);
220     }
221 
222     public void checkPermissions(Collection<Permission> permissions) throws AuthorizationException {
223         assertAuthzCheckPossible();
224         securityManager.checkPermissions(getPrincipals(), permissions);
225     }
226 
227     public boolean hasRole(String roleIdentifier) {
228         return hasPrincipals() && securityManager.hasRole(getPrincipals(), roleIdentifier);
229     }
230 
231     public boolean[] hasRoles(List<String> roleIdentifiers) {
232         if (hasPrincipals()) {
233             return securityManager.hasRoles(getPrincipals(), roleIdentifiers);
234         } else {
235             return new boolean[roleIdentifiers.size()];
236         }
237     }
238 
239     public boolean hasAllRoles(Collection<String> roleIdentifiers) {
240         return hasPrincipals() && securityManager.hasAllRoles(getPrincipals(), roleIdentifiers);
241     }
242 
243     public void checkRole(String role) throws AuthorizationException {
244         assertAuthzCheckPossible();
245         securityManager.checkRole(getPrincipals(), role);
246     }
247 
248     public void checkRoles(String... roleIdentifiers) throws AuthorizationException {
249         assertAuthzCheckPossible();
250         securityManager.checkRoles(getPrincipals(), roleIdentifiers);
251     }
252 
253     public void checkRoles(Collection<String> roles) throws AuthorizationException {
254         assertAuthzCheckPossible();
255         securityManager.checkRoles(getPrincipals(), roles);
256     }
257 
258     public void login(AuthenticationToken token) throws AuthenticationException {
259         clearRunAsIdentitiesInternal();
260         Subject subject = securityManager.login(this, token);
261 
262         PrincipalCollection principals;
263 
264         String host = null;
265 
266         if (subject instanceof DelegatingSubject) {
267             DelegatingSubjectorg/apache/shiro/subject/support/DelegatingSubject.html#DelegatingSubject">DelegatingSubject delegating = (DelegatingSubject) subject;
268             //we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
269             principals = delegating.principals;
270             host = delegating.host;
271         } else {
272             principals = subject.getPrincipals();
273         }
274 
275         if (principals == null || principals.isEmpty()) {
276             String msg = "Principals returned from securityManager.login( token ) returned a null or " +
277                     "empty value.  This value must be non null and populated with one or more elements.";
278             throw new IllegalStateException(msg);
279         }
280         this.principals = principals;
281         this.authenticated = true;
282         if (token instanceof HostAuthenticationToken) {
283             host = ((HostAuthenticationToken) token).getHost();
284         }
285         if (host != null) {
286             this.host = host;
287         }
288         Session session = subject.getSession(false);
289         if (session != null) {
290             this.session = decorate(session);
291         } else {
292             this.session = null;
293         }
294     }
295 
296     public boolean isAuthenticated() {
297         return authenticated && hasPrincipals();
298     }
299 
300     public boolean isRemembered() {
301         PrincipalCollection principals = getPrincipals();
302         return principals != null && !principals.isEmpty() && !isAuthenticated();
303     }
304 
305     /**
306      * Returns {@code true} if this Subject is allowed to create sessions, {@code false} otherwise.
307      *
308      * @return {@code true} if this Subject is allowed to create sessions, {@code false} otherwise.
309      * @since 1.2
310      */
311     protected boolean isSessionCreationEnabled() {
312         return this.sessionCreationEnabled;
313     }
314 
315     public Session getSession() {
316         return getSession(true);
317     }
318 
319     public Session getSession(boolean create) {
320         if (log.isTraceEnabled()) {
321             log.trace("attempting to get session; create = " + create +
322                     "; session is null = " + (this.session == null) +
323                     "; session has id = " + (this.session != null && session.getId() != null));
324         }
325 
326         if (this.session == null && create) {
327 
328             //added in 1.2:
329             if (!isSessionCreationEnabled()) {
330                 String msg = "Session creation has been disabled for the current subject.  This exception indicates " +
331                         "that there is either a programming error (using a session when it should never be " +
332                         "used) or that Shiro's configuration needs to be adjusted to allow Sessions to be created " +
333                         "for the current Subject.  See the " + DisabledSessionException.class.getName() + " JavaDoc " +
334                         "for more.";
335                 throw new DisabledSessionException(msg);
336             }
337 
338             log.trace("Starting session for host {}", getHost());
339             SessionContext sessionContext = createSessionContext();
340             Session session = this.securityManager.start(sessionContext);
341             this.session = decorate(session);
342         }
343         return this.session;
344     }
345 
346     protected SessionContext createSessionContext() {
347         SessionContext sessionContext = new DefaultSessionContext();
348         if (StringUtils.hasText(host)) {
349             sessionContext.setHost(host);
350         }
351         return sessionContext;
352     }
353 
354     private void clearRunAsIdentitiesInternal() {
355         //try/catch added for SHIRO-298
356         try {
357             clearRunAsIdentities();
358         } catch (SessionException se) {
359             log.debug("Encountered session exception trying to clear 'runAs' identities during logout.  This " +
360                     "can generally safely be ignored.", se);
361         }
362     }
363 
364     public void logout() {
365         try {
366             clearRunAsIdentitiesInternal();
367             this.securityManager.logout(this);
368         } finally {
369             this.session = null;
370             this.principals = null;
371             this.authenticated = false;
372             //Don't set securityManager to null here - the Subject can still be
373             //used, it is just considered anonymous at this point.  The SecurityManager instance is
374             //necessary if the subject would log in again or acquire a new session.  This is in response to
375             //https://issues.apache.org/jira/browse/JSEC-22
376             //this.securityManager = null;
377         }
378     }
379 
380     private void sessionStopped() {
381         this.session = null;
382     }
383 
384     public <V> V execute(Callable<V> callable) throws ExecutionException {
385         Callable<V> associated = associateWith(callable);
386         try {
387             return associated.call();
388         } catch (Throwable t) {
389             throw new ExecutionException(t);
390         }
391     }
392 
393     public void execute(Runnable runnable) {
394         Runnable associated = associateWith(runnable);
395         associated.run();
396     }
397 
398     public <V> Callable<V> associateWith(Callable<V> callable) {
399         return new SubjectCallable<V>(this, callable);
400     }
401 
402     public Runnable associateWith(Runnable runnable) {
403         if (runnable instanceof Thread) {
404             String msg = "This implementation does not support Thread arguments because of JDK ThreadLocal " +
405                     "inheritance mechanisms required by Shiro.  Instead, the method argument should be a non-Thread " +
406                     "Runnable and the return value from this method can then be given to an ExecutorService or " +
407                     "another Thread.";
408             throw new UnsupportedOperationException(msg);
409         }
410         return new SubjectRunnable(this, runnable);
411     }
412 
413     private class StoppingAwareProxiedSession extends ProxiedSession {
414 
415         private final DelegatingSubject owner;
416 
417         private StoppingAwareProxiedSession(Session target, DelegatingSubject owningSubject) {
418             super(target);
419             owner = owningSubject;
420         }
421 
422         public void stop() throws InvalidSessionException {
423             super.stop();
424             owner.sessionStopped();
425         }
426     }
427 
428 
429     // ======================================
430     // 'Run As' support implementations
431     // ======================================
432 
433     public void runAs(PrincipalCollection principals) {
434         if (!hasPrincipals()) {
435             String msg = "This subject does not yet have an identity.  Assuming the identity of another " +
436                     "Subject is only allowed for Subjects with an existing identity.  Try logging this subject in " +
437                     "first, or using the " + Subject.Builder.class.getName() + " to build ad hoc Subject instances " +
438                     "with identities as necessary.";
439             throw new IllegalStateException(msg);
440         }
441         pushIdentity(principals);
442     }
443 
444     public boolean isRunAs() {
445         List<PrincipalCollection> stack = getRunAsPrincipalsStack();
446         return !CollectionUtils.isEmpty(stack);
447     }
448 
449     public PrincipalCollection getPreviousPrincipals() {
450         PrincipalCollection previousPrincipals = null;
451         List<PrincipalCollection> stack = getRunAsPrincipalsStack();
452         int stackSize = stack != null ? stack.size() : 0;
453         if (stackSize > 0) {
454             if (stackSize == 1) {
455                 previousPrincipals = this.principals;
456             } else {
457                 //always get the one behind the current:
458                 assert stack != null;
459                 previousPrincipals = stack.get(1);
460             }
461         }
462         return previousPrincipals;
463     }
464 
465     public PrincipalCollection releaseRunAs() {
466         return popIdentity();
467     }
468 
469     @SuppressWarnings("unchecked")
470     private List<PrincipalCollection> getRunAsPrincipalsStack() {
471         Session session = getSession(false);
472         if (session != null) {
473             return (List<PrincipalCollection>) session.getAttribute(RUN_AS_PRINCIPALS_SESSION_KEY);
474         }
475         return null;
476     }
477 
478     private void clearRunAsIdentities() {
479         Session session = getSession(false);
480         if (session != null) {
481             session.removeAttribute(RUN_AS_PRINCIPALS_SESSION_KEY);
482         }
483     }
484 
485     private void pushIdentity(PrincipalCollection principals) throws NullPointerException {
486         if (isEmpty(principals)) {
487             String msg = "Specified Subject principals cannot be null or empty for 'run as' functionality.";
488             throw new NullPointerException(msg);
489         }
490         List<PrincipalCollection> stack = getRunAsPrincipalsStack();
491         if (stack == null) {
492             stack = new CopyOnWriteArrayList<PrincipalCollection>();
493         }
494         stack.add(0, principals);
495         Session session = getSession();
496         session.setAttribute(RUN_AS_PRINCIPALS_SESSION_KEY, stack);
497     }
498 
499     private PrincipalCollection popIdentity() {
500         PrincipalCollection popped = null;
501 
502         List<PrincipalCollection> stack = getRunAsPrincipalsStack();
503         if (!CollectionUtils.isEmpty(stack)) {
504             popped = stack.remove(0);
505             Session session;
506             if (!CollectionUtils.isEmpty(stack)) {
507                 //persist the changed stack to the session
508                 session = getSession();
509                 session.setAttribute(RUN_AS_PRINCIPALS_SESSION_KEY, stack);
510             } else {
511                 //stack is empty, remove it from the session:
512                 clearRunAsIdentities();
513             }
514         }
515 
516         return popped;
517     }
518 }