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.mgt;
020    
021    import org.apache.shiro.authc.*;
022    import org.apache.shiro.authz.Authorizer;
023    import org.apache.shiro.realm.Realm;
024    import org.apache.shiro.session.InvalidSessionException;
025    import org.apache.shiro.session.Session;
026    import org.apache.shiro.session.mgt.DefaultSessionContext;
027    import org.apache.shiro.session.mgt.DefaultSessionKey;
028    import org.apache.shiro.session.mgt.SessionContext;
029    import org.apache.shiro.session.mgt.SessionKey;
030    import org.apache.shiro.subject.PrincipalCollection;
031    import org.apache.shiro.subject.Subject;
032    import org.apache.shiro.subject.SubjectContext;
033    import org.apache.shiro.subject.support.DefaultSubjectContext;
034    import org.apache.shiro.util.CollectionUtils;
035    import org.slf4j.Logger;
036    import org.slf4j.LoggerFactory;
037    
038    import java.io.Serializable;
039    import java.util.Collection;
040    
041    /**
042     * The Shiro framework's default concrete implementation of the {@link SecurityManager} interface,
043     * based around a collection of {@link org.apache.shiro.realm.Realm}s.  This implementation delegates its
044     * authentication, authorization, and session operations to wrapped {@link Authenticator}, {@link Authorizer}, and
045     * {@link org.apache.shiro.session.mgt.SessionManager SessionManager} instances respectively via superclass
046     * implementation.
047     * <p/>
048     * To greatly reduce and simplify configuration, this implementation (and its superclasses) will
049     * create suitable defaults for all of its required dependencies, <em>except</em> the required one or more
050     * {@link Realm Realm}s.  Because {@code Realm} implementations usually interact with an application's data model,
051     * they are almost always application specific;  you will want to specify at least one custom
052     * {@code Realm} implementation that 'knows' about your application's data/security model
053     * (via {@link #setRealm} or one of the overloaded constructors).  All other attributes in this class hierarchy
054     * will have suitable defaults for most enterprise applications.
055     * <p/>
056     * <b>RememberMe notice</b>: This class supports the ability to configure a
057     * {@link #setRememberMeManager RememberMeManager}
058     * for {@code RememberMe} identity services for login/logout, BUT, a default instance <em>will not</em> be created
059     * for this attribute at startup.
060     * <p/>
061     * Because RememberMe services are inherently client tier-specific and
062     * therefore aplication-dependent, if you want {@code RememberMe} services enabled, you will have to specify an
063     * instance yourself via the {@link #setRememberMeManager(RememberMeManager) setRememberMeManager}
064     * mutator.  However if you're reading this JavaDoc with the
065     * expectation of operating in a Web environment, take a look at the
066     * {@code org.apache.shiro.web.DefaultWebSecurityManager} implementation, which
067     * <em>does</em> support {@code RememberMe} services by default at startup.
068     *
069     * @author Les Hazlewood
070     * @author Jeremy Haile
071     * @since 0.2
072     */
073    public class DefaultSecurityManager extends SessionsSecurityManager {
074    
075        //TODO - complete JavaDoc
076    
077        private static final Logger log = LoggerFactory.getLogger(DefaultSecurityManager.class);
078    
079        protected RememberMeManager rememberMeManager;
080    
081        protected SubjectFactory subjectFactory;
082    
083        /**
084         * Default no-arg constructor.
085         */
086        public DefaultSecurityManager() {
087            super();
088            this.subjectFactory = new DefaultSubjectFactory();
089        }
090    
091        /**
092         * Supporting constructor for a single-realm application.
093         *
094         * @param singleRealm the single realm used by this SecurityManager.
095         */
096        public DefaultSecurityManager(Realm singleRealm) {
097            this();
098            setRealm(singleRealm);
099        }
100    
101        /**
102         * Supporting constructor for multiple {@link #setRealms realms}.
103         *
104         * @param realms the realm instances backing this SecurityManager.
105         */
106        public DefaultSecurityManager(Collection<Realm> realms) {
107            this();
108            setRealms(realms);
109        }
110    
111        public SubjectFactory getSubjectFactory() {
112            return subjectFactory;
113        }
114    
115        public void setSubjectFactory(SubjectFactory subjectFactory) {
116            this.subjectFactory = subjectFactory;
117        }
118    
119        public RememberMeManager getRememberMeManager() {
120            return rememberMeManager;
121        }
122    
123        public void setRememberMeManager(RememberMeManager rememberMeManager) {
124            this.rememberMeManager = rememberMeManager;
125        }
126    
127        protected SubjectContext createSubjectContext() {
128            return new DefaultSubjectContext();
129        }
130    
131        /**
132         * Creates a {@code Subject} instance for the user represented by the given method arguments.
133         *
134         * @param token    the {@code AuthenticationToken} submitted for the successful authentication.
135         * @param info     the {@code AuthenticationInfo} of a newly authenticated user.
136         * @param existing the existing {@code Subject} instance that initiated the authentication attempt
137         * @return the {@code Subject} instance that represents the context and session data for the newly
138         *         authenticated subject.
139         */
140        protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) {
141            SubjectContext context = createSubjectContext();
142            context.setAuthenticated(true);
143            context.setAuthenticationToken(token);
144            context.setAuthenticationInfo(info);
145            if (existing != null) {
146                context.setSubject(existing);
147            }
148            return createSubject(context);
149        }
150    
151        /**
152         * Binds a {@code Subject} instance created after authentication to the application for later use.
153         * <p/>
154         * The default implementation simply stores the Subject's principals and authentication state to the
155         * {@code Subject}'s {@link Subject#getSession() session} to ensure it is available for reference later.
156         *
157         * @param subject the {@code Subject} instance created after authentication to be bound to the application
158         *                for later use.
159         */
160        protected void bind(Subject subject) {
161            // TODO consider refactoring to use Subject.Binder.
162            // This implementation was copied from SessionSubjectBinder that was removed
163            PrincipalCollection principals = subject.getPrincipals();
164            if (principals != null && !principals.isEmpty()) {
165                Session session = subject.getSession();
166                bindPrincipalsToSession(principals, session);
167            } else {
168                Session session = subject.getSession(false);
169                if (session != null) {
170                    session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
171                }
172            }
173    
174            if (subject.isAuthenticated()) {
175                Session session = subject.getSession();
176                session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, subject.isAuthenticated());
177            } else {
178                Session session = subject.getSession(false);
179                if (session != null) {
180                    session.removeAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);
181                }
182            }
183        }
184    
185        /**
186         * Saves the specified identity to the given session, making the session no longer anonymous.
187         *
188         * @param principals the Subject identity to save to the session
189         * @param session    the Session to retain the Subject identity.
190         * @throws IllegalArgumentException if the principals are null or empty or the session is null
191         * @since 1.0
192         */
193        private void bindPrincipalsToSession(PrincipalCollection principals, Session session) throws IllegalArgumentException {
194            if (session == null) {
195                throw new IllegalArgumentException("Session argument cannot be null.");
196            }
197            if (CollectionUtils.isEmpty(principals)) {
198                throw new IllegalArgumentException("Principals cannot be null or empty.");
199            }
200            session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, principals);
201        }
202    
203        protected void rememberMeSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) {
204            RememberMeManager rmm = getRememberMeManager();
205            if (rmm != null) {
206                try {
207                    rmm.onSuccessfulLogin(subject, token, info);
208                } catch (Exception e) {
209                    if (log.isWarnEnabled()) {
210                        String msg = "Delegate RememberMeManager instance of type [" + rmm.getClass().getName() +
211                                "] threw an exception during onSuccessfulLogin.  RememberMe services will not be " +
212                                "performed for account [" + info + "].";
213                        log.warn(msg, e);
214                    }
215                }
216            } else {
217                if (log.isTraceEnabled()) {
218                    log.trace("This " + getClass().getName() + " instance does not have a " +
219                            "[" + RememberMeManager.class.getName() + "] instance configured.  RememberMe services " +
220                            "will not be performed for account [" + info + "].");
221                }
222            }
223        }
224    
225        protected void rememberMeFailedLogin(AuthenticationToken token, AuthenticationException ex, Subject subject) {
226            RememberMeManager rmm = getRememberMeManager();
227            if (rmm != null) {
228                try {
229                    rmm.onFailedLogin(subject, token, ex);
230                } catch (Exception e) {
231                    if (log.isWarnEnabled()) {
232                        String msg = "Delegate RememberMeManager instance of type [" + rmm.getClass().getName() +
233                                "] threw an exception during onFailedLogin for AuthenticationToken [" +
234                                token + "].";
235                        log.warn(msg, e);
236                    }
237                }
238            }
239        }
240    
241        protected void rememberMeLogout(Subject subject) {
242            RememberMeManager rmm = getRememberMeManager();
243            if (rmm != null) {
244                try {
245                    rmm.onLogout(subject);
246                } catch (Exception e) {
247                    if (log.isWarnEnabled()) {
248                        String msg = "Delegate RememberMeManager instance of type [" + rmm.getClass().getName() +
249                                "] threw an exception during onLogout for subject with principals [" +
250                                (subject != null ? subject.getPrincipals() : null) + "]";
251                        log.warn(msg, e);
252                    }
253                }
254            }
255        }
256    
257        /**
258         * First authenticates the {@code AuthenticationToken} argument, and if successful, constructs a
259         * {@code Subject} instance representing the authenticated account's identity.
260         * <p/>
261         * Once constructed, the {@code Subject} instance is then {@link #bind bound} to the application for
262         * subsequent access before being returned to the caller.
263         *
264         * @param token the authenticationToken to process for the login attempt.
265         * @return a Subject representing the authenticated user.
266         * @throws AuthenticationException if there is a problem authenticating the specified {@code token}.
267         */
268        public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
269            AuthenticationInfo info;
270            try {
271                info = authenticate(token);
272            } catch (AuthenticationException ae) {
273                try {
274                    onFailedLogin(token, ae, subject);
275                } catch (Exception e) {
276                    if (log.isInfoEnabled()) {
277                        log.info("onFailedLogin method threw an " +
278                                "exception.  Logging and propagating original AuthenticationException.", e);
279                    }
280                }
281                throw ae; //propagate
282            }
283    
284            Subject loggedIn = createSubject(token, info, subject);
285    
286            bind(loggedIn);
287    
288            onSuccessfulLogin(token, info, loggedIn);
289            return loggedIn;
290        }
291    
292        protected void onSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) {
293            rememberMeSuccessfulLogin(token, info, subject);
294        }
295    
296        protected void onFailedLogin(AuthenticationToken token, AuthenticationException ae, Subject subject) {
297            rememberMeFailedLogin(token, ae, subject);
298        }
299    
300        protected void beforeLogout(Subject subject) {
301            rememberMeLogout(subject);
302        }
303    
304        protected SubjectContext copy(SubjectContext subjectContext) {
305            return new DefaultSubjectContext(subjectContext);
306        }
307    
308        /**
309         * This implementation attempts to resolve any session ID that may exist in the context by
310         * passing it to the {@link #resolveSession(SubjectContext)} method.  The
311         * return value from that call is then used to attempt to resolve the subject identity via the
312         * {@link #resolvePrincipals(SubjectContext)} method.  The return value from that call is then used to create
313         * the {@code Subject} instance by calling
314         * <code>{@link #getSubjectFactory() getSubjectFactory()}.{@link SubjectFactory#createSubject createSubject}(resolvedContext);</code>
315         *
316         * @param subjectContext any data needed to direct how the Subject should be constructed.
317         * @return the {@code Subject} instance reflecting the specified initialization data.
318         * @see SubjectFactory#createSubject
319         * @since 1.0
320         */
321        public Subject createSubject(SubjectContext subjectContext) {
322            //create a copy so we don't modify the argument's backing map:
323            SubjectContext context = copy(subjectContext);
324    
325            //ensure that the context has a SecurityManager instance, and if not, add one:
326            context = ensureSecurityManager(context);
327    
328            //Resolve an associated Session (usually based on a referenced session ID), and place it in the context before
329            //sending to the SubjectFactory.  The SubjectFactory should not need to know how to acquire sessions as the
330            //process is often environment specific - better to shield the SF from these details:
331            context = resolveSession(context);
332    
333            //Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first
334            //if possible before handing off to the SubjectFactory:
335            context = resolvePrincipals(context);
336    
337            return getSubjectFactory().createSubject(context);
338        }
339    
340        /**
341         * Determines if there is a {@code SecurityManager} instance in the context, and if not, adds 'this' to the
342         * context.  This ensures the SubjectFactory instance will have access to a SecurityManager during Subject
343         * construction if necessary.
344         *
345         * @param context the subject context data that may contain a SecurityManager instance.
346         * @return The SubjectContext to use to pass to a {@link SubjectFactory} for subject creation.
347         * @since 1.0
348         */
349        @SuppressWarnings({"unchecked"})
350        protected SubjectContext ensureSecurityManager(SubjectContext context) {
351            if (context.resolveSecurityManager() != null) {
352                log.trace("Context already contains a SecurityManager instance.  Returning.");
353                return context;
354            }
355            log.trace("No SecurityManager found in context.  Adding self reference.");
356            context.setSecurityManager(this);
357            return context;
358        }
359    
360        /**
361         * Attempts to resolve any associated session based on the context and returns a
362         * context that represents this resolved {@code Session} to ensure it may be referenced if necessary by the
363         * invoked {@link SubjectFactory} that performs actual {@link Subject} construction.
364         * <p/>
365         * If there is a {@code Session} already in the context because that is what the caller wants to be used for
366         * {@code Subject} construction, or if no session is resolved, this method effectively does nothing
367         * returns the Map method argument unaltered.
368         *
369         * @param context the subject context data that may resolve a Session instance.
370         * @return The context to use to pass to a {@link SubjectFactory} for subject creation.
371         * @since 1.0
372         */
373        @SuppressWarnings({"unchecked"})
374        protected SubjectContext resolveSession(SubjectContext context) {
375            if (context.resolveSession() != null) {
376                log.debug("Context already contains a session.  Returning.");
377                return context;
378            }
379            try {
380                //Context couldn't resolve it directly, let's see if we can since we have direct access to 
381                //the session manager:
382                Session session = resolveContextSession(context);
383                if (session != null) {
384                    context.setSession(session);
385                }
386            } catch (InvalidSessionException e) {
387                log.debug("Resolved SubjectContext context session is invalid.  Ignoring and creating an anonymous " +
388                        "(session-less) Subject instance.", e);
389            }
390            return context;
391        }
392    
393        protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException {
394            SessionKey key = getSessionKey(context);
395            if (key != null) {
396                return getSession(key);
397            }
398            return null;
399        }
400    
401        protected SessionKey getSessionKey(SubjectContext context) {
402            Serializable sessionId = context.getSessionId();
403            if (sessionId != null) {
404                return new DefaultSessionKey(sessionId);
405            }
406            return null;
407        }
408    
409        /**
410         * Attempts to resolve an identity (a {@link PrincipalCollection}) for the context using heuristics.  The
411         * implementation strategy:
412         * <ol>
413         * <li>Check the context to see if it can already {@link SubjectContext#resolvePrincipals resolve an identity}.  If
414         * so, this method does nothing and returns the method argument unaltered.</li>
415         * <li>Check for a RememberMe identity by calling {@link #getRememberedIdentity}.  If that method returns a
416         * non-null value, place the remembered {@link PrincipalCollection} in the context.</li>
417         * <li>If the remembered identity is discovered, associate it with the session to eliminate unnecessary
418         * rememberMe accesses for the remainder of the session</li>
419         * </ol>
420         *
421         * @param context the subject context data that may provide (directly or indirectly through one of its values) a
422         *                {@link PrincipalCollection} identity.
423         * @return The Subject context to use to pass to a {@link SubjectFactory} for subject creation.
424         * @since 1.0
425         */
426        @SuppressWarnings({"unchecked"})
427        protected SubjectContext resolvePrincipals(SubjectContext context) {
428    
429            PrincipalCollection principals = context.resolvePrincipals();
430    
431            if (CollectionUtils.isEmpty(principals)) {
432                log.trace("No identity (PrincipalCollection) found in the context.  Looking for a remembered identity.");
433    
434                principals = getRememberedIdentity(context);
435    
436                if (!CollectionUtils.isEmpty(principals)) {
437                    log.debug("Found remembered PrincipalCollection.  Adding to the context to be used " +
438                            "for subject construction by the SubjectFactory.");
439    
440                    context.setPrincipals(principals);
441                    bindPrincipalsToSession(principals, context);
442                } else {
443                    log.trace("No remembered identity found.  Returning original context.");
444                }
445            }
446    
447            return context;
448        }
449    
450        /**
451         * Satisfies SHIRO-157: associate a known identity with the current session to ensure that we don't need to
452         * continually perform rememberMe operations for sessions that already have an identity.  Doing this prevents the
453         * need to continually reference, decrypt and deserialize the rememberMe cookie every time - something that can
454         * be computationally expensive if many requests are intercepted.
455         * <p/>
456         * Note that if the SubjectContext cannot {@link SubjectContext#resolveSession resolve} a session, a new session
457         * will be created receive the principals and then appended to the SubjectContext so it can be used later when
458         * constructing the Subject.
459         *
460         * @param principals the non-null, non-empty principals to bind to the SubjectContext's session
461         * @param context    the context to use to locate or create a session to which the principals will be saved
462         * @since 1.0
463         */
464        private void bindPrincipalsToSession(PrincipalCollection principals, SubjectContext context) {
465            SecurityManager securityManager = context.resolveSecurityManager();
466            if (securityManager == null) {
467                throw new IllegalStateException("SecurityManager instance should already be present in the " +
468                        "SubjectContext argument.");
469            }
470            Session session = context.resolveSession();
471            if (session == null) {
472                log.trace("No session in the current subject context.  One will be created to persist principals [{}] " +
473                        "Doing this prevents unnecessary repeated RememberMe operations since an identity has been " +
474                        "discovered.", principals);
475                //no session - start one:
476                SessionContext sessionContext = createSessionContext(context);
477                session = start(sessionContext);
478                context.setSession(session);
479                log.debug("Created session with id {} to retain discovered principals {}", session.getId(), principals);
480            }
481            bindPrincipalsToSession(principals, session);
482        }
483    
484        protected SessionContext createSessionContext(SubjectContext subjectContext) {
485            DefaultSessionContext sessionContext = new DefaultSessionContext();
486            if (!CollectionUtils.isEmpty(subjectContext)) {
487                sessionContext.putAll(subjectContext);
488            }
489            Serializable sessionId = subjectContext.getSessionId();
490            if (sessionId != null) {
491                sessionContext.setSessionId(sessionId);
492            }
493            String host = subjectContext.resolveHost();
494            if (host != null) {
495                sessionContext.setHost(host);
496            }
497            return sessionContext;
498        }
499    
500        public void logout(Subject subject) {
501    
502            if (subject == null) {
503                throw new IllegalArgumentException("Subject method argument cannot be null.");
504            }
505    
506            beforeLogout(subject);
507    
508            PrincipalCollection principals = subject.getPrincipals();
509            if (principals != null && !principals.isEmpty()) {
510                if (log.isDebugEnabled()) {
511                    log.debug("Logging out subject with primary principal {}" + principals.getPrimaryPrincipal());
512                }
513                Authenticator authc = getAuthenticator();
514                if (authc instanceof LogoutAware) {
515                    ((LogoutAware) authc).onLogout(principals);
516                }
517            }
518    
519            try {
520                unbind(subject);
521            } catch (Exception e) {
522                if (log.isDebugEnabled()) {
523                    String msg = "Unable to cleanly unbind Subject.  Ignoring (logging out).";
524                    log.debug(msg, e);
525                }
526            } finally {
527                try {
528                    stopSession(subject);
529                } catch (Exception e) {
530                    if (log.isDebugEnabled()) {
531                        String msg = "Unable to cleanly stop Session for Subject [" + subject.getPrincipal() + "] " +
532                                "Ignoring (logging out).";
533                        log.debug(msg, e);
534                    }
535                }
536            }
537        }
538    
539        protected void stopSession(Subject subject) {
540            Session s = subject.getSession(false);
541            if (s != null) {
542                s.stop();
543            }
544        }
545    
546        /**
547         * Unbinds or removes the Subject's state from the application, typically called during {@link #logout}.
548         * <p/>
549         * This implementation is symmetric with the {@link #bind} method in that it will remove any principals and
550         * authentication state from the session if the session exists.  If there is no subject session, this method
551         * does not do anything.
552         *
553         * @param subject the subject to unbind from the application as it will no longer be used.
554         */
555        protected void unbind(Subject subject) {
556            Session session = subject.getSession(false);
557            if (session != null) {
558                session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
559                session.removeAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);
560            }
561        }
562    
563        protected PrincipalCollection getRememberedIdentity(SubjectContext subjectContext) {
564            RememberMeManager rmm = getRememberMeManager();
565            if (rmm != null) {
566                try {
567                    return rmm.getRememberedPrincipals(subjectContext);
568                } catch (Exception e) {
569                    if (log.isWarnEnabled()) {
570                        String msg = "Delegate RememberMeManager instance of type [" + rmm.getClass().getName() +
571                                "] threw an exception during getRememberedPrincipals().";
572                        log.warn(msg, e);
573                    }
574                }
575            }
576            return null;
577        }
578    }