All Downloads are FREE. Search and download functionalities are using the official Maven repository.

templates.security.XmlAuthorization Maven / Gradle / Ivy

There is a newer version: 2.0-BETA
Show newest version
/*
 * Copyright (c) 2010-2015 Pivotal Software, Inc. All rights reserved.
 *
 * Licensed 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. See accompanying
 * LICENSE file.
 */
package templates.security;

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 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.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.QueryOperationContext;
import com.gemstone.gemfire.cache.operations.OperationContext.OperationCode;
import com.gemstone.gemfire.distributed.DistributedMember;
import com.gemstone.gemfire.security.AccessControl;
import com.gemstone.gemfire.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 . 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 QUERY permission is provided at the cache-level for
 * the user or when 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. 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.
 * 
 * @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> userRoles = null;

  private static Map>> rolePermissions = null;

  private static NotAuthorizedException xmlLoadFailure = null;

  private static final Object sync = new Object();

  private static final String EMPTY_VALUE = "";

  private final Map> allowedOps;

  protected LogWriter logger;

  protected LogWriter securityLogger;

  private XmlAuthorization() {

    this.allowedOps = new HashMap>();
    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
   * XmlAuthorization. The fully qualified name of the class
   * (templates.security.XmlAuthorization.create)
   * should be mentioned as the security-client-accessor system
   * property to enable pre-operation authorization checks as implemented in
   * this class.
   * 
   * @return an object of XmlAuthorization 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);
      Document xmlDocument = builder.parse(xmlDocumentUri);

      XmlAuthorization.userRoles = new HashMap>();
      XmlAuthorization.rolePermissions = new HashMap>>();
      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 userRoleSet = XmlAuthorization.userRoles
                .get(userName);
            if (userRoleSet == null) {
              userRoleSet = new HashSet();
              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>
          regionOperationMap = XmlAuthorization.rolePermissions.get(roleName);
        if (regionOperationMap == null) {
          regionOperationMap = new HashMap>();
          XmlAuthorization.rolePermissions.put(roleName, regionOperationMap);
        }
        NodeList operationNodes = rolePermissionNode.getChildNodes();
        HashMap operationMap =
          new HashMap();
        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 functionIds;
              HashSet 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();
                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();
                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);
      throw XmlAuthorization.xmlLoadFailure;
    }
  }

  /**
   * Initialize the 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
   * principal to speed up lookup the
   * 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
   *                 NotAuthorizedException
   */
  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 roles = XmlAuthorization.userRoles.get(name);
    if (roles != null) {
      for (String roleName : roles) {
        Map>
          regionOperationMap = XmlAuthorization.rolePermissions.get(roleName);
        if (regionOperationMap != null) {
          for (Map.Entry>
              regionEntry : regionOperationMap.entrySet()) {
            String regionName = regionEntry.getKey();
            Map regionOperations =
              this.allowedOps.get(regionName);
            if (regionOperations == null) {
              regionOperations =
                new HashMap();
              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 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 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 resultList = (ArrayList)functionContext
                    .getResult();
                HashSet nonAllowedKeys = functionParameter.getKeySet();
                if (resultList.containsAll(nonAllowedKeys)) {
                  return false;
                }
              }
              return true;
            }
            else {
              ArrayList 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;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy