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