/*
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  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 Sun Microsystems, Inc.
 */

package org.opends.server.authorization.dseecompat;
import org.opends.messages.Message;

import static org.opends.messages.AccessControlMessages.*;
import static org.opends.server.authorization.dseecompat.Aci.*;
import org.opends.server.types.AttributeType;
import org.opends.server.types.DN;
import org.opends.server.types.SearchScope;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 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 = null ;

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

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

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

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

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

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

    /*
     * 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 patern 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) {
                Message 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) {
                Message 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) {
                Message 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
                {
                  Message 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
              {
                Message 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
              {
                Message 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 {
                  Message 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) {
                    Message 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 {
                  Message 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) {
                      Message message =
                              WARN_ACI_SYNTAX_INVALID_TARGET_NOT_OPERATOR.
                                      get(operator, targetKeyword.name());
                      throw new AciException(message);
                    }
                    targAttrFilters = TargAttrFilters.decode(targetOperator,
                            expression);
                }
                else {
                  Message 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.SUBORDINATE_SUBTREE;
        else {
            Message 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) {
        boolean ret=true;
        TargetFilter targetFilter=aci.getTargets().getTargetFilter();
        if(targetFilter != null)
             ret=targetFilter.isApplicable(matchCtx);
        return ret;
    }

    /**
     * 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) {
      boolean ret=false;
      TargetControl targetControl=aci.getTargets().getTargetControl();
      if(targetControl != null)
        ret=targetControl.isApplicable(matchCtx);
      return ret;
    }

    /**
     * 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) {
      boolean ret=false;
      ExtOp extOp=aci.getTargets().getExtOp();
      if(extOp != null)
        ret=extOp.isApplicable(matchCtx);
      return ret;
    }


    /**
     * 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()) {
            AciTargets targets=aci.getTargets();
            AttributeType a=targetMatchCtx.getCurrentAttributeType();
            int rights=targetMatchCtx.getRights();
            boolean isFirstAttr=targetMatchCtx.isFirstAttribute();
            if((a != null) && (targets.getTargetAttr() != null))  {
              ret=TargetAttr.isApplicable(a,targets.getTargetAttr());
              setEvalAttributes(targetMatchCtx,targets,ret);
            } else if((a != null) || (targets.getTargetAttr() != null)) {
                if((aci.hasRights(skipRights)) &&
                                                (skipRightsHasRights(rights)))
                    ret=true;
                else if ((targets.getTargetAttr() != null) &&
                        (a == null) && (aci.hasRights(ACI_WRITE)))
                    ret = true;
                else
                    ret = false;
            }
            if((isFirstAttr) && (aci.getTargets().getTargetAttr() == 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().getDN());
    }

    /*
     * 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 specifed 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) {
        boolean ret=true;
        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()) {
        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(!entryDN.getParent().equals(targetDN))
                return false;
            break;
        case WHOLE_SUBTREE:
            if(!entryDN.isDescendantOf(targetDN))
                return false;
            break;
        case SUBORDINATE_SUBTREE:
            if ((entryDN.getNumComponents() <= targetDN.getNumComponents()) ||
                 !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()))  {
            ret=targets.getTarget().matchesPattern(entryDN);
            EnumTargetOperator op=targets.getTarget().getOperator();
            if(op == EnumTargetOperator.NOT_EQUALITY)
                ret=!ret;
        }
        return ret;
    }


    /**
     * 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 targets The targets 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, AciTargets targets,
                           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 && targets.getTargetAttr().isAllUserAttributes() &&
                !ctx.hasEvalUserAttributes())
          ctx.setEvalUserAttributes(ACI_USER_ATTR_STAR_MATCHED);
        else
          ctx.setEvalUserAttributes(ACI_FOUND_USER_ATTR_RULE);
        if(ret && targets.getTargetAttr().isAllOpAttributes() &&
                !ctx.hasEvalOpAttributes())
          ctx.setEvalOpAttributes(ACI_OP_ATTR_PLUS_MATCHED);
        else
          ctx.setEvalOpAttributes(ACI_FOUND_OP_ATTR_RULE);
    }
}
