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.mgt.DefaultSessionContext;
032    import org.apache.shiro.session.mgt.SessionContext;
033    import org.apache.shiro.subject.ExecutionException;
034    import org.apache.shiro.subject.PrincipalCollection;
035    import org.apache.shiro.subject.Subject;
036    import org.apache.shiro.util.CollectionUtils;
037    import org.apache.shiro.util.StringUtils;
038    import org.apache.shiro.util.ThreadContext;
039    import org.slf4j.Logger;
040    import org.slf4j.LoggerFactory;
041    
042    import java.io.Serializable;
043    import java.util.ArrayList;
044    import java.util.Collection;
045    import java.util.List;
046    import java.util.concurrent.Callable;
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 suited.
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     * @author Les Hazlewood
071     * @author Jeremy Haile
072     * @since 0.1
073     */
074    public class DelegatingSubject implements Subject, Serializable {
075    
076        private static final long serialVersionUID = -5094259915319399138L;
077    
078        private static final Logger log = LoggerFactory.getLogger(DelegatingSubject.class);
079    
080        private static final String RUN_AS_PRINCIPALS_SESSION_KEY =
081                DelegatingSubject.class.getName() + ".RUN_AS_PRINCIPALS_SESSION_KEY";
082    
083        protected PrincipalCollection principals;
084        protected boolean authenticated;
085        protected String host;
086        protected Session session;
087        private List<PrincipalCollection> runAsPrincipals; //supports assumed identities (aka 'run as')
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            if (securityManager == null) {
098                throw new IllegalArgumentException("SecurityManager argument cannot be null.");
099            }
100            this.securityManager = securityManager;
101            this.principals = principals;
102            this.authenticated = authenticated;
103            this.host = host;
104            if (session != null) {
105                this.session = decorate(session);
106                this.runAsPrincipals = getRunAsPrincipals(this.session);
107            }
108        }
109    
110        protected Session decorate(Session session) {
111            if (session == null) {
112                throw new IllegalArgumentException("session cannot be null");
113            }
114            return new StoppingAwareProxiedSession(session, this);
115        }
116    
117        public SecurityManager getSecurityManager() {
118            return securityManager;
119        }
120    
121        protected boolean hasPrincipals() {
122            return !CollectionUtils.isEmpty(getPrincipals());
123        }
124    
125        /**
126         * Returns the host name or IP associated with the client who created/is interacting with this Subject.
127         *
128         * @return the host name or IP associated with the client who created/is interacting with this Subject.
129         */
130        public String getHost() {
131            return this.host;
132        }
133    
134        private Object getPrimaryPrincipal(PrincipalCollection principals) {
135            if (!CollectionUtils.isEmpty(principals)) {
136                return principals.getPrimaryPrincipal();
137            }
138            return null;
139        }
140    
141        /**
142         * @see Subject#getPrincipal()
143         */
144        public Object getPrincipal() {
145            return getPrimaryPrincipal(getPrincipals());
146        }
147    
148        public PrincipalCollection getPrincipals() {
149            return CollectionUtils.isEmpty(this.runAsPrincipals) ? this.principals : this.runAsPrincipals.get(0);
150        }
151    
152        public boolean isPermitted(String permission) {
153            return hasPrincipals() && securityManager.isPermitted(getPrincipals(), permission);
154        }
155    
156        public boolean isPermitted(Permission permission) {
157            return hasPrincipals() && securityManager.isPermitted(getPrincipals(), permission);
158        }
159    
160        public boolean[] isPermitted(String... permissions) {
161            if (hasPrincipals()) {
162                return securityManager.isPermitted(getPrincipals(), permissions);
163            } else {
164                return new boolean[permissions.length];
165            }
166        }
167    
168        public boolean[] isPermitted(List<Permission> permissions) {
169            if (hasPrincipals()) {
170                return securityManager.isPermitted(getPrincipals(), permissions);
171            } else {
172                return new boolean[permissions.size()];
173            }
174        }
175    
176        public boolean isPermittedAll(String... permissions) {
177            return hasPrincipals() && securityManager.isPermittedAll(getPrincipals(), permissions);
178        }
179    
180        public boolean isPermittedAll(Collection<Permission> permissions) {
181            return hasPrincipals() && securityManager.isPermittedAll(getPrincipals(), permissions);
182        }
183    
184        protected void assertAuthzCheckPossible() throws AuthorizationException {
185            if (!hasPrincipals()) {
186                String msg = "This subject is anonymous - it does not have any identifying principals and " +
187                        "authorization operations require an identity to check against.  A Subject instance will " +
188                        "acquire these identifying principals automatically after a successful login is performed " +
189                        "be executing " + Subject.class.getName() + ".login(AuthenticationToken) or when 'Remember Me' " +
190                        "functionality is enabled by the SecurityManager.  This exception can also occur when a " +
191                        "previously logged-in Subject has logged out which " +
192                        "makes it anonymous again.  Because an identity is currently not known due to any of these " +
193                        "conditions, authorization is denied.";
194                throw new UnauthenticatedException(msg);
195            }
196        }
197    
198        public void checkPermission(String permission) throws AuthorizationException {
199            assertAuthzCheckPossible();
200            securityManager.checkPermission(getPrincipals(), permission);
201        }
202    
203        public void checkPermission(Permission permission) throws AuthorizationException {
204            assertAuthzCheckPossible();
205            securityManager.checkPermission(getPrincipals(), permission);
206        }
207    
208        public void checkPermissions(String... permissions) throws AuthorizationException {
209            assertAuthzCheckPossible();
210            securityManager.checkPermissions(getPrincipals(), permissions);
211        }
212    
213        public void checkPermissions(Collection<Permission> permissions) throws AuthorizationException {
214            assertAuthzCheckPossible();
215            securityManager.checkPermissions(getPrincipals(), permissions);
216        }
217    
218        public boolean hasRole(String roleIdentifier) {
219            return hasPrincipals() && securityManager.hasRole(getPrincipals(), roleIdentifier);
220        }
221    
222        public boolean[] hasRoles(List<String> roleIdentifiers) {
223            if (hasPrincipals()) {
224                return securityManager.hasRoles(getPrincipals(), roleIdentifiers);
225            } else {
226                return new boolean[roleIdentifiers.size()];
227            }
228        }
229    
230        public boolean hasAllRoles(Collection<String> roleIdentifiers) {
231            return hasPrincipals() && securityManager.hasAllRoles(getPrincipals(), roleIdentifiers);
232        }
233    
234        public void checkRole(String role) throws AuthorizationException {
235            assertAuthzCheckPossible();
236            securityManager.checkRole(getPrincipals(), role);
237        }
238    
239        public void checkRoles(Collection<String> roles) throws AuthorizationException {
240            assertAuthzCheckPossible();
241            securityManager.checkRoles(getPrincipals(), roles);
242        }
243    
244        public void login(AuthenticationToken token) throws AuthenticationException {
245            clearRunAsIdentities();
246            Subject subject = securityManager.login(this, token);
247    
248            PrincipalCollection principals;
249    
250            String host = null;
251    
252            if (subject instanceof DelegatingSubject) {
253                DelegatingSubject delegating = (DelegatingSubject) subject;
254                //we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
255                principals = delegating.principals;
256                host = delegating.host;
257            } else {
258                principals = subject.getPrincipals();
259            }
260    
261            if (principals == null || principals.isEmpty()) {
262                String msg = "Principals returned from securityManager.login( token ) returned a null or " +
263                        "empty value.  This value must be non null and populated with one or more elements.";
264                throw new IllegalStateException(msg);
265            }
266            this.principals = principals;
267            this.authenticated = true;
268            if (token instanceof HostAuthenticationToken) {
269                host = ((HostAuthenticationToken) token).getHost();
270            }
271            if (host != null) {
272                this.host = host;
273            }
274            Session session = subject.getSession(false);
275            if (session != null) {
276                this.session = decorate(session);
277                this.runAsPrincipals = getRunAsPrincipals(this.session);
278            } else {
279                this.session = null;
280            }
281            ThreadContext.bind(this);
282        }
283    
284        public boolean isAuthenticated() {
285            return authenticated;
286        }
287    
288        public boolean isRemembered() {
289            PrincipalCollection principals = getPrincipals();
290            return principals != null && !principals.isEmpty() && !isAuthenticated();
291        }
292    
293        public Session getSession() {
294            return getSession(true);
295        }
296    
297        public Session getSession(boolean create) {
298            if (log.isTraceEnabled()) {
299                log.trace("attempting to get session; create = " + create + "; session is null = " + (this.session == null) + "; session has id = " + (this.session != null && session.getId() != null));
300            }
301    
302            if (this.session == null && create) {
303                log.trace("Starting session for host {}", getHost());
304                SessionContext sessionContext = createSessionContext();
305                Session session = this.securityManager.start(sessionContext);
306                this.session = decorate(session);
307            }
308            return this.session;
309        }
310    
311        protected SessionContext createSessionContext() {
312            SessionContext sessionContext = new DefaultSessionContext();
313            if (StringUtils.hasText(host)) {
314                sessionContext.setHost(host);
315            }
316            return sessionContext;
317        }
318    
319        public void logout() {
320            try {
321                clearRunAsIdentities();
322                this.securityManager.logout(this);
323            } finally {
324                this.session = null;
325                this.principals = null;
326                this.authenticated = false;
327                this.runAsPrincipals = null;
328                //Don't set securityManager to null here - the Subject can still be
329                //used, it is just considered anonymous at this point.  The SecurityManager instance is
330                //necessary if the subject would log in again or acquire a new session.  This is in response to
331                //https://issues.apache.org/jira/browse/JSEC-22
332                //this.securityManager = null;
333            }
334        }
335    
336        private void sessionStopped() {
337            this.session = null;
338        }
339    
340        public <V> V execute(Callable<V> callable) throws ExecutionException {
341            Callable<V> associated = associateWith(callable);
342            try {
343                return associated.call();
344            } catch (Throwable t) {
345                throw new ExecutionException(t);
346            }
347        }
348    
349        public void execute(Runnable runnable) {
350            Runnable associated = associateWith(runnable);
351            associated.run();
352        }
353    
354        public <V> Callable<V> associateWith(Callable<V> callable) {
355            return new SubjectCallable<V>(this, callable);
356        }
357    
358        public Runnable associateWith(Runnable runnable) {
359            if (runnable instanceof Thread) {
360                String msg = "This implementation does not support Thread arguments because of JDK ThreadLocal " +
361                        "inheritance mechanisms required by Shiro.  Instead, the method argument should be a non-Thread " +
362                        "Runnable and the return value from this method can then be given to an ExecutorService or " +
363                        "another Thread.";
364                throw new UnsupportedOperationException(msg);
365            }
366            return new SubjectRunnable(this, runnable);
367        }
368    
369        private class StoppingAwareProxiedSession extends ProxiedSession {
370    
371            private final DelegatingSubject owner;
372    
373            private StoppingAwareProxiedSession(Session target, DelegatingSubject owningSubject) {
374                super(target);
375                owner = owningSubject;
376            }
377    
378            public void stop() throws InvalidSessionException {
379                super.stop();
380                owner.sessionStopped();
381            }
382        }
383    
384    
385        // ======================================
386        // 'Run As' support implementations
387        // ======================================
388    
389        public void runAs(PrincipalCollection principals) {
390            if (!hasPrincipals()) {
391                String msg = "This subject does not yet have an identity.  Assuming the identity of another " +
392                        "Subject is only allowed for Subjects with an existing identity.  Try logging this subject in " +
393                        "first, or using the " + Subject.Builder.class.getName() + " to build ad hoc Subject instances " +
394                        "with identities as necessary.";
395                throw new IllegalStateException(msg);
396            }
397            pushIdentity(principals);
398        }
399    
400        public boolean isRunAs() {
401            return !CollectionUtils.isEmpty(this.runAsPrincipals);
402        }
403    
404        public PrincipalCollection getPreviousPrincipals() {
405            return isRunAs() ? this.principals : null;
406        }
407    
408        public PrincipalCollection releaseRunAs() {
409            return popIdentity();
410        }
411    
412        @SuppressWarnings({"unchecked"})
413        private List<PrincipalCollection> getRunAsPrincipals(Session session) {
414            if (session != null) {
415                return (List<PrincipalCollection>) session.getAttribute(RUN_AS_PRINCIPALS_SESSION_KEY);
416            }
417            return null;
418        }
419    
420        private void clearRunAsIdentities() {
421            Session session = getSession(false);
422            if (session != null) {
423                session.removeAttribute(RUN_AS_PRINCIPALS_SESSION_KEY);
424            }
425            this.runAsPrincipals = null;
426        }
427    
428        private void pushIdentity(PrincipalCollection principals) throws NullPointerException {
429            if (CollectionUtils.isEmpty(principals)) {
430                String msg = "Specified Subject principals cannot be null or empty for 'run as' functionality.";
431                throw new NullPointerException(msg);
432            }
433            if (this.runAsPrincipals == null) {
434                this.runAsPrincipals = new ArrayList<PrincipalCollection>();
435            }
436            this.runAsPrincipals.add(0, principals);
437            Session session = getSession();
438            session.setAttribute(RUN_AS_PRINCIPALS_SESSION_KEY, this.runAsPrincipals);
439        }
440    
441        private PrincipalCollection popIdentity() {
442            PrincipalCollection popped = null;
443            if (!CollectionUtils.isEmpty(this.runAsPrincipals)) {
444                popped = this.runAsPrincipals.remove(0);
445                Session session;
446                if (!CollectionUtils.isEmpty(this.runAsPrincipals)) {
447                    //persist the changed deque to the session
448                    session = getSession();
449                    session.setAttribute(RUN_AS_PRINCIPALS_SESSION_KEY, this.runAsPrincipals);
450                } else {
451                    //deque is empty, remove it from the session:
452                    session = getSession(false);
453                    if (session != null) {
454                        session.removeAttribute(RUN_AS_PRINCIPALS_SESSION_KEY);
455                    }
456                }
457            }
458    
459            return popped;
460        }
461    
462    }