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    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            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}