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