| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.ki.realm; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import org.apache.ki.authc.credential.CredentialsMatcher; |
| import org.apache.ki.authz.AuthorizationException; |
| import org.apache.ki.authz.AuthorizationInfo; |
| import org.apache.ki.authz.AuthorizingAccount; |
| import org.apache.ki.authz.Permission; |
| import org.apache.ki.authz.UnauthorizedException; |
| import org.apache.ki.authz.permission.PermissionResolver; |
| import org.apache.ki.authz.permission.PermissionResolverAware; |
| import org.apache.ki.authz.permission.WildcardPermissionResolver; |
| import org.apache.ki.cache.Cache; |
| import org.apache.ki.cache.CacheManager; |
| import org.apache.ki.subject.PrincipalCollection; |
| import org.apache.ki.util.Initializable; |
| |
| |
| /** |
| * An <tt>AuthorizingRealm</tt> extends the <tt>AuthenticatingRealm</tt>'s capabilities by adding Authorization |
| * (access control) support. |
| * |
| * <p>This implementation will perform all role and permission checks automatically (and subclasses do not have to |
| * write this logic) as long as the |
| * {@link #getAuthorizationInfo(org.apache.ki.subject.PrincipalCollection)} method returns an |
| * {@link AuthorizationInfo}. Please see that method's JavaDoc for an in-depth explanation. |
| * |
| * <p>If you find that you do not want to utilize the {@link AuthorizationInfo AuthorizationInfo} construct, |
| * you are of course free to subclass the {@link AuthenticatingRealm AuthenticatingRealm} directly instead and |
| * implement the remaining Realm interface methods directly. You might do this if you want have better control |
| * over how the Role and Permission checks occur for your specific data source. However, using AuthorizationInfo |
| * (and its default implementation {@link org.apache.ki.authz.SimpleAuthorizationInfo SimpleAuthorizationInfo}) is sufficient in the large |
| * majority of Realm cases. |
| * |
| * @author Les Hazlewood |
| * @author Jeremy Haile |
| * @see org.apache.ki.authz.SimpleAuthorizationInfo |
| * @since 0.2 |
| */ |
| public abstract class AuthorizingRealm extends AuthenticatingRealm implements Initializable, PermissionResolverAware { |
| |
| //TODO - complete JavaDoc |
| |
| /*-------------------------------------------- |
| | C O N S T A N T S | |
| ============================================*/ |
| private static final Logger log = LoggerFactory.getLogger(AuthorizingRealm.class); |
| |
| /** |
| * The default postfix appended to the realm name for caching AuthorizationInfos. |
| */ |
| private static final String DEFAULT_AUTHORIZATION_CACHE_POSTFIX = "-authorization"; |
| |
| private static int INSTANCE_COUNT = 0; |
| |
| /*-------------------------------------------- |
| | I N S T A N C E V A R I A B L E S | |
| ============================================*/ |
| /** |
| * The cache used by this realm to store AuthorizationInfos associated with individual Subject principals. |
| */ |
| private Cache authorizationCache = null; |
| private String authorizationCacheName = null; |
| |
| private PermissionResolver permissionResolver = new WildcardPermissionResolver(); |
| |
| /*-------------------------------------------- |
| | C O N S T R U C T O R S | |
| ============================================*/ |
| public AuthorizingRealm() { |
| } |
| |
| public AuthorizingRealm(CacheManager cacheManager) { |
| super(cacheManager); |
| } |
| |
| public AuthorizingRealm(CredentialsMatcher matcher) { |
| super(matcher); |
| } |
| |
| public AuthorizingRealm(CacheManager cacheManager, CredentialsMatcher matcher) { |
| super(cacheManager, matcher); |
| } |
| |
| /*-------------------------------------------- |
| | A C C E S S O R S / M O D I F I E R S | |
| ============================================*/ |
| public void setAuthorizationCache(Cache authorizationCache) { |
| this.authorizationCache = authorizationCache; |
| if (this.authorizationCache != null) { |
| afterAuthorizationCacheSet(); |
| } |
| } |
| |
| public Cache getAuthorizationCache() { |
| return this.authorizationCache; |
| } |
| |
| public String getAuthorizationCacheName() { |
| return authorizationCacheName; |
| } |
| |
| public void setAuthorizationCacheName(String authorizationCacheName) { |
| this.authorizationCacheName = authorizationCacheName; |
| } |
| |
| public PermissionResolver getPermissionResolver() { |
| return permissionResolver; |
| } |
| |
| public void setPermissionResolver(PermissionResolver permissionResolver) { |
| this.permissionResolver = permissionResolver; |
| } |
| |
| /*-------------------------------------------- |
| | M E T H O D S | |
| ============================================*/ |
| /** |
| * Initializes this realm and potentially enables a cache, depending on configuration. |
| * |
| * <p>When this method is called, the following logic is executed: |
| * <ol> |
| * <li>If the {@link #setAuthorizationCache cache} property has been set, it will be |
| * used to cache the AuthorizationInfo objects returned from {@link #getAuthorizationInfo} |
| * method invocations. |
| * All future calls to <tt>getAuthorizationInfo</tt> will attempt to use this cache first |
| * to alleviate any potentially unnecessary calls to an underlying data store.</li> |
| * <li>If the {@link #setAuthorizationCache cache} property has <b>not</b> been set, |
| * the {@link #setCacheManager cacheManager} property will be checked. |
| * If a <tt>cacheManager</tt> has been set, it will be used to create an authorization |
| * <tt>cache</tt>, and this newly created cache which will be used as specified in #1.</li> |
| * <li>If neither the {@link #setAuthorizationCache (org.apache.ki.cache.Cache) cache} |
| * or {@link #setCacheManager(org.apache.ki.cache.CacheManager) cacheManager} |
| * properties are set, caching will be disabled and authorization lookups will be delegated to |
| * subclass implementations for each authorization check.</li> |
| * </ol> |
| */ |
| public final void init() { |
| initAuthorizationCache(); |
| } |
| |
| protected void afterCacheManagerSet() { |
| this.authorizationCache = null; |
| initAuthorizationCache(); |
| } |
| |
| protected void afterAuthorizationCacheSet() { |
| } |
| |
| public void initAuthorizationCache() { |
| if (log.isTraceEnabled()) { |
| log.trace("Initializing authorization cache."); |
| } |
| |
| Cache cache = getAuthorizationCache(); |
| |
| if (cache == null) { |
| |
| if (log.isDebugEnabled()) { |
| log.debug("No cache implementation set. Checking cacheManager..."); |
| } |
| |
| CacheManager cacheManager = getCacheManager(); |
| |
| if (cacheManager != null) { |
| String cacheName = getAuthorizationCacheName(); |
| if (cacheName == null) { |
| //Simple default in case they didn't provide one: |
| cacheName = getClass().getName() + "-" + INSTANCE_COUNT++ + DEFAULT_AUTHORIZATION_CACHE_POSTFIX; |
| setAuthorizationCacheName(cacheName); |
| } |
| if (log.isDebugEnabled()) { |
| log.debug("CacheManager [" + cacheManager + "] has been configured. Building " + |
| "authorization cache named [" + cacheName + "]"); |
| } |
| cache = cacheManager.getCache(cacheName); |
| setAuthorizationCache(cache); |
| } else { |
| if (log.isInfoEnabled()) { |
| log.info("No cache or cacheManager properties have been set. Authorization caching is " + |
| "disabled."); |
| } |
| } |
| } |
| } |
| |
| |
| /** |
| * Returns an account's authorization-specific information for the specified <code>principals</code>, |
| * or <tt>null</tt> if no account could be found. The resulting <code>AuthorizationInfo</code> object is used |
| * by the other method implementations in this class to automatically perform access control checks for the |
| * corresponding <code>Subject</code>. |
| * |
| * <p>This implementation obtains the actual <code>AuthorizationInfo</code> object from the subclass's |
| * implementation of |
| * {@link #doGetAuthorizationInfo(org.apache.ki.subject.PrincipalCollection) doGetAuthorizationInfo}, and then |
| * caches it for efficient reuse if caching is enabled (see below). |
| * |
| * <p>Invocations of this method should be thought of as completely orthogonal to acquiring |
| * {@link #getAuthenticationInfo(org.apache.ki.authc.AuthenticationToken) authenticationInfo}, since either could |
| * occur in any order. |
| * |
| * <p>For example, in "Remember Me" scenarios, the user identity is remembered (and |
| * assumed) for their current session and an authentication attempt during that session might never occur. |
| * But because their identity would be remembered, that is sufficient enough information to call this method to |
| * execute any necessary authorization checks. For this reason, authentication and authorization should be |
| * loosely coupled and not depend on each other. |
| * |
| * <h4>Caching</h4> |
| * |
| * <p>The <code>AuthorizationInfo</code> values returned from this method are cached for performant reuse |
| * if caching is enabled. Caching is enabled automatically when a <code>CacheManager</code> has been |
| * {@link #setCacheManager injected} and then the realm is {@link #init initialized}. It can also be enabled by explictly |
| * calling {@link #initAuthorizationCache() initAuthorizationCache()}. |
| * |
| * <p>If caching is enabled, the authorization cache will be checked first and if found, will return the cached |
| * <code>AuthorizationInfo</code> immediately. If caching is disabled, or there is a cache miss from the cache |
| * lookup, the authorization info will be looked up from the underlying data store via the |
| * {@link #doGetAuthorizationInfo(org.apache.ki.subject.PrincipalCollection)} method, which must be implemented by subclasses. |
| * |
| * <p><b>Please note:</b> If caching is enabled and if any authorization data for an account is changed at |
| * runtime, such as adding or removing roles and/or permissions, the subclass imlementation should clear the |
| * cached AuthorizationInfo for that account via the |
| * {@link #clearCachedAuthorizationInfo(org.apache.ki.subject.PrincipalCollection) clearCachedAuthorizationInfo} |
| * method. This ensures that the next call to <code>getAuthorizationInfo(PrincipalCollection)</code> will |
| * acquire the account's fresh authorization data, where it will then be cached for efficient reuse. This |
| * ensures that stale authorization data will not be reused. |
| * |
| * @param principals the corresponding Subject's identifying principals with which to look up the Subject's |
| * <code>AuthorizationInfo</code>. |
| * @return the authorization information for the account associated with the specified <code>principals</code>, |
| * or <tt>null</tt> if no account could be found. |
| */ |
| public AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) { |
| |
| if (principals == null) { |
| return null; |
| } |
| |
| AuthorizationInfo info = null; |
| |
| if (log.isTraceEnabled()) { |
| log.trace("Retrieving AuthorizationInfo for principals [" + principals + "]"); |
| } |
| |
| Cache authzCache = getAuthorizationCache(); |
| if (authzCache != null) { |
| if (log.isTraceEnabled()) { |
| log.trace("Attempting to retrieve the AuthorizationIfno from cache."); |
| } |
| Object key = getAuthorizationCacheKey(principals); |
| info = (AuthorizationInfo) authzCache.get(key); |
| if (log.isTraceEnabled()) { |
| if (info == null) { |
| log.trace("No AuthorizationInfo found in cache for principals [" + principals + "]"); |
| } else { |
| log.trace("AuthorizationInfo found in cache for principals [" + principals + "]"); |
| } |
| } |
| } |
| |
| |
| if (info == null) { |
| // Call template method if tbe info was not found in a cache |
| info = doGetAuthorizationInfo(principals); |
| // If the info is not null and the cache has been created, then cache the authorization info. |
| if (info != null && authzCache != null) { |
| if (log.isTraceEnabled()) { |
| log.trace("Caching authorization info for principals: [" + principals + "]."); |
| } |
| Object key = getAuthorizationCacheKey(principals); |
| authzCache.put(key, info); |
| } |
| } |
| |
| return info; |
| } |
| |
| protected Object getAuthorizationCacheKey(PrincipalCollection principals) { |
| return principals; |
| } |
| |
| /** |
| * Clears out the AuthorizationInfo cache entry for the specified account. |
| * <p/> |
| * This method is provided as a convenience to subclasses so they can invalidate a cache entry when they |
| * change an account's authorization data (add/remove roles or permissions) during runtime. Because an account's |
| * AuthorizationInfo can be cached, there needs to be a way to invalidate the cache for only that account so that |
| * subsequent authorization operations don't used the (old) cached value if account data changes. |
| * <p/> |
| * After this method is called, the next authorization check for that same account will result in a call to |
| * {@link #getAuthorizationInfo(org.apache.ki.subject.PrincipalCollection) getAuthorizationInfo}, and the |
| * resulting return value will be cached before being returned so it can be reused for later authorization checks. |
| * |
| * @param principals the principals of the account for which to clear the cached AuthorizationInfo. |
| */ |
| protected void clearCachedAuthorizationInfo(PrincipalCollection principals) { |
| if ( principals == null ) { |
| return; |
| } |
| |
| Cache cache = getAuthorizationCache(); |
| //cache instance will be non-null if caching is enabled: |
| if (cache != null) { |
| Object key = getAuthorizationCacheKey(principals); |
| cache.remove(key); |
| } |
| } |
| |
| /** |
| * Retrieves the AuthorizationInfo for the given principals from the underlying data store. When returning |
| * an instance from this method, you might want to consider using an instance of |
| * {@link org.apache.ki.authz.SimpleAuthorizationInfo SimpleAuthorizationInfo}, as it is suitable in most cases. |
| * |
| * @param principals the primary identifying principals of the AuthorizationInfo that should be retrieved. |
| * @return the AuthorizationInfo associated with this principals. |
| * @see org.apache.ki.authz.SimpleAuthorizationInfo |
| */ |
| protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals); |
| |
| @SuppressWarnings({"unchecked"}) |
| private Collection<Permission> getPermissions(AuthorizationInfo info) { |
| Set<Permission> permissions = new HashSet<Permission>(); |
| |
| if (info != null) { |
| if (info.getObjectPermissions() != null) { |
| permissions.addAll(info.getObjectPermissions()); |
| } |
| |
| if (info.getStringPermissions() != null) { |
| for (String strPermission : info.getStringPermissions()) { |
| Permission permission = getPermissionResolver().resolvePermission(strPermission); |
| permissions.add(permission); |
| } |
| } |
| } |
| |
| if (permissions.isEmpty()) { |
| return Collections.EMPTY_SET; |
| } else { |
| return Collections.unmodifiableSet(permissions); |
| } |
| } |
| |
| public boolean isPermitted(PrincipalCollection principals, String permission) { |
| Permission p = getPermissionResolver().resolvePermission(permission); |
| return isPermitted(principals, p); |
| } |
| |
| public boolean isPermitted(PrincipalCollection principals, Permission permission) { |
| AuthorizationInfo info = getAuthorizationInfo(principals); |
| return isPermitted(permission, info); |
| } |
| |
| @SuppressWarnings("deprecation") |
| private boolean isPermitted(Permission permission, AuthorizationInfo info) { |
| //todo Remove this once AuthorizingAccount class is deleted |
| if (info instanceof AuthorizingAccount) { |
| return ((AuthorizingAccount) info).isPermitted(permission); |
| } |
| |
| Collection<Permission> perms = getPermissions(info); |
| if (perms != null && !perms.isEmpty()) { |
| for (Permission perm : perms) { |
| if (perm.implies(permission)) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| public boolean[] isPermitted(PrincipalCollection subjectIdentifier, String... permissions) { |
| List<Permission> perms = new ArrayList<Permission>(permissions.length); |
| for (String permString : permissions) { |
| perms.add(getPermissionResolver().resolvePermission(permString)); |
| } |
| return isPermitted(subjectIdentifier, perms); |
| } |
| |
| public boolean[] isPermitted(PrincipalCollection principals, List<Permission> permissions) { |
| AuthorizationInfo info = getAuthorizationInfo(principals); |
| return isPermitted(permissions, info); |
| } |
| |
| @SuppressWarnings("deprecation") |
| protected boolean[] isPermitted(List<Permission> permissions, AuthorizationInfo info) { |
| //todo Remove this once AuthorizingAccount class is deleted |
| if (info instanceof AuthorizingAccount) { |
| return ((AuthorizingAccount) info).isPermitted(permissions); |
| } |
| |
| boolean[] result; |
| if (permissions != null && !permissions.isEmpty()) { |
| int size = permissions.size(); |
| result = new boolean[size]; |
| int i = 0; |
| for (Permission p : permissions) { |
| result[i++] = isPermitted(p, info); |
| } |
| } else { |
| result = new boolean[0]; |
| } |
| return result; |
| } |
| |
| public boolean isPermittedAll(PrincipalCollection subjectIdentifier, String... permissions) { |
| if (permissions != null && permissions.length > 0) { |
| Collection<Permission> perms = new ArrayList<Permission>(permissions.length); |
| for (String permString : permissions) { |
| perms.add(getPermissionResolver().resolvePermission(permString)); |
| } |
| return isPermittedAll(subjectIdentifier, perms); |
| } |
| return false; |
| } |
| |
| public boolean isPermittedAll(PrincipalCollection principal, Collection<Permission> permissions) { |
| AuthorizationInfo info = getAuthorizationInfo(principal); |
| return info != null && isPermittedAll(permissions, info); |
| } |
| |
| @SuppressWarnings("deprecation") |
| protected boolean isPermittedAll(Collection<Permission> permissions, AuthorizationInfo info) { |
| //todo Remove this once AuthorizingAccount class is deleted |
| if (info instanceof AuthorizingAccount) { |
| return ((AuthorizingAccount) info).isPermittedAll(permissions); |
| } |
| |
| if (permissions != null && !permissions.isEmpty()) { |
| for (Permission p : permissions) { |
| if (!isPermitted(p, info)) { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| public void checkPermission(PrincipalCollection subjectIdentifier, String permission) throws AuthorizationException { |
| Permission p = getPermissionResolver().resolvePermission(permission); |
| checkPermission(subjectIdentifier, p); |
| } |
| |
| public void checkPermission(PrincipalCollection principal, Permission permission) throws AuthorizationException { |
| AuthorizationInfo info = getAuthorizationInfo(principal); |
| checkPermission(permission, info); |
| } |
| |
| @SuppressWarnings("deprecation") |
| protected void checkPermission(Permission permission, AuthorizationInfo info) { |
| //todo Remove this once AuthorizingAccount class is deleted |
| if (info instanceof AuthorizingAccount) { |
| ((AuthorizingAccount) info).checkPermission(permission); |
| } else { |
| if (!isPermitted(permission, info)) { |
| String msg = "User is not permitted [" + permission + "]"; |
| throw new UnauthorizedException(msg); |
| } |
| } |
| } |
| |
| public void checkPermissions(PrincipalCollection subjectIdentifier, String... permissions) throws AuthorizationException { |
| if (permissions != null) { |
| for (String permString : permissions) { |
| checkPermission(subjectIdentifier, permString); |
| } |
| } |
| } |
| |
| public void checkPermissions(PrincipalCollection principal, Collection<Permission> permissions) throws AuthorizationException { |
| AuthorizationInfo info = getAuthorizationInfo(principal); |
| checkPermissions(permissions, info); |
| } |
| |
| @SuppressWarnings("deprecation") |
| protected void checkPermissions(Collection<Permission> permissions, AuthorizationInfo info) { |
| //todo Remove this once AuthorizingAccount class is deleted |
| if (info instanceof AuthorizingAccount) { |
| ((AuthorizingAccount) info).checkPermissions(permissions); |
| } else { |
| if (permissions != null && !permissions.isEmpty()) { |
| for (Permission p : permissions) { |
| checkPermission(p, info); |
| } |
| } |
| } |
| } |
| |
| public boolean hasRole(PrincipalCollection principal, String roleIdentifier) { |
| AuthorizationInfo info = getAuthorizationInfo(principal); |
| return hasRole(roleIdentifier, info); |
| } |
| |
| @SuppressWarnings("deprecation") |
| protected boolean hasRole(String roleIdentifier, AuthorizationInfo info) { |
| //todo Remove this once AuthorizingAccount class is deleted |
| if (info instanceof AuthorizingAccount) { |
| return ((AuthorizingAccount) info).hasRole(roleIdentifier); |
| } |
| return info != null && info.getRoles() != null && info.getRoles().contains(roleIdentifier); |
| } |
| |
| public boolean[] hasRoles(PrincipalCollection principal, List<String> roleIdentifiers) { |
| AuthorizationInfo info = getAuthorizationInfo(principal); |
| boolean[] result = new boolean[roleIdentifiers != null ? roleIdentifiers.size() : 0]; |
| if (info != null) { |
| result = hasRoles(roleIdentifiers, info); |
| } |
| return result; |
| } |
| |
| @SuppressWarnings("deprecation") |
| protected boolean[] hasRoles(List<String> roleIdentifiers, AuthorizationInfo info) { |
| //todo Remove this once AuthorizingAccount class is deleted |
| if (info instanceof AuthorizingAccount) { |
| return ((AuthorizingAccount) info).hasRoles(roleIdentifiers); |
| } |
| |
| boolean[] result; |
| if (roleIdentifiers != null && !roleIdentifiers.isEmpty()) { |
| int size = roleIdentifiers.size(); |
| result = new boolean[size]; |
| int i = 0; |
| for (String roleName : roleIdentifiers) { |
| result[i++] = hasRole(roleName, info); |
| } |
| } else { |
| result = new boolean[0]; |
| } |
| return result; |
| } |
| |
| public boolean hasAllRoles(PrincipalCollection principal, Collection<String> roleIdentifiers) { |
| AuthorizationInfo info = getAuthorizationInfo(principal); |
| return info != null && hasAllRoles(roleIdentifiers, info); |
| } |
| |
| @SuppressWarnings("deprecation") |
| private boolean hasAllRoles(Collection<String> roleIdentifiers, AuthorizationInfo info) { |
| //todo Remove this once AuthorizingAccount class is deleted |
| if (info instanceof AuthorizingAccount) { |
| return ((AuthorizingAccount) info).hasAllRoles(roleIdentifiers); |
| } |
| |
| if (roleIdentifiers != null && !roleIdentifiers.isEmpty()) { |
| for (String roleName : roleIdentifiers) { |
| if (!hasRole(roleName, info)) { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| public void checkRole(PrincipalCollection principal, String role) throws AuthorizationException { |
| AuthorizationInfo info = getAuthorizationInfo(principal); |
| checkRole(role, info); |
| } |
| |
| @SuppressWarnings("deprecation") |
| protected void checkRole(String role, AuthorizationInfo info) { |
| //todo Remove this once AuthorizingAccount class is deleted |
| if (info instanceof AuthorizingAccount) { |
| ((AuthorizingAccount) info).checkRole(role); |
| } else { |
| if (!hasRole(role, info)) { |
| String msg = "User does not have role [" + role + "]"; |
| throw new UnauthorizedException(msg); |
| } |
| } |
| } |
| |
| public void checkRoles(PrincipalCollection principal, Collection<String> roles) throws AuthorizationException { |
| AuthorizationInfo info = getAuthorizationInfo(principal); |
| checkRoles(roles, info); |
| } |
| |
| @SuppressWarnings("deprecation") |
| protected void checkRoles(Collection<String> roles, AuthorizationInfo info) { |
| //todo Remove this once AuthorizingAccount class is deleted |
| if (info instanceof AuthorizingAccount) { |
| ((AuthorizingAccount) info).checkRoles(roles); |
| } else { |
| if (roles != null && !roles.isEmpty()) { |
| for (String roleName : roles) { |
| checkRole(roleName, info); |
| } |
| } |
| } |
| } |
| |
| /** |
| * If authorization caching is enabled, this will remove the AuthorizationInfo from the cache. |
| * Subclasses are free to override for additional behavior, but be sure to call <tt>super.onLogout</tt> |
| * to ensure cache cleanup. |
| * |
| * @param principals the application-specific Subject/user identifier. |
| */ |
| public void onLogout(PrincipalCollection principals) { |
| clearCachedAuthorizationInfo(principals); |
| } |
| } |