blob: aa4aad87250edef87e1fb42abd5809cae5ac5c9e [file] [log] [blame]
/*
* 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.jsecurity.realm;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jsecurity.authc.credential.CredentialsMatcher;
import org.jsecurity.authz.*;
import org.jsecurity.authz.permission.PermissionResolver;
import org.jsecurity.authz.permission.PermissionResolverAware;
import org.jsecurity.authz.permission.WildcardPermissionResolver;
import org.jsecurity.cache.Cache;
import org.jsecurity.cache.CacheManager;
import org.jsecurity.subject.PrincipalCollection;
import org.jsecurity.util.Initializable;
import java.util.*;
/**
* 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.jsecurity.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 SimpleAuthorizationInfo SimpleAuthorizationInfo}) is sufficient in the large
* majority of Realm cases.
*
* @author Les Hazlewood
* @author Jeremy Haile
* @see SimpleAuthorizationInfo
* @since 0.2
*/
public abstract class AuthorizingRealm extends AuthenticatingRealm implements Initializable, PermissionResolverAware {
/*--------------------------------------------
| C O N S T A N T S |
============================================*/
private static final Log log = LogFactory.getLog(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.jsecurity.cache.Cache) cache}
* or {@link #setCacheManager(org.jsecurity.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 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.jsecurity.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.jsecurity.authc.AuthenticationToken) authenticationInfo}, since either could
* occur in any order.
*
* <p>For example, in &quot;Remember Me&quot; 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(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.jsecurity.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.jsecurity.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) {
Object key = getAuthorizationCacheKey(principals);
Cache cache = getAuthorizationCache();
if (key != null && cache != null) {
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 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 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) {
Cache cache = getAuthorizationCache();
//cache instance will be non-null if caching is enabled:
if (cache != null && principals != null) {
Object key = getAuthorizationCacheKey(principals);
cache.remove(key);
}
}
}