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

Valery Kharseko
6 hours ago a315ca0785c844792b2f18a27450236637a07425
[#857] Iterate a snapshot of the connection set in AuthenticatedUsers.doPostResponse(modify) (#858)
1 files modified
1 files added
309 ■■■■■ changed files
opendj-server-legacy/src/main/java/org/opends/server/core/AuthenticatedUsers.java 7 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/core/AuthenticatedUsersTestCase.java 302 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/core/AuthenticatedUsers.java
@@ -214,7 +214,12 @@
    if (connectionSet != null)
    {
      Entry newEntry = null;
      for (ClientConnection conn : connectionSet)
      // updateAuthenticationInfo() re-registers the connection (remove + put),
      // appending it back to the tail of the live set's hash-bin chain. A
      // weakly-consistent iterator over the set itself would then meet the
      // connection again and, with two or more connections in one bin,
      // ping-pong between them forever (issue #857) - so iterate a snapshot.
      for (ClientConnection conn : connectionSet.toArray(new ClientConnection[0]))
      {
        if (newEntry == null)
        {
opendj-server-legacy/src/test/java/org/opends/server/core/AuthenticatedUsersTestCase.java
New file
@@ -0,0 +1,302 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.core;
import static org.mockito.Mockito.*;
import static org.testng.Assert.*;
import java.net.InetAddress;
import java.util.Collection;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.ldap.ResultCode;
import org.opends.server.TestCaseUtils;
import org.opends.server.api.ClientConnection;
import org.opends.server.api.ConnectionHandler;
import org.opends.server.types.AuthenticationInfo;
import org.opends.server.types.CancelRequest;
import org.opends.server.types.CancelResult;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.DisconnectReason;
import org.opends.server.types.Entry;
import org.opends.server.types.IntermediateResponse;
import org.opends.server.types.Operation;
import org.opends.server.types.SearchResultEntry;
import org.opends.server.types.SearchResultReference;
import org.opends.server.types.operation.PostResponseModifyOperation;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
/** Tests for the {@link AuthenticatedUsers} plugin. */
public class AuthenticatedUsersTestCase extends CoreTestCase
{
  @BeforeClass
  public void startServer() throws Exception
  {
    TestCaseUtils.startServer();
  }
  /**
   * A minimal client connection whose hash code is constant, so that every
   * instance lands in the same hash bin of the per-user connection set.
   * {@link ClientConnection#setAuthenticationInfo} deregisters and re-registers
   * the connection, appending it back to the tail of that bin's node chain:
   * a weakly-consistent iterator positioned in the bin then meets the
   * connection again, and with two such connections
   * {@link AuthenticatedUsers#doPostResponse(PostResponseModifyOperation)}
   * ping-pongs between them forever (issue #857).
   */
  private static final class CollidingClientConnection extends ClientConnection
  {
    private final AtomicInteger authInfoUpdates = new AtomicInteger();
    @Override
    public void setAuthenticationInfo(AuthenticationInfo authenticationInfo)
    {
      authInfoUpdates.incrementAndGet();
      super.setAuthenticationInfo(authenticationInfo);
    }
    @Override
    public int hashCode()
    {
      return 42;
    }
    @Override
    public long getConnectionID()
    {
      return -1;
    }
    @Override
    public ConnectionHandler<?> getConnectionHandler()
    {
      return null;
    }
    @Override
    public String getProtocol()
    {
      return "internal";
    }
    @Override
    public String getClientAddress()
    {
      return null;
    }
    @Override
    public int getClientPort()
    {
      return 0;
    }
    @Override
    public String getServerAddress()
    {
      return null;
    }
    @Override
    public int getServerPort()
    {
      return 0;
    }
    @Override
    public InetAddress getRemoteAddress()
    {
      return null;
    }
    @Override
    public InetAddress getLocalAddress()
    {
      return null;
    }
    @Override
    public boolean isConnectionValid()
    {
      return true;
    }
    @Override
    public boolean isSecure()
    {
      return false;
    }
    @Override
    public long getNumberOfOperations()
    {
      return 0;
    }
    @Override
    public void sendResponse(Operation operation)
    {
    }
    @Override
    public void sendSearchEntry(SearchOperation searchOperation, SearchResultEntry searchEntry)
        throws DirectoryException
    {
    }
    @Override
    public boolean sendSearchReference(SearchOperation searchOperation, SearchResultReference searchReference)
        throws DirectoryException
    {
      return false;
    }
    @Override
    protected boolean sendIntermediateResponseMessage(IntermediateResponse intermediateResponse)
    {
      return false;
    }
    @Override
    public void disconnect(DisconnectReason disconnectReason, boolean sendNotification, LocalizableMessage message)
    {
    }
    @Override
    public Collection<Operation> getOperationsInProgress()
    {
      return null;
    }
    @Override
    public Operation getOperationInProgress(int messageID)
    {
      return null;
    }
    @Override
    public boolean removeOperationInProgress(int messageID)
    {
      return false;
    }
    @Override
    public CancelResult cancelOperation(int messageID, CancelRequest cancelRequest)
    {
      return null;
    }
    @Override
    public void cancelAllOperations(CancelRequest cancelRequest)
    {
    }
    @Override
    public void cancelAllOperationsExcept(CancelRequest cancelRequest, int messageID)
    {
    }
    @Override
    public String getMonitorSummary()
    {
      return "";
    }
    @Override
    public void toString(StringBuilder buffer)
    {
      buffer.append("CollidingClientConnection");
    }
    @Override
    public int getSSF()
    {
      return 0;
    }
  }
  /**
   * Modifying an entry two or more connections are authenticated as must
   * terminate and update every connection exactly once, even though each
   * update re-registers the connection in the set being iterated.
   */
  @Test(timeOut = 60000)
  public void testDoPostResponseModifyTerminatesWhenConnectionsReRegister() throws Exception
  {
    Entry oldEntry = TestCaseUtils.makeEntry(
        "dn: uid=issue857.user,o=test",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: issue857.user",
        "givenName: Issue857",
        "sn: User",
        "cn: Issue857 User");
    Entry newEntry = TestCaseUtils.makeEntry(
        "dn: uid=issue857.user,o=test",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: issue857.user",
        "givenName: Issue857",
        "sn: User",
        "cn: Issue857 User",
        "description: updated");
    AuthenticatedUsers users = DirectoryServer.getAuthenticatedUsers();
    CollidingClientConnection conn1 = new CollidingClientConnection();
    CollidingClientConnection conn2 = new CollidingClientConnection();
    conn1.setAuthenticationInfo(new AuthenticationInfo(oldEntry, false));
    conn2.setAuthenticationInfo(new AuthenticationInfo(oldEntry, false));
    try
    {
      assertEquals(users.get(oldEntry.getName()).size(), 2);
      conn1.authInfoUpdates.set(0);
      conn2.authInfoUpdates.set(0);
      PostResponseModifyOperation op = mock(PostResponseModifyOperation.class);
      when(op.getResultCode()).thenReturn(ResultCode.SUCCESS);
      when(op.getCurrentEntry()).thenReturn(oldEntry);
      when(op.getModifiedEntry()).thenReturn(newEntry);
      users.doPostResponse(op);
      assertEquals(conn1.authInfoUpdates.get(), 1);
      assertEquals(conn2.authInfoUpdates.get(), 1);
      Set<ClientConnection> registered = users.get(oldEntry.getName());
      assertNotNull(registered);
      assertTrue(registered.contains(conn1));
      assertTrue(registered.contains(conn2));
      assertEquals(conn1.getAuthenticationInfo().getAuthenticationEntry()
          .parseAttribute("description").asString(), "updated");
      assertEquals(conn2.getAuthenticationInfo().getAuthenticationEntry()
          .parseAttribute("description").asString(), "updated");
    }
    finally
    {
      conn1.setAuthenticationInfo(new AuthenticationInfo());
      conn2.setAuthenticationInfo(new AuthenticationInfo());
    }
  }
}