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