/*
 * 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 Sun Microsystems, Inc.
 *      Portions Copyright 2013-2015 ForgeRock AS
 */
package org.opends.server.authorization.dseecompat;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.SearchScope;
import org.opends.server.core.DirectoryServer;
import org.opends.server.types.*;

import static org.opends.messages.AccessControlMessages.*;

/**
 * This class represents the userdn keyword in a bind rule.
 */
public class UserDN implements KeywordBindRule {

    /**
     * A dummy URL for invalid URLs such as: all, parent, anyone, self.
     */
    private static String urlStr="ldap:///";

    /**
     * This list holds a list of objects representing a EnumUserDNType
     * URL mapping.
     */
    private List<UserDNTypeURL> urlList;

    /** Enumeration of the userdn operation type. */
    private EnumBindRuleType type;

    /**
     * Constructor that creates the userdn class. It also sets up an attribute
     * type ("userdn") needed  for wild-card matching.
     * @param type The type of  operation.
     * @param urlList  A list of enumerations containing the URL type and URL
     * object that can be retrieved at evaluation time.
     */
    private UserDN(EnumBindRuleType type, List<UserDNTypeURL> urlList) {
       this.type=type;
       this.urlList=urlList;
    }

    /**
     * Decodes an expression string representing a userdn bind rule.
     * @param expression The string representation of the userdn bind rule
     * expression.
     * @param type An enumeration of the type of the bind rule.
     * @return A KeywordBindRule class that represents the bind rule.
     * @throws AciException If the expression failed to LDAP URL decode.
     */
    public static KeywordBindRule decode(String expression,
            EnumBindRuleType type) throws AciException {

        String[] vals=expression.split("[|][|]");
        List<UserDNTypeURL> urlList = new LinkedList<>();
        for (String val : vals)
        {
            StringBuilder value = new StringBuilder(val.trim());
           /*
            * TODO Evaluate using a wild-card in the dn portion of LDAP url.
            * The current implementation (DS6) does not treat a "*"
            * as a wild-card.
            *
            * Is it allowed to have a full LDAP URL (i.e., including a base,
            * scope, and filter) in which the base DN contains asterisks to
            * make it a wildcard?  If so, then I don't think that the current
            * implementation handles that correctly.  It will probably fail
            * when attempting to create the LDAP URL because the base DN isn't a
            * valid DN.
            */
            EnumUserDNType userDNType = UserDN.getType(value);
            LDAPURL url;
            try {
               url=LDAPURL.decode(value.toString(), true);
            } catch (DirectoryException de) {
                LocalizableMessage message = WARN_ACI_SYNTAX_INVALID_USERDN_URL.get(
                    de.getMessageObject());
                throw new AciException(message);
            }
            UserDNTypeURL dnTypeURL=new UserDNTypeURL(userDNType, url);
            urlList.add(dnTypeURL);
        }
        return new UserDN(type, urlList);
      }

    /**
     * This method determines the type of the DN (suffix in URL terms)
     * part of a URL, by examining the full URL itself for known strings
     * such as (corresponding type shown in parenthesis)
     *
     *      "ldap:///anyone"    (EnumUserDNType.ANYONE)
     *      "ldap:///parent"    (EnumUserDNType.PARENT)
     *      "ldap:///all"       (EnumUserDNType.ALL)
     *      "ldap:///self"      (EnumUserDNType.SELF)
     *
     * If one of the four above are found, the URL is replaced with a dummy
     * pattern "ldap:///". This is done because the above four are invalid
     * URLs; but the syntax is valid for an userdn keyword expression. The
     * dummy URLs are never used.
     *
     * If none of the above are found, it determine if the URL DN is a
     * substring pattern, such as:
     *
     *      "ldap:///uid=*, dc=example, dc=com" (EnumUserDNType.PATTERN)
     *
     * If none of the above are determined, it checks if the URL
     * is a complete URL with scope and filter defined:
     *
     *  "ldap:///uid=test,dc=example,dc=com??sub?(cn=j*)"  (EnumUserDNType.URL)
     *
     * If none of these those types can be identified, it defaults to
     * EnumUserDNType.DN.
     *
     * @param bldr A string representation of the URL that can be modified.
     * @return  The user DN type of the URL.
     */
    private static EnumUserDNType getType(StringBuilder bldr) {
        EnumUserDNType type;
        String str=bldr.toString();

        if (str.contains("?")) {
            type = EnumUserDNType.URL;
        } else  if(str.equalsIgnoreCase("ldap:///self")) {
            type = EnumUserDNType.SELF;
            bldr.replace(0, bldr.length(), urlStr);
        } else if(str.equalsIgnoreCase("ldap:///anyone")) {
            type = EnumUserDNType.ANYONE;
            bldr.replace(0, bldr.length(), urlStr);
        } else if(str.equalsIgnoreCase("ldap:///parent")) {
            type = EnumUserDNType.PARENT;
            bldr.replace(0, bldr.length(), urlStr);
        } else if(str.equalsIgnoreCase("ldap:///all")) {
            type = EnumUserDNType.ALL;
            bldr.replace(0, bldr.length(), urlStr);
        } else if (str.contains("*")) {
            type = EnumUserDNType.DNPATTERN;
        } else {
            type = EnumUserDNType.DN;
        }
        return type;
    }

    /**
     * Performs the evaluation of a userdn bind rule based on the
     * evaluation context passed to it. The evaluation stops when there
     * are no more UserDNTypeURLs to evaluate or if an UserDNTypeURL
     * evaluates to true.
     * @param evalCtx The evaluation context to evaluate with.
     * @return  An evaluation result enumeration containing the result
     * of the evaluation.
     */
    @Override
    public EnumEvalResult evaluate(AciEvalContext evalCtx) {
        EnumEvalResult matched = EnumEvalResult.FALSE;
        boolean undefined=false;

        boolean isAnonUser=evalCtx.isAnonymousUser();
        Iterator<UserDNTypeURL> it=urlList.iterator();
        for(; it.hasNext() && matched != EnumEvalResult.TRUE &&
                matched != EnumEvalResult.ERR;) {
            UserDNTypeURL dnTypeURL=it.next();
            //Handle anonymous checks here
            if(isAnonUser) {
                if(dnTypeURL.getUserDNType() == EnumUserDNType.ANYONE)
                {
                  matched = EnumEvalResult.TRUE;
                }
            }
            else
            {
              matched=evalNonAnonymous(evalCtx, dnTypeURL);
            }
        }
        return matched.getRet(type, undefined);
    }

    /**
     * Performs an evaluation of a single UserDNTypeURL of a userdn bind
     * rule using the evaluation context provided. This method is called
     * for the non-anonymous user case.
     * @param evalCtx  The evaluation context to evaluate with.
     * @param dnTypeURL The URL dn type mapping to evaluate.
     * @return An evaluation result enumeration containing the result
     * of the evaluation.
     */
    private EnumEvalResult evalNonAnonymous(AciEvalContext evalCtx,
                                            UserDNTypeURL dnTypeURL) {
        DN clientDN=evalCtx.getClientDN();
        DN resDN=evalCtx.getResourceDN();
        EnumEvalResult matched = EnumEvalResult.FALSE;
        EnumUserDNType type=dnTypeURL.getUserDNType();
        LDAPURL url=dnTypeURL.getURL();
        switch (type) {
            case URL:
            {
                matched = evalURL(evalCtx, url);
                break;
            }
            case ANYONE:
            {
                matched = EnumEvalResult.TRUE;
                break;
            }
            case SELF:
            {
                if (clientDN.equals(resDN))
                {
                  matched = EnumEvalResult.TRUE;
                }
                break;
            }
            case PARENT:
            {
                DN parentDN = resDN.parent();
                if (parentDN != null && parentDN.equals(clientDN))
                {
                  matched = EnumEvalResult.TRUE;
                }
                break;
            }
            case ALL:
            {
                matched = EnumEvalResult.TRUE;
                break;
            }
            case DNPATTERN:
            {
                matched = evalDNPattern(evalCtx, url);
                break;
            }
            case DN:
            {
                try
                {
                    DN dn = url.getBaseDN();
                    if (clientDN.equals(dn))
                    {
                      matched = EnumEvalResult.TRUE;
                    }
                    else {
                        //This code handles the case where a root dn entry does
                        //not have bypass-acl privilege and the ACI bind rule
                        //userdn DN possible is an alternate root DN.
                        DN actualDN=DirectoryServer.getActualRootBindDN(dn);
                        DN clientActualDN=
                                DirectoryServer.getActualRootBindDN(clientDN);
                        if(actualDN != null)
                        {
                          dn=actualDN;
                        }
                        if(clientActualDN != null)
                        {
                          clientDN=clientActualDN;
                        }
                        if(clientDN.equals(dn))
                        {
                          matched=EnumEvalResult.TRUE;
                        }
                    }
                } catch (DirectoryException ex) {
                    //TODO add message
                }
            }
        }
        return matched;
    }

    /**
     * This method evaluates a DN pattern userdn expression.
     * @param evalCtx  The evaluation context to use.
     * @param url The LDAP URL containing the pattern.
     * @return An enumeration evaluation result.
     */
    private EnumEvalResult evalDNPattern(AciEvalContext evalCtx, LDAPURL url) {
        PatternDN pattern;
        try {
          pattern = PatternDN.decode(url.getRawBaseDN());
        } catch (DirectoryException ex) {
          return EnumEvalResult.FALSE;
        }

        return pattern.matchesDN(evalCtx.getClientDN()) ?
             EnumEvalResult.TRUE : EnumEvalResult.FALSE;
    }


    /**
     * This method evaluates an URL userdn expression. Something like:
     * ldap:///suffix??sub?(filter). It also searches for the client DN
     * entry and saves it in the evaluation context for repeat evaluations
     * that might come later in processing.
     *
     * @param evalCtx  The evaluation context to use.
     * @param url URL containing the URL to use in the evaluation.
     * @return An enumeration of the evaluation result.
     */
    public static EnumEvalResult evalURL(AciEvalContext evalCtx, LDAPURL url) {
        EnumEvalResult ret=EnumEvalResult.FALSE;
        DN urlDN;
        SearchFilter filter;
        try {
            urlDN=url.getBaseDN();
            filter=url.getFilter();
        } catch (DirectoryException ex) {
            return EnumEvalResult.FALSE;
        }
        SearchScope scope=url.getScope();
        if(scope == SearchScope.WHOLE_SUBTREE) {
            if(!evalCtx.getClientDN().isDescendantOf(urlDN))
            {
              return EnumEvalResult.FALSE;
            }
        } else if(scope == SearchScope.SINGLE_LEVEL) {
            DN parent=evalCtx.getClientDN().parent();
            if(parent != null && !parent.equals(urlDN))
            {
              return EnumEvalResult.FALSE;
            }
        } else if(scope == SearchScope.SUBORDINATES) {
            DN userDN = evalCtx.getClientDN();
            if (userDN.size() <= urlDN.size() ||
                 !userDN.isDescendantOf(urlDN)) {
              return EnumEvalResult.FALSE;
            }
        } else {
            if(!evalCtx.getClientDN().equals(urlDN))
            {
              return EnumEvalResult.FALSE;
            }
        }
        try {
            if(filter.matchesEntry(evalCtx.getClientEntry()))
            {
              ret=EnumEvalResult.TRUE;
            }
        } catch (DirectoryException ex) {
            return EnumEvalResult.FALSE;
        }
        return ret;
    }

    /*
     * TODO Evaluate making this method more efficient.
     *
     * The evalDNEntryAttr method isn't as efficient as it could be.
     * It would probably be faster to to convert the clientDN to a ByteString
     * and see if the entry has that value than to decode each value as a DN
     * and see if it matches the clientDN.
     */
    /**
     * This method searches an entry for an attribute value that is
     * treated as a DN. That DN is then compared against the client
     * DN.
     * @param e The entry to get the attribute type from.
     * @param clientDN The client authorization DN to check for.
     * @param attrType The attribute type from the bind rule.
     * @return An enumeration with the result.
     */
    public static EnumEvalResult evaluate(Entry e, DN clientDN,
                                           AttributeType attrType) {
        EnumEvalResult matched= EnumEvalResult.FALSE;
        List<Attribute> attrs =  e.getAttribute(attrType);
        for(ByteString v : attrs.get(0)) {
            try {
                DN dn = DN.valueOf(v.toString());
                if(dn.equals(clientDN)) {
                    matched=EnumEvalResult.TRUE;
                    break;
                }
            } catch (DirectoryException ex) {
                break;
            }
        }
        return matched;
    }

    /** {@inheritDoc} */
    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        toString(sb);
        return sb.toString();
    }

    /** {@inheritDoc} */
    @Override
    public final void toString(StringBuilder buffer) {
        buffer.append("userdn");
        buffer.append(this.type.getType());
        for (UserDNTypeURL url : this.urlList) {
            buffer.append("\"");
            buffer.append(urlStr);
            buffer.append(url.getUserDNType().toString().toLowerCase());
            buffer.append("\"");
        }
    }

}