/** * 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.List; import java.util.Set; 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.subject.PrincipalCollection; import org.apache.shiro.util.CollectionUtils; import org.codice.ddf.parser.Parser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ddf.security.common.audit.SecurityLogger; import ddf.security.pdp.realm.xacml.XacmlPdp; import ddf.security.pdp.realm.xacml.processor.PdpException; 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 AuthzRealm extends AbstractAuthorizingRealm { private static final Logger LOGGER = LoggerFactory.getLogger(AuthzRealm.class); 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 ["; private List<PolicyExtension> policyExtensions = new ArrayList<>(); private HashMap<String, String> matchAllMap = new HashMap<>(); private HashMap<String, String> matchOneMap = new HashMap<>(); private List<String> environmentAttributes = new ArrayList<>(); private XacmlPdp xacmlPdp; public AuthzRealm(String dirPath, Parser parser) throws PdpException { super(); xacmlPdp = new XacmlPdp(dirPath, parser, environmentAttributes); } // this realm is for authorization only @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { return null; } /** * 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) { 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 authorizationInfo = getAuthorizationInfo(subjectPrincipal); List<Permission> expandedPermissions = expandPermissions(permissions); int i = 0; for (Permission permission : expandedPermissions) { results[i++] = isPermitted(subjectPrincipal, permission, authorizationInfo); } return results; } /** * Checks if the corresponding Subject/user contained within the AuthorizationInfo object * implies the given Permission. * * @param permission the permission being checked. * @param authorizationInfo the application-specific subject/user identifier. * @return true if the user is permitted */ private boolean isPermitted(PrincipalCollection subjectPrincipal, Permission permission, AuthorizationInfo authorizationInfo) { Collection<Permission> perms = getPermissions(authorizationInfo); String curUser = "<user>"; if (subjectPrincipal != null && subjectPrincipal.getPrimaryPrincipal() != null) { curUser = subjectPrincipal.getPrimaryPrincipal() .toString(); } if (!CollectionUtils.isEmpty(perms)) { if (permission instanceof KeyValuePermission) { permission = new KeyValueCollectionPermission(CollectionPermission.UNKNOWN_ACTION, (KeyValuePermission) permission); LOGGER.debug( "Should not execute subject.isPermitted with KeyValuePermission. Instead create a KeyValueCollectionPermission with an action."); } if (permission != null && permission instanceof KeyValueCollectionPermission) { KeyValueCollectionPermission kvcp = (KeyValueCollectionPermission) permission; List<KeyValuePermission> keyValuePermissions = kvcp.getKeyValuePermissionList(); List<KeyValuePermission> matchOnePermissions = new ArrayList<>(); List<KeyValuePermission> matchAllPermissions = new ArrayList<>(); List<KeyValuePermission> matchAllPreXacmlPermissions = new ArrayList<>(); for (KeyValuePermission keyValuePermission : keyValuePermissions) { String metacardKey = keyValuePermission.getKey(); // user specified this key in the match all list - remap key if (matchAllMap.containsKey(metacardKey)) { KeyValuePermission kvp = new KeyValuePermission(matchAllMap.get(metacardKey), keyValuePermission.getValues()); matchAllPermissions.add(kvp); // user specified this key in the match one list - remap key } else if (matchOneMap.containsKey(metacardKey)) { KeyValuePermission kvp = new KeyValuePermission(matchOneMap.get(metacardKey), keyValuePermission.getValues()); matchOnePermissions.add(kvp); // this key was not specified in either - default to match all with the // same key value } else { //creating a KeyValuePermission list to try to quick match all of these permissions //if that fails, then XACML will try to match them //this covers the case where attributes on the user match up perfectly with the permissions being implied //this also allows the xacml permissions to run through the policy extensions matchAllPreXacmlPermissions.add(keyValuePermission); } } CollectionPermission subjectAllCollection = new CollectionPermission( CollectionPermission.UNKNOWN_ACTION, perms); KeyValueCollectionPermission matchAllCollection = new KeyValueCollectionPermission( kvcp.getAction(), matchAllPermissions); KeyValueCollectionPermission matchAllPreXacmlCollection = new KeyValueCollectionPermission(kvcp.getAction(), matchAllPreXacmlPermissions); KeyValueCollectionPermission matchOneCollection = new KeyValueCollectionPermission( kvcp.getAction(), matchOnePermissions); matchAllCollection = isPermittedByExtensionAll(subjectAllCollection, matchAllCollection); matchAllPreXacmlCollection = isPermittedByExtensionAll(subjectAllCollection, matchAllPreXacmlCollection); matchOneCollection = isPermittedByExtensionOne(subjectAllCollection, matchOneCollection); MatchOneCollectionPermission subjectOneCollection = new MatchOneCollectionPermission(perms); boolean matchAll = subjectAllCollection.implies(matchAllCollection); boolean matchAllXacml = subjectAllCollection.implies(matchAllPreXacmlCollection); boolean matchOne = subjectOneCollection.implies(matchOneCollection); if (!matchAll || !matchOne) { SecurityLogger.audit( PERMISSION_FINISH_1_MSG + curUser + PERMISSION_FINISH_2_MSG + permission + "] is not implied."); } //if we weren't able to automatically imply these permissions, call out to XACML if (!matchAllXacml) { KeyValueCollectionPermission xacmlPermissions = new KeyValueCollectionPermission(kvcp.getAction(), matchAllPreXacmlPermissions); matchAllXacml = xacmlPdp.isPermitted(curUser, authorizationInfo, xacmlPermissions); if (!matchAllXacml) { SecurityLogger.audit( PERMISSION_FINISH_1_MSG + curUser + PERMISSION_FINISH_2_MSG + permission + "] is not implied via XACML."); } } return matchAll && matchOne && matchAllXacml; } for (Permission perm : perms) { if (permission != null && perm.implies(permission)) { return true; } } } SecurityLogger.audit( 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()); resultCollection.setAction(matchAllCollection.getAction()); for (PolicyExtension policyExtension : policyExtensions) { try { resultCollection = policyExtension.isPermittedMatchAll(subjectAllCollection, resultCollection); } catch (Exception e) { SecurityLogger.auditWarn("Policy Extension plugin did not complete correctly. This could allow access to a resource.", e); LOGGER.warn("Policy Extension plugin did not complete correctly. This could allow access to a resource.", e); } } return resultCollection; } return matchAllCollection; } private KeyValueCollectionPermission isPermittedByExtensionOne( CollectionPermission subjectAllCollection, KeyValueCollectionPermission matchOneCollection) { if (!CollectionUtils.isEmpty(policyExtensions)) { KeyValueCollectionPermission resultCollection = new KeyValueCollectionPermission(); resultCollection.addAll(matchOneCollection.getPermissionList()); resultCollection.setAction(matchOneCollection.getAction()); for (PolicyExtension policyExtension : policyExtensions) { try { resultCollection = policyExtension.isPermittedMatchOne(subjectAllCollection, resultCollection); } catch (Exception e) { SecurityLogger.auditWarn("Policy Extension plugin did not complete correctly. This could allow access to a resource.", e); LOGGER.warn("Policy Extension plugin did not complete correctly. This could allow access to a resource.", e); } } return resultCollection; } return matchOneCollection; } /** * Returns a collection of {@link Permission} objects that the {@link AuthorizationInfo} object * of a {@link ddf.security.Subject} is asserting. * * @param authorizationInfo the application-specific subject/user identifier. * @return collection of Permissions. */ protected Collection<Permission> getPermissions(AuthorizationInfo authorizationInfo) { Set<Permission> permissions = new HashSet<>(); if (authorizationInfo != null) { Collection<Permission> perms = authorizationInfo.getObjectPermissions(); if (!CollectionUtils.isEmpty(perms)) { permissions.addAll(perms); } perms = resolvePermissions(authorizationInfo.getStringPermissions()); if (!CollectionUtils.isEmpty(perms)) { permissions.addAll(perms); } perms = resolveRolePermissions(authorizationInfo.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 HashSet<>(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 HashSet<>(roleNames.size()); for (String roleName : roleNames) { Collection<Permission> resolved = resolver.resolvePermissionsInRole(roleName); if (!CollectionUtils.isEmpty(resolved)) { perms.addAll(resolved); } } } return perms; } /** * 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>subjectAttrName=metacardAttrName</code><br/> * where <code>metacardAttrName</code> is the name of an attribute in the metacard and * <code>subjectAttrName</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) { SecurityLogger.audit("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); } } } } /** * 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>subjectAttrName=metacardAttrName</code><br/> * where <code>metacardAttrName</code> is the name of an attribute in the metacard and * <code>subjectAttrName</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) { SecurityLogger.audit("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); } } } } public void setEnvironmentAttributes(List<String> environmentAttributes) { this.environmentAttributes.clear(); this.environmentAttributes.addAll(environmentAttributes); } }