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