/* * Licensed to the Apache Software Foundation (ASF) under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional information regarding * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance with the License. You may obtain a * copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package org.apache.geode.security.templates; 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 org.apache.geode.LogWriter; import org.apache.geode.cache.Cache; import org.apache.geode.cache.operations.ExecuteFunctionOperationContext; import org.apache.geode.cache.operations.OperationContext; import org.apache.geode.cache.operations.OperationContext.OperationCode; import org.apache.geode.cache.operations.QueryOperationContext; import org.apache.geode.distributed.DistributedMember; import org.apache.geode.security.AccessControl; import org.apache.geode.security.NotAuthorizedException; /** * An implementation of the {@link AccessControl} 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} permission is * provided at the cache-level for the user or when {@code QUERY} 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}). * * 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. * * @since GemFire 5.5 */ public class XmlAuthorization implements AccessControl { public static final String DOC_URI_PROP_NAME = "security-authz-xml-uri"; private static final Object sync = new Object(); private static final String EMPTY_VALUE = ""; 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 final Map<String, Map<OperationCode, FunctionSecurityPrmsHolder>> allowedOps; protected LogWriter systemLogWriter; protected LogWriter securityLogWriter; /** * Public static factory method to create an instance of {@code XmlAuthorization}. The fully * qualified name of the class * ({@code org.apache.geode.security.templates.XmlAuthorization.create}) should be mentioned as * the {@code security-client-accessor} system property to enable pre-operation authorization * checks as implemented in this class. * * @return an object of {@code XmlAuthorization} class */ public static AccessControl create() { return new XmlAuthorization(); } /** * 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; } /** * 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(final 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; } } private XmlAuthorization() { this.allowedOps = new HashMap<String, Map<OperationCode, FunctionSecurityPrmsHolder>>(); this.systemLogWriter = null; this.securityLogWriter = null; } /** * Initialize the {@code XmlAuthorization} 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} to speed up lookup the * {@code authorizeOperation} 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} */ @Override public void init(final Principal principal, final DistributedMember remoteMember, final Cache cache) throws NotAuthorizedException { synchronized (sync) { XmlAuthorization.init(cache); } this.systemLogWriter = cache.getLogger(); this.securityLogWriter = 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 */ @Override 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(); Set<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. */ @Override public void close() { this.allowedOps.clear(); } /** Get the attribute value for a given attribute name of a node. */ private static String getAttributeValue(final Node node, final 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(final 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; } /** * 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(final Cache cache) throws NotAuthorizedException { final LogWriter systemLogWriter = cache.getLogger(); final 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; } final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setIgnoringComments(true); factory.setIgnoringElementContentWhitespace(true); factory.setValidating(true); final DocumentBuilder builder = factory.newDocumentBuilder(); final XmlErrorHandler errorHandler = new XmlErrorHandler(systemLogWriter, xmlDocumentUri); builder.setErrorHandler(errorHandler); builder.setEntityResolver(new AuthzDtdResolver()); final Document xmlDocument = builder.parse(xmlDocumentUri); XmlAuthorization.userRoles = new HashMap<String, HashSet<String>>(); XmlAuthorization.rolePermissions = new HashMap<String, Map<String, Map<OperationCode, FunctionSecurityPrmsHolder>>>(); final NodeList roleUserNodes = xmlDocument.getElementsByTagName(TAG_ROLE); for (int roleIndex = 0; roleIndex < roleUserNodes.getLength(); roleIndex++) { final Node roleUserNode = roleUserNodes.item(roleIndex); final String roleName = getAttributeValue(roleUserNode, ATTR_ROLENAME); final NodeList userNodes = roleUserNode.getChildNodes(); for (int userIndex = 0; userIndex < userNodes.getLength(); userIndex++) { final Node userNode = userNodes.item(userIndex); if (TAG_USER.equals(userNode.getNodeName())) { final 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); } } } final NodeList rolePermissionNodes = xmlDocument.getElementsByTagName(TAG_PERMS); for (int permIndex = 0; permIndex < rolePermissionNodes.getLength(); permIndex++) { final Node rolePermissionNode = rolePermissionNodes.item(permIndex); final 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); } final NodeList operationNodes = rolePermissionNode.getChildNodes(); final HashMap<OperationCode, FunctionSecurityPrmsHolder> operationMap = new HashMap<OperationCode, FunctionSecurityPrmsHolder>(); for (int opIndex = 0; opIndex < operationNodes.getLength(); opIndex++) { final Node operationNode = operationNodes.item(opIndex); if (TAG_OP.equals(operationNode.getNodeName())) { final String operationName = getNodeValue(operationNode); final OperationCode code = OperationCode.valueOf(operationName); if (code == null) { throw new SAXParseException("Unknown operation [" + operationName + ']', null); } if (code != OperationCode.EXECUTE_FUNCTION) { operationMap.put(code, null); } else { final String optimizeForWrite = getAttributeValue(operationNode, ATTR_FUNCTION_OPTIMIZE_FOR_WRITE); final String functionAttr = getAttributeValue(operationNode, ATTR_FUNCTION_IDS); final 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 { final 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 { final String[] keySetArray = keysAttr.split(","); keySet = new HashSet<String>(); for (int strIndex = 0; strIndex < keySetArray.length; ++strIndex) { keySet.add((keySetArray[strIndex])); } } final 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); } } final String regionNames = getAttributeValue(rolePermissionNode, ATTR_REGIONS); if (regionNames == null || regionNames.length() == 0) { regionOperationMap.put(EMPTY_VALUE, operationMap); } else { final 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 message; if (ex instanceof NotAuthorizedException) { message = ex.getMessage(); } else { message = ex.getClass().getName() + ": " + ex.getMessage(); } systemLogWriter.warning("XmlAuthorization.init: " + message); XmlAuthorization.xmlLoadFailure = new NotAuthorizedException(message, ex); throw XmlAuthorization.xmlLoadFailure; } } private static class AuthzDtdResolver implements EntityResolver { final Pattern authzPattern = Pattern.compile("authz.*\\.dtd"); @Override public InputSource resolveEntity(final String publicId, final String systemId) throws SAXException, IOException { try { final Matcher matcher = authzPattern.matcher(systemId); if (matcher.find()) { final String dtdName = matcher.group(0); final InputStream stream = XmlAuthorization.class.getResourceAsStream(dtdName); return new InputSource(stream); } } catch (Exception e) { // do nothing, use the default resolver } return null; } } }