1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one
3  * or more contributor license agreements.  See the NOTICE file
4  * distributed with this work for additional information
5  * regarding copyright ownership.  The ASF licenses this file
6  * to you under the Apache License, Version 2.0 (the
7  * "License"); you may not use this file except in compliance
8  * with the License.  You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing,
13  * software distributed under the License is distributed on an
14  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15  * KIND, either express or implied.  See the License for the
16  * specific language governing permissions and limitations
17  * under the License.
18  */
19 module hunt.shiro.mgt.DefaultSubjectDAO;
20 
21 
22 import hunt.shiro.mgt.DefaultSessionStorageEvaluator;
23 import hunt.shiro.mgt.SessionStorageEvaluator;
24 import hunt.shiro.mgt.SubjectDAO;
25 
26 
27 import hunt.shiro.session.Session;
28 import hunt.shiro.subject.PrincipalCollection;
29 import hunt.shiro.subject.Subject;
30 import hunt.shiro.subject.support.DefaultSubjectContext;
31 import hunt.shiro.subject.support.DelegatingSubject;
32 
33 import hunt.Boolean;
34 import hunt.Exceptions;
35 import hunt.logging.Logger;
36 import hunt.String;
37 
38 // import java.lang.reflect.Field;
39 
40 /**
41  * Default {@code SubjectDAO} implementation that stores Subject state in the Subject's Session by default (but this
42  * can be disabled - see below).  The Subject instance
43  * can be re-created at a later time by first acquiring the associated Session (typically from a
44  * {@link hunt.shiro.session.mgt.SessionManager SessionManager}) via a session ID or session key and then
45  * building a {@code Subject} instance from {@code Session} attributes.
46  * <h2>Controlling how Sessions are used</h2>
47  * Whether or not a {@code Subject}'s {@code Session} is used or not to persist its own state is controlled on a
48  * <em>per-Subject</em> basis as determined by the configured
49  * {@link #setSessionStorageEvaluator(SessionStorageEvaluator) sessionStorageEvaluator}.
50  * The default {@code Evaluator} is a {@link DefaultSessionStorageEvaluator}, which supports enabling or disabling
51  * session usage for Subject persistence at a global level for all subjects (and defaults to allowing sessions to be
52  * used).
53  * <h3>Disabling Session Persistence Entirely</h3>
54  * Because the default {@code SessionStorageEvaluator} instance is a {@link DefaultSessionStorageEvaluator}, you
55  * can disable Session usage for Subject state entirely by configuring that instance directly, e.g.:
56  * <pre>
57  *     ((DefaultSessionStorageEvaluator)sessionDAO.getSessionStorageEvaluator()).setSessionStorageEnabled(false);
58  * </pre>
59  * or, for example, in {@code shiro.ini}:
60  * <pre>
61  *     securityManager.subjectDAO.sessionStorageEvaluator.sessionStorageEnabled = false
62  * </pre>
63  * but <b>note:</b> ONLY do this your
64  * application is 100% stateless and you <em>DO NOT</em> need subjects to be remembered across remote
65  * invocations, or in a web environment across HTTP requests.
66  * <h3>Supporting Both Stateful and Stateless Subject paradigms</h3>
67  * Perhaps your application needs to support a hybrid approach of both stateful and stateless Subjects:
68  * <ul>
69  * <li>Stateful: Stateful subjects might represent web end-users that need their identity and authentication
70  * state to be remembered from page to page.</li>
71  * <li>Stateless: Stateless subjects might represent API clients (e.g. REST clients) that authenticate on every
72  * request, and therefore don't need authentication state to be stored across requests in a session.</li>
73  * </ul>
74  * To support the hybrid <em>per-Subject</em> approach, you will need to create your own implementation of the
75  * {@link SessionStorageEvaluator} interface and configure it via the
76  * {@link #setSessionStorageEvaluator(SessionStorageEvaluator)} method, or, with {@code shiro.ini}:
77  * <pre>
78  *     myEvaluator = com.my.CustomSessionStorageEvaluator
79  *     securityManager.subjectDAO.sessionStorageEvaluator = $myEvaluator
80  * </pre>
81  * <p/>
82  * Unless overridden, the default evaluator is a {@link DefaultSessionStorageEvaluator}, which enables session usage for
83  * Subject state by default.
84  *
85  * @see #isSessionStorageEnabled(hunt.shiro.subject.Subject)
86  * @see SessionStorageEvaluator
87  * @see DefaultSessionStorageEvaluator
88  */
89 class DefaultSubjectDAO : SubjectDAO {
90 
91 
92 
93     /**
94      * Evaluator that determines if a Subject's session may be used to store the Subject's own state.
95      */
96     private SessionStorageEvaluator sessionStorageEvaluator;
97 
98     this() {
99         //default implementation allows enabling/disabling session usages at a global level for all subjects:
100         this.sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
101     }
102 
103     /**
104      * Determines if the subject's session will be used to persist subject state or not.  This implementation
105      * merely delegates to the internal {@link SessionStorageEvaluator} (a
106      * {@code DefaultSessionStorageEvaluator} by default).
107      *
108      * @param subject the subject to inspect to determine if the subject's session will be used to persist subject
109      *                state or not.
110      * @return {@code true} if the subject's session will be used to persist subject state, {@code false} otherwise.
111      * @see #setSessionStorageEvaluator(SessionStorageEvaluator)
112      * @see DefaultSessionStorageEvaluator
113      */
114     protected bool isSessionStorageEnabled(Subject subject) {
115         return getSessionStorageEvaluator().isSessionStorageEnabled(subject);
116     }
117 
118     /**
119      * Returns the {@code SessionStorageEvaluator} that will determine if a {@code Subject}'s state may be persisted in
120      * the Subject's session.  The default instance is a {@link DefaultSessionStorageEvaluator}.
121      *
122      * @return the {@code SessionStorageEvaluator} that will determine if a {@code Subject}'s state may be persisted in
123      *         the Subject's session.
124      * @see DefaultSessionStorageEvaluator
125      */
126      SessionStorageEvaluator getSessionStorageEvaluator() {
127         return sessionStorageEvaluator;
128     }
129 
130     /**
131      * Sets the {@code SessionStorageEvaluator} that will determine if a {@code Subject}'s state may be persisted in
132      * the Subject's session. The default instance is a {@link DefaultSessionStorageEvaluator}.
133      *
134      * @param sessionStorageEvaluator the {@code SessionStorageEvaluator} that will determine if a {@code Subject}'s
135      *                                state may be persisted in the Subject's session.
136      * @see DefaultSessionStorageEvaluator
137      */
138      void setSessionStorageEvaluator(SessionStorageEvaluator sessionStorageEvaluator) {
139         this.sessionStorageEvaluator = sessionStorageEvaluator;
140     }
141 
142     /**
143      * Saves the subject's state to the subject's {@link hunt.shiro.subject.Subject#getSession() session} only
144      * if {@link #isSessionStorageEnabled(Subject) sessionStorageEnabled(subject)}.  If session storage is not enabled
145      * for the specific {@code Subject}, this method does nothing.
146      * <p/>
147      * In either case, the argument {@code Subject} is returned directly (a new Subject instance is not created).
148      *
149      * @param subject the Subject instance for which its state will be created or updated.
150      * @return the same {@code Subject} passed in (a new Subject instance is not created).
151      */
152      Subject save(Subject subject) {
153         if (isSessionStorageEnabled(subject)) {
154             saveToSession(subject);
155         } else {
156             tracef("Session storage of subject state for Subject [%s] has been disabled: identity and " ~
157                     "authentication state are expected to be initialized on every request or invocation.", subject);
158         }
159 
160         return subject;
161     }
162 
163     /**
164      * Saves the subject's state (it's principals and authentication state) to its
165      * {@link hunt.shiro.subject.Subject#getSession() session}.  The session can be retrieved at a later time
166      * (typically from a {@link hunt.shiro.session.mgt.SessionManager SessionManager} to be used to recreate
167      * the {@code Subject} instance.
168      *
169      * @param subject the subject for which state will be persisted to its session.
170      */
171     protected void saveToSession(Subject subject) {
172         //performs merge logic, only updating the Subject's session if it does not match the current state:
173         mergePrincipals(subject);
174         mergeAuthenticationState(subject);
175     }
176 
177     private static bool isEmpty(PrincipalCollection pc) {
178         return pc  is null || pc.isEmpty();
179     }
180 
181     /**
182      * Merges the Subject's current {@link hunt.shiro.subject.Subject#getPrincipals()} with whatever may be in
183      * any available session.  Only updates the Subject's session if the session does not match the current principals
184      * state.
185      *
186      * @param subject the Subject for which principals will potentially be merged into the Subject's session.
187      */
188     protected void mergePrincipals(Subject subject) {
189         //merge PrincipalCollection state:
190 
191         PrincipalCollection currentPrincipals = null;
192 
193         //SHIRO-380: added if/else block - need to retain original (source) principals
194         //This technique (reflection) is only temporary - a proper long term solution needs to be found,
195         //but this technique allowed an immediate fix that is API point-version forwards and backwards compatible
196         //
197         //A more comprehensive review / cleaning of runAs should be performed for Shiro 1.3 / 2.0 +
198         DelegatingSubject ds = cast(DelegatingSubject)subject;
199         if (subject.isRunAs() && ds !is null) {
200             implementationMissing(false);
201             // try {
202             //     Field field = DelegatingSubject.class.getDeclaredField("principals");
203             //     field.setAccessible(true);
204             //     currentPrincipals = cast(PrincipalCollection)field.get(subject);
205             // } catch (Exception e) {
206             //     throw new IllegalStateException("Unable to access DelegatingSubject principals property.", e);
207             // }
208         }
209         if (currentPrincipals is null || currentPrincipals.isEmpty()) {
210             currentPrincipals = subject.getPrincipals();
211         }
212 
213         Session session = subject.getSession(false);
214 
215         if (session  is null) {
216             if (!isEmpty(currentPrincipals)) {
217                 session = subject.getSession();
218                 session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, cast(Object)currentPrincipals);
219             }
220             // otherwise no session and no principals - nothing to save
221         } else {
222             PrincipalCollection existingPrincipals =
223                     cast(PrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
224 
225             if (isEmpty(currentPrincipals)) {
226                 if (!isEmpty(existingPrincipals)) {
227                     session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
228                 }
229                 // otherwise both are null or empty - no need to update the session
230             } else {
231                 if (currentPrincipals != existingPrincipals) {
232                     session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, cast(Object)currentPrincipals);
233                 }
234                 // otherwise they're the same - no need to update the session
235             }
236         }
237     }
238 
239     /**
240      * Merges the Subject's current authentication state with whatever may be in
241      * any available session.  Only updates the Subject's session if the session does not match the current
242      * authentication state.
243      *
244      * @param subject the Subject for which principals will potentially be merged into the Subject's session.
245      */
246     protected void mergeAuthenticationState(Subject subject) {
247 
248         Session session = subject.getSession(false);
249 
250         if (session is null) {
251             if (subject.isAuthenticated()) {
252                 session = subject.getSession();
253                 session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE);
254             }
255             //otherwise no session and not authenticated - nothing to save
256         } else {
257             Boolean existingAuthc = cast(Boolean) session.getAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);
258 
259             if (subject.isAuthenticated()) {
260                 if (existingAuthc is null || !existingAuthc.value) {
261                     session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE);
262                 }
263                 //otherwise authc state matches - no need to update the session
264             } else {
265                 if (existingAuthc !is null) {
266                     //existing doesn't match the current state - remove it:
267                     session.removeAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);
268                 }
269                 //otherwise not in the session and not authenticated - no need to update the session
270             }
271         }
272     }
273 
274     /**
275      * Removes any existing subject state from the Subject's session (if the session exists).  If the session
276      * does not exist, this method does not do anything.
277      *
278      * @param subject the subject for which any existing subject state will be removed from its session.
279      */
280     protected void removeFromSession(Subject subject) {
281         Session session = subject.getSession(false);
282         if (session !is null) {
283             session.removeAttribute(new String(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY));
284             session.removeAttribute(new String(DefaultSubjectContext.PRINCIPALS_SESSION_KEY));
285         }
286     }
287 
288     /**
289      * Removes any existing subject state from the subject's session (if the session exists).
290      *
291      * @param subject the Subject instance for which any persistent state should be deleted.
292      */
293      void remove(Subject subject) {
294         removeFromSession(subject);
295     }
296 }