/*
 * 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-2009 Sun Microsystems, Inc.
 *      Portions Copyright 2012-2015 ForgeRock AS
 *      Portions Copyrighted 2019 OGIS-RI Co., Ltd.
 */
package org.opends.server.authorization.dseecompat;

import static org.opends.server.TestCaseUtils.*;
import static org.opends.server.config.ConfigConstants.*;
import static org.testng.Assert.*;
import org.testng.Assert;
import java.util.Map;

import org.opends.server.core.DirectoryServer;
import org.opends.server.types.AttributeType;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

@SuppressWarnings("javadoc")
public class TargetAttrTestCase extends AciTestCase {

  private static String attrList="sn uid l";
  private static String attrList1="sn uid";
  private static String opAttrList="sn uid aci";
  private static final String user1="uid=user.1,ou=People,o=test";
  private static final String user3="uid=user.3,ou=People,o=test";
  private static final String aciFilter = "(aci=*)";


  private static final
  String grpAttrAci = "(targetattr=\"*\")" +
        "(version 3.0; acl \"user attr URL example\"; " +
        "allow (search,read) " +
        "userattr=\"ldap:///ou=People,o=test?manager#GROUPDN\";)";


  private static final
  String grp1AttrAci = "(targetattr=\"*\")" +
        "(version 3.0; acl \"user attr1 URL example\"; " +
        "allow (search,read) " +
        "userattr=\"ldap:///ou=People1,o=test?manager#GROUPDN\";)";

  private static final
  String starAciAttrs = "(targetattr=\"* || aci\")" +
          "(version 3.0;acl \"read/search all user, aci op\";" +
          "allow (search, read) " +
          "userattr=\"l#Austin\";)";

  private static final
  String ocOpAttrs = "(targetattr=\"objectclass || +\")" +
          "(version 3.0;acl \"read/search all op, oc user\";" +
          "allow (search, read) " +
          "userattr=\"l#Austin\";)";

  private static final
  String OpSrchAttrs = "(targetattr=\"sn || uid || +\")" +
          "(version 3.0;acl \"read/search all op, sn uid user\";" +
          "allow (search, read) " +
          "userattr=\"l#Austin\";)";

  private static final
  String allAttrs = "(targetattr=\"* || +\")" +
          "(version 3.0;acl \"read/search all user and all op lattr\";" +
          "allow (search, read) " +
          "userattr=\"l#Austin\";)";

  private static final
  String allOpAttrAci1 = "(targetattr=\"+\")" +
          "(version 3.0;acl \"read/search all op attr\";" +
          "allow (search, read) " +
          "userattr!=\"l#New York\";)";

  private static final
  String notAllOpAttrAci1 = "(targetattr!=\"+\")" +
          "(version 3.0;acl \"read/search not all op attr\";" +
          "allow (search, read) " +
          "userattr!=\"l#New York\";)";

  private static final
  String userAttrAci = "(targetattr=\"*\")" +
          "(version 3.0;acl \"read/search all userattr\";" +
          "allow (search, read) " +
          "userattr=\"l#Austin\";)";

  private static final
  String userAttrAci1 = "(targetattr=\"*\")" +
          "(version 3.0;acl \"read/search userattr\";" +
          "allow (search, read) " +
          "userattr!=\"l#New York\";)";

  private static final
  String nonOpAttrAci = "(targetattr=\"*\")" +
          "(version 3.0;acl \"read/search non-operational attr\";" +
          "allow (search, read) " +
          "userattr=\"l#Austin\";)";

  private static final
  String opAttrAci = "(targetattr=\"aci\")" +
          "(version 3.0;acl \"read/search operational attr\";" +
          "allow (search, read) " +
          "userattr=\"l#Austin\";)";

  private static final
  String controlAci = "(targetcontrol=\"1.3.6.1.1.13.1\")" +
          "(version 3.0;acl \"use pre-read control\";" +
          "allow (read) " +
          "userdn=\"ldap:///anyone\";)";

  private static final String user3ForbiddenUserAttr = "sn";
  private static final String user3ForbiddenOperationalAttr = "createTimestamp";
  private static final String user3AllowedUserAttr = "uid";
  private static final String user3AllowedOperationalAttr = "ds-privilege-name";
  private static final String user3WritableAttr = "description";

    private static final
  String selfWriteAci = "(targetattr=\"" + user3WritableAttr + "\")" +
          "(version 3.0;acl \"self write description\";" +
          "allow (write) " +
          "userdn=\"ldap:///self\";)";

  private static final
  String selfDenyAttrReadAci = "(targetattr=\"" + user3ForbiddenUserAttr + "||" +
          user3ForbiddenOperationalAttr + "\")" +
          "(version 3.0;acl \"self deny attribute reads\";" +
          "deny (read,search) " +
          "userdn=\"ldap:///self\";)";

  private static final
  String selfReadAllAttrsAci = "(targetattr=\"*||+\")" +
          "(version 3.0; acl \"self read/search all attributes\";" +
          "allow (read,search) " +
          "userdn=\"ldap:///self\";)";

  @BeforeClass
  public void setupClass() throws Exception {
    deleteAttrFromAdminEntry(ACCESS_HANDLER_DN, ATTR_AUTHZ_GLOBAL_ACI);
    String aciLdif3 = makeAddLDIF(ATTR_AUTHZ_GLOBAL_ACI, ACCESS_HANDLER_DN, controlAci, selfWriteAci);
    LDIFModify(aciLdif3, DIR_MGR_DN, PWD);
    addEntries("o=test");
  }

  /**
   * Test targetattr behavior using userattr bind rule.
   *
   * @throws Exception  If a test result is unexpected.
   */
  @Test
  public void testTargetAttrUserAttr() throws Exception {
    String aciLdif=makeAddLDIF("aci", user1, userAttrAci);
    LDIFModify(aciLdif, DIR_MGR_DN, PWD);
    String userResults =
            LDAPSearchParams(user3, PWD, null, null, null,
                    user1, filter, attrList);
    Assert.assertNotEquals(userResults, "");
    Map<String, String> attrMap = getAttrMap(userResults);
    checkAttributeVal(attrMap, "l", "Austin");
    checkAttributeVal(attrMap, "sn", "1");
    checkAttributeVal(attrMap, "uid", "user.1");
    String userResults1 =
            LDAPSearchParams(user3, PWD, null, null, null,
                    user1, filter, attrList1);
    Assert.assertNotEquals(userResults1, "");
    Map<String, String> attrMap1 = getAttrMap(userResults1);
    checkAttributeVal(attrMap1, "sn", "1");
    checkAttributeVal(attrMap1, "uid", "user.1");
    deleteAttrFromEntry(user1, "aci");
    String aciLdif2=makeAddLDIF("aci", user1, userAttrAci1);
    LDIFModify(aciLdif2, DIR_MGR_DN, PWD);
    String userResults2 =
            LDAPSearchParams(user3, PWD, null, null, null,
                    user1, filter, attrList);
    Assert.assertNotEquals(userResults2, "");
    Map<String, String> attrMap2 = getAttrMap(userResults2);
    checkAttributeVal(attrMap2, "l", "Austin");
    checkAttributeVal(attrMap2, "sn", "1");
    checkAttributeVal(attrMap2, "uid", "user.1");
    deleteAttrFromEntry(user1, "aci");
  }

  /**
   * Test targetattr and operational attribute behavior. See comments.
   *
   * @throws Exception If a test result is unexpected.
   */
  @Test
  public void testTargetAttrOpAttr() throws Exception {
    //Add aci that only allows non-operational attributes search/read.
    String aciLdif=makeAddLDIF("aci", user1, nonOpAttrAci);
    LDIFModify(aciLdif, DIR_MGR_DN, PWD);
    String userResults =
            LDAPSearchParams(user3, PWD, null, null, null,
                    user1, filter, opAttrList);
    Assert.assertNotEquals(userResults, "");
    Map<String, String> attrMap = getAttrMap(userResults);
    //The aci attribute type is operational, it should not be there.
    //The other two should be there.
    assertFalse(attrMap.containsKey("aci"));
    assertTrue(attrMap.containsKey("sn"));
    assertTrue(attrMap.containsKey("uid"));
    deleteAttrFromEntry(user1, "aci");
    //Add aci that allows both non-operational attributes and the operational
    //attribute "aci" search/read.
    String aciLdif1=makeAddLDIF("aci", user1, nonOpAttrAci, opAttrAci);
    LDIFModify(aciLdif1, DIR_MGR_DN, PWD);
    String userResults1 =
            LDAPSearchParams(user3, PWD, null, null, null,
                    user1, filter, opAttrList);
    Assert.assertNotEquals(userResults1, "");
    Map<String, String> attrMap1 = getAttrMap(userResults1);
    //All three attributes should be there.
    assertTrue(attrMap1.containsKey("aci"));
    assertTrue(attrMap1.containsKey("sn"));
    assertTrue(attrMap1.containsKey("uid"));
    deleteAttrFromEntry(user1, "aci");
    //Add ACI that only allows only aci operational attribute search/read.
    String aciLdif2=makeAddLDIF("aci", user1, opAttrAci);
    LDIFModify(aciLdif2, DIR_MGR_DN, PWD);
    String userResults2 =
            LDAPSearchParams(user3, PWD, null, null, null,
                    user1, aciFilter, opAttrList);
    Assert.assertNotEquals(userResults2, "");
    Map<String, String> attrMap2 = getAttrMap(userResults2);
    // Only operational attribute aci should be there, the other two should not.
    assertTrue(attrMap2.containsKey("aci"));
    assertFalse(attrMap2.containsKey("sn"));
    assertFalse(attrMap2.containsKey("uid"));
    deleteAttrFromEntry(user1, "aci");
  }

  /**
   * Test targetattr behaviour with modify and pre-read controls
   *
   * @throws Exception  If a test result is unexpected.
   */
  @Test
  public void testTargetAttrPreRead() throws Exception {
    String aciLdif=makeAddLDIF("aci", user3,
            controlAci, selfWriteAci, selfDenyAttrReadAci, selfReadAllAttrsAci);
    LDIFModify(aciLdif, DIR_MGR_DN, PWD);

    // sanity check that search does not return the forbidden attributes
    String searchResults =
            LDAPSearchParams(user3, PWD, null, null, null, user3, filter, "+ *");
    Assert.assertNotEquals(searchResults, "");
    Map<String, String> attrMap = getAttrMap(searchResults);
    assertFalse(attrMap.containsKey(user3ForbiddenUserAttr));
    assertFalse(attrMap.containsKey(user3ForbiddenOperationalAttr));
    assertTrue(attrMap.containsKey(user3AllowedUserAttr));

    // check we can't pre-read the forbidden user attribute
    String modifyLdif1 = makeAddLDIF(user3WritableAttr, user3, "don't care 1");
    String modifyResults1 = preReadModify(user3, PWD, modifyLdif1, user3ForbiddenUserAttr);
    Assert.assertNotEquals(modifyResults1, "");
    Map<String, String> modifyMap1 = getAttrMap(modifyResults1, true);
    assertFalse(modifyMap1.containsKey(user3ForbiddenUserAttr));

    // check we can't pre-read the forbidden operational attribute
    String modifyLdif2 = makeAddLDIF(user3WritableAttr, user3, "don't care 2");
    String modifyResults2 = preReadModify(user3, PWD, modifyLdif2, user3ForbiddenOperationalAttr);
    Assert.assertNotEquals(modifyResults2, "");
    Map<String, String> modifyMap2 = getAttrMap(modifyResults2, true);
    assertFalse(modifyMap2.containsKey(user3ForbiddenOperationalAttr));

    // check we can pre-read the allowed user attribute
    String modifyLdif3 = makeAddLDIF(user3WritableAttr, user3, "don't care 3");
    String modifyResults3 = preReadModify(user3, PWD, modifyLdif3, user3AllowedUserAttr);
    Assert.assertNotEquals(modifyResults3, "");
    Map<String, String> modifyMap3 = getAttrMap(modifyResults3, true);
    assertTrue(modifyMap3.containsKey(user3AllowedUserAttr));

    // check we can pre-read the allowed operational attribute
    String modifyLdif4 = makeAddLDIF(user3WritableAttr, user3, "don't care 4");
    String modifyResults4 = preReadModify(user3, PWD, modifyLdif4, user3AllowedOperationalAttr);
    Assert.assertNotEquals(modifyResults4, "");
    Map<String, String> modifyMap4 = getAttrMap(modifyResults4, true);
    assertTrue(modifyMap4.containsKey(user3AllowedOperationalAttr));

    deleteAttrFromEntry(user3, "aci");
  }

  /**
   * Test targetattr shorthand behavior, all attrs both user and operational.
   * See comments.
   *
   * @throws Exception  If a test result is unexpected.
   */
  @Test
  public void testTargetAttrAllAttr() throws Exception {
    //Add aci with: (targetattr = "+ || *")
    String aciLdif=makeAddLDIF("aci", user1, allAttrs);
    LDIFModify(aciLdif, DIR_MGR_DN, PWD);
    String userResults =
            LDAPSearchParams(user3, PWD, null, null, null,
                    user1, filter, opAttrList);
    Assert.assertNotEquals(userResults, "");
    Map<String, String> attrMap = getAttrMap(userResults);
    //All should be returned.
    assertTrue(attrMap.containsKey("aci"));
    assertTrue(attrMap.containsKey("sn"));
    assertTrue(attrMap.containsKey("uid"));
    deleteAttrFromEntry(user1, "aci");
  }


  /**
   * Test targetattr shorthand behavior, userattr and plus sign (all op attrs).
   * See comments.
   *
   * @throws Exception If a test result is unexpected.
   */
  @Test
  public void testTargetAttrOpPlusAttr() throws Exception {
    //Add aci with: (targetattr = "objectclass|| +")
    String aciLdif=makeAddLDIF("aci", user1, ocOpAttrs);
    LDIFModify(aciLdif, DIR_MGR_DN, PWD);
    String userResults =
            LDAPSearchParams(user3, PWD, null, null, null,
                    user1, filter, opAttrList);
    Assert.assertNotEquals(userResults, "");
    Map<String, String> attrMap = getAttrMap(userResults);
    //Only aci should be returned.
    assertTrue(attrMap.containsKey("aci"));
    assertFalse(attrMap.containsKey("sn"));
    assertFalse(attrMap.containsKey("uid"));
    deleteAttrFromEntry(user1, "aci");
  }


  /**
   * Test targetattr shorthand behavior, star (all user attr) or aci attr.
   * See comments.
   *
   * @throws Exception  If a test result is unexpected.
   */
  @Test
  public void testTargetAttrUserStarAttr() throws Exception {
    //Add aci with: (targetattr = "*|| aci")
    String aciLdif=makeAddLDIF("aci", user1, starAciAttrs);
    LDIFModify(aciLdif, DIR_MGR_DN, PWD);
    String userResults =
            LDAPSearchParams(user3, PWD, null, null, null,
                    user1, filter, opAttrList);
    Assert.assertNotEquals(userResults, "");
    Map<String, String> attrMap = getAttrMap(userResults);
    //All should be returned.
    assertTrue(attrMap.containsKey("aci"));
    assertTrue(attrMap.containsKey("sn"));
    assertTrue(attrMap.containsKey("uid"));
    deleteAttrFromEntry(user1, "aci");
  }

  /**
   * Test targetattr shorthand behavior using '+' in expression and an
   * operational attribute in the filter. The second test is two ACIs one
   * with targetattr='+' and the other with targetattr='*'.
   *
   * @throws Exception If test result is unexpected.
   */
  @Test
  public void testTargetAttrSrchShorthand() throws Exception {
    //Aci: (targetattrs="sn || uid || +) and search with an
    //operational attr (aci).
    String aciLdif=makeAddLDIF("aci", user1, OpSrchAttrs);
    LDIFModify(aciLdif, DIR_MGR_DN, PWD);
    String userResults =
            LDAPSearchParams(user3, PWD, null, null, null,
                    user1, aciFilter, opAttrList);
    Assert.assertNotEquals(userResults, "");
    Map<String, String> attrMap = getAttrMap(userResults);
    //All should be returned.
    assertTrue(attrMap.containsKey("aci"));
    assertTrue(attrMap.containsKey("sn"));
    assertTrue(attrMap.containsKey("uid"));
    deleteAttrFromEntry(user1, "aci");
    //Add two ACIs, one with '+' and the other with '*'.
    String aciLdif1=makeAddLDIF("aci", user1, allOpAttrAci1, userAttrAci);
    LDIFModify(aciLdif1, DIR_MGR_DN, PWD);
    String userResults1 =
            LDAPSearchParams(user3, PWD, null, null, null,
                    user1, aciFilter, opAttrList);
    Assert.assertNotEquals(userResults1, "");
    Map<String, String> attrMap1 = getAttrMap(userResults1);
    //All should be returned.
    assertTrue(attrMap1.containsKey("aci"));
    assertTrue(attrMap1.containsKey("sn"));
    assertTrue(attrMap1.containsKey("uid"));
    deleteAttrFromEntry(user1, "aci");
        //Add two ACIs, one with '+' and the other with '*'.
    String aciLdif2=makeAddLDIF("aci", user1, notAllOpAttrAci1, userAttrAci);
    LDIFModify(aciLdif2, DIR_MGR_DN, PWD);
    String userResults2 =
            LDAPSearchParams(user3, PWD, null, null, null,
                    user1, filter, opAttrList);
    Assert.assertNotEquals(userResults2, "");
    Map<String, String> attrMap2 = getAttrMap(userResults2);
    //Only non-operation should be returned.
    assertFalse(attrMap2.containsKey("aci"));
    assertTrue(attrMap2.containsKey("sn"));
    assertTrue(attrMap2.containsKey("uid"));
    deleteAttrFromEntry(user1, "aci");
  }

  /**
   * Test two scenarios with userattr LDAP URL and groupdn keyword.
   *
   * @throws Exception Exception If test result is unexpected.
   */
  @Test
  public void testTargetAttrGrpDN() throws Exception {
    String aciLdif=makeAddLDIF("aci", user1, grpAttrAci);
    LDIFModify(aciLdif, DIR_MGR_DN, PWD);
    String userResults =
            LDAPSearchParams(user3, PWD, null, null, null,
                    user1, filter, attrList);
    Assert.assertNotEquals(userResults, "");
    Map<String, String> attrMap = getAttrMap(userResults);
    assertTrue(attrMap.containsKey("l"));
    assertTrue(attrMap.containsKey("sn"));
    assertTrue(attrMap.containsKey("uid"));
    deleteAttrFromEntry(user1, "aci");
    String aciLdif1=makeAddLDIF("aci", user1, grp1AttrAci);
    LDIFModify(aciLdif1, DIR_MGR_DN, PWD);
    String userResults1 =
            LDAPSearchParams(user3, PWD, null, null, null,
                    user1, filter, attrList);
    //This search should return nothing since the URL has a bogus DN.
    assertEquals("", userResults1);
  }

  private void
  checkAttributeVal(Map<String, String> attrMap, String attr,
                      String val) throws Exception {
    String mapVal=attrMap.get(attr);
    assertEquals(mapVal, val);
  }

  /** New tests to really unit test the isApplicable method. */
  @DataProvider(name = "targetAttrData")
  public Object[][] createData() throws Exception {
    return new Object[][] {
        /*
         * 4 elements:
         *  Operator ( = or !=),
         *  TartgetAttr Attributes list,
         *  Attribute to eval,
         *  Expected result
         */
        { "=", "cn", "cn", true },
        { "=", "cn || sn", "cn", true },
        { "=", "cn || sn", "sn", true },
        { "=", "cn", "sn", false },
        { "=", "*", "cn", true },
        { "=", "*", "modifytimestamp", false },
        { "=", "+", "modifytimestamp", true },
        { "=", "+", "cn", false },
        { "=", "* || +", "cn", true }, // Always true
        { "=", "* || +", "modifytimestamp", true }, // Always true
        { "=", "+ || *", "foo", true }, // Always true
        { "=", "* || +", "foo", true }, // Always true
        { "!=", "cn", "cn", false },
        { "!=", "cn || sn", "cn", false },
        { "!=", "cn || sn", "sn", false },
        { "!=", "cn", "sn", true }, // Not eq user attr
        { "!=", "cn || sn", "description", true }, // Not eq user attr
        { "!=", "cn || sn", "modifytimestamp", false }, // Not eq op attr
        { "!=", "aci", "cn", false },
        { "!=", "aci", "modifytimestamp", true },
    };
  }

  @Test(dataProvider = "targetAttrData")
  public void testTargetAttrStrings(String eqOperator, String targetAttrString,
    String attribute, boolean expectedResult) throws Exception
  {
    EnumTargetOperator op = EnumTargetOperator.createOperator(eqOperator);
    TargetAttr targetAttr = TargetAttr.decode(op, targetAttrString);
    AttributeType attrType = DirectoryServer.getAttributeTypeOrDefault(attribute);
    assertEquals(TargetAttr.isApplicable(attrType, targetAttr), expectedResult);
  }
}
