| /*========================================================================= |
| * This implementation is provided on an "AS IS" BASIS, WITHOUT WARRANTIES |
| * OR CONDITIONS OF ANY KIND, either express or implied." |
| *========================================================================== |
| */ |
| |
| package templates.security; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.security.Principal; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import javax.xml.parsers.DocumentBuilder; |
| import javax.xml.parsers.DocumentBuilderFactory; |
| |
| import org.w3c.dom.Attr; |
| import org.w3c.dom.Document; |
| import org.w3c.dom.NamedNodeMap; |
| import org.w3c.dom.Node; |
| import org.w3c.dom.NodeList; |
| import org.xml.sax.EntityResolver; |
| import org.xml.sax.InputSource; |
| import org.xml.sax.SAXException; |
| import org.xml.sax.SAXParseException; |
| |
| import com.gemstone.gemfire.LogWriter; |
| import com.gemstone.gemfire.cache.Cache; |
| import com.gemstone.gemfire.cache.operations.ExecuteFunctionOperationContext; |
| import com.gemstone.gemfire.cache.operations.OperationContext; |
| import com.gemstone.gemfire.cache.operations.OperationContext.OperationCode; |
| import com.gemstone.gemfire.cache.operations.QueryOperationContext; |
| import com.gemstone.gemfire.distributed.DistributedMember; |
| import com.gemstone.gemfire.security.AccessControl; |
| import com.gemstone.gemfire.security.NotAuthorizedException; |
| |
| /** |
| * An implementation of the <code>{@link AccessControl}</code> interface that |
| * allows authorization using the permissions as specified in the given XML |
| * file. |
| * |
| * The format of the XML file is specified in <a href="authz5_5.dtd"/>. It |
| * implements a role-based authorization at the operation level for each region. |
| * Each principal name may be associated with a set of roles. The name of the |
| * principal is obtained using the {@link Principal#getName} method and no other |
| * information of the principal is utilized. Each role can be provided |
| * permissions to execute operations for each region. |
| * |
| * The top-level element in the XML is "acl" tag that contains the "role" and |
| * "permission" tags. The "role" tag contains the list of users that have been |
| * given that role. The name of the role is specified in the "role" attribute |
| * and the users are contained in the "user" tags insided the "role" tag. |
| * |
| * The "permissions" tag contains the list of operations allowed for a |
| * particular region. The role name is specified as the "role" attribute, the |
| * list of comma separated region names as the optional "regions" attribute and |
| * the operation names are contained in the "operation" tags inside the |
| * "permissions" tag. The allowed operation names are: GET, PUT, PUTALL, |
| * DESTROY, REGISTER_INTEREST, UNREGISTER_INTEREST, CONTAINS_KEY, KEY_SET, |
| * QUERY, EXECUTE_CQ, STOP_CQ, CLOSE_CQ, REGION_CLEAR, REGION_CREATE, |
| * REGION_DESTROY. These correspond to the operations in the |
| * {@link OperationCode} enumeration with the same name. |
| * |
| * When no region name is specified then the operation is allowed for all |
| * regions in the cache. Any permissions specified for regions using the |
| * "regions" attribute override these permissions. This allows users to provide |
| * generic permissions without any region name, and override for specific |
| * regions specified using the "regions" attribute. A cache-level operation |
| * (e.g. {@link OperationCode#REGION_DESTROY}) specified for a particular region |
| * is ignored i.e. the cache-level operations are only applicable when no region |
| * name is specified. A {@link OperationCode#QUERY} operation is permitted when |
| * either the <code>QUERY</code> permission is provided at the cache-level for |
| * the user or when <code>QUERY</code> permission is provided for all the |
| * regions that are part of the query string. |
| * |
| * Any roles specified in the "user" tag that do not have a specified permission |
| * set using the "permission" tags are ignored. When no {@link Principal} is |
| * associated with the current connection, then empty user name is used to |
| * search for the roles so an empty user name can be used to specify roles of |
| * unauthenticated clients (i.e. <code>Everyone</code>). |
| * |
| * This sample implementation is useful only for pre-operation checks and should |
| * not be used for post-operation authorization since it does nothing useful for |
| * post-operation case. |
| * |
| * @author Sumedh Wale |
| * @since 5.5 |
| */ |
| public class XmlAuthorization implements AccessControl { |
| |
| public static final String DOC_URI_PROP_NAME = "security-authz-xml-uri"; |
| |
| private static final String TAG_ROLE = "role"; |
| |
| private static final String TAG_USER = "user"; |
| |
| private static final String TAG_PERMS = "permission"; |
| |
| private static final String TAG_OP = "operation"; |
| |
| private static final String ATTR_ROLENAME = "name"; |
| |
| private static final String ATTR_ROLE = "role"; |
| |
| private static final String ATTR_REGIONS = "regions"; |
| |
| private static final String ATTR_FUNCTION_IDS = "functionIds"; |
| |
| private static final String ATTR_FUNCTION_OPTIMIZE_FOR_WRITE = |
| "optimizeForWrite"; |
| |
| private static final String ATTR_FUNCTION_KEY_SET = "keySet"; |
| |
| private static String currentDocUri = null; |
| |
| private static Map<String, HashSet<String>> userRoles = null; |
| |
| private static Map<String, Map<String, |
| Map<OperationCode, FunctionSecurityPrmsHolder>>> rolePermissions = null; |
| |
| private static NotAuthorizedException xmlLoadFailure = null; |
| |
| private static final Object sync = new Object(); |
| |
| private static final String EMPTY_VALUE = ""; |
| |
| private final Map<String, Map<OperationCode, |
| FunctionSecurityPrmsHolder>> allowedOps; |
| |
| protected LogWriter logger; |
| |
| protected LogWriter securityLogger; |
| |
| private XmlAuthorization() { |
| |
| this.allowedOps = new HashMap<String, Map<OperationCode, |
| FunctionSecurityPrmsHolder>>(); |
| this.logger = null; |
| this.securityLogger = null; |
| } |
| |
| /** |
| * Change the region name to a standard format having single '/' as separator |
| * and starting with a '/' as in standard POSIX paths |
| */ |
| public static String normalizeRegionName(String regionName) { |
| |
| if (regionName == null || regionName.length() == 0) { |
| return EMPTY_VALUE; |
| } |
| char[] resultName = new char[regionName.length() + 1]; |
| boolean changed = false; |
| boolean isPrevCharSlash = false; |
| int startIndex; |
| if (regionName.charAt(0) != '/') { |
| changed = true; |
| startIndex = 0; |
| } |
| else { |
| isPrevCharSlash = true; |
| startIndex = 1; |
| } |
| resultName[0] = '/'; |
| int resultLength = 1; |
| // Replace all more than one '/'s with a single '/' |
| for (int index = startIndex; index < regionName.length(); ++index) { |
| char currChar = regionName.charAt(index); |
| if (currChar == '/') { |
| if (isPrevCharSlash) { |
| changed = true; |
| continue; |
| } |
| isPrevCharSlash = true; |
| } |
| else { |
| isPrevCharSlash = false; |
| } |
| resultName[resultLength++] = currChar; |
| } |
| // Remove any trailing slash |
| if (resultName[resultLength - 1] == '/') { |
| --resultLength; |
| changed = true; |
| } |
| if (changed) { |
| return new String(resultName, 0, resultLength); |
| } |
| else { |
| return regionName; |
| } |
| } |
| |
| /** Get the attribute value for a given attribute name of a node. */ |
| private static String getAttributeValue(Node node, String attrName) { |
| |
| NamedNodeMap attrMap = node.getAttributes(); |
| Node attrNode; |
| if (attrMap != null && (attrNode = attrMap.getNamedItem(attrName)) != null) { |
| return ((Attr)attrNode).getValue(); |
| } |
| return EMPTY_VALUE; |
| } |
| |
| /** Get the string contained in the first text child of the node. */ |
| private static String getNodeValue(Node node) { |
| |
| NodeList childNodes = node.getChildNodes(); |
| for (int index = 0; index < childNodes.getLength(); index++) { |
| Node childNode = childNodes.item(index); |
| if (childNode.getNodeType() == Node.TEXT_NODE) { |
| return childNode.getNodeValue(); |
| } |
| } |
| return EMPTY_VALUE; |
| } |
| |
| /** |
| * Public static factory method to create an instance of |
| * <code>XmlAuthorization</code>. The fully qualified name of the class |
| * (<code>templates.security.XmlAuthorization.create</code>) |
| * should be mentioned as the <code>security-client-accessor</code> system |
| * property to enable pre-operation authorization checks as implemented in |
| * this class. |
| * |
| * @return an object of <code>XmlAuthorization</code> class |
| */ |
| public static AccessControl create() { |
| |
| return new XmlAuthorization(); |
| } |
| |
| /** |
| * Cache authorization information for all users statically. This method is |
| * not thread-safe and is should either be invoked only once, or the caller |
| * should take the appropriate locks. |
| * |
| * @param cache |
| * reference to the cache object for the distributed system |
| */ |
| private static void init(Cache cache) throws NotAuthorizedException { |
| |
| LogWriter logger = cache.getLogger(); |
| String xmlDocumentUri = (String)cache.getDistributedSystem() |
| .getSecurityProperties().get(DOC_URI_PROP_NAME); |
| try { |
| if (xmlDocumentUri == null) { |
| throw new NotAuthorizedException("No ACL file defined using tag [" |
| + DOC_URI_PROP_NAME + "] in system properties"); |
| } |
| if (xmlDocumentUri.equals(XmlAuthorization.currentDocUri)) { |
| if (XmlAuthorization.xmlLoadFailure != null) { |
| throw XmlAuthorization.xmlLoadFailure; |
| } |
| return; |
| } |
| DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); |
| factory.setIgnoringComments(true); |
| factory.setIgnoringElementContentWhitespace(true); |
| factory.setValidating(true); |
| DocumentBuilder builder = factory.newDocumentBuilder(); |
| XmlErrorHandler errorHandler = new XmlErrorHandler(logger, xmlDocumentUri); |
| builder.setErrorHandler(errorHandler); |
| builder.setEntityResolver(new AuthzDtdResolver()); |
| Document xmlDocument = builder.parse(xmlDocumentUri); |
| |
| XmlAuthorization.userRoles = new HashMap<String, HashSet<String>>(); |
| XmlAuthorization.rolePermissions = new HashMap<String, Map<String, |
| Map<OperationCode, FunctionSecurityPrmsHolder>>>(); |
| NodeList roleUserNodes = xmlDocument.getElementsByTagName(TAG_ROLE); |
| for (int roleIndex = 0; roleIndex < roleUserNodes.getLength(); |
| roleIndex++) { |
| Node roleUserNode = roleUserNodes.item(roleIndex); |
| String roleName = getAttributeValue(roleUserNode, ATTR_ROLENAME); |
| NodeList userNodes = roleUserNode.getChildNodes(); |
| for (int userIndex = 0; userIndex < userNodes.getLength(); |
| userIndex++) { |
| Node userNode = userNodes.item(userIndex); |
| if (userNode.getNodeName() == TAG_USER) { |
| String userName = getNodeValue(userNode); |
| HashSet<String> userRoleSet = XmlAuthorization.userRoles |
| .get(userName); |
| if (userRoleSet == null) { |
| userRoleSet = new HashSet<String>(); |
| XmlAuthorization.userRoles.put(userName, userRoleSet); |
| } |
| userRoleSet.add(roleName); |
| } |
| else { |
| throw new SAXParseException("Unknown tag [" |
| + userNode.getNodeName() + "] as child of tag [" + TAG_ROLE |
| + ']', null); |
| } |
| } |
| } |
| NodeList rolePermissionNodes = xmlDocument |
| .getElementsByTagName(TAG_PERMS); |
| for (int permIndex = 0; permIndex < rolePermissionNodes.getLength(); |
| permIndex++) { |
| Node rolePermissionNode = rolePermissionNodes.item(permIndex); |
| String roleName = getAttributeValue(rolePermissionNode, ATTR_ROLE); |
| Map<String, Map<OperationCode, FunctionSecurityPrmsHolder>> |
| regionOperationMap = XmlAuthorization.rolePermissions.get(roleName); |
| if (regionOperationMap == null) { |
| regionOperationMap = new HashMap<String, |
| Map<OperationCode, FunctionSecurityPrmsHolder>>(); |
| XmlAuthorization.rolePermissions.put(roleName, regionOperationMap); |
| } |
| NodeList operationNodes = rolePermissionNode.getChildNodes(); |
| HashMap<OperationCode, FunctionSecurityPrmsHolder> operationMap = |
| new HashMap<OperationCode, FunctionSecurityPrmsHolder>(); |
| for (int opIndex = 0; opIndex < operationNodes.getLength(); opIndex++) { |
| Node operationNode = operationNodes.item(opIndex); |
| if (operationNode.getNodeName() == TAG_OP) { |
| String operationName = getNodeValue(operationNode); |
| OperationCode code = OperationCode.parse(operationName); |
| if (code == null) { |
| throw new SAXParseException("Unknown operation [" + operationName |
| + ']', null); |
| } |
| if (code != OperationCode.EXECUTE_FUNCTION) { |
| operationMap.put(code, null); |
| } |
| else { |
| String optimizeForWrite = getAttributeValue(operationNode, |
| ATTR_FUNCTION_OPTIMIZE_FOR_WRITE); |
| String functionAttr = getAttributeValue(operationNode, |
| ATTR_FUNCTION_IDS); |
| String keysAttr = getAttributeValue(operationNode, |
| ATTR_FUNCTION_KEY_SET); |
| |
| Boolean isOptimizeForWrite; |
| HashSet<String> functionIds; |
| HashSet<String> keySet; |
| |
| if (optimizeForWrite == null || optimizeForWrite.length() == 0) { |
| isOptimizeForWrite = null; |
| } |
| else { |
| isOptimizeForWrite = Boolean.parseBoolean(optimizeForWrite); |
| } |
| |
| if (functionAttr == null || functionAttr.length() == 0) { |
| functionIds = null; |
| } |
| else { |
| String[] functionArray = functionAttr.split(","); |
| functionIds = new HashSet<String>(); |
| for (int strIndex = 0; strIndex < functionArray.length; |
| ++strIndex) { |
| functionIds.add((functionArray[strIndex])); |
| } |
| } |
| |
| if (keysAttr == null || keysAttr.length() == 0) { |
| keySet = null; |
| } |
| else { |
| String[] keySetArray = keysAttr.split(","); |
| keySet = new HashSet<String>(); |
| for (int strIndex = 0; strIndex < keySetArray.length; |
| ++strIndex) { |
| keySet.add((keySetArray[strIndex])); |
| } |
| } |
| FunctionSecurityPrmsHolder functionContext = |
| new FunctionSecurityPrmsHolder(isOptimizeForWrite, |
| functionIds, keySet); |
| operationMap.put(code, functionContext); |
| } |
| } |
| else { |
| throw new SAXParseException("Unknown tag [" |
| + operationNode.getNodeName() + "] as child of tag [" |
| + TAG_PERMS + ']', null); |
| } |
| } |
| String regionNames = getAttributeValue(rolePermissionNode, ATTR_REGIONS); |
| if (regionNames == null || regionNames.length() == 0) { |
| regionOperationMap.put(EMPTY_VALUE, operationMap); |
| } |
| else { |
| String[] regionNamesSplit = regionNames.split(","); |
| for (int strIndex = 0; strIndex < regionNamesSplit.length; |
| ++strIndex) { |
| regionOperationMap.put( |
| normalizeRegionName(regionNamesSplit[strIndex]), operationMap); |
| } |
| } |
| } |
| XmlAuthorization.currentDocUri = xmlDocumentUri; |
| } |
| catch (Exception ex) { |
| String exStr; |
| if (ex instanceof NotAuthorizedException) { |
| exStr = ex.getMessage(); |
| } |
| else { |
| exStr = ex.getClass().getName() + ": " + ex.getMessage(); |
| } |
| logger.warning("XmlAuthorization.init: " + exStr); |
| XmlAuthorization.xmlLoadFailure = new NotAuthorizedException(exStr, ex); |
| throw XmlAuthorization.xmlLoadFailure; |
| } |
| } |
| |
| /** |
| * Initialize the <code>XmlAuthorization</code> callback for a client having |
| * the given principal. |
| * |
| * This method caches the full XML authorization file the first time it is |
| * invoked and caches all the permissions for the provided |
| * <code>principal</code> to speed up lookup the |
| * <code>authorizeOperation</code> calls. The permissions for the principal |
| * are maintained as a {@link Map} of region name to the {@link HashSet} of |
| * operations allowed for that region. A global entry with region name as |
| * empty string is also made for permissions provided for all the regions. |
| * |
| * @param principal |
| * the principal associated with the authenticated client |
| * @param cache |
| * reference to the cache object |
| * @param remoteMember |
| * the {@link DistributedMember} object for the remote |
| * authenticated client |
| * |
| * @throws NotAuthorizedException |
| * if some exception condition happens during the |
| * initialization while reading the XML; in such a case all |
| * subsequent client operations will throw |
| * <code>NotAuthorizedException</code> |
| */ |
| public void init(Principal principal, DistributedMember remoteMember, |
| Cache cache) throws NotAuthorizedException { |
| |
| synchronized (sync) { |
| XmlAuthorization.init(cache); |
| } |
| this.logger = cache.getLogger(); |
| this.securityLogger = cache.getSecurityLogger(); |
| |
| String name; |
| if (principal != null) { |
| name = principal.getName(); |
| } |
| else { |
| name = EMPTY_VALUE; |
| } |
| HashSet<String> roles = XmlAuthorization.userRoles.get(name); |
| if (roles != null) { |
| for (String roleName : roles) { |
| Map<String, Map<OperationCode, FunctionSecurityPrmsHolder>> |
| regionOperationMap = XmlAuthorization.rolePermissions.get(roleName); |
| if (regionOperationMap != null) { |
| for (Map.Entry<String, Map<OperationCode, FunctionSecurityPrmsHolder>> |
| regionEntry : regionOperationMap.entrySet()) { |
| String regionName = regionEntry.getKey(); |
| Map<OperationCode, FunctionSecurityPrmsHolder> regionOperations = |
| this.allowedOps.get(regionName); |
| if (regionOperations == null) { |
| regionOperations = |
| new HashMap<OperationCode, FunctionSecurityPrmsHolder>(); |
| this.allowedOps.put(regionName, regionOperations); |
| } |
| regionOperations.putAll(regionEntry.getValue()); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Return true if the given operation is allowed for the cache/region. |
| * |
| * This looks up the cached permissions of the principal in the map for the |
| * provided region name. If none are found then the global permissions with |
| * empty region name are looked up. The operation is allowed if it is found |
| * this permission list. |
| * |
| * @param regionName |
| * When null then it indicates a cache-level operation, else |
| * the name of the region for the operation. |
| * @param context |
| * the data required by the operation |
| * |
| * @return true if the operation is authorized and false otherwise |
| * |
| */ |
| public boolean authorizeOperation(String regionName, |
| final OperationContext context) { |
| |
| Map<OperationCode, FunctionSecurityPrmsHolder> operationMap; |
| // Check GET permissions for updates from server to client |
| if (context.isClientUpdate()) { |
| operationMap = this.allowedOps.get(regionName); |
| if (operationMap == null && regionName.length() > 0) { |
| operationMap = this.allowedOps.get(EMPTY_VALUE); |
| } |
| if (operationMap != null) { |
| return operationMap.containsKey(OperationCode.GET); |
| } |
| return false; |
| } |
| |
| OperationCode opCode = context.getOperationCode(); |
| if (opCode.isQuery() || opCode.isExecuteCQ() || opCode.isCloseCQ() |
| || opCode.isStopCQ()) { |
| // First check if cache-level permission has been provided |
| operationMap = this.allowedOps.get(EMPTY_VALUE); |
| boolean globalPermission = (operationMap != null && operationMap |
| .containsKey(opCode)); |
| Set<String> regionNames = ((QueryOperationContext)context) |
| .getRegionNames(); |
| if (regionNames == null || regionNames.size() == 0) { |
| return globalPermission; |
| } |
| for (String r : regionNames) { |
| regionName = normalizeRegionName(r); |
| operationMap = this.allowedOps.get(regionName); |
| if (operationMap == null) { |
| if (!globalPermission) { |
| return false; |
| } |
| } |
| else if (!operationMap.containsKey(opCode)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| final String normalizedRegionName = normalizeRegionName(regionName); |
| operationMap = this.allowedOps.get(normalizedRegionName); |
| if (operationMap == null && normalizedRegionName.length() > 0) { |
| operationMap = this.allowedOps.get(EMPTY_VALUE); |
| } |
| if (operationMap != null) { |
| if (context.getOperationCode() != OperationCode.EXECUTE_FUNCTION) { |
| return operationMap.containsKey(context.getOperationCode()); |
| }else { |
| if (!operationMap.containsKey(context.getOperationCode())) { |
| return false; |
| } |
| else { |
| if (!context.isPostOperation()) { |
| FunctionSecurityPrmsHolder functionParameter = |
| operationMap.get( |
| context.getOperationCode()); |
| ExecuteFunctionOperationContext functionContext = |
| (ExecuteFunctionOperationContext)context; |
| // OnRegion execution |
| if (functionContext.getRegionName() != null) { |
| if (functionParameter.isOptimizeForWrite() != null |
| && functionParameter.isOptimizeForWrite().booleanValue() |
| != functionContext.isOptimizeForWrite()) { |
| return false; |
| } |
| if (functionParameter.getFunctionIds() != null |
| && !functionParameter.getFunctionIds().contains( |
| functionContext.getFunctionId())) { |
| return false; |
| } |
| if (functionParameter.getKeySet() != null |
| && functionContext.getKeySet() != null) { |
| if (functionContext.getKeySet().containsAll( |
| functionParameter.getKeySet())) { |
| return false; |
| } |
| } |
| return true; |
| } |
| else {// On Server execution |
| if (functionParameter.getFunctionIds() != null |
| && !functionParameter.getFunctionIds().contains( |
| functionContext.getFunctionId())) { |
| return false; |
| } |
| return true; |
| } |
| } |
| else { |
| ExecuteFunctionOperationContext functionContext = |
| (ExecuteFunctionOperationContext)context; |
| FunctionSecurityPrmsHolder functionParameter = operationMap.get( |
| context.getOperationCode()); |
| if (functionContext.getRegionName() != null) { |
| if (functionContext.getResult() instanceof ArrayList |
| && functionParameter.getKeySet() != null) { |
| ArrayList<String> resultList = (ArrayList)functionContext |
| .getResult(); |
| HashSet<String> nonAllowedKeys = functionParameter.getKeySet(); |
| if (resultList.containsAll(nonAllowedKeys)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| else { |
| ArrayList<String> resultList = (ArrayList)functionContext |
| .getResult(); |
| final String inSecureItem = "Insecure item"; |
| if (resultList.contains(inSecureItem)) { |
| return false; |
| } |
| return true; |
| } |
| } |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Clears the cached information for this principal. |
| */ |
| public void close() { |
| |
| this.allowedOps.clear(); |
| } |
| |
| /** |
| * Clear all the statically cached information. |
| */ |
| public static void clear() { |
| |
| XmlAuthorization.currentDocUri = null; |
| if (XmlAuthorization.userRoles != null) { |
| XmlAuthorization.userRoles.clear(); |
| XmlAuthorization.userRoles = null; |
| } |
| if (XmlAuthorization.rolePermissions != null) { |
| XmlAuthorization.rolePermissions.clear(); |
| XmlAuthorization.rolePermissions = null; |
| } |
| XmlAuthorization.xmlLoadFailure = null; |
| } |
| |
| private static class AuthzDtdResolver implements EntityResolver { |
| Pattern authzPattern = Pattern.compile("authz.*\\.dtd"); |
| |
| @Override |
| public InputSource resolveEntity(String publicId, String systemId) |
| throws SAXException, IOException { |
| try { |
| Matcher matcher = authzPattern.matcher(systemId); |
| if(matcher.find()) { |
| String dtdName = matcher.group(0); |
| InputStream stream = XmlAuthorization.class.getResourceAsStream(dtdName); |
| return new InputSource(stream); |
| } |
| } catch(Exception e) { |
| //do nothing, use the default resolver |
| } |
| |
| return null; |
| } |
| } |
| } |