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.subject.support.DelegatingSubject;
20 
21 import hunt.shiro.subject.support.SubjectCallable;
22 import hunt.shiro.subject.support.SubjectRunnable;
23 
24 import hunt.shiro.Exceptions;
25 import hunt.shiro.authc.AuthenticationToken;
26 import hunt.shiro.authc.HostAuthenticationToken;
27 import hunt.shiro.Exceptions;
28 import hunt.shiro.authz.permission.Permission;
29 
30 import hunt.shiro.mgt.SecurityManager;
31 import hunt.shiro.Exceptions;
32 import hunt.shiro.session.ProxiedSession;
33 import hunt.shiro.session.Session;
34 
35 import hunt.shiro.session.mgt.DefaultSessionContext;
36 import hunt.shiro.session.mgt.SessionContext;
37 
38 import hunt.shiro.subject.PrincipalCollection;
39 import hunt.shiro.subject.Subject;
40 import hunt.shiro.util.CollectionUtils;
41 
42 // import hunt.shiro.util.StringUtils;
43 import hunt.logging.Logger;
44 
45 import hunt.collection;
46 import hunt.concurrency.thread;
47 import hunt.Exceptions;
48 import hunt.String;
49 import hunt.util.Common;
50 import hunt.util.Runnable;
51 
52 import std.array;
53 import std.traits;
54 
55 /**
56  * Implementation of the {@code Subject} interface that delegates
57  * method calls to an underlying {@link hunt.shiro.mgt.SecurityManager SecurityManager} instance for security checks.
58  * It is essentially a {@code SecurityManager} proxy.
59  * <p/>
60  * This implementation does not maintain state such as roles and permissions (only {@code Subject}
61  * {@link #getPrincipals() principals}, such as usernames or user primary keys) for better performance in a stateless
62  * architecture.  It instead asks the underlying {@code SecurityManager} every time to perform
63  * the authorization check.
64  * <p/>
65  * A common misconception in using this implementation is that an EIS resource (RDBMS, etc) would
66  * be &quot;hit&quot; every time a method is called.  This is not necessarily the case and is
67  * up to the implementation of the underlying {@code SecurityManager} instance.  If caching of authorization
68  * data is desired (to eliminate EIS round trips and therefore improve database performance), it is considered
69  * much more elegant to let the underlying {@code SecurityManager} implementation or its delegate components
70  * manage caching, not this class.  A {@code SecurityManager} is considered a business-tier component,
71  * where caching strategies are better managed.
72  * <p/>
73  * Applications from large and clustered to simple and JVM-local all benefit from
74  * stateless architectures.  This implementation plays a part in the stateless programming
75  * paradigm and should be used whenever possible.
76  *
77  */
78 class DelegatingSubject : Subject {
79 
80     private enum string RUN_AS_PRINCIPALS_SESSION_KEY = fullyQualifiedName!(DelegatingSubject)
81         ~ ".RUN_AS_PRINCIPALS_SESSION_KEY";
82 
83     protected PrincipalCollection principals;
84     protected bool authenticated;
85     protected string host;
86     protected Session session;
87     /**
88      */
89     protected bool sessionCreationEnabled;
90 
91     protected SecurityManager securityManager;
92 
93     this(SecurityManager securityManager) {
94         this(null, false, null, null, securityManager);
95     }
96 
97     this(PrincipalCollection principals, bool authenticated, string host,
98             Session session, SecurityManager securityManager) {
99         this(principals, authenticated, host, session, true, securityManager);
100     }
101 
102     //since 1.2
103     this(PrincipalCollection principals, bool authenticated, string host,
104             Session session, bool sessionCreationEnabled, SecurityManager securityManager) {
105         if (securityManager is null) {
106             throw new IllegalArgumentException("SecurityManager argument cannot be null.");
107         }
108         this.securityManager = securityManager;
109         this.principals = principals;
110         this.authenticated = authenticated;
111         this.host = host;
112         if (session !is null) {
113             this.session = decorate(session);
114         }
115         this.sessionCreationEnabled = sessionCreationEnabled;
116     }
117 
118     protected Session decorate(Session session) {
119         if (session is null) {
120             throw new IllegalArgumentException("session cannot be null");
121         }
122         return new StoppingAwareProxiedSession(session, this);
123     }
124 
125     SecurityManager getSecurityManager() {
126         return securityManager;
127     }
128 
129     private static bool isEmpty(PrincipalCollection pc) {
130         return pc is null || pc.isEmpty();
131     }
132 
133     protected bool hasPrincipals() {
134         return !isEmpty(getPrincipals());
135     }
136 
137     /**
138      * Returns the host name or IP associated with the client who created/is interacting with this Subject.
139      *
140      * @return the host name or IP associated with the client who created/is interacting with this Subject.
141      */
142     string getHost() {
143         return this.host;
144     }
145 
146     private Object getPrimaryPrincipal(PrincipalCollection principals) {
147         if (!isEmpty(principals)) {
148             return principals.getPrimaryPrincipal();
149         }
150         return null;
151     }
152 
153     /**
154      * @see Subject#getPrincipal()
155      */
156     Object getPrincipal() {
157         return getPrimaryPrincipal(getPrincipals());
158     }
159 
160     PrincipalCollection getPrincipals() {
161         List!(PrincipalCollection) runAsPrincipals = getRunAsPrincipalsStack();
162 
163         // if(runAsPrincipals !is null) {
164         //     foreach(PrincipalCollection c; runAsPrincipals) {
165         //         trace(c);
166         //     }
167         // }
168 
169         // if(this.principals !is null) {
170         //     trace((cast(Object)this.principals).toString());
171         // }
172 
173         return CollectionUtils.isEmpty(runAsPrincipals) ? this.principals : runAsPrincipals.get(0);
174     }
175 
176     bool isPermitted(string permission) {
177         return hasPrincipals() && securityManager.isPermitted(getPrincipals(), permission);
178     }
179 
180     bool isPermitted(Permission permission) {
181         return hasPrincipals() && securityManager.isPermitted(getPrincipals(), permission);
182     }
183 
184     bool[] isPermitted(string[] permissions...) {
185         if (hasPrincipals()) {
186             return securityManager.isPermitted(getPrincipals(), permissions);
187         } else {
188             return new bool[permissions.length];
189         }
190     }
191 
192     bool[] isPermitted(List!(Permission) permissions) {
193         if (hasPrincipals()) {
194             return securityManager.isPermitted(getPrincipals(), permissions);
195         } else {
196             return new bool[permissions.size()];
197         }
198     }
199 
200     bool isPermittedAll(string[] permissions...) {
201         return hasPrincipals() && securityManager.isPermittedAll(getPrincipals(), permissions);
202     }
203 
204     bool isPermittedAll(Collection!(Permission) permissions) {
205         return hasPrincipals() && securityManager.isPermittedAll(getPrincipals(), permissions);
206     }
207 
208     protected void assertAuthzCheckPossible() {
209         // dfmt off
210         if (!hasPrincipals()) {
211             string msg = "This subject is anonymous - it does not have any identifying principals and " ~ 
212                 "authorization operations require an identity to check against.  A Subject instance will " ~ 
213                 "acquire these identifying principals automatically after a successful login is performed " ~ 
214                 "be executing " ~ typeid(Subject).toString() ~ 
215                 ".login(AuthenticationToken) or when 'Remember Me' " ~ 
216                 "functionality is enabled by the SecurityManager.  This exception can also occur when a " ~ 
217                 "previously logged-in Subject has logged out which " ~ 
218                 "makes it anonymous again.  Because an identity is currently not known due to any of these " ~ 
219                 "conditions, authorization is denied.";
220             throw new UnauthenticatedException(msg);
221         }
222         // dfmt on
223     }
224 
225     void checkPermission(string permission) {
226         assertAuthzCheckPossible();
227         securityManager.checkPermission(getPrincipals(), permission);
228     }
229 
230     void checkPermission(Permission permission) {
231         assertAuthzCheckPossible();
232         securityManager.checkPermission(getPrincipals(), permission);
233     }
234 
235     void checkPermissions(string[] permissions...) {
236         assertAuthzCheckPossible();
237         securityManager.checkPermissions(getPrincipals(), permissions);
238     }
239 
240     void checkPermissions(Collection!(Permission) permissions) {
241         assertAuthzCheckPossible();
242         securityManager.checkPermissions(getPrincipals(), permissions);
243     }
244 
245     bool hasRole(string roleIdentifier) {
246         return hasPrincipals() && securityManager.hasRole(getPrincipals(), roleIdentifier);
247     }
248 
249     bool[] hasRoles(List!(string) roleIdentifiers) {
250         return hasRoles(roleIdentifiers.toArray());
251     }
252 
253     bool[] hasRoles(string[] roleIdentifiers...) {
254         if (hasPrincipals()) {
255             return securityManager.hasRoles(getPrincipals(), roleIdentifiers);
256         } else {
257             return new bool[roleIdentifiers.length];
258         }
259     }
260 
261     bool hasAllRoles(Collection!(string) roleIdentifiers) {
262         return hasAllRoles(roleIdentifiers.toArray());
263     }
264 
265     bool hasAllRoles(string[] roleIdentifiers) {
266         return hasPrincipals() && securityManager.hasAllRoles(getPrincipals(), roleIdentifiers);
267     }
268 
269     void checkRole(string role) {
270         assertAuthzCheckPossible();
271         securityManager.checkRole(getPrincipals(), role);
272     }
273 
274     void checkRoles(string[] roleIdentifiers...) {
275         assertAuthzCheckPossible();
276         securityManager.checkRoles(getPrincipals(), roleIdentifiers);
277     }
278 
279     void checkRoles(Collection!(string) roles) {
280         assertAuthzCheckPossible();
281         securityManager.checkRoles(getPrincipals(), roles);
282     }
283 
284     void login(AuthenticationToken token) {
285         clearRunAsIdentitiesInternal();
286         Subject subject = securityManager.login(this, token);
287 
288         PrincipalCollection principals;
289 
290         string host = null;
291 
292         DelegatingSubject delegating = cast(DelegatingSubject) subject;
293         if (delegating !is null) {
294             //we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
295             principals = delegating.principals;
296             host = delegating.host;
297         } else {
298             principals = subject.getPrincipals();
299         }
300 
301         if (principals is null || principals.isEmpty()) {
302             string msg = "Principals returned from securityManager.login( token ) returned a null or "
303                 ~ "empty value.  This value must be non null and populated with one or more elements.";
304             throw new IllegalStateException(msg);
305         }
306         this.principals = principals;
307         this.authenticated = true;
308         HostAuthenticationToken hat = cast(HostAuthenticationToken) token;
309         if (hat !is null) {
310             host = hat.getHost();
311         }
312         if (host !is null) {
313             this.host = host;
314         }
315         Session session = subject.getSession(false);
316         if (session !is null) {
317             this.session = decorate(session);
318         } else {
319             this.session = null;
320         }
321     }
322 
323     bool isAuthenticated() {
324         return authenticated;
325     }
326 
327     bool isRemembered() {
328         PrincipalCollection principals = getPrincipals();
329         return principals !is null && !principals.isEmpty() && !isAuthenticated();
330     }
331 
332     /**
333      * Returns {@code true} if this Subject is allowed to create sessions, {@code false} otherwise.
334      *
335      * @return {@code true} if this Subject is allowed to create sessions, {@code false} otherwise.
336      */
337     protected bool isSessionCreationEnabled() {
338         return this.sessionCreationEnabled;
339     }
340 
341     Session getSession() {
342         return getSession(true);
343     }
344 
345     Session getSession(bool create) {
346         // version(HUNT_DEBUG) {
347         //     tracef("attempting to get session; create = %s; session is null = %s; session has id = %s" ,
348         //             create, (this.session is null), 
349         //             (this.session !is null && session.getId() !is null));
350         // }
351 
352         if (this.session is null && create) {
353 
354             //added in 1.2:
355             if (!isSessionCreationEnabled()) {
356                 string msg = "Session creation has been disabled for the current subject.  This exception indicates "
357                     ~ "that there is either a programming error (using a session when it should never be "
358                     ~ "used) or that Shiro's configuration needs to be adjusted to allow Sessions to be created "
359                     ~ "for the current Subject.  See the " ~ typeid(DisabledSessionException)
360                     .name ~ " JavaDoc " ~ "for more.";
361                 throw new DisabledSessionException(msg);
362             }
363 
364             version (HUNT_SHIRO_DEBUG)
365                 tracef("Starting session for host %s", getHost());
366             SessionContext sessionContext = createSessionContext();
367             Session session = this.securityManager.start(sessionContext);
368             this.session = decorate(session);
369         }
370         return this.session;
371     }
372 
373     protected SessionContext createSessionContext() {
374         SessionContext sessionContext = new DefaultSessionContext();
375         if (!host.empty()) {
376             sessionContext.setHost(host);
377         }
378         return sessionContext;
379     }
380 
381     private void clearRunAsIdentitiesInternal() {
382         //try/catch added for SHIRO-298
383         try {
384             clearRunAsIdentities();
385         } catch (SessionException se) {
386             tracef("Encountered session exception trying to clear 'runAs' identities during logout.  This "
387                     ~ "can generally safely be ignored.", se);
388         }
389     }
390 
391     void logout() {
392         try {
393             clearRunAsIdentitiesInternal();
394             this.securityManager.logout(this);
395         } catch(Exception ex) {
396             warning(ex.msg);
397             version(HUNT_SHIRO_DEBUG) warning(ex);
398         } finally {
399             this.session = null;
400             this.principals = null;
401             this.authenticated = false;
402             //Don't set securityManager to null here - the Subject can still be
403             //used, it is just considered anonymous at this point.  The SecurityManager instance is
404             //necessary if the subject would log in again or acquire a new session.  This is in response to
405             //https://issues.apache.org/jira/browse/JSEC-22
406             //this.securityManager = null;
407         }
408     }
409 
410     private void sessionStopped() {
411         this.session = null;
412     }
413 
414     V execute(V)(Callable!(V) callable) {
415         Callable!(V) associated = associateWith(callable);
416         try {
417             return associated.call();
418         } catch (Throwable t) {
419             throw new ExecutionException(t);
420         }
421     }
422 
423     void execute(Runnable runnable) {
424         Runnable associated = associateWith(runnable);
425         associated.run();
426     }
427 
428     Callable!(V) associateWith(V)(Callable!(V) callable) {
429         return new SubjectCallable!(V)(this, callable);
430     }
431 
432     Runnable associateWith(Runnable runnable) {
433         ThreadEx tx = cast(ThreadEx) runnable;
434         if (tx !is null) {
435             string msg = "This implementation does not support Thread arguments because of JDK ThreadLocal "
436                 ~ "inheritance mechanisms required by Shiro.  Instead, the method argument should be a non-Thread "
437                 ~ "Runnable and the return value from this method can then be given to an ExecutorService or "
438                 ~ "another Thread.";
439             throw new UnsupportedOperationException(msg);
440         }
441         return new SubjectRunnable(this, runnable);
442     }
443 
444     private class StoppingAwareProxiedSession : ProxiedSession {
445 
446         private DelegatingSubject owner;
447 
448         private this(Session target, DelegatingSubject owningSubject) {
449             super(target);
450             owner = owningSubject;
451         }
452 
453         override void stop() {
454             super.stop();
455             owner.sessionStopped();
456         }
457     }
458 
459     // ======================================
460     // 'Run As' support implementations
461     // ======================================
462 
463     void runAs(PrincipalCollection principals) {
464         if (!hasPrincipals()) {
465             string msg = "This subject does not yet have an identity.  Assuming the identity of another "
466                 ~ "Subject is only allowed for Subjects with an existing identity.  Try logging this subject in "
467                 ~ "first, or using the " ~ typeid(SubjectBuilder)
468                 .name ~ " to build ad hoc Subject instances " ~ "with identities as necessary.";
469             throw new IllegalStateException(msg);
470         }
471         pushIdentity(principals);
472     }
473 
474     bool isRunAs() {
475         List!(PrincipalCollection) stack = getRunAsPrincipalsStack();
476         return !CollectionUtils.isEmpty(stack);
477     }
478 
479     PrincipalCollection getPreviousPrincipals() {
480         PrincipalCollection previousPrincipals = null;
481         List!(PrincipalCollection) stack = getRunAsPrincipalsStack();
482         int stackSize = stack !is null ? stack.size() : 0;
483         if (stackSize > 0) {
484             if (stackSize == 1) {
485                 previousPrincipals = this.principals;
486             } else {
487                 //always get the one behind the current:
488                 assert(stack !is null);
489                 previousPrincipals = stack.get(1);
490             }
491         }
492         return previousPrincipals;
493     }
494 
495     PrincipalCollection releaseRunAs() {
496         return popIdentity();
497     }
498 
499     private List!(PrincipalCollection) getRunAsPrincipalsStack() {
500         Session session = getSession(false);
501         if (session !is null) {
502             Object obj = session.getAttribute(RUN_AS_PRINCIPALS_SESSION_KEY);
503             if (obj !is null) {
504                 List!(PrincipalCollection) r = cast(List!(PrincipalCollection)) obj;
505                 if (r is null) {
506                     warning(typeid(obj));
507                 } else
508                     return r;
509             }
510         }
511         return null;
512     }
513 
514     private void clearRunAsIdentities() {
515         Session session = getSession(false);
516         if (session !is null) {
517             session.removeAttribute(RUN_AS_PRINCIPALS_SESSION_KEY);
518         }
519     }
520 
521     private void pushIdentity(PrincipalCollection principals) {
522         if (isEmpty(principals)) {
523             string msg = "Specified Subject principals cannot be null or empty for 'run as' functionality.";
524             throw new NullPointerException(msg);
525         }
526         List!(PrincipalCollection) stack = getRunAsPrincipalsStack();
527         if (stack is null) {
528             // stack = new CopyOnWriteArrayList!(PrincipalCollection)();
529             stack = new ArrayList!(PrincipalCollection)();
530         }
531         stack.add(0, principals);
532         Session session = getSession();
533         session.setAttribute(RUN_AS_PRINCIPALS_SESSION_KEY, cast(Object) stack);
534     }
535 
536     private PrincipalCollection popIdentity() {
537         PrincipalCollection popped = null;
538 
539         List!(PrincipalCollection) stack = getRunAsPrincipalsStack();
540         if (!CollectionUtils.isEmpty!(PrincipalCollection)(stack)) {
541             popped = stack.removeAt(0);
542             Session session;
543             if (!CollectionUtils.isEmpty(stack)) {
544                 //persist the changed stack to the session
545                 session = getSession();
546                 session.setAttribute(RUN_AS_PRINCIPALS_SESSION_KEY, cast(Object) stack);
547             } else {
548                 //stack is empty, remove it from the session:
549                 clearRunAsIdentities();
550             }
551         }
552 
553         return popped;
554     }
555 }