mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valery Kharseko
11 hours ago 26defc28efac7cc706352bea7126a19308065468
[#1021] Reset the objectClass attribute of an entry when its object classes change (#1022)
4 files modified
252 ■■■■■ changed files
opendj-server-legacy/src/main/java/org/opends/server/types/Entry.java 12 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/PluggableBackendImplTestCase.java 70 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/ModifyConflictTest.java 73 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/types/TestEntry.java 97 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/types/Entry.java
@@ -120,6 +120,13 @@
  /** The set of objectclasses for this entry. */
  private Map<ObjectClass,String> objectClasses;
  /**
   * The set of objectclasses of this entry in its attribute form, built on demand by
   * {@link #getObjectClassAttribute()}. Every change of {@link #objectClasses} must reset it:
   * a backend stores the objectclasses themselves but indexes the entry through this attribute,
   * so an attribute left behind by a change takes the objectClass index out of step with the
   * stored entry, without anything failing.
   */
  private Attribute objectClassAttribute;
  /** The DN for this entry. */
@@ -295,6 +302,7 @@
    }
    objectClasses.put(oc, oc.getNameOrOID());
    objectClassAttribute = null;
  }
@@ -1268,6 +1276,7 @@
    if (attributeType.isObjectClass())
    {
      objectClasses.clear();
      objectClassAttribute = null;
      return true;
    }
    return userAttributes.remove(attributeType) != null
@@ -1320,6 +1329,7 @@
    if (attribute.isEmpty())
    {
      objectClasses.clear();
      objectClassAttribute = null;
      return true;
    }
@@ -1338,6 +1348,7 @@
        if (oc.hasNameOrOID(ocName))
        {
          objectClasses.remove(oc);
          objectClassAttribute = null;
          matchFound = true;
          break;
        }
@@ -4364,6 +4375,7 @@
    AttributeType attrType = attribute.getAttributeDescription().getAttributeType();
    // We will not do any validation of the object classes - this is
    // left to the caller.
    objectClassAttribute = null;
    if (replace)
    {
      objectClasses.clear();
opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/PluggableBackendImplTestCase.java
@@ -68,6 +68,7 @@
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.protocols.internal.SearchRequest;
import org.opends.server.types.Attribute;
import org.opends.server.types.BackupConfig;
import org.opends.server.types.BackupDirectory;
import org.opends.server.types.DirectoryException;
@@ -104,6 +105,7 @@
  private Map<String, IndexType[]> backendIndexes = new HashMap<>();
  {
    backendIndexes.put("objectClass", new IndexType[] { IndexType.EQUALITY });
    backendIndexes.put("entryUUID", new IndexType[] { IndexType.EQUALITY });
    backendIndexes.put("cn", new IndexType[] { IndexType.SUBSTRING });
    backendIndexes.put("sn", new IndexType[] { IndexType.PRESENCE, IndexType.EQUALITY, IndexType.SUBSTRING });
@@ -751,6 +753,74 @@
    assertThat((Object) returnedEntries.get(0).getName()).isEqualTo(newEntry.getName());
  }
  /**
   * A modification of the object classes of an entry must reach the objectClass index even when
   * the objectClass attribute of the modified entry has already been read - which the conflict
   * resolution of a replayed modification does with every delete of an object class value, before
   * the core applies the modifications to the entry.
   */
  @Test
  public void testModifyObjectClassOfAnAlreadyReadEntry() throws Exception
  {
    final AttributeType objectClassType = CoreSchema.getObjectClassAttributeType();
    final Entry oldEntry = TestCaseUtils.makeEntry(
        "dn: uid=user.13,ou=People," + testBaseDN,
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "givenName: Abbey",
        "sn: Abbie",
        "cn: Abbey Abbie",
        "uid: user.13");
    backend.addEntry(oldEntry, mock(AddOperation.class));
    try
    {
      final Entry newEntry = oldEntry.duplicate(false);
      // The read of the entry done by the conflict resolution of a replayed modification.
      newEntry.hasValue(AttributeDescription.create(objectClassType), ByteString.valueOfUtf8("person"));
      final Attribute addedClass = create(objectClassType, "extensibleObject");
      final Attribute deletedClass = create(objectClassType, "organizationalPerson");
      // The core applies the modifications with addAttribute() and removeAttribute().
      newEntry.addAttribute(addedClass, new LinkedList<ByteString>());
      newEntry.removeAttribute(deletedClass, new LinkedList<ByteString>());
      ModifyOperation modifyOp = mock(ModifyOperation.class);
      when(modifyOp.getModifications()).thenReturn(
          Arrays.asList(new Modification(ADD, addedClass), new Modification(DELETE, deletedClass)));
      backend.replaceEntry(oldEntry, newEntry, modifyOp);
      // The added object class must be searchable through the index,
      final List<Entry> returnedEntries = new ArrayList<>();
      backend.search(createSearchOperation(
          testBaseDN, SearchScope.WHOLE_SUBTREE, "(objectClass=extensibleObject)", returnedEntries));
      assertThat(returnedEntries).hasSize(1);
      assertThat((Object) returnedEntries.get(0).getName()).isEqualTo(newEntry.getName());
      // and the deleted one must be gone from it.
      assertThat(verifyObjectClassIndex()).isEqualTo(0);
    }
    finally
    {
      backend.deleteEntry(oldEntry.getName(), mock(DeleteOperation.class));
    }
  }
  /** Returns the number of both the missing and the stale records of the objectClass index. */
  private long verifyObjectClassIndex() throws Exception
  {
    VerifyConfig completeness = new VerifyConfig();
    completeness.setBaseDN(testBaseDN);
    completeness.addCompleteIndex("objectClass");
    VerifyConfig cleanliness = new VerifyConfig();
    cleanliness.setBaseDN(testBaseDN);
    cleanliness.addCleanIndex("objectClass");
    return backend.verifyBackend(completeness) + backend.verifyBackend(cleanliness);
  }
  private SearchOperation createSearchOperation(DN baseDN, SearchScope scope, String searchFilter,
      final List<Entry> returnedEntries) throws DirectoryException
  {
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/ModifyConflictTest.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2010 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.replication.plugin;
@@ -69,6 +70,8 @@
public class ModifyConflictTest extends ReplicationTestCase
{
  private static final String ORGANIZATION = "organization";
  private static final String OBJECTCLASS = "objectClass";
  private static final String EXTENSIBLEOBJECT = "extensibleObject";
  private static final String DISPLAYNAME = "displayName";
  private static final String EMPLOYEENUMBER = "employeeNumber";
  private static final String DESCRIPTION = "description";
@@ -1042,6 +1045,76 @@
  }
  /**
   * Test that a replayed modification of the objectClass attribute leaves the entry handing out
   * the object classes it ends up with.
   * <p>
   * Solving the conflicts of a delete reads the entry, and it does so before the core applies the
   * modifications to it. The read must not fix what the entry answers afterwards: the backend
   * stores the object classes of the entry but indexes it through its objectClass attribute, so
   * an attribute left behind by the modification silently takes the objectClass index out of step
   * with the stored entry.
   */
  @Test
  public void replayObjectClassAddAndDelete() throws Exception
  {
    Entry entry = initializeEntry();
    EntryHistorical hist = EntryHistorical.newInstanceFromEntry(entry);
    List<Modification> mods = newArrayList(
        newModification(ADD, OBJECTCLASS, EXTENSIBLEOBJECT),
        newModification(DELETE, OBJECTCLASS, ORGANIZATION));
    // The conflict resolution, which reads the entry to solve the delete.
    replayModifies(entry, hist, 10, mods);
    assertThat(mods).hasSize(2);
    // The core, which applies the modifications to the entry.
    applyModificationsTheWayTheCoreDoes(entry, mods);
    assertThat(entry.getObjectClasses().values()).containsOnly(EXTENSIBLEOBJECT);
    assertThat(objectClassAttributeOf(entry)).containsOnly(EXTENSIBLEOBJECT);
  }
  /**
   * Applies the modifications to the entry the way {@code LocalBackendModifyOperation} does, that
   * is with {@code addAttribute()} and {@code removeAttribute()} rather than with
   * {@code Entry.applyModifications()}.
   */
  private void applyModificationsTheWayTheCoreDoes(Entry entry, List<Modification> mods)
  {
    for (Modification mod : mods)
    {
      Attribute attr = mod.getAttribute();
      switch (mod.getModificationType().asEnum())
      {
      case ADD:
        entry.addAttribute(attr, new LinkedList<ByteString>());
        break;
      case DELETE:
        entry.removeAttribute(attr, new LinkedList<ByteString>());
        break;
      default:
        entry.replaceAttribute(attr);
        break;
      }
    }
  }
  /** Returns the object classes of the entry as its objectClass attribute reports them. */
  private List<String> objectClassAttributeOf(Entry entry)
  {
    List<String> values = new LinkedList<>();
    for (Attribute attr : entry.getAllAttributes(getObjectClassAttributeType()))
    {
      for (ByteString value : attr)
      {
        values.add(value.toString());
      }
    }
    return values;
  }
  /**
   * Check that the mods given as first parameter match the next parameters.
   *
   * @param mods The mods that must be tested.
opendj-server-legacy/src/test/java/org/opends/server/types/TestEntry.java
@@ -110,7 +110,7 @@
    TestCaseUtils.startServer();
  }
  /** Returns an entry to delete object class values from. */
  /** Returns an entry to change the object classes of. */
  private Entry newTestUserEntry() throws Exception
  {
    return TestCaseUtils.makeEntry(
@@ -171,6 +171,101 @@
    assertThat(e.getObjectClasses().values()).containsOnly("top", "person", "organizationalPerson");
  }
  /*
   * An entry holds its object classes apart from its attributes, and hands them out as an
   * attribute which it builds on demand and keeps. Every change of the object classes must be
   * visible to the next read of that attribute: a backend stores the object classes themselves
   * but indexes the entry through the attribute, so an attribute left behind by a change silently
   * takes the object class index out of step with the stored entry. The tests below read the
   * attribute first, the way the server reads an entry before modifying it.
   */
  /** Returns the object classes of the entry as its object class attribute reports them. */
  private List<String> objectClassAttributeOf(Entry e)
  {
    List<String> values = new ArrayList<>();
    for (Attribute attr : e.getAllAttributes(getObjectClassAttributeType()))
    {
      for (ByteString value : attr)
      {
        values.add(value.toString());
      }
    }
    return values;
  }
  /** An object class added with {@code addAttribute()}, the way the core applies "add: objectClass". */
  @Test
  public void testObjectClassAttributeAfterAddAttribute() throws Exception
  {
    Entry e = newTestUserEntry();
    assertThat(objectClassAttributeOf(e)).doesNotContain("extensibleObject");
    e.addAttribute(Attributes.create(getObjectClassAttributeType(), "extensibleObject"),
        new LinkedList<ByteString>());
    assertThat(objectClassAttributeOf(e))
        .containsOnly("top", "person", "organizationalPerson", "inetOrgPerson", "extensibleObject");
  }
  /**
   * An object class removed with {@code removeAttribute()}, the way the core applies
   * "delete: objectClass".
   */
  @Test
  public void testObjectClassAttributeAfterRemoveAttribute() throws Exception
  {
    Entry e = newTestUserEntry();
    assertThat(objectClassAttributeOf(e)).contains("organizationalPerson");
    e.removeAttribute(Attributes.create(getObjectClassAttributeType(), "organizationalPerson"),
        new LinkedList<ByteString>());
    assertThat(objectClassAttributeOf(e)).containsOnly("top", "person", "inetOrgPerson");
  }
  /**
   * The object classes replaced with {@code replaceAttribute()}, the way the core applies
   * "replace: objectClass".
   */
  @Test
  public void testObjectClassAttributeAfterReplaceAttribute() throws Exception
  {
    Entry e = newTestUserEntry();
    assertThat(objectClassAttributeOf(e)).contains("inetOrgPerson");
    e.replaceAttribute(Attributes.create(getObjectClassAttributeType(), "domain"));
    assertThat(objectClassAttributeOf(e)).containsOnly("domain");
  }
  /** An object class added with {@code addObjectClass()}. */
  @Test
  public void testObjectClassAttributeAfterAddObjectClass() throws Exception
  {
    Entry e = newTestUserEntry();
    assertThat(objectClassAttributeOf(e)).doesNotContain("extensibleObject");
    e.addObjectClass(CoreSchema.getExtensibleObjectObjectClass());
    assertThat(objectClassAttributeOf(e))
        .containsOnly("top", "person", "organizationalPerson", "inetOrgPerson", "extensibleObject");
  }
  /** All the object classes removed with {@code removeAttribute()}, then a new one added. */
  @Test
  public void testObjectClassAttributeAfterRemoveOfTheWholeAttribute() throws Exception
  {
    Entry e = newTestUserEntry();
    assertThat(objectClassAttributeOf(e)).contains("inetOrgPerson");
    e.removeAttribute(getObjectClassAttributeType());
    assertThat(objectClassAttributeOf(e)).isEmpty();
    e.addObjectClass(CoreSchema.getTopObjectClass());
    assertThat(objectClassAttributeOf(e)).containsOnly("top");
  }
  /**
   * Test the {@link Entry#parseAttribute(String)} method.
   */