DonalEvans commented on a change in pull request #7063:
URL: https://github.com/apache/geode/pull/7063#discussion_r741408553



##########
File path: 
geode-core/src/main/java/org/apache/geode/internal/cache/tier/sockets/ClientUserAuths.java
##########
@@ -16,29 +16,44 @@
 
 import static 
org.apache.geode.cache.client.internal.AuthenticateUserOp.NOT_A_USER_ID;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import java.util.Random;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
+import java.util.stream.Collectors;
 
 import org.apache.logging.log4j.Logger;
 import org.apache.shiro.subject.Subject;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.TestOnly;
 
 import org.apache.geode.annotations.VisibleForTesting;
 import org.apache.geode.internal.security.AuthorizeRequest;
 import org.apache.geode.internal.security.AuthorizeRequestPP;
 import org.apache.geode.logging.internal.log4j.api.LogService;
 
+/**
+ * This is per ServerConnection or per CacheClientProxy, corresponding to only 
one client
+ * connection.
+ * Credentials should usually be just one, only multiple in multi-user case.
+ */
 public class ClientUserAuths {
   private static final Logger logger = LogService.getLogger();
 
   private final ConcurrentMap<Long, UserAuthAttributes> uniqueIdVsUserAuth =
       new ConcurrentHashMap<>();
   private final ConcurrentMap<String, UserAuthAttributes> cqNameVsUserAuth =
       new ConcurrentHashMap<>();
-  private final ConcurrentMap<Long, Subject> uniqueIdVsSubject = new 
ConcurrentHashMap<>();
+  // use a list to store all the subjects that's created for this uniqueId
+  // In the expirable credential case, there will be multiple
+  // subjects created associated with one uniqueId. We always save the current 
subject to the top of
+  // the list. The rest are "to-be-retired".
+  private final ConcurrentMap<Long, List<Subject>> uniqueIdVsSubject =

Review comment:
       Could this be renamed to "uniqueIdVsSubject" so that it's clearer that 
each ID may map to more than one Subject?

##########
File path: 
geode-core/src/integrationTest/java/org/apache/geode/management/internal/security/SecurityWithExpirationIntegrationTest.java
##########
@@ -57,6 +58,14 @@ public void testAuthorizationWhenUserExpired() {
         .isInstanceOf(AuthenticationExpiredException.class);
   }
 
+  @Test
+  public void logoutMultipleTimeOnTheSameSubjectShouldNotThrowExcpetion() {

Review comment:
       Typo here, should be "ThrowException"

##########
File path: 
geode-core/src/main/java/org/apache/geode/internal/cache/tier/sockets/ClientUserAuths.java
##########
@@ -90,28 +120,29 @@ public UserAuthAttributes getUserAuthAttributes(final Long 
userId) {
   }
 
   @VisibleForTesting
+  @TestOnly
   protected Collection<Subject> getSubjects() {

Review comment:
       Could this be "getAllSubjects" to make it clear that it's getting all 
subjects for all unique IDs?

##########
File path: 
geode-core/src/test/java/org/apache/geode/internal/cache/tier/sockets/CacheClientProxyTest.java
##########
@@ -109,23 +122,59 @@ public void deliverMessageWhenSubjectIsNotNull() {
   public void deliverMessageWhenSubjectIsNull() {
     when(proxyStatsFactory.create(any(), any(), any()))
         .thenReturn(mock(CacheClientProxyStats.class));
-    proxy = spy(new CacheClientProxy(cache, notifier, socket, id, true, (byte) 
1, version, 1L, true,
-        securityService, null, clock, statsFactory, proxyStatsFactory, 
dispatcherFactory));
-    assertThat(proxy.getSubject()).isNull();
+    proxyWithMultiUser =
+        new CacheClientProxy(cache, notifier, socket, id, true, (byte) 1, 
version, 1L, true,
+            securityService, null, clock, statsFactory, proxyStatsFactory, 
dispatcherFactory,
+            clientUserAuths);
+    assertThat(proxyWithMultiUser.getSubject()).isNull();
     Conflatable message = mock(ClientUpdateMessage.class);
     when(securityService.needPostProcess()).thenReturn(true);
     when(proxyStatsFactory.create(any(), any(), any()))
         .thenReturn(mock(CacheClientProxyStats.class));
-    proxy.deliverMessage(message);
+    proxyWithMultiUser.deliverMessage(message);
     verify(securityService, never()).bindSubject(subject);
     verify(securityService, never()).postProcess(any(), any(), any(), 
anyBoolean());
   }
 
   @Test
   public void replacingSubjectShouldNotLogout() {
-    proxy = new CacheClientProxy(cache, notifier, socket, id, true, (byte) 1, 
version, 1L, true,
-        securityService, subject, clock, statsFactory, proxyStatsFactory, 
dispatcherFactory);
-    proxy.setSubject(mock(Subject.class));
+    proxyWithSingleUser.setSubject(mock(Subject.class));
+    verify(subject, never()).logout();
+  }
+
+  @Test
+  public void close_keepProxy_ShouldNotLogoutUser() {
+    when(id.isDurable()).thenReturn(true);
+    boolean keepProxy = proxyWithSingleUser.close(true, false);
+    assertThat(keepProxy).isTrue();
+    verify(subject, never()).logout();
+    verify(clientUserAuths, never()).cleanup(anyBoolean());
+
+    keepProxy = proxyWithMultiUser.close(true, false);
+    assertThat(keepProxy).isTrue();
+    verify(subject, never()).logout();
+    verify(clientUserAuths, never()).cleanup(anyBoolean());
+  }
+
+  @Test
+  public void close_singleUser() {

Review comment:
       Could this test name be a little more descriptive? Adding what the 
expected behaviour is would make it much clearer what the test is actually 
testing. This also applies to the `close_multiUser()` test.

##########
File path: 
geode-core/src/test/java/org/apache/geode/internal/cache/tier/sockets/CacheClientProxyTest.java
##########
@@ -75,32 +79,41 @@ public void before() throws Exception {
     statsFactory = mock(StatisticsFactory.class);
     proxyStatsFactory = mock(CacheClientProxyStatsFactory.class);
     dispatcherFactory = mock(MessageDispatcherFactory.class);
+    clientUserAuths = mock(ClientUserAuths.class);
+
+    proxyWithSingleUser =
+        new CacheClientProxy(cache, notifier, socket, id, true, (byte) 1, 
version, 1L, true,
+            securityService, subject, clock, statsFactory, proxyStatsFactory, 
dispatcherFactory,
+            clientUserAuths);
+
+    proxyWithMultiUser =
+        new CacheClientProxy(cache, notifier, socket, id, true, (byte) 1, 
version, 1L, true,
+            securityService, null, clock, statsFactory, proxyStatsFactory, 
dispatcherFactory,
+            clientUserAuths);
   }
 
   @Test
   public void noExceptionWhenGettingSubjectForCQWhenSubjectIsNotNull() {
-    proxy = spy(new CacheClientProxy(cache, notifier, socket, id, true, (byte) 
1, version, 1L, true,
-        securityService, subject, clock, statsFactory, proxyStatsFactory, 
dispatcherFactory));
-    proxy.getSubject("cq");
+    proxyWithSingleUser.getSubject("cq");
   }
 
   @Test
   public void noExceptionWhenGettingSubjectForCQWhenSubjectIsNull() {
-    proxy = spy(new CacheClientProxy(cache, notifier, socket, id, true, (byte) 
1, version, 1L, true,
-        securityService, null, clock, statsFactory, proxyStatsFactory, 
dispatcherFactory));
-    proxy.getSubject("cq");
+    proxyWithMultiUser.getSubject("cq");
   }
 
   @Test
   public void deliverMessageWhenSubjectIsNotNull() {
     when(proxyStatsFactory.create(any(), any(), any()))
         .thenReturn(mock(CacheClientProxyStats.class));

Review comment:
       If this mocking is moved to the `@Before` method then you can avoid 
having to create a new `CacheClientProxy` on the following lines. This also 
applies to the `deliverMessageWhenSubjectIsNull()` test.

##########
File path: 
geode-core/src/test/java/org/apache/geode/internal/cache/tier/sockets/CacheClientProxyTest.java
##########
@@ -109,23 +122,59 @@ public void deliverMessageWhenSubjectIsNotNull() {
   public void deliverMessageWhenSubjectIsNull() {
     when(proxyStatsFactory.create(any(), any(), any()))
         .thenReturn(mock(CacheClientProxyStats.class));
-    proxy = spy(new CacheClientProxy(cache, notifier, socket, id, true, (byte) 
1, version, 1L, true,
-        securityService, null, clock, statsFactory, proxyStatsFactory, 
dispatcherFactory));
-    assertThat(proxy.getSubject()).isNull();
+    proxyWithMultiUser =
+        new CacheClientProxy(cache, notifier, socket, id, true, (byte) 1, 
version, 1L, true,
+            securityService, null, clock, statsFactory, proxyStatsFactory, 
dispatcherFactory,
+            clientUserAuths);
+    assertThat(proxyWithMultiUser.getSubject()).isNull();
     Conflatable message = mock(ClientUpdateMessage.class);
     when(securityService.needPostProcess()).thenReturn(true);
     when(proxyStatsFactory.create(any(), any(), any()))
         .thenReturn(mock(CacheClientProxyStats.class));
-    proxy.deliverMessage(message);
+    proxyWithMultiUser.deliverMessage(message);
     verify(securityService, never()).bindSubject(subject);
     verify(securityService, never()).postProcess(any(), any(), any(), 
anyBoolean());
   }
 
   @Test
   public void replacingSubjectShouldNotLogout() {
-    proxy = new CacheClientProxy(cache, notifier, socket, id, true, (byte) 1, 
version, 1L, true,
-        securityService, subject, clock, statsFactory, proxyStatsFactory, 
dispatcherFactory);
-    proxy.setSubject(mock(Subject.class));
+    proxyWithSingleUser.setSubject(mock(Subject.class));
+    verify(subject, never()).logout();
+  }
+
+  @Test
+  public void close_keepProxy_ShouldNotLogoutUser() {
+    when(id.isDurable()).thenReturn(true);
+    boolean keepProxy = proxyWithSingleUser.close(true, false);
+    assertThat(keepProxy).isTrue();
+    verify(subject, never()).logout();
+    verify(clientUserAuths, never()).cleanup(anyBoolean());
+
+    keepProxy = proxyWithMultiUser.close(true, false);
+    assertThat(keepProxy).isTrue();
+    verify(subject, never()).logout();
+    verify(clientUserAuths, never()).cleanup(anyBoolean());
+  }
+
+  @Test
+  public void close_singleUser() {
+    when(id.isDurable()).thenReturn(false);
+    CacheClientProxy spy = spy(proxyWithSingleUser);
+    doNothing().when(spy).closeTransientFields();
+    boolean keepProxy = spy.close(true, false);
+    assertThat(keepProxy).isFalse();
+    verify(subject, times(1)).logout();

Review comment:
       When `times()` is 1, it can be omitted, as the default behaviour of 
`verify()` with no second argument is to verify that the method was invoked 
once.

##########
File path: 
geode-core/src/main/java/org/apache/geode/internal/cache/tier/sockets/ClientUserAuths.java
##########
@@ -52,17 +62,32 @@ public Long putUserAuth(UserAuthAttributes userAuthAttr) {
     return newId;
   }
 
-  public Long putSubject(Subject subject, long existingUniqueId) {
-    final Long newId;
+
+  public long putSubject(@NotNull Subject subject, long existingUniqueId) {
+    final long newId;
     if (existingUniqueId == 0 || existingUniqueId == NOT_A_USER_ID) {
       newId = getNextID();
     } else {
       newId = existingUniqueId;
     }
 
-    Subject oldSubject = uniqueIdVsSubject.put(newId, subject);
-    removeSubject(oldSubject);
-    logger.debug("Subject of {} added.", newId);
+    // we are saving all the subjects that's related to this uniqueId
+    // we cannot immediately log out the old subject of this userId because
+    // it might already be bound to another thread and doing operations. If
+    // we log out that subject immediately, that thread "authorize" would get 
null principal.
+    synchronized (this) {
+      CopyOnWriteArrayList<Subject> subjects;
+      if (!uniqueIdVsSubject.containsKey(newId)) {
+        logger.debug("Subject of {} added.", newId);
+        subjects = new CopyOnWriteArrayList<>();
+        uniqueIdVsSubject.put(newId, subjects);
+      } else {
+        logger.debug("Subject of {} replaced.", newId);
+        subjects = uniqueIdVsSubject.get(newId);
+      }
+      // always add the latest subject to the top of the list;
+      subjects.add(0, subject);

Review comment:
       Why does the latest subject have to be inserted as the first element in 
the list? Could we instead add that subject to the end of the list, and then in 
`getSubject(final Long userId)` retrieve the last element in the list? This 
would be a simple solution to the performance issue that Jianxia described.

##########
File path: 
geode-core/src/main/java/org/apache/geode/internal/cache/tier/sockets/ClientUserAuths.java
##########
@@ -90,28 +120,29 @@ public UserAuthAttributes getUserAuthAttributes(final Long 
userId) {
   }
 
   @VisibleForTesting
+  @TestOnly
   protected Collection<Subject> getSubjects() {
-    return Collections.unmodifiableCollection(uniqueIdVsSubject.values());
+    List<Subject> all = uniqueIdVsSubject.values().stream()
+        .flatMap(List::stream)
+        .collect(Collectors.toList());
+    return Collections.unmodifiableCollection(all);
   }
 
-  public Subject getSubject(final Long userId) {
-    return uniqueIdVsSubject.get(userId);
+  public synchronized Subject getSubject(final Long userId) {
+    List<Subject> subjects = uniqueIdVsSubject.get(userId);
+    if (subjects == null || subjects.isEmpty()) {
+      return null;
+    }
+    return subjects.get(0);
   }
 
-  public void removeSubject(final Long userId) {
+  public synchronized void removeSubject(final Long userId) {
     logger.debug("Subject of {} removed.", userId);

Review comment:
       Given that this call may result in multiple subjects being removed, 
would it be worth modifying the debug logging to give a little more 
information, such as how many subjects were removed (if any)?

##########
File path: 
geode-core/src/test/java/org/apache/geode/internal/cache/tier/sockets/ClientUserAuthsTest.java
##########
@@ -45,37 +47,78 @@ public void before() throws Exception {
   }
 
   @Test
-  public void putSubjectWithNegativeOneWillProduceNewId() throws Exception {
+  public void putSubjectWithNegativeOneWillProduceNewId() {
     Long uniqueId = auth.putSubject(subject1, -1);
-    verify(auth).removeSubject((Subject) null);
+    verify(auth, never()).removeSubject(anyLong());
     assertThat(uniqueId).isEqualTo(123L);
   }
 
   @Test
-  public void putSubjectWithZeroWillProduceNewId() throws Exception {
+  public void putSubjectWithZeroWillProduceNewId() {
     Long uniqueId = auth.putSubject(subject1, 0);
-    verify(auth).removeSubject((Subject) null);
+    verify(auth, never()).removeSubject(anyLong());
     assertThat(uniqueId).isEqualTo(123L);
+    verify(subject1, never()).logout();
   }
 
   @Test
-  public void putSubjectWithExistingId() throws Exception {
+  public void putSubjectWithExistingId() {
     Long uniqueId = auth.putSubject(subject1, 456L);
-    verify(auth).removeSubject((Subject) null);
+    verify(auth, never()).removeSubject(anyLong());
     assertThat(uniqueId).isEqualTo(456L);
+    verify(subject1, never()).logout();
   }
 
   @Test
-  public void replacedSubjectShouldLogout() throws Exception {
+  public void replacedSubjectShouldNotLogout() {
     Long id1 = auth.putSubject(subject1, -1);
     Long id2 = auth.putSubject(subject2, id1);
     assertThat(id1).isEqualTo(id2);
-    verify(auth).removeSubject(eq(subject1));
+    verify(auth, never()).removeSubject(anyLong());
+    verify(subject1, never()).logout();
+
+  }
+
+  @Test
+  public void getSubjectReturnsTheLatestOne() {
+    Long id1 = auth.putSubject(subject1, -1);
+    assertThat(auth.getSubject(id1)).isSameAs(subject1);
+    auth.putSubject(subject2, id1);
+    assertThat(auth.getSubject(id1)).isSameAs(subject2);
+  }
+
+  @Test
+  public void getSubjectOfNonExistentId() {

Review comment:
       Could this test name include the expected behaviour? Something like 
"getSubjectWithNonExistentIdReturnsNull" maybe? This also applies to 
`removeSubjectOfNonExistentId()`, `getSubjectWithCq()` and `removeSubject()`.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to