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     */
019    package org.apache.shiro.subject.support;
020    
021    import org.apache.shiro.authc.AuthenticationException;
022    import org.apache.shiro.authc.AuthenticationToken;
023    import org.apache.shiro.authc.HostAuthenticationToken;
024    import org.apache.shiro.authz.AuthorizationException;
025    import org.apache.shiro.authz.Permission;
026    import org.apache.shiro.authz.UnauthenticatedException;
027    import org.apache.shiro.mgt.SecurityManager;
028    import org.apache.shiro.session.InvalidSessionException;
029    import org.apache.shiro.session.ProxiedSession;
030    import org.apache.shiro.session.Session;
031    import org.apache.shiro.session.SessionException;
032    import org.apache.shiro.session.mgt.DefaultSessionContext;
033    import org.apache.shiro.session.mgt.SessionContext;
034    import org.apache.shiro.subject.ExecutionException;
035    import org.apache.shiro.subject.PrincipalCollection;
036    import org.apache.shiro.subject.Subject;
037    import org.apache.shiro.util.CollectionUtils;
038    import org.apache.shiro.util.StringUtils;
039    import org.slf4j.Logger;
040    import org.slf4j.LoggerFactory;
041    
042    import java.util.Collection;
043    import java.util.List;
044    import java.util.concurrent.Callable;
045    import 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     */
071    public 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    }