/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2008-2010 Sun Microsystems, Inc.
 *      Portions Copyright 2013-2015 ForgeRock AS
 */
package org.opends.server.authorization.dseecompat;

import static org.opends.messages.AccessControlMessages.*;
import static org.opends.server.authorization.dseecompat.Aci.*;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.forgerock.i18n.LocalizableMessage;
import org.opends.server.types.AttributeType;
import org.opends.server.types.DN;
import org.forgerock.opendj.ldap.SearchScope;

/**
 * This class represents target part of an ACI's syntax. This is the part
 * of an ACI before the ACI body and specifies the entry, attributes, or set
 * of entries and attributes which the ACI controls access.
 *
 * The supported  ACI target keywords are: target, targetattr,
 * targetscope, targetfilter, targattrfilters, targetcontrol and extop.
 */
public class AciTargets {

    /**
     * ACI syntax has a target keyword.
     */
    private Target target;

    /**
     * ACI syntax has a targetscope keyword.
     */
    private SearchScope targetScope = SearchScope.WHOLE_SUBTREE;

    /**
     * ACI syntax has a targetattr keyword.
     */
    private TargetAttr targetAttr;

    /**
     * ACI syntax has a targetfilter keyword.
     */
    private TargetFilter targetFilter;

    /**
     * ACI syntax has a targattrtfilters keyword.
     */
    private TargAttrFilters targAttrFilters;

   /**
    * The ACI syntax has a targetcontrol keyword.
    */
    private TargetControl targetControl;

   /**
    * The ACI syntax has a extop keyword.
    */
    private ExtOp extOp;

    /**
     * The number of regular expression group positions in a valid ACI target
     * expression.
     */
    private static final int targetElementCount = 3;

    /**
     *  Regular expression group position of a target keyword.
     */
    private static final int targetKeywordPos       = 1;

    /**
     *  Regular expression group position of a target operator enumeration.
     */
    private static final int targetOperatorPos      = 2;

    /**
     *  Regular expression group position of a target expression statement.
     */
    private static final int targetExpressionPos    = 3;

    /**
     * Regular expression used to match a single target rule.
     */
    private static final String targetRegex =
           OPEN_PAREN +  ZERO_OR_MORE_WHITESPACE  +  WORD_GROUP +
           ZERO_OR_MORE_WHITESPACE + "(!?=)" + ZERO_OR_MORE_WHITESPACE +
           "\"([^\"]+)\"" + ZERO_OR_MORE_WHITESPACE + CLOSED_PAREN +
           ZERO_OR_MORE_WHITESPACE;

    /**
     * Regular expression used to match one or more target rules. The pattern is
     * part of a general ACI verification.
     */
    public static final String targetsRegex = "(" + targetRegex + ")*";

    /**
     * Rights that are skipped for certain target evaluations.
     * The test is use the skipRights array is:
     *
     * Either the ACI has a targetattr's rule and the current
     * attribute type is null or the current attribute type has
     * a type specified and the targetattr's rule is null.
     *
     * The actual check against the skipRights array is:
     *
     *  1. Is the ACI's rights in this array? For example,
     *     allow(all) or deny(add)
     *
     *  AND
     *
     *  2. Is the rights from the LDAP operation in this array? For
     *      example, an LDAP add would have rights of add and all.
     *
     *  If both are true, than the target match test returns true
     *  for this ACI.
     */
    private static final int skipRights = ACI_ADD | ACI_DELETE | ACI_PROXY;

    /**
     * Creates an ACI target from the specified arguments. All of these
     * may be null. If the ACI has no targets defaults will be used.
     *
     * @param targetEntry The ACI target keyword class.
     * @param targetAttr The ACI targetattr keyword class.
     * @param targetFilter The ACI targetfilter keyword class.
     * @param targetScope The ACI targetscope keyword class.
     * @param targAttrFilters The ACI targAttrFilters keyword class.
     * @param targetControl The ACI targetControl keyword class.
     * @param extOp The ACI extop keyword class.
     */
    private AciTargets(Target targetEntry, TargetAttr targetAttr,
                       TargetFilter targetFilter,
                       SearchScope targetScope,
                       TargAttrFilters targAttrFilters,
                       TargetControl targetControl,
                       ExtOp extOp) {
       this.target=targetEntry;
       this.targetAttr=targetAttr;
       this.targetScope=targetScope;
       this.targetFilter=targetFilter;
       this.targAttrFilters=targAttrFilters;
       this.targetControl=targetControl;
       this.extOp=extOp;
    }

    /**
     * Return class representing the ACI target keyword. May be
     * null. The default is the use the DN of the entry containing
     * the ACI and check if the resource entry is a descendant of that.
     * @return The ACI target class.
     */
    private Target getTarget() {
        return target;
    }

    /**
     * Return class representing the ACI targetattr keyword. May be null.
     * The default is to not match any attribute types in an entry.
     * @return The ACI targetattr class.
     */
    public TargetAttr getTargetAttr() {
        return targetAttr;
    }

    /**
     * Return the ACI targetscope keyword. Default is WHOLE_SUBTREE.
     * @return The ACI targetscope information.
     */
    public SearchScope getTargetScope() {
        return targetScope;
    }

    /**
     * Return class representing the  ACI targetfilter keyword. May be null.
     * @return The targetscope information.
     */
    public TargetFilter getTargetFilter() {
        return targetFilter;
    }

    /**
     * Return the class representing the ACI targattrfilters keyword. May be
     * null.
     * @return The targattrfilters information.
     */
    public TargAttrFilters getTargAttrFilters() {
        return targAttrFilters;
    }

   /**
    * Return the class representing the ACI targetcontrol keyword. May be
    * null.
    * @return The targetcontrol information.
   */
    public TargetControl getTargetControl() {
      return targetControl;
    }


   /**
    * Return the class representing the ACI extop keyword. May be
    * null.
    * @return The extop information.
   */
    public ExtOp getExtOp() {
      return extOp;
    }

    /**
     * Decode an ACI's target part of the syntax from the string provided.
     * @param input String representing an ACI target part of syntax.
     * @param dn The DN of the entry containing the ACI.
     * @return An AciTargets class representing the decoded ACI target string.
     * @throws AciException If the provided string contains errors.
     */
    public static AciTargets decode(String input, DN dn)
    throws AciException {
        Target target=null;
        TargetAttr targetAttr=null;
        TargetFilter targetFilter=null;
        TargAttrFilters targAttrFilters=null;
        TargetControl targetControl=null;
        ExtOp extOp=null;
        SearchScope targetScope=SearchScope.WHOLE_SUBTREE;
        Pattern targetPattern = Pattern.compile(targetRegex);
        Matcher targetMatcher = targetPattern.matcher(input);
        while (targetMatcher.find())
        {
            if (targetMatcher.groupCount() != targetElementCount) {
                LocalizableMessage message =
                    WARN_ACI_SYNTAX_INVALID_TARGET_SYNTAX.get(input);
                throw new AciException(message);
            }
            String keyword = targetMatcher.group(targetKeywordPos);
            EnumTargetKeyword targetKeyword  =
                EnumTargetKeyword.createKeyword(keyword);
            if (targetKeyword == null) {
                LocalizableMessage message =
                    WARN_ACI_SYNTAX_INVALID_TARGET_KEYWORD.get(keyword);
                throw new AciException(message);
            }
            String operator =
                targetMatcher.group(targetOperatorPos);
            EnumTargetOperator targetOperator =
                EnumTargetOperator.createOperator(operator);
            if (targetOperator == null) {
                LocalizableMessage message =
                    WARN_ACI_SYNTAX_INVALID_TARGETS_OPERATOR.get(operator);
                throw new AciException(message);
            }
            String expression = targetMatcher.group(targetExpressionPos);
            switch(targetKeyword)
            {
            case KEYWORD_TARGET:
            {
                if (target == null){
                    target =  Target.decode(targetOperator, expression, dn);
                }
                else
                {
                  LocalizableMessage message =
                          WARN_ACI_SYNTAX_INVALID_TARGET_DUPLICATE_KEYWORDS.
                                  get("target", input);
                  throw new AciException(message);
                }
                break;
            }
            case KEYWORD_TARGETCONTROL:
            {
              if (targetControl == null){
                targetControl =
                        TargetControl.decode(targetOperator, expression);
              }
              else
              {
                LocalizableMessage message =
                        WARN_ACI_SYNTAX_INVALID_TARGET_DUPLICATE_KEYWORDS.
                                get("targetcontrol", input);
                throw new AciException(message);
              }
              break;
            }
            case KEYWORD_EXTOP:
            {
              if (extOp == null){
                extOp =  ExtOp.decode(targetOperator, expression);
              }
              else
              {
                LocalizableMessage message =
                        WARN_ACI_SYNTAX_INVALID_TARGET_DUPLICATE_KEYWORDS.
                                get("extop", input);
                throw new AciException(message);
              }
              break;
            }
            case KEYWORD_TARGETATTR:
            {
                if (targetAttr == null){
                    targetAttr = TargetAttr.decode(targetOperator,
                            expression);
                }
                else {
                  LocalizableMessage message =
                          WARN_ACI_SYNTAX_INVALID_TARGET_DUPLICATE_KEYWORDS.
                                  get("targetattr", input);
                  throw new AciException(message);
                }
                break;
            }
            case KEYWORD_TARGETSCOPE:
            {
                // Check the operator for the targetscope is EQUALITY
                if (targetOperator == EnumTargetOperator.NOT_EQUALITY) {
                    LocalizableMessage message =
                            WARN_ACI_SYNTAX_INVALID_TARGET_NOT_OPERATOR.
                              get(operator, targetKeyword.name());
                    throw new AciException(message);
                }
                targetScope=createScope(expression);
                break;
            }
            case KEYWORD_TARGETFILTER:
            {
                if (targetFilter == null){
                    targetFilter = TargetFilter.decode(targetOperator,
                            expression);
                }
                else {
                  LocalizableMessage message =
                          WARN_ACI_SYNTAX_INVALID_TARGET_DUPLICATE_KEYWORDS.
                                  get("targetfilter", input);
                  throw new AciException(message);
                }
                break;
            }
            case KEYWORD_TARGATTRFILTERS:
            {
                if (targAttrFilters == null){
                    // Check the operator for the targattrfilters is EQUALITY
                    if (targetOperator == EnumTargetOperator.NOT_EQUALITY) {
                      LocalizableMessage message =
                              WARN_ACI_SYNTAX_INVALID_TARGET_NOT_OPERATOR.
                                      get(operator, targetKeyword.name());
                      throw new AciException(message);
                    }
                    targAttrFilters = TargAttrFilters.decode(targetOperator,
                            expression);
                }
                else {
                  LocalizableMessage message =
                          WARN_ACI_SYNTAX_INVALID_TARGET_DUPLICATE_KEYWORDS.
                                  get("targattrfilters", input);
                  throw new AciException(message);
                }
                break;
            }
            }
        }
        return new AciTargets(target, targetAttr, targetFilter,
                              targetScope, targAttrFilters, targetControl,
                              extOp);
    }

    /**
     * Evaluates a provided scope string and returns an appropriate
     * SearchScope enumeration.
     * @param expression The expression string.
     * @return An search scope enumeration matching the string.
     * @throws AciException If the expression is an invalid targetscope
     * string.
     */
    private static SearchScope createScope(String expression)
    throws AciException {
        if(expression.equalsIgnoreCase("base"))
        {
          return SearchScope.BASE_OBJECT;
        }
        else if(expression.equalsIgnoreCase("onelevel"))
        {
          return SearchScope.SINGLE_LEVEL;
        }
        else if(expression.equalsIgnoreCase("subtree"))
        {
          return SearchScope.WHOLE_SUBTREE;
        }
        else if(expression.equalsIgnoreCase("subordinate"))
        {
          return SearchScope.SUBORDINATES;
        }
        else {
            LocalizableMessage message =
                WARN_ACI_SYNTAX_INVALID_TARGETSCOPE_EXPRESSION.get(expression);
            throw new AciException(message);
        }
    }

    /**
     * Checks an ACI's targetfilter rule information against a target match
     * context.
     * @param aci The ACI to try an match the targetfilter of.
     * @param matchCtx The target match context containing information needed
     * to perform a target match.
     * @return True if the targetfilter rule matched the target context.
     */
    public static boolean isTargetFilterApplicable(Aci aci,
                                              AciTargetMatchContext matchCtx) {
        TargetFilter targetFilter=aci.getTargets().getTargetFilter();
        return targetFilter == null || targetFilter.isApplicable(matchCtx);
    }

    /**
     * Check an ACI's targetcontrol rule against a target match context.
     *
     * @param aci The ACI to match the targetcontrol against.
     * @param matchCtx The target match context containing the information
     *                 needed to perform the target match.
     * @return  True if the targetcontrol rule matched the target context.
     */
    public static boolean isTargetControlApplicable(Aci aci,
                                            AciTargetMatchContext matchCtx) {
      TargetControl targetControl=aci.getTargets().getTargetControl();
      return targetControl != null && targetControl.isApplicable(matchCtx);
    }

    /**
     * Check an ACI's extop rule against a target match context.
     *
     * @param aci The ACI to match the extop rule against.
     * @param matchCtx The target match context containing the information
     *                 needed to perform the target match.
     * @return  True if the extop rule matched the target context.
     */
    public static boolean isExtOpApplicable(Aci aci,
                                              AciTargetMatchContext matchCtx) {
      ExtOp extOp=aci.getTargets().getExtOp();
      return extOp != null && extOp.isApplicable(matchCtx);
    }


    /**
     * Check an ACI's targattrfilters rule against a target match context.
     *
     * @param aci The ACI to match the targattrfilters against.
     * @param matchCtx  The target match context containing the information
     * needed to perform the target match.
     * @return True if the targattrfilters rule matched the target context.
     */
    public static boolean isTargAttrFiltersApplicable(Aci aci,
                                               AciTargetMatchContext matchCtx) {
        boolean ret=true;
        TargAttrFilters targAttrFilters=aci.getTargets().getTargAttrFilters();
        if(targAttrFilters != null) {
            if((matchCtx.hasRights(ACI_ADD) &&
                targAttrFilters.hasMask(TARGATTRFILTERS_ADD)) ||
              (matchCtx.hasRights(ACI_DELETE) &&
               targAttrFilters.hasMask(TARGATTRFILTERS_DELETE)))
            {
              ret=targAttrFilters.isApplicableAddDel(matchCtx);
            }
            else if((matchCtx.hasRights(ACI_WRITE_ADD) &&
                     targAttrFilters.hasMask(TARGATTRFILTERS_ADD)) ||
                    (matchCtx.hasRights(ACI_WRITE_DELETE) &&
                    targAttrFilters.hasMask(TARGATTRFILTERS_DELETE)))
            {
              ret=targAttrFilters.isApplicableMod(matchCtx, aci);
            }
        }
        return ret;
    }

    /*
     * TODO Evaluate making this method more efficient.
     * The isTargetAttrApplicable method looks a lot less efficient than it
     * could be with regard to the logic that it employs and the repeated use
     * of method calls over local variables.
     */
    /**
     * Checks an provided ACI's targetattr rule against a target match
     * context.
     *
     * @param aci The ACI to evaluate.
     * @param targetMatchCtx The target match context to check the ACI against.
     * @return True if the targetattr matched the target context.
     */
    public static boolean isTargetAttrApplicable(Aci aci,
                                         AciTargetMatchContext targetMatchCtx) {
        boolean ret=true;
        if(!targetMatchCtx.getTargAttrFiltersMatch()) {
            TargetAttr targetAttr = aci.getTargets().getTargetAttr();
            AttributeType attrType = targetMatchCtx.getCurrentAttributeType();
            boolean isFirstAttr=targetMatchCtx.isFirstAttribute();

            if (attrType != null && targetAttr != null)  {
              ret=TargetAttr.isApplicable(attrType,targetAttr);
              setEvalAttributes(targetMatchCtx,targetAttr,ret);
            } else if (attrType != null || targetAttr != null) {
                if (aci.hasRights(skipRights)
                        && skipRightsHasRights(targetMatchCtx.getRights())) {
                    ret = true;
                } else {
                    ret = attrType == null
                        && targetAttr != null
                        && aci.hasRights(ACI_WRITE);
                }
            }
            if (isFirstAttr && targetAttr == null
                && aci.getTargets().getTargAttrFilters() == null)
            {
              targetMatchCtx.setEntryTestRule(true);
            }
        }
        return ret;
    }

    /**
     * Try and match a one or more of the specified rights in the skiprights
     * mask.
     * @param rights The rights to check for.
     * @return  True if the one or more of the specified rights are in the
     * skiprights rights mask.
     */
    public static boolean skipRightsHasRights(int rights) {
      //geteffectiverights sets this flag, turn it off before evaluating.
      int tmpRights=rights & ~ACI_SKIP_PROXY_CHECK;
      return (skipRights & tmpRights) == tmpRights;
    }


    /**
     * Wrapper class that passes an ACI, an ACI's targets and the specified
     * target match context's resource entry DN to the main isTargetApplicable
     * method.
     * @param aci The ACI currently be matched.
     * @param matchCtx The target match context to match against.
     * @return True if the target matched the ACI.
     */
    public static boolean isTargetApplicable(Aci aci,
                                             AciTargetMatchContext matchCtx) {
        return isTargetApplicable(aci, aci.getTargets(),
                                        matchCtx.getResourceEntry().getName());
    }

    /*
     * TODO Investigate supporting alternative representations of the scope.
     *
     * Should we also consider supporting alternate representations of the
     * scope values (in particular, allow "one" in addition to "onelevel"
     * and "sub" in addition to "subtree") to match the very common
     * abbreviations in widespread use for those terms?
     */
    /**
     * Main target isApplicable method. This method performs the target keyword
     * match functionality, which allows for directory entry "targeting" using
     * the specified ACI, ACI targets class and DN.
     *
     * @param aci The ACI to match the target against.
     * @param targets The targets to use in this evaluation.
     * @param entryDN The DN to use in this evaluation.
     * @return True if the ACI matched the target and DN.
     */
    public static boolean isTargetApplicable(Aci aci,
            AciTargets targets, DN entryDN) {
        DN targetDN=aci.getDN();
        /*
         * Scoping of the ACI uses either the DN of the entry
         * containing the ACI (aci.getDN above), or if the ACI item
         * contains a simple target DN and a equality operator, that
         * simple target DN is used as the target DN.
         */
        if(targets.getTarget() != null && !targets.getTarget().isPattern()) {
            EnumTargetOperator op=targets.getTarget().getOperator();
            if(op != EnumTargetOperator.NOT_EQUALITY)
            {
              targetDN=targets.getTarget().getDN();
            }
        }
        //Check if the scope is correct.
        switch(targets.getTargetScope().asEnum()) {
        case BASE_OBJECT:
            if(!targetDN.equals(entryDN))
            {
              return false;
            }
            break;
        case SINGLE_LEVEL:
            /*
             * We use the standard definition of single level to mean the
             * immediate children only -- not the target entry itself.
             * Sun CR 6535035 has been raised on DSEE:
             * Non-standard interpretation of onelevel in ACI targetScope.
             */
            if(!targetDN.equals(entryDN.parent()))
            {
              return false;
            }
            break;
        case WHOLE_SUBTREE:
            if(!entryDN.isDescendantOf(targetDN))
            {
              return false;
            }
            break;
        case SUBORDINATES:
            if (entryDN.size() <= targetDN.size() ||
                 !entryDN.isDescendantOf(targetDN)) {
              return false;
            }
            break;
        default:
            return false;
        }
        /*
         * The entry is in scope. For inequality checks, scope was tested
         * against the entry containing the ACI. If operator is inequality,
         * check that it doesn't match the target DN.
         */
        if(targets.getTarget() != null &&
                !targets.getTarget().isPattern()) {
            EnumTargetOperator op=targets.getTarget().getOperator();
            if(op == EnumTargetOperator.NOT_EQUALITY) {
                DN tmpDN=targets.getTarget().getDN();
                if(entryDN.isDescendantOf(tmpDN))
                {
                  return false;
                }
            }
        }
        /*
         * There is a pattern, need to match the substring filter
         * created when the ACI was decoded. If inequality flip the
         * result.
         */
        if(targets.getTarget() != null &&
                targets.getTarget().isPattern())  {
            final boolean ret = targets.getTarget().matchesPattern(entryDN);
            EnumTargetOperator op=targets.getTarget().getOperator();
            if(op == EnumTargetOperator.NOT_EQUALITY)
            {
              return !ret;
            }
            return ret;
        }
        return true;
    }


    /**
     * The method is used to try and determine if a targetAttr expression that
     * is applicable has a '*' (or '+' operational attributes) token or if it
     * was applicable because of a specific attribute type declared in the
     * targetattrs expression (i.e., targetattrs=cn).
     *
     *
     * @param ctx  The ctx to check against.
     * @param targetAttr The targetattrs part of the ACI.
     * @param ret  The is true if the ACI has already been evaluated to be
     *             applicable.
     */
    private static
    void setEvalAttributes(AciTargetMatchContext ctx, TargetAttr targetAttr,
                           boolean ret) {
        ctx.clearEvalAttributes(ACI_USER_ATTR_STAR_MATCHED);
        ctx.clearEvalAttributes(ACI_OP_ATTR_PLUS_MATCHED);
        /*
         If an applicable targetattr's match rule has not
         been seen (~ACI_FOUND_OP_ATTR_RULE or ~ACI_FOUND_USER_ATTR_RULE) and
         the current attribute type is applicable because of a targetattr all
         user (or operational) attributes rule match,
         set a flag to indicate this situation (ACI_USER_ATTR_STAR_MATCHED or
         ACI_OP_ATTR_PLUS_MATCHED). This check also catches the following case
         where the match was by a specific attribute type (either user or
         operational) and the other attribute type has an all attribute token.
         For example, the expression is: (targetattrs="cn || +) and the current
         attribute type is cn.
        */
        if(ret && targetAttr.isAllUserAttributes() &&
                !ctx.hasEvalUserAttributes())
        {
          ctx.setEvalUserAttributes(ACI_USER_ATTR_STAR_MATCHED);
        }
        else
        {
          ctx.setEvalUserAttributes(ACI_FOUND_USER_ATTR_RULE);
        }

        if(ret && targetAttr.isAllOpAttributes() &&
                !ctx.hasEvalOpAttributes())
        {
          ctx.setEvalOpAttributes(ACI_OP_ATTR_PLUS_MATCHED);
        }
        else
        {
          ctx.setEvalOpAttributes(ACI_FOUND_OP_ATTR_RULE);
        }
    }
}
