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 "hit" 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 }