001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.shiro.subject.support;
020
021import org.apache.shiro.authc.AuthenticationException;
022import org.apache.shiro.authc.AuthenticationToken;
023import org.apache.shiro.authc.HostAuthenticationToken;
024import org.apache.shiro.authz.AuthorizationException;
025import org.apache.shiro.authz.Permission;
026import org.apache.shiro.authz.UnauthenticatedException;
027import org.apache.shiro.mgt.SecurityManager;
028import org.apache.shiro.session.InvalidSessionException;
029import org.apache.shiro.session.ProxiedSession;
030import org.apache.shiro.session.Session;
031import org.apache.shiro.session.SessionException;
032import org.apache.shiro.session.mgt.DefaultSessionContext;
033import org.apache.shiro.session.mgt.SessionContext;
034import org.apache.shiro.subject.ExecutionException;
035import org.apache.shiro.subject.PrincipalCollection;
036import org.apache.shiro.subject.Subject;
037import org.apache.shiro.util.CollectionUtils;
038import org.apache.shiro.util.StringUtils;
039import org.slf4j.Logger;
040import org.slf4j.LoggerFactory;
041
042import java.util.Collection;
043import java.util.List;
044import java.util.concurrent.Callable;
045import java.util.concurrent.CopyOnWriteArrayList;
046
047/**
048 * Implementation of the {@code Subject} interface that delegates
049 * method calls to an underlying {@link org.apache.shiro.mgt.SecurityManager SecurityManager} instance for security checks.
050 * It is essentially a {@code SecurityManager} proxy.
051 * <p/>
052 * This implementation does not maintain state such as roles and permissions (only {@code Subject}
053 * {@link #getPrincipals() principals}, such as usernames or user primary keys) for better performance in a stateless
054 * architecture.  It instead asks the underlying {@code SecurityManager} every time to perform
055 * the authorization check.
056 * <p/>
057 * A common misconception in using this implementation is that an EIS resource (RDBMS, etc) would
058 * be &quot;hit&quot; every time a method is called.  This is not necessarily the case and is
059 * up to the implementation of the underlying {@code SecurityManager} instance.  If caching of authorization
060 * data is desired (to eliminate EIS round trips and therefore improve database performance), it is considered
061 * much more elegant to let the underlying {@code SecurityManager} implementation or its delegate components
062 * manage caching, not this class.  A {@code SecurityManager} is considered a business-tier component,
063 * where caching strategies are better managed.
064 * <p/>
065 * Applications from large and clustered to simple and JVM-local all benefit from
066 * stateless architectures.  This implementation plays a part in the stateless programming
067 * paradigm and should be used whenever possible.
068 *
069 * @since 0.1
070 */
071public class DelegatingSubject implements Subject {
072
073    private static final Logger log = LoggerFactory.getLogger(DelegatingSubject.class);
074
075    private static final String RUN_AS_PRINCIPALS_SESSION_KEY =
076            DelegatingSubject.class.getName() + ".RUN_AS_PRINCIPALS_SESSION_KEY";
077
078    protected PrincipalCollection principals;
079    protected boolean authenticated;
080    protected String host;
081    protected Session session;
082    /**
083     * @since 1.2
084     */
085    protected boolean sessionCreationEnabled;
086
087    protected transient SecurityManager securityManager;
088
089    public DelegatingSubject(SecurityManager securityManager) {
090        this(null, false, null, null, securityManager);
091    }
092
093    public DelegatingSubject(PrincipalCollection principals, boolean authenticated, String host,
094                             Session session, SecurityManager securityManager) {
095        this(principals, authenticated, host, session, true, securityManager);
096    }
097
098    //since 1.2
099    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 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    protected boolean hasPrincipals() {
126        return !CollectionUtils.isEmpty(getPrincipals());
127    }
128
129    /**
130     * Returns the host name or IP associated with the client who created/is interacting with this Subject.
131     *
132     * @return the host name or IP associated with the client who created/is interacting with this Subject.
133     */
134    public String getHost() {
135        return this.host;
136    }
137
138    private Object getPrimaryPrincipal(PrincipalCollection principals) {
139        if (!CollectionUtils.isEmpty(principals)) {
140            return principals.getPrimaryPrincipal();
141        }
142        return null;
143    }
144
145    /**
146     * @see Subject#getPrincipal()
147     */
148    public Object getPrincipal() {
149        return getPrimaryPrincipal(getPrincipals());
150    }
151
152    public PrincipalCollection getPrincipals() {
153        List<PrincipalCollection> runAsPrincipals = getRunAsPrincipalsStack();
154        return CollectionUtils.isEmpty(runAsPrincipals) ? this.principals : runAsPrincipals.get(0);
155    }
156
157    public boolean isPermitted(String permission) {
158        return hasPrincipals() && securityManager.isPermitted(getPrincipals(), permission);
159    }
160
161    public boolean isPermitted(Permission permission) {
162        return hasPrincipals() && securityManager.isPermitted(getPrincipals(), permission);
163    }
164
165    public boolean[] isPermitted(String... permissions) {
166        if (hasPrincipals()) {
167            return securityManager.isPermitted(getPrincipals(), permissions);
168        } else {
169            return new boolean[permissions.length];
170        }
171    }
172
173    public boolean[] isPermitted(List<Permission> permissions) {
174        if (hasPrincipals()) {
175            return securityManager.isPermitted(getPrincipals(), permissions);
176        } else {
177            return new boolean[permissions.size()];
178        }
179    }
180
181    public boolean isPermittedAll(String... permissions) {
182        return hasPrincipals() && securityManager.isPermittedAll(getPrincipals(), permissions);
183    }
184
185    public boolean isPermittedAll(Collection<Permission> permissions) {
186        return hasPrincipals() && securityManager.isPermittedAll(getPrincipals(), permissions);
187    }
188
189    protected void assertAuthzCheckPossible() throws AuthorizationException {
190        if (!hasPrincipals()) {
191            String msg = "This subject is anonymous - it does not have any identifying principals and " +
192                    "authorization operations require an identity to check against.  A Subject instance will " +
193                    "acquire these identifying principals automatically after a successful login is performed " +
194                    "be executing " + Subject.class.getName() + ".login(AuthenticationToken) or when 'Remember Me' " +
195                    "functionality is enabled by the SecurityManager.  This exception can also occur when a " +
196                    "previously logged-in Subject has logged out which " +
197                    "makes it anonymous again.  Because an identity is currently not known due to any of these " +
198                    "conditions, authorization is denied.";
199            throw new UnauthenticatedException(msg);
200        }
201    }
202
203    public void checkPermission(String permission) throws AuthorizationException {
204        assertAuthzCheckPossible();
205        securityManager.checkPermission(getPrincipals(), permission);
206    }
207
208    public void checkPermission(Permission permission) throws AuthorizationException {
209        assertAuthzCheckPossible();
210        securityManager.checkPermission(getPrincipals(), permission);
211    }
212
213    public void checkPermissions(String... permissions) throws AuthorizationException {
214        assertAuthzCheckPossible();
215        securityManager.checkPermissions(getPrincipals(), permissions);
216    }
217
218    public void checkPermissions(Collection<Permission> permissions) throws AuthorizationException {
219        assertAuthzCheckPossible();
220        securityManager.checkPermissions(getPrincipals(), permissions);
221    }
222
223    public boolean hasRole(String roleIdentifier) {
224        return hasPrincipals() && securityManager.hasRole(getPrincipals(), roleIdentifier);
225    }
226
227    public boolean[] hasRoles(List<String> roleIdentifiers) {
228        if (hasPrincipals()) {
229            return securityManager.hasRoles(getPrincipals(), roleIdentifiers);
230        } else {
231            return new boolean[roleIdentifiers.size()];
232        }
233    }
234
235    public boolean hasAllRoles(Collection<String> roleIdentifiers) {
236        return hasPrincipals() && securityManager.hasAllRoles(getPrincipals(), roleIdentifiers);
237    }
238
239    public void checkRole(String role) throws AuthorizationException {
240        assertAuthzCheckPossible();
241        securityManager.checkRole(getPrincipals(), role);
242    }
243
244    public void checkRoles(String... roleIdentifiers) throws AuthorizationException {
245        assertAuthzCheckPossible();
246        securityManager.checkRoles(getPrincipals(), roleIdentifiers);
247    }
248
249    public void checkRoles(Collection<String> roles) throws AuthorizationException {
250        assertAuthzCheckPossible();
251        securityManager.checkRoles(getPrincipals(), roles);
252    }
253
254    public void login(AuthenticationToken token) throws AuthenticationException {
255        clearRunAsIdentitiesInternal();
256        Subject subject = securityManager.login(this, token);
257
258        PrincipalCollection principals;
259
260        String host = null;
261
262        if (subject instanceof DelegatingSubject) {
263            DelegatingSubject delegating = (DelegatingSubject) subject;
264            //we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
265            principals = delegating.principals;
266            host = delegating.host;
267        } else {
268            principals = subject.getPrincipals();
269        }
270
271        if (principals == null || principals.isEmpty()) {
272            String msg = "Principals returned from securityManager.login( token ) returned a null or " +
273                    "empty value.  This value must be non null and populated with one or more elements.";
274            throw new IllegalStateException(msg);
275        }
276        this.principals = principals;
277        this.authenticated = true;
278        if (token instanceof HostAuthenticationToken) {
279            host = ((HostAuthenticationToken) token).getHost();
280        }
281        if (host != null) {
282            this.host = host;
283        }
284        Session session = subject.getSession(false);
285        if (session != null) {
286            this.session = decorate(session);
287        } else {
288            this.session = null;
289        }
290    }
291
292    public boolean isAuthenticated() {
293        return authenticated;
294    }
295
296    public boolean isRemembered() {
297        PrincipalCollection principals = getPrincipals();
298        return principals != null && !principals.isEmpty() && !isAuthenticated();
299    }
300
301    /**
302     * Returns {@code true} if this Subject is allowed to create sessions, {@code false} otherwise.
303     *
304     * @return {@code true} if this Subject is allowed to create sessions, {@code false} otherwise.
305     * @since 1.2
306     */
307    protected boolean isSessionCreationEnabled() {
308        return this.sessionCreationEnabled;
309    }
310
311    public Session getSession() {
312        return getSession(true);
313    }
314
315    public Session getSession(boolean create) {
316        if (log.isTraceEnabled()) {
317            log.trace("attempting to get session; create = " + create +
318                    "; session is null = " + (this.session == null) +
319                    "; session has id = " + (this.session != null && session.getId() != null));
320        }
321
322        if (this.session == null && create) {
323
324            //added in 1.2:
325            if (!isSessionCreationEnabled()) {
326                String msg = "Session creation has been disabled for the current subject.  This exception indicates " +
327                        "that there is either a programming error (using a session when it should never be " +
328                        "used) or that Shiro's configuration needs to be adjusted to allow Sessions to be created " +
329                        "for the current Subject.  See the " + DisabledSessionException.class.getName() + " JavaDoc " +
330                        "for more.";
331                throw new DisabledSessionException(msg);
332            }
333
334            log.trace("Starting session for host {}", getHost());
335            SessionContext sessionContext = createSessionContext();
336            Session session = this.securityManager.start(sessionContext);
337            this.session = decorate(session);
338        }
339        return this.session;
340    }
341
342    protected SessionContext createSessionContext() {
343        SessionContext sessionContext = new DefaultSessionContext();
344        if (StringUtils.hasText(host)) {
345            sessionContext.setHost(host);
346        }
347        return sessionContext;
348    }
349
350    private void clearRunAsIdentitiesInternal() {
351        //try/catch added for SHIRO-298
352        try {
353            clearRunAsIdentities();
354        } catch (SessionException se) {
355            log.debug("Encountered session exception trying to clear 'runAs' identities during logout.  This " +
356                    "can generally safely be ignored.", se);
357        }
358    }
359
360    public void logout() {
361        try {
362            clearRunAsIdentitiesInternal();
363            this.securityManager.logout(this);
364        } finally {
365            this.session = null;
366            this.principals = null;
367            this.authenticated = false;
368            //Don't set securityManager to null here - the Subject can still be
369            //used, it is just considered anonymous at this point.  The SecurityManager instance is
370            //necessary if the subject would log in again or acquire a new session.  This is in response to
371            //https://issues.apache.org/jira/browse/JSEC-22
372            //this.securityManager = null;
373        }
374    }
375
376    private void sessionStopped() {
377        this.session = null;
378    }
379
380    public <V> V execute(Callable<V> callable) throws ExecutionException {
381        Callable<V> associated = associateWith(callable);
382        try {
383            return associated.call();
384        } catch (Throwable t) {
385            throw new ExecutionException(t);
386        }
387    }
388
389    public void execute(Runnable runnable) {
390        Runnable associated = associateWith(runnable);
391        associated.run();
392    }
393
394    public <V> Callable<V> associateWith(Callable<V> callable) {
395        return new SubjectCallable<V>(this, callable);
396    }
397
398    public Runnable associateWith(Runnable runnable) {
399        if (runnable instanceof Thread) {
400            String msg = "This implementation does not support Thread arguments because of JDK ThreadLocal " +
401                    "inheritance mechanisms required by Shiro.  Instead, the method argument should be a non-Thread " +
402                    "Runnable and the return value from this method can then be given to an ExecutorService or " +
403                    "another Thread.";
404            throw new UnsupportedOperationException(msg);
405        }
406        return new SubjectRunnable(this, runnable);
407    }
408
409    private class StoppingAwareProxiedSession extends ProxiedSession {
410
411        private final DelegatingSubject owner;
412
413        private StoppingAwareProxiedSession(Session target, DelegatingSubject owningSubject) {
414            super(target);
415            owner = owningSubject;
416        }
417
418        public void stop() throws InvalidSessionException {
419            super.stop();
420            owner.sessionStopped();
421        }
422    }
423
424
425    // ======================================
426    // 'Run As' support implementations
427    // ======================================
428
429    public void runAs(PrincipalCollection principals) {
430        if (!hasPrincipals()) {
431            String msg = "This subject does not yet have an identity.  Assuming the identity of another " +
432                    "Subject is only allowed for Subjects with an existing identity.  Try logging this subject in " +
433                    "first, or using the " + Subject.Builder.class.getName() + " to build ad hoc Subject instances " +
434                    "with identities as necessary.";
435            throw new IllegalStateException(msg);
436        }
437        pushIdentity(principals);
438    }
439
440    public boolean isRunAs() {
441        List<PrincipalCollection> stack = getRunAsPrincipalsStack();
442        return !CollectionUtils.isEmpty(stack);
443    }
444
445    public PrincipalCollection getPreviousPrincipals() {
446        PrincipalCollection previousPrincipals = null;
447        List<PrincipalCollection> stack = getRunAsPrincipalsStack();
448        int stackSize = stack != null ? stack.size() : 0;
449        if (stackSize > 0) {
450            if (stackSize == 1) {
451                previousPrincipals = this.principals;
452            } else {
453                //always get the one behind the current:
454                assert stack != null;
455                previousPrincipals = stack.get(1);
456            }
457        }
458        return previousPrincipals;
459    }
460
461    public PrincipalCollection releaseRunAs() {
462        return popIdentity();
463    }
464
465    @SuppressWarnings("unchecked")
466    private List<PrincipalCollection> getRunAsPrincipalsStack() {
467        Session session = getSession(false);
468        if (session != null) {
469            return (List<PrincipalCollection>) session.getAttribute(RUN_AS_PRINCIPALS_SESSION_KEY);
470        }
471        return null;
472    }
473
474    private void clearRunAsIdentities() {
475        Session session = getSession(false);
476        if (session != null) {
477            session.removeAttribute(RUN_AS_PRINCIPALS_SESSION_KEY);
478        }
479    }
480
481    private void pushIdentity(PrincipalCollection principals) throws NullPointerException {
482        if (CollectionUtils.isEmpty(principals)) {
483            String msg = "Specified Subject principals cannot be null or empty for 'run as' functionality.";
484            throw new NullPointerException(msg);
485        }
486        List<PrincipalCollection> stack = getRunAsPrincipalsStack();
487        if (stack == null) {
488            stack = new CopyOnWriteArrayList<PrincipalCollection>();
489        }
490        stack.add(0, principals);
491        Session session = getSession();
492        session.setAttribute(RUN_AS_PRINCIPALS_SESSION_KEY, stack);
493    }
494
495    private PrincipalCollection popIdentity() {
496        PrincipalCollection popped = null;
497
498        List<PrincipalCollection> stack = getRunAsPrincipalsStack();
499        if (!CollectionUtils.isEmpty(stack)) {
500            popped = stack.remove(0);
501            Session session;
502            if (!CollectionUtils.isEmpty(stack)) {
503                //persist the changed stack to the session
504                session = getSession();
505                session.setAttribute(RUN_AS_PRINCIPALS_SESSION_KEY, stack);
506            } else {
507                //stack is empty, remove it from the session:
508                clearRunAsIdentities();
509            }
510        }
511
512        return popped;
513    }
514}