/**
* Copyright (c) Codice Foundation
* <p/>
* This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or any later version.
* <p/>
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details. A copy of the GNU Lesser General Public License
* is distributed along with this program and can be found at
* <http://www.gnu.org/licenses/lgpl.html>.
*/
package ddf.security.pdp.realm;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.Permission;
import org.apache.shiro.authz.permission.PermissionResolver;
import org.apache.shiro.authz.permission.RolePermissionResolver;
import org.apache.shiro.authz.permission.WildcardPermission;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.CollectionUtils;
import org.slf4j.LoggerFactory;
import org.slf4j.ext.XLogger;
import ddf.security.common.audit.SecurityLogger;
import ddf.security.permission.ActionPermission;
import ddf.security.permission.CollectionPermission;
import ddf.security.permission.KeyValueCollectionPermission;
import ddf.security.permission.KeyValuePermission;
import ddf.security.permission.MatchOneCollectionPermission;
import ddf.security.policy.extension.PolicyExtension;
import ddf.security.service.impl.AbstractAuthorizingRealm;
/**
* This simple Authz {@link ddf.security.service.impl.AbstractAuthorizingRealm} provides the ability
* to check permissions without making calls out to an external PDP. {@link Permission} objects are
* checked against each other to ensure that the subject permissions imply the resource permissions.
*
* @author tustisos
*/
public class SimpleAuthzRealm extends AbstractAuthorizingRealm {
/**
* Identifies the key used to retrieve a List of Strings that represent the mapping between
* metacard and user attributes. The mappings defined in this List of Strings are used by the
* "match all" evaluation to determine if this user should be authorized to access this data.
* <p/>
* Each string is of the format: <code>metacardAttribute=userAttribute</code> where
* metacardAttribute is the name of an attribute in the metacard and userAttribute is the name
* of the corresponding attribute in the user credentials. It is the value of each of these
* attributes that will be evaluated against each other when determining if authorization should
* be allowed.
*/
public static final String MATCH_ALL_MAPPINGS = "matchAllMappings";
/**
* Identifies the key used to retrieve a List of Strings that represent the mapping between
* metacard and user attributes. The mappings defined in this List of Strings are used by the
* "match one" evaluation to determine if this user should be authorized to access this data.
* <p/>
* Each string is of the format: <code>metacardAttribute=userAttribute</code> where
* metacardAttribute is the name of an attribute in the metacard and userAttribute is the name
* of the corresponding attribute in the user credentials. It is the value of each of these
* attributes that will be evaluated against each other when determining if authorization should
* be allowed.
*/
public static final String MATCH_ONE_MAPPINGS = "matchOneMappings";
/**
* Identifies the key used to retrieve a List of Strings that represent roles that will be
* allowed to perform restricted actions. Each string defines the role name.
*/
public static final String ACCESS_ROLE_LIST = "accessRoleList";
/**
* Identifies the key used to retrieve a List of Strings that represent actions that are open
* for any role to access. Each string is the action corresponding to the SOAP action presented
* in the SOAP request. If the specified string is contained in the SOAP action string, then
* access is automatically allowed for this operation.
*/
public static final String OPEN_ACCESS_ACTION_LIST = "openAccessActionList";
private static final XLogger LOGGER = new XLogger(LoggerFactory.getLogger(SimpleAuthzRealm.class));
private static final String ACCESS_DENIED_MSG = "User not authorized";
private static final String PERMISSION_FINISH_1_MSG = "Finished permission check for user [";
private static final String PERMISSION_FINISH_2_MSG = "]. Result is that permission [";
// This method is for testing purposes only, Mockito was not able to mock the
// getAuthorizationInfo method
AuthorizationInfo info = null;
private List<String> accessRoleList;
private List<String> openAccessActionList;
private List<PolicyExtension> policyExtensions = new ArrayList<>();
private HashMap<String, String> matchAllMap = new HashMap<String, String>();
private HashMap<String, String> matchOneMap = new HashMap<String, String>();
// this realm is for authorization only
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
return null;
}
public void setAuthorizationInfo(AuthorizationInfo info) {
this.info = info;
}
/**
* Returns an account's authorization-specific information for the specified {@code principals},
* or {@code null} if no account could be found. The resulting {@code AuthorizationInfo} object
* is used by the other method implementations in this class to automatically perform access
* control checks for the corresponding {@code Subject}.
* <p/>
* This implementation obtains the actual {@code AuthorizationInfo} object from the subclass's
* implementation of
* {@link #doGetAuthorizationInfo(org.apache.shiro.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.shiro.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.
* <h3>Caching</h3>
* The {@code AuthorizationInfo} values returned from this method are cached for efficient reuse
* if caching is enabled. Caching is enabled automatically when an
* {@link #setAuthorizationCache authorizationCache} instance has been explicitly configured, or
* if a {@link #setCacheManager cacheManager} has been configured, which will be used to lazily
* create the {@code authorizationCache} as needed.
* <p/>
* If caching is enabled, the authorization cache will be checked first and if found, will
* return the cached {@code AuthorizationInfo} immediately. If caching is disabled, or there is
* a cache miss, the authorization info will be looked up from the underlying data store via the
* {@link #doGetAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)} method, which
* must be implemented by subclasses.
* <h4>Changed Data</h4>
* 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 implementation should clear
* the cached AuthorizationInfo for that account via the
* {@link #clearCachedAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)
* clearCachedAuthorizationInfo} method. This ensures that the next call to
* {@code getAuthorizationInfo(PrincipalCollection)} 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}.
* @return the authorization information for the account associated with the specified
* {@code principals}, or {@code null} if no account could be found.
*/
@Override
public AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
if (info != null) {
return info;
}
return super.getAuthorizationInfo(principals);
}
/**
* Returns <tt>true</tt> if the corresponding subject/user is permitted to perform an action or
* access a resource summarized by the specified permission.
* <p/>
* <p/>
* More specifically, this method determines if any <tt>Permission</tt>s associated with the
* subject {@link Permission#implies(Permission) imply} the specified permission.
*
* @param subjectPrincipal the application-specific subject/user identifier.
* @param permission the permission that is being checked.
* @return true if the corresponding Subject/user is permitted, false otherwise.
*/
@Override
public boolean isPermitted(PrincipalCollection subjectPrincipal, Permission permission) {
return isPermitted(subjectPrincipal, Collections.singletonList(permission))[0];
}
/**
* Checks if the corresponding Subject/user implies the given Permissions and returns a boolean
* array indicating which permissions are implied.
* <p/>
* <p/>
* More specifically, this method should determine if each <tt>Permission</tt> in the array is
* {@link Permission#implies(Permission) implied} by permissions already associated with the
* subject.
* <p/>
* <p/>
* This is primarily a performance-enhancing method to help reduce the number of
* {@link #isPermitted} invocations over the wire in client/server systems.
*
* @param subjectPrincipal the application-specific subject/user identifier.
* @param permissions the permissions that are being checked.
* @return an array of booleans whose indices correspond to the index of the permissions in the
* given list. A true value at an index indicates the user is permitted for for the
* associated <tt>Permission</tt> object in the list. A false value at an index
* indicates otherwise.
*/
@Override
public boolean[] isPermitted(PrincipalCollection subjectPrincipal, List<Permission> permissions) {
boolean[] results = new boolean[permissions.size()];
AuthorizationInfo info = getAuthorizationInfo(subjectPrincipal);
int i = 0;
for (Permission permission : permissions) {
results[i++] = isPermitted(subjectPrincipal, permission, info);
}
return results;
}
/**
* Checks if the corresponding Subject/user contained within the AuthorizationInfo object
* implies the given Permission.
*
* @param permission the permission being checked.
* @param info the application-specific subject/user identifier.
* @return true if the user is permitted
*/
private boolean isPermitted(PrincipalCollection subjectPrincipal, Permission permission, AuthorizationInfo info) {
Collection<Permission> perms = getPermissions(info);
String curUser = "<user>";
if (subjectPrincipal != null && subjectPrincipal.getPrimaryPrincipal() != null) {
curUser = subjectPrincipal.getPrimaryPrincipal().toString();
}
if (SecurityLogger.isDebugEnabled()) {
SecurityLogger.logDebug("Starting permissions check for user [" + curUser + "]");
}
if (!CollectionUtils.isEmpty(perms)) {
if (permission instanceof KeyValuePermission) {
permission = new KeyValueCollectionPermission((KeyValuePermission) permission);
}
if (permission != null && permission instanceof KeyValueCollectionPermission) {
List<KeyValuePermission> metacardPermissions = ((KeyValueCollectionPermission) permission).getKeyValuePermissionList();
List<KeyValuePermission> matchOnePermissions = new ArrayList<KeyValuePermission>();
List<KeyValuePermission> matchAllPermissions = new ArrayList<KeyValuePermission>();
for (KeyValuePermission metacardPermission : metacardPermissions) {
String metacardKey = metacardPermission.getKey();
// user specificied this metacard key in the match all list - remap key
if (matchAllMap.containsKey(metacardKey)) {
if (SecurityLogger.isDebugEnabled()) {
SecurityLogger.logDebug("Mapping metacard key " + metacardKey + " to " + matchAllMap.get(metacardKey));
}
KeyValuePermission kvp = new KeyValuePermission(matchAllMap.get(metacardKey), metacardPermission.getValues());
matchAllPermissions.add(kvp);
// user specified this metacard key in the match one list - remap key
} else if (matchOneMap.containsKey(metacardKey)) {
if (SecurityLogger.isDebugEnabled()) {
SecurityLogger.logDebug("Mapping metacard key " + metacardKey + " to " + matchOneMap.get(metacardKey));
}
KeyValuePermission kvp = new KeyValuePermission(matchOneMap.get(metacardKey), metacardPermission.getValues());
matchOnePermissions.add(kvp);
// this metacard key was not specified in either - default to match all with the
// same key value
} else {
matchAllPermissions.add(metacardPermission);
}
}
CollectionPermission subjectAllCollection = new CollectionPermission(perms);
KeyValueCollectionPermission matchAllCollection = new KeyValueCollectionPermission(matchAllPermissions);
KeyValueCollectionPermission matchOneCollection = new KeyValueCollectionPermission(matchOnePermissions);
matchAllCollection = isPermittedByExtensionAll(subjectAllCollection, matchAllCollection);
matchOneCollection = isPermittedByExtensionOne(subjectAllCollection, matchOneCollection);
MatchOneCollectionPermission subjectOneCollection = new MatchOneCollectionPermission(perms);
boolean matchAll = subjectAllCollection.implies(matchAllCollection);
boolean matchOne = subjectOneCollection.implies(matchOneCollection);
if (SecurityLogger.isDebugEnabled()) {
if (matchAll && matchOne) {
SecurityLogger.logDebug(PERMISSION_FINISH_1_MSG + curUser + PERMISSION_FINISH_2_MSG + permission + "] is implied.");
} else {
SecurityLogger.logDebug(PERMISSION_FINISH_1_MSG + curUser + PERMISSION_FINISH_2_MSG + permission + "] is not implied.");
}
}
return (matchAll && matchOne);
}
for (Permission perm : perms) {
if (permission instanceof ActionPermission && isPermitted((ActionPermission) permission, info)) {
if (SecurityLogger.isDebugEnabled()) {
SecurityLogger.logDebug(PERMISSION_FINISH_1_MSG + curUser + PERMISSION_FINISH_2_MSG + permission + "] is implied.");
}
return true;
} else if (permission != null && perm.implies(permission)) {
if (SecurityLogger.isDebugEnabled()) {
SecurityLogger.logDebug(PERMISSION_FINISH_1_MSG + curUser + PERMISSION_FINISH_2_MSG + permission + "] is implied.");
}
return true;
}
}
} else if (permission instanceof ActionPermission && isPermitted((ActionPermission) permission, info)) {
if (SecurityLogger.isDebugEnabled()) {
SecurityLogger.logDebug(PERMISSION_FINISH_1_MSG + curUser + PERMISSION_FINISH_2_MSG + permission + "] is implied.");
}
return true;
}
if (SecurityLogger.isDebugEnabled()) {
SecurityLogger.logDebug(PERMISSION_FINISH_1_MSG + curUser + PERMISSION_FINISH_2_MSG + permission + "] is not implied.");
}
return false;
}
private KeyValueCollectionPermission isPermittedByExtensionAll(CollectionPermission subjectAllCollection,
KeyValueCollectionPermission matchAllCollection) {
if (!CollectionUtils.isEmpty(policyExtensions)) {
KeyValueCollectionPermission resultCollection = new KeyValueCollectionPermission();
resultCollection.addAll(matchAllCollection.getPermissionList());
for (PolicyExtension policyExtension : policyExtensions) {
try {
resultCollection = policyExtension.isPermittedMatchAll(subjectAllCollection, resultCollection);
} catch (Exception e) {
SecurityLogger.logWarn("Policy Extension plugin did not complete correctly.", e);
LOGGER.warn("Policy Extension plugin did not complete correctly.", e);
}
}
return resultCollection;
}
return matchAllCollection;
}
private KeyValueCollectionPermission isPermittedByExtensionOne(CollectionPermission subjectAllCollection,
KeyValueCollectionPermission matchOneCollection) {
if (!CollectionUtils.isEmpty(policyExtensions)) {
KeyValueCollectionPermission resultCollection = new KeyValueCollectionPermission();
resultCollection.addAll(matchOneCollection.getPermissionList());
for (PolicyExtension policyExtension : policyExtensions) {
try {
resultCollection = policyExtension.isPermittedMatchOne(subjectAllCollection, resultCollection);
} catch (Exception e) {
SecurityLogger.logWarn("Policy Extension plugin did not complete correctly.", e);
LOGGER.warn("Policy Extension plugin did not complete correctly.", e);
}
}
return resultCollection;
}
return matchOneCollection;
}
private boolean isPermitted(ActionPermission actionPermission, AuthorizationInfo info) {
String action = actionPermission.getAction();
if (StringUtils.isNotEmpty(action)) {
// check to see if the action they are trying to perform is an action anyone can do
if (openAccessActionList != null) {
for (String openAction : openAccessActionList) {
if (action.contains(openAction)) {
if (SecurityLogger.isDebugEnabled()) {
SecurityLogger.logDebug("Action permission [" + actionPermission + "] implied as an open action.");
}
return true;
}
}
}
// it must be a restricted action, so check if the user has the correct role
if (accessRoleList != null) {
for (String accessRole : accessRoleList) {
if (info.getRoles().contains(accessRole)) {
if (SecurityLogger.isDebugEnabled()) {
SecurityLogger.logDebug("User has access role " + accessRole);
}
return true;
}
}
}
}
if (SecurityLogger.isDebugEnabled()) {
SecurityLogger.logDebug("Action permission [" + actionPermission + "] not implied.");
}
return false;
}
/**
* Returns a {@link WildcardPermission} representing a {@link KeyValuePermission}
*
* @param perm the permission to convert.
* @return new equivalent permission
*/
private WildcardPermission buildWildcardFromKeyValue(KeyValuePermission perm) {
StringBuilder wildcardString = new StringBuilder();
for (String value : perm.getValues()) {
wildcardString.append(value);
wildcardString.append(",");
}
return new WildcardPermission(wildcardString.toString().substring(0, wildcardString.length() - 1));
}
/**
* Returns a collection of {@link Permission} objects that the {@link AuthorizationInfo} object
* of a {@link ddf.security.Subject} is asserting.
*
* @param info the application-specific subject/user identifier.
* @return collection of Permissions.
*/
private Collection<Permission> getPermissions(AuthorizationInfo info) {
Set<Permission> permissions = new HashSet<Permission>();
if (info != null) {
Collection<Permission> perms = info.getObjectPermissions();
if (!CollectionUtils.isEmpty(perms)) {
permissions.addAll(perms);
}
perms = resolvePermissions(info.getStringPermissions());
if (!CollectionUtils.isEmpty(perms)) {
permissions.addAll(perms);
}
perms = resolveRolePermissions(info.getRoles());
if (!CollectionUtils.isEmpty(perms)) {
permissions.addAll(perms);
}
}
return Collections.unmodifiableSet(permissions);
}
/**
* Returns a collection of {@link Permission} objects that are built from the associated
* collection of Strings.
*
* @param stringPerms collection of Strings that represent permissions.
* @return collection of Permissions
*/
private Collection<Permission> resolvePermissions(Collection<String> stringPerms) {
Collection<Permission> perms = Collections.emptySet();
PermissionResolver resolver = getPermissionResolver();
if (resolver != null && !CollectionUtils.isEmpty(stringPerms)) {
perms = new LinkedHashSet<Permission>(stringPerms.size());
for (String strPermission : stringPerms) {
Permission permission = getPermissionResolver().resolvePermission(strPermission);
perms.add(permission);
}
}
return perms;
}
/**
* Returns a collection of {@link Permission} objects that are built from the associated
* collection of Strings that represent the roles that a user possesses.
*
* @param roleNames user roles.
* @return collection of Permissions
*/
private Collection<Permission> resolveRolePermissions(Collection<String> roleNames) {
Collection<Permission> perms = Collections.emptySet();
RolePermissionResolver resolver = getRolePermissionResolver();
if (resolver != null && !CollectionUtils.isEmpty(roleNames)) {
perms = new LinkedHashSet<Permission>(roleNames.size());
for (String roleName : roleNames) {
Collection<Permission> resolved = resolver.resolvePermissionsInRole(roleName);
if (!CollectionUtils.isEmpty(resolved)) {
perms.addAll(resolved);
}
}
}
return perms;
}
/**
* Sets the list of roles that are allowed to execute privileged operations. Each string in the
* list defines a specific role that users may be assigned.
*
* @param accessRoleList List of String values that correspond to roles that are allowed to execute
* privileged operations.
*/
public void setAccessRoleList(List<String> accessRoleList) {
this.accessRoleList = accessRoleList;
}
/**
* Returns a comma-delimited string of privileged access roles.
*
* @return comma delimited String of access roles
*/
public String getAccessRoleList() {
String accessRoleCsv = null;
if (accessRoleList != null) {
accessRoleCsv = StringUtils.join(accessRoleList, ",");
}
return accessRoleCsv;
}
/**
* Takes in a comma-delimited string of privileged access roles.
*
* @param commaStr
* @see SimpleAuthzRealm#setAccessRoleList(List)
*/
public void setAccessRoleList(String commaStr) {
setAccessRoleList(convertToList(commaStr));
}
/**
* Sets the list of SOAP actions that are open for users in any role to access. Each string is
* the action corresponding to the SOAP action presented in the SOAP request. If the specified
* string is contained in the SOAP action string, then access is automatically allowed for this
* operation.
*
* @param openAccessActionList List of SOAP action strings that may be accessed by users in any role.
*/
public void setOpenAccessActionList(List<String> openAccessActionList) {
this.openAccessActionList = openAccessActionList;
}
/**
* Takes in a comma-delimited string of open access roles.
*
* @param comma-delimited string of open access roles.
* @see SimpleAuthzRealm#setOpenAccessActionList(List)
*/
public void setOpenAccessActionList(String commaStr) {
setOpenAccessActionList(convertToList(commaStr));
}
/**
* Sets list of policy extension objects
*
* @param policyExtensions
*/
public void setPolicyExtensions(List<PolicyExtension> policyExtensions) {
this.policyExtensions = policyExtensions;
}
public void addPolicyExtension(PolicyExtension policyExtension) {
if (policyExtensions != null) {
policyExtensions.add(policyExtension);
}
}
public void removePolicyExtension(PolicyExtension policyExtension) {
if (policyExtensions != null) {
policyExtensions.remove(policyExtension);
}
}
/**
* Sets the mappings used by the "match all" evaluation to determine if this user should be
* authorized to access requested data.
* <p/>
* Each string is of the format: <code>metacardAttribute=userAttribute</code><br/>
* where <code>metacardAttribute</code> is the name of an attribute in the metacard and
* <code>userAttribute</code> is the name of the corresponding attribute in the user
* credentials.<br/>
* It is the values corresponding to each of these attributes that will be evaluated against
* each other when determining if authorization should be allowed.
*
* @param list List of Strings that define mappings between metadata attributes and user
* attributes
*/
public void setMatchAllMappings(List<String> list) {
String[] values;
matchAllMap.clear();
if (list != null) {
for (String mapping : list) {
values = mapping.split("=");
if (values.length == 2) {
LOGGER.debug("Adding mapping: {} = {} to matchAllMap.", values[1].trim(), values[0].trim());
matchAllMap.put(values[1].trim(), values[0].trim());
} else {
LOGGER.warn("Match all mapping ignored: {} doesn't match expected format of metacardAttribute=userAttribute", mapping);
}
}
}
}
/**
* Takes in a comma-delimited string of match all mappings.
*
* @param commaStr
* @see SimpleAuthzRealm#setMatchAllMappings(List)
*/
public void setMatchAllMappings(String commaStr) {
setMatchAllMappings(convertToList(commaStr));
}
/**
* Sets the mappings used by the "match one" evaluation to determine if this user should be
* authorized to access requested data.
* <p/>
* Each string is of the format: <code>metacardAttribute=userAttribute</code><br/>
* where <code>metacardAttribute</code> is the name of an attribute in the metacard and
* <code>userAttribute</code> is the name of the corresponding attribute in the user
* credentials.<br/>
* It is the values corresponding to each of these attributes that will be evaluated against
* each other when determining if authorization should be allowed.
*
* @param list List of Strings that define mappings between metadata attributes and user
* attributes
*/
public void setMatchOneMappings(List<String> list) {
String[] values;
matchOneMap.clear();
if (list != null) {
for (String mapping : list) {
values = mapping.split("=");
if (values.length == 2) {
LOGGER.debug("Adding mapping: {} = {} to matchOneMap.", values[1].trim(), values[0].trim());
matchOneMap.put(values[1].trim(), values[0].trim());
} else {
LOGGER.warn("Match one mapping ignored: {} doesn't match expected format of metacardAttribute=userAttribute", mapping);
}
}
}
}
/**
* Takes in a comma-delimited string of match one mappings.
*
* @param commaStr
* @see SimpleAuthzRealm#setMatchOneMappings(List)
*/
public void setMatchOneMappings(String commaStr) {
setMatchOneMappings(convertToList(commaStr));
}
private List<String> convertToList(String commaStr) {
List<String> list = new ArrayList<String>();
if (commaStr != null) {
for (String curValue : commaStr.split(",")) {
curValue = curValue.trim();
list.add(curValue);
}
}
return list;
}
}