This is an automated email from the ASF dual-hosted git repository.

jbertram pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/artemis.git


The following commit(s) were added to refs/heads/main by this push:
     new 20bb13c765 ARTEMIS-5958 refactor security store to expose 
hasPermission bool, to get a fast path for rbac chechs that restrict access. 
the regular check is still in play for invocations
20bb13c765 is described below

commit 20bb13c7657041a3b9367c49a2c44faa52fa49a8
Author: Gary Tully <[email protected]>
AuthorDate: Thu Mar 19 21:00:12 2026 +0000

    ARTEMIS-5958 refactor security store to expose hasPermission bool, to get a 
fast path for rbac chechs that restrict access. the regular check is still in 
play for invocations
    
    Assisted-by: Claude
---
 .../artemis/core/security/SecurityStore.java       |   2 +
 .../core/security/impl/SecurityStoreImpl.java      | 152 ++--
 .../management/ArtemisRbacInvocationHandler.java   |  22 +-
 .../core/security/impl/SecurityStoreImplTest.java  | 905 +++++++++++++++++++++
 4 files changed, 1007 insertions(+), 74 deletions(-)

diff --git 
a/artemis-server/src/main/java/org/apache/activemq/artemis/core/security/SecurityStore.java
 
b/artemis-server/src/main/java/org/apache/activemq/artemis/core/security/SecurityStore.java
index 4307bbeeb5..3cd78c687a 100644
--- 
a/artemis-server/src/main/java/org/apache/activemq/artemis/core/security/SecurityStore.java
+++ 
b/artemis-server/src/main/java/org/apache/activemq/artemis/core/security/SecurityStore.java
@@ -30,6 +30,8 @@ public interface SecurityStore {
 
    void check(SimpleString address, SimpleString queue, CheckType checkType, 
SecurityAuth session) throws Exception;
 
+   boolean hasPermission(SimpleString address, SimpleString queue, CheckType 
checkType, SecurityAuth session) throws Exception;
+
    boolean isSecurityEnabled();
 
    void setSecurityEnabled(boolean securityEnabled);
diff --git 
a/artemis-server/src/main/java/org/apache/activemq/artemis/core/security/impl/SecurityStoreImpl.java
 
b/artemis-server/src/main/java/org/apache/activemq/artemis/core/security/impl/SecurityStoreImpl.java
index c07d7bafef..0ba198aaa5 100644
--- 
a/artemis-server/src/main/java/org/apache/activemq/artemis/core/security/impl/SecurityStoreImpl.java
+++ 
b/artemis-server/src/main/java/org/apache/activemq/artemis/core/security/impl/SecurityStoreImpl.java
@@ -297,22 +297,42 @@ public class SecurityStoreImpl implements SecurityStore, 
HierarchicalRepositoryC
    }
 
    @Override
-   public void check(final SimpleString address,
-                     final SimpleString queue,
-                     final CheckType checkType,
-                     final SecurityAuth session) throws Exception {
-      if (securityEnabled) {
+   public boolean hasPermission(final SimpleString address,
+                                final SimpleString queue,
+                                final CheckType checkType,
+                                final SecurityAuth session) throws Exception {
+      if (!securityEnabled) {
+         return true;
+      }
+
+      // bypass permission checks for management cluster user
+      String user = session.getUsername();
+      if (managementClusterUser.equals(user) && 
session.getPassword().equals(managementClusterPassword)) {
+         return true;
+      }
+
+      // Special case: detect authentication failure for 
ActiveMQSecurityManager5
+      // This must throw an authentication exception, not an authorization 
exception
+      Subject subjectForManager5 = null;
+      if (securityManager instanceof ActiveMQSecurityManager5 manager5) {
+         subjectForManager5 = getSubjectForAuthorization(session, manager5);
+         /*
+          * A user may authenticate successfully at first, but then later when 
their Subject is evicted from the
+          * local cache re-authentication may fail. This could happen, for 
example, if the user was removed from LDAP
+          * or the user's token expired.
+          *
+          * If the subject is null then authorization will *always* fail.
+          */
+         if (subjectForManager5 == null) {
+            authenticationFailed(session.getUsername(), 
session.getRemotingConnection());
+         }
+      }
+      try {
          SimpleString bareAddress = 
CompositeAddress.extractAddressName(address);
          SimpleString bareQueue = CompositeAddress.extractQueueName(queue);
 
          logger.trace("checking access permissions to {}", bareAddress);
 
-         // bypass permission checks for management cluster user
-         String user = session.getUsername();
-         if (managementClusterUser.equals(user) && 
session.getPassword().equals(managementClusterPassword)) {
-            AUTHORIZATION_SUCCESS_COUNT_UPDATER.incrementAndGet(this);
-            return;
-         }
 
          Set<Role> roles = securityRepository.getMatch(bareAddress.toString());
 
@@ -325,27 +345,14 @@ public class SecurityStoreImpl implements SecurityStore, 
HierarchicalRepositoryC
             }
          }
 
-         if (checkAuthorizationCache(fqqn != null  ? fqqn : bareAddress, user, 
checkType)) {
-            AUTHORIZATION_SUCCESS_COUNT_UPDATER.incrementAndGet(this);
-            return;
+         if (checkAuthorizationCache(fqqn != null ? fqqn : bareAddress, user, 
checkType)) {
+            return true;
          }
 
          final Boolean validated;
          if (securityManager instanceof ActiveMQSecurityManager5 manager5) {
-            Subject subject = getSubjectForAuthorization(session, manager5);
-
-            /*
-             * A user may authenticate successfully at first, but then later 
when their Subject is evicted from the
-             * local cache re-authentication may fail. This could happen, for 
example, if the user was removed from LDAP
-             * or the user's token expired.
-             *
-             * If the subject is null then authorization will *always* fail.
-             */
-            if (subject == null) {
-               authenticationFailed(user, session.getRemotingConnection());
-            }
-
-            validated = manager5.authorize(subject, roles, checkType, fqqn != 
null ? fqqn.toString() : bareAddress.toString());
+            // Reuse subject from earlier authentication check
+            validated = manager5.authorize(subjectForManager5, roles, 
checkType, fqqn != null ? fqqn.toString() : bareAddress.toString());
          } else if (securityManager instanceof ActiveMQSecurityManager4 
manager4) {
             validated = manager4.validateUserAndRole(user, 
session.getPassword(), roles, checkType, bareAddress.toString(), 
session.getRemotingConnection(), session.getSecurityDomain()) != null;
          } else if (securityManager instanceof ActiveMQSecurityManager3 
manager3) {
@@ -356,52 +363,67 @@ public class SecurityStoreImpl implements SecurityStore, 
HierarchicalRepositoryC
             validated = securityManager.validateUserAndRole(user, 
session.getPassword(), roles, checkType);
          }
 
-         if (!validated) {
-            if (notificationService != null) {
-               TypedProperties props = new TypedProperties();
-
-               props.putSimpleStringProperty(ManagementHelper.HDR_ADDRESS, 
bareAddress);
-               props.putSimpleStringProperty(ManagementHelper.HDR_CHECK_TYPE, 
SimpleString.of(checkType.toString()));
-               props.putSimpleStringProperty(ManagementHelper.HDR_USER, 
SimpleString.of(getCaller(user, session.getRemotingConnection().getSubject())));
-
-               Notification notification = new Notification(null, 
CoreNotificationType.SECURITY_PERMISSION_VIOLATION, props);
-
-               notificationService.sendNotification(notification);
-            }
-
-            Exception ex;
-            if (bareQueue == null) {
-               ex = 
ActiveMQMessageBundle.BUNDLE.userNoPermissions(getCaller(user, 
session.getRemotingConnection().getSubject()), checkType, bareAddress);
+         if (validated && user != null) {
+            // if we get here we're granted, add to the cache
+            ConcurrentHashSet<SimpleString> set;
+            String key = createAuthorizationCacheKey(user, checkType);
+            ConcurrentHashSet<SimpleString> act = 
getAuthorizationCacheEntry(key);
+            if (act != null) {
+               set = act;
             } else {
-               ex = 
ActiveMQMessageBundle.BUNDLE.userNoPermissionsQueue(getCaller(user, 
session.getRemotingConnection().getSubject()), checkType, bareQueue, 
bareAddress);
+               set = new ConcurrentHashSet<>();
+               putAuthorizationCacheEntry(set, key);
             }
-            
AuditLogger.securityFailure(session.getRemotingConnection().getSubject(), 
session.getRemotingConnection().getRemoteAddress(), ex.getMessage(), ex);
-            AUTHORIZATION_FAILURE_COUNT_UPDATER.incrementAndGet(this);
-            throw ex;
+            set.add(Objects.requireNonNullElse(fqqn, bareAddress));
          }
 
-         // if we get here we're granted, add to the cache
+         return validated;
+      } catch (Exception e) {
+         logger.debug("Permission check failed", e);
+         return false;
+      }
+   }
+
+   @Override
+   public void check(final SimpleString address,
+                     final SimpleString queue,
+                     final CheckType checkType,
+                     final SecurityAuth session) throws Exception {
+      if (!securityEnabled) {
+         return;
+      }
 
+      if (hasPermission(address, queue, checkType, session)) {
          AUTHORIZATION_SUCCESS_COUNT_UPDATER.incrementAndGet(this);
+         return;
+      }
 
-         if (user == null) {
-            // should get all user/pass into a subject and only cache subjects
-            // till then when subject is in play, the user may be null and
-            // we cannot cache as we don't have a unique key
-            return;
-         }
+      // Permission denied - handle side effects
+      SimpleString bareAddress = CompositeAddress.extractAddressName(address);
+      SimpleString bareQueue = CompositeAddress.extractQueueName(queue);
+      String user = session.getUsername();
 
-         ConcurrentHashSet<SimpleString> set;
-         String key = createAuthorizationCacheKey(user, checkType);
-         ConcurrentHashSet<SimpleString> act = getAuthorizationCacheEntry(key);
-         if (act != null) {
-            set = act;
-         } else {
-            set = new ConcurrentHashSet<>();
-            putAuthorizationCacheEntry(set, key);
-         }
-         set.add(Objects.requireNonNullElse(fqqn, bareAddress));
+      if (notificationService != null) {
+         TypedProperties props = new TypedProperties();
+
+         props.putSimpleStringProperty(ManagementHelper.HDR_ADDRESS, 
bareAddress);
+         props.putSimpleStringProperty(ManagementHelper.HDR_CHECK_TYPE, 
SimpleString.of(checkType.toString()));
+         props.putSimpleStringProperty(ManagementHelper.HDR_USER, 
SimpleString.of(getCaller(user, session.getRemotingConnection().getSubject())));
+
+         Notification notification = new Notification(null, 
CoreNotificationType.SECURITY_PERMISSION_VIOLATION, props);
+
+         notificationService.sendNotification(notification);
+      }
+
+      Exception ex;
+      if (bareQueue == null) {
+         ex = ActiveMQMessageBundle.BUNDLE.userNoPermissions(getCaller(user, 
session.getRemotingConnection().getSubject()), checkType, bareAddress);
+      } else {
+         ex = 
ActiveMQMessageBundle.BUNDLE.userNoPermissionsQueue(getCaller(user, 
session.getRemotingConnection().getSubject()), checkType, bareQueue, 
bareAddress);
       }
+      
AuditLogger.securityFailure(session.getRemotingConnection().getSubject(), 
session.getRemotingConnection().getRemoteAddress(), ex.getMessage(), ex);
+      AUTHORIZATION_FAILURE_COUNT_UPDATER.incrementAndGet(this);
+      throw ex;
    }
 
    @Override
diff --git 
a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/management/ArtemisRbacInvocationHandler.java
 
b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/management/ArtemisRbacInvocationHandler.java
index 9453d90e5b..40b225ebd4 100644
--- 
a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/management/ArtemisRbacInvocationHandler.java
+++ 
b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/management/ArtemisRbacInvocationHandler.java
@@ -329,17 +329,12 @@ public class ArtemisRbacInvocationHandler implements 
GuardInvocationHandler {
    }
 
    private boolean viewPermissionCheckFails(Object candidate) {
-      boolean failed = false;
       ObjectName objectName = candidate instanceof ObjectInstance oi ? 
oi.getObjectName() : (ObjectName) candidate;
-      if (!isUncheckedDomain(objectName)) {
-         try {
-            final SimpleString rbacAddress = addressFrom(objectName);
-            securityStoreCheck(rbacAddress, CheckType.VIEW);
-         } catch (Exception checkFailed) {
-            failed = true;
-         }
+      if (isUncheckedDomain(objectName)) {
+         return false;
       }
-      return failed;
+      final SimpleString rbacAddress = addressFrom(objectName);
+      return !hasPermission(rbacAddress, CheckType.VIEW);
    }
 
    private void securityStoreCheck(SimpleString rbacAddress, CheckType 
checkType) throws Exception {
@@ -347,6 +342,15 @@ public class ArtemisRbacInvocationHandler implements 
GuardInvocationHandler {
       activeMQServer.getSecurityStore().check(rbacAddress, checkType, 
delegateToAccessController);
    }
 
+   private boolean hasPermission(SimpleString rbacAddress, CheckType 
checkType) {
+      // use accessor as security store can be updated on config reload
+      try {
+         return activeMQServer.getSecurityStore().hasPermission(rbacAddress, 
null, checkType, delegateToAccessController);
+      } catch (Exception notAuthenticated) {
+         return false;
+      }
+   }
+
    // sufficiently empty to delegate to use of AccessController
    // ideally AccessController should be the source of truth
    private final SecurityAuth delegateToAccessController = new SecurityAuth() {
diff --git 
a/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/impl/SecurityStoreImplTest.java
 
b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/impl/SecurityStoreImplTest.java
index 558c4764e8..9aea05f044 100644
--- 
a/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/impl/SecurityStoreImplTest.java
+++ 
b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/impl/SecurityStoreImplTest.java
@@ -23,10 +23,14 @@ import java.util.concurrent.Callable;
 
 import org.apache.activemq.artemis.api.core.ActiveMQSecurityException;
 import org.apache.activemq.artemis.api.core.SimpleString;
+import org.apache.activemq.artemis.api.core.management.CoreNotificationType;
+import org.apache.activemq.artemis.api.core.management.ManagementHelper;
 import 
org.apache.activemq.artemis.core.management.impl.ManagementRemotingConnection;
 import org.apache.activemq.artemis.core.security.CheckType;
 import org.apache.activemq.artemis.core.security.Role;
 import org.apache.activemq.artemis.core.security.SecurityAuth;
+import org.apache.activemq.artemis.core.server.management.Notification;
+import org.apache.activemq.artemis.core.server.management.NotificationService;
 import 
org.apache.activemq.artemis.core.settings.impl.HierarchicalObjectRepository;
 import org.apache.activemq.artemis.logs.AssertionLoggerHandler;
 import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
@@ -36,6 +40,7 @@ import 
org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal;
 import org.apache.activemq.artemis.utils.RandomUtil;
 import org.apache.activemq.artemis.utils.sm.SecurityManagerShim;
 import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
 import org.mockito.ArgumentMatchers;
 import org.mockito.Mockito;
 
@@ -228,4 +233,904 @@ public class SecurityStoreImplTest {
          assertFalse(handler.findText("AMQ224163"));
       }
    }
+
+   @Test
+   public void testHasPermissionSecurityDisabled() throws Exception {
+      SecurityStoreImpl securityStore = new SecurityStoreImpl(new 
HierarchicalObjectRepository<>(), securityManager, 999, false, "", null, null, 
0, 0);
+
+      SecurityAuth session = new SecurityAuth() {
+         @Override
+         public String getUsername() {
+            return "user";
+         }
+
+         @Override
+         public String getPassword() {
+            return "pass";
+         }
+
+         @Override
+         public RemotingConnection getRemotingConnection() {
+            return null;
+         }
+
+         @Override
+         public String getSecurityDomain() {
+            return null;
+         }
+      };
+
+      assertTrue(securityStore.hasPermission(SimpleString.of("test.address"), 
null, CheckType.SEND, session));
+   }
+
+   @Test
+   public void testHasPermissionClusterUser() throws Exception {
+      final String clusterUser = "clusterUser";
+      final String clusterPassword = "clusterPassword";
+      SecurityStoreImpl securityStore = new SecurityStoreImpl(new 
HierarchicalObjectRepository<>(), securityManager, 999, true, clusterUser, 
clusterPassword, null, 0, 0);
+
+      SecurityAuth session = new SecurityAuth() {
+         @Override
+         public String getUsername() {
+            return clusterUser;
+         }
+
+         @Override
+         public String getPassword() {
+            return clusterPassword;
+         }
+
+         @Override
+         public RemotingConnection getRemotingConnection() {
+            return null;
+         }
+
+         @Override
+         public String getSecurityDomain() {
+            return null;
+         }
+      };
+
+      assertTrue(securityStore.hasPermission(SimpleString.of("test.address"), 
null, CheckType.SEND, session));
+   }
+
+   @Test
+   public void testHasPermissionAuthenticationFailure() throws Exception {
+      ActiveMQSecurityManager5 nullSubjectManager = new 
ActiveMQSecurityManager5() {
+         @Override
+         public Subject authenticate(String user, String password, 
RemotingConnection remotingConnection, String securityDomain) {
+            return null; // Simulate authentication failure
+         }
+
+         @Override
+         public boolean authorize(Subject subject, Set<Role> roles, CheckType 
checkType, String address) {
+            return true;
+         }
+
+         @Override
+         public boolean validateUser(String user, String password) {
+            return false;
+         }
+
+         @Override
+         public boolean validateUserAndRole(String user, String password, 
Set<Role> roles, CheckType checkType) {
+            return false;
+         }
+      };
+
+      SecurityStoreImpl securityStore = new SecurityStoreImpl(new 
HierarchicalObjectRepository<>(), nullSubjectManager, 999, true, "", null, 
null, 0, 0);
+
+      SecurityAuth session = new SecurityAuth() {
+         @Override
+         public String getUsername() {
+            return "user";
+         }
+
+         @Override
+         public String getPassword() {
+            return "pass";
+         }
+
+         @Override
+         public RemotingConnection getRemotingConnection() {
+            return Mockito.mock(RemotingConnection.class);
+         }
+
+         @Override
+         public String getSecurityDomain() {
+            return null;
+         }
+      };
+
+      try {
+         securityStore.hasPermission(SimpleString.of("test.address"), null, 
CheckType.SEND, session);
+         fail("Should throw authentication exception");
+      } catch (ActiveMQSecurityException e) {
+         // Expected
+         assertEquals(1, securityStore.getAuthenticationFailureCount());
+      }
+   }
+
+   @Test
+   public void testHasPermissionAuthorized() throws Exception {
+      SecurityStoreImpl securityStore = new SecurityStoreImpl(new 
HierarchicalObjectRepository<>(), securityManager, 999, true, "", null, null, 
10, 10);
+
+      final String user = "authorizedUser";
+      securityStore.authenticate(user, "password", 
Mockito.mock(RemotingConnection.class));
+
+      SecurityAuth session = new SecurityAuth() {
+         @Override
+         public String getUsername() {
+            return user;
+         }
+
+         @Override
+         public String getPassword() {
+            return "password";
+         }
+
+         @Override
+         public RemotingConnection getRemotingConnection() {
+            return Mockito.mock(RemotingConnection.class);
+         }
+
+         @Override
+         public String getSecurityDomain() {
+            return null;
+         }
+      };
+
+      assertTrue(securityStore.hasPermission(SimpleString.of("test.address"), 
null, CheckType.SEND, session));
+   }
+
+   @Test
+   public void testHasPermissionNotAuthorized() throws Exception {
+      ActiveMQSecurityManager5 denyingManager = new ActiveMQSecurityManager5() 
{
+         @Override
+         public Subject authenticate(String user, String password, 
RemotingConnection remotingConnection, String securityDomain) {
+            Subject subject = new Subject();
+            subject.getPrincipals().add(new UserPrincipal(user));
+            return subject;
+         }
+
+         @Override
+         public boolean authorize(Subject subject, Set<Role> roles, CheckType 
checkType, String address) {
+            return false; // Deny authorization
+         }
+
+         @Override
+         public boolean validateUser(String user, String password) {
+            return false;
+         }
+
+         @Override
+         public boolean validateUserAndRole(String user, String password, 
Set<Role> roles, CheckType checkType) {
+            return false;
+         }
+      };
+
+      SecurityStoreImpl securityStore = new SecurityStoreImpl(new 
HierarchicalObjectRepository<>(), denyingManager, 999, true, "", null, null, 
10, 10);
+
+      final String user = "unauthorizedUser";
+      securityStore.authenticate(user, "password", 
Mockito.mock(RemotingConnection.class));
+
+      SecurityAuth session = new SecurityAuth() {
+         @Override
+         public String getUsername() {
+            return user;
+         }
+
+         @Override
+         public String getPassword() {
+            return "password";
+         }
+
+         @Override
+         public RemotingConnection getRemotingConnection() {
+            return Mockito.mock(RemotingConnection.class);
+         }
+
+         @Override
+         public String getSecurityDomain() {
+            return null;
+         }
+      };
+
+      assertFalse(securityStore.hasPermission(SimpleString.of("test.address"), 
null, CheckType.SEND, session));
+   }
+
+   @Test
+   public void testHasPermissionUsesCache() throws Exception {
+      SecurityStoreImpl securityStore = new SecurityStoreImpl(new 
HierarchicalObjectRepository<>(), securityManager, 999, true, "", null, null, 
10, 10);
+
+      final String user = "cachedUser";
+      RemotingConnection connection = Mockito.mock(RemotingConnection.class);
+      securityStore.authenticate(user, "password", connection);
+
+      SecurityAuth session = new SecurityAuth() {
+         @Override
+         public String getUsername() {
+            return user;
+         }
+
+         @Override
+         public String getPassword() {
+            return "password";
+         }
+
+         @Override
+         public RemotingConnection getRemotingConnection() {
+            return connection;
+         }
+
+         @Override
+         public String getSecurityDomain() {
+            return null;
+         }
+      };
+
+      SimpleString address = SimpleString.of("test.address");
+
+      // First call - should authorize and cache
+      assertTrue(securityStore.hasPermission(address, null, CheckType.SEND, 
session));
+      assertEquals(1, securityStore.getAuthorizationCacheSize());
+
+      // Second call - should use cache
+      assertTrue(securityStore.hasPermission(address, null, CheckType.SEND, 
session));
+      assertEquals(1, securityStore.getAuthorizationCacheSize());
+   }
+
+   @Test
+   public void testHasPermissionNoSideEffects() throws Exception {
+      ActiveMQSecurityManager5 denyingManager = new ActiveMQSecurityManager5() 
{
+         @Override
+         public Subject authenticate(String user, String password, 
RemotingConnection remotingConnection, String securityDomain) {
+            Subject subject = new Subject();
+            subject.getPrincipals().add(new UserPrincipal(user));
+            return subject;
+         }
+
+         @Override
+         public boolean authorize(Subject subject, Set<Role> roles, CheckType 
checkType, String address) {
+            return false; // Deny
+         }
+
+         @Override
+         public boolean validateUser(String user, String password) {
+            return false;
+         }
+
+         @Override
+         public boolean validateUserAndRole(String user, String password, 
Set<Role> roles, CheckType checkType) {
+            return false;
+         }
+      };
+
+      SecurityStoreImpl securityStore = new SecurityStoreImpl(new 
HierarchicalObjectRepository<>(), denyingManager, 999, true, "", null, null, 
10, 10);
+
+      final String user = "deniedUser";
+      securityStore.authenticate(user, "password", 
Mockito.mock(RemotingConnection.class));
+
+      SecurityAuth session = new SecurityAuth() {
+         @Override
+         public String getUsername() {
+            return user;
+         }
+
+         @Override
+         public String getPassword() {
+            return "password";
+         }
+
+         @Override
+         public RemotingConnection getRemotingConnection() {
+            return Mockito.mock(RemotingConnection.class);
+         }
+
+         @Override
+         public String getSecurityDomain() {
+            return null;
+         }
+      };
+
+      // hasPermission should return false without incrementing failure counter
+      long initialFailureCount = securityStore.getAuthorizationFailureCount();
+      assertFalse(securityStore.hasPermission(SimpleString.of("test.address"), 
null, CheckType.SEND, session));
+      assertEquals(initialFailureCount, 
securityStore.getAuthorizationFailureCount(), "hasPermission should not 
increment failure counter");
+   }
+
+   @Test
+   public void testCheckSecurityDisabled() throws Exception {
+      SecurityStoreImpl securityStore = new SecurityStoreImpl(new 
HierarchicalObjectRepository<>(), securityManager, 999, false, "", null, null, 
0, 0);
+
+      SecurityAuth session = new SecurityAuth() {
+         @Override
+         public String getUsername() {
+            return "user";
+         }
+
+         @Override
+         public String getPassword() {
+            return "pass";
+         }
+
+         @Override
+         public RemotingConnection getRemotingConnection() {
+            return null;
+         }
+
+         @Override
+         public String getSecurityDomain() {
+            return null;
+         }
+      };
+
+      // Should not throw exception when security is disabled
+      securityStore.check(SimpleString.of("test.address"), null, 
CheckType.SEND, session);
+      assertEquals(0, securityStore.getAuthorizationSuccessCount());
+      assertEquals(0, securityStore.getAuthorizationFailureCount());
+   }
+
+   @Test
+   public void testCheckAuthorizedIncrementsSuccessCounter() throws Exception {
+      SecurityStoreImpl securityStore = new SecurityStoreImpl(new 
HierarchicalObjectRepository<>(), securityManager, 999, true, "", null, null, 
10, 10);
+
+      final String user = "authorizedUser";
+      RemotingConnection connection = Mockito.mock(RemotingConnection.class);
+      securityStore.authenticate(user, "password", connection);
+
+      SecurityAuth session = new SecurityAuth() {
+         @Override
+         public String getUsername() {
+            return user;
+         }
+
+         @Override
+         public String getPassword() {
+            return "password";
+         }
+
+         @Override
+         public RemotingConnection getRemotingConnection() {
+            return connection;
+         }
+
+         @Override
+         public String getSecurityDomain() {
+            return null;
+         }
+      };
+
+      long initialSuccessCount = securityStore.getAuthorizationSuccessCount();
+      securityStore.check(SimpleString.of("test.address"), null, 
CheckType.SEND, session);
+      assertEquals(initialSuccessCount + 1, 
securityStore.getAuthorizationSuccessCount(), "check should increment success 
counter");
+      assertEquals(0, securityStore.getAuthorizationFailureCount());
+   }
+
+   @Test
+   public void testCheckDeniedThrowsException() throws Exception {
+      ActiveMQSecurityManager5 denyingManager = new ActiveMQSecurityManager5() 
{
+         @Override
+         public Subject authenticate(String user, String password, 
RemotingConnection remotingConnection, String securityDomain) {
+            Subject subject = new Subject();
+            subject.getPrincipals().add(new UserPrincipal(user));
+            return subject;
+         }
+
+         @Override
+         public boolean authorize(Subject subject, Set<Role> roles, CheckType 
checkType, String address) {
+            return false; // Deny authorization
+         }
+
+         @Override
+         public boolean validateUser(String user, String password) {
+            return false;
+         }
+
+         @Override
+         public boolean validateUserAndRole(String user, String password, 
Set<Role> roles, CheckType checkType) {
+            return false;
+         }
+      };
+
+      SecurityStoreImpl securityStore = new SecurityStoreImpl(new 
HierarchicalObjectRepository<>(), denyingManager, 999, true, "", null, null, 
10, 10);
+
+      final String user = "deniedUser";
+      RemotingConnection connection = Mockito.mock(RemotingConnection.class);
+      securityStore.authenticate(user, "password", connection);
+
+      SecurityAuth session = new SecurityAuth() {
+         @Override
+         public String getUsername() {
+            return user;
+         }
+
+         @Override
+         public String getPassword() {
+            return "password";
+         }
+
+         @Override
+         public RemotingConnection getRemotingConnection() {
+            return connection;
+         }
+
+         @Override
+         public String getSecurityDomain() {
+            return null;
+         }
+      };
+
+      SimpleString address = SimpleString.of("test.address");
+      long initialFailureCount = securityStore.getAuthorizationFailureCount();
+
+      try {
+         securityStore.check(address, null, CheckType.SEND, session);
+         fail("Should throw ActiveMQSecurityException");
+      } catch (ActiveMQSecurityException e) {
+         // Expected
+         assertTrue(e.getMessage().contains(user), "Exception should contain 
username");
+         assertTrue(e.getMessage().contains(address.toString()), "Exception 
should contain address");
+         assertTrue(e.getMessage().contains("SEND"), "Exception should contain 
check type");
+         assertEquals(initialFailureCount + 1, 
securityStore.getAuthorizationFailureCount(), "check should increment failure 
counter");
+      }
+   }
+
+   @Test
+   public void testCheckDeniedWithQueueThrowsCorrectException() throws 
Exception {
+      ActiveMQSecurityManager5 denyingManager = new ActiveMQSecurityManager5() 
{
+         @Override
+         public Subject authenticate(String user, String password, 
RemotingConnection remotingConnection, String securityDomain) {
+            Subject subject = new Subject();
+            subject.getPrincipals().add(new UserPrincipal(user));
+            return subject;
+         }
+
+         @Override
+         public boolean authorize(Subject subject, Set<Role> roles, CheckType 
checkType, String address) {
+            return false;
+         }
+
+         @Override
+         public boolean validateUser(String user, String password) {
+            return false;
+         }
+
+         @Override
+         public boolean validateUserAndRole(String user, String password, 
Set<Role> roles, CheckType checkType) {
+            return false;
+         }
+      };
+
+      SecurityStoreImpl securityStore = new SecurityStoreImpl(new 
HierarchicalObjectRepository<>(), denyingManager, 999, true, "", null, null, 
10, 10);
+
+      final String user = "deniedUser";
+      RemotingConnection connection = Mockito.mock(RemotingConnection.class);
+      securityStore.authenticate(user, "password", connection);
+
+      SecurityAuth session = new SecurityAuth() {
+         @Override
+         public String getUsername() {
+            return user;
+         }
+
+         @Override
+         public String getPassword() {
+            return "password";
+         }
+
+         @Override
+         public RemotingConnection getRemotingConnection() {
+            return connection;
+         }
+
+         @Override
+         public String getSecurityDomain() {
+            return null;
+         }
+      };
+
+      SimpleString address = SimpleString.of("test.address");
+      SimpleString queue = SimpleString.of("test.queue");
+
+      try {
+         securityStore.check(address, queue, CheckType.CONSUME, session);
+         fail("Should throw ActiveMQSecurityException");
+      } catch (ActiveMQSecurityException e) {
+         // Expected
+         assertTrue(e.getMessage().contains(user), "Exception should contain 
username");
+         assertTrue(e.getMessage().contains(queue.toString()), "Exception 
should contain queue name");
+         assertTrue(e.getMessage().contains(address.toString()), "Exception 
should contain address");
+         assertTrue(e.getMessage().contains("CONSUME"), "Exception should 
contain check type");
+      }
+   }
+
+   @Test
+   public void testCheckDelegatesClusterUserBypass() throws Exception {
+      final String clusterUser = "clusterUser";
+      final String clusterPassword = "clusterPassword";
+      SecurityStoreImpl securityStore = new SecurityStoreImpl(new 
HierarchicalObjectRepository<>(), securityManager, 999, true, clusterUser, 
clusterPassword, null, 0, 0);
+
+      SecurityAuth session = new SecurityAuth() {
+         @Override
+         public String getUsername() {
+            return clusterUser;
+         }
+
+         @Override
+         public String getPassword() {
+            return clusterPassword;
+         }
+
+         @Override
+         public RemotingConnection getRemotingConnection() {
+            return null;
+         }
+
+         @Override
+         public String getSecurityDomain() {
+            return null;
+         }
+      };
+
+      long initialSuccessCount = securityStore.getAuthorizationSuccessCount();
+      securityStore.check(SimpleString.of("test.address"), null, 
CheckType.SEND, session);
+      assertEquals(initialSuccessCount + 1, 
securityStore.getAuthorizationSuccessCount(), "Cluster user should increment 
success counter");
+   }
+
+   @Test
+   public void testCheckCachesSuccessfulAuthorization() throws Exception {
+      SecurityStoreImpl securityStore = new SecurityStoreImpl(new 
HierarchicalObjectRepository<>(), securityManager, 999, true, "", null, null, 
10, 10);
+
+      final String user = "cachedUser";
+      RemotingConnection connection = Mockito.mock(RemotingConnection.class);
+      securityStore.authenticate(user, "password", connection);
+
+      SecurityAuth session = new SecurityAuth() {
+         @Override
+         public String getUsername() {
+            return user;
+         }
+
+         @Override
+         public String getPassword() {
+            return "password";
+         }
+
+         @Override
+         public RemotingConnection getRemotingConnection() {
+            return connection;
+         }
+
+         @Override
+         public String getSecurityDomain() {
+            return null;
+         }
+      };
+
+      SimpleString address = SimpleString.of("test.address");
+
+      // First check should cache
+      securityStore.check(address, null, CheckType.SEND, session);
+      assertEquals(1, securityStore.getAuthorizationCacheSize());
+
+      // Second check should use cache
+      securityStore.check(address, null, CheckType.SEND, session);
+      assertEquals(1, securityStore.getAuthorizationCacheSize());
+   }
+
+   @Test
+   public void testCheckSendsNotificationOnFailure() throws Exception {
+      ActiveMQSecurityManager5 denyingManager = new ActiveMQSecurityManager5() 
{
+         @Override
+         public Subject authenticate(String user, String password, 
RemotingConnection remotingConnection, String securityDomain) {
+            Subject subject = new Subject();
+            subject.getPrincipals().add(new UserPrincipal(user));
+            return subject;
+         }
+
+         @Override
+         public boolean authorize(Subject subject, Set<Role> roles, CheckType 
checkType, String address) {
+            return false; // Deny
+         }
+
+         @Override
+         public boolean validateUser(String user, String password) {
+            return false;
+         }
+
+         @Override
+         public boolean validateUserAndRole(String user, String password, 
Set<Role> roles, CheckType checkType) {
+            return false;
+         }
+      };
+
+      NotificationService notificationService = 
Mockito.mock(NotificationService.class);
+      SecurityStoreImpl securityStore = new SecurityStoreImpl(new 
HierarchicalObjectRepository<>(), denyingManager, 999, true, "", null, 
notificationService, 10, 10);
+
+      final String user = "deniedUser";
+      RemotingConnection connection = Mockito.mock(RemotingConnection.class);
+      Mockito.when(connection.getSubject()).thenReturn(new Subject());
+      securityStore.authenticate(user, "password", connection);
+
+      SecurityAuth session = new SecurityAuth() {
+         @Override
+         public String getUsername() {
+            return user;
+         }
+
+         @Override
+         public String getPassword() {
+            return "password";
+         }
+
+         @Override
+         public RemotingConnection getRemotingConnection() {
+            return connection;
+         }
+
+         @Override
+         public String getSecurityDomain() {
+            return null;
+         }
+      };
+
+      SimpleString address = SimpleString.of("test.address");
+
+      try {
+         securityStore.check(address, null, CheckType.SEND, session);
+         fail("Should throw ActiveMQSecurityException");
+      } catch (ActiveMQSecurityException e) {
+         // Expected
+      }
+
+      // Verify notification was sent
+      ArgumentCaptor<Notification> notificationCaptor = 
ArgumentCaptor.forClass(Notification.class);
+      Mockito.verify(notificationService, 
Mockito.times(1)).sendNotification(notificationCaptor.capture());
+
+      Notification notification = notificationCaptor.getValue();
+      assertEquals(CoreNotificationType.SECURITY_PERMISSION_VIOLATION, 
notification.getType());
+      assertEquals(address, 
notification.getProperties().getSimpleStringProperty(ManagementHelper.HDR_ADDRESS));
+      assertEquals(SimpleString.of(CheckType.SEND.toString()), 
notification.getProperties().getSimpleStringProperty(ManagementHelper.HDR_CHECK_TYPE));
+      assertEquals(SimpleString.of(user), 
notification.getProperties().getSimpleStringProperty(ManagementHelper.HDR_USER));
+   }
+
+   @Test
+   public void testCheckSendsNotificationWithQueueInfo() throws Exception {
+      ActiveMQSecurityManager5 denyingManager = new ActiveMQSecurityManager5() 
{
+         @Override
+         public Subject authenticate(String user, String password, 
RemotingConnection remotingConnection, String securityDomain) {
+            Subject subject = new Subject();
+            subject.getPrincipals().add(new UserPrincipal(user));
+            return subject;
+         }
+
+         @Override
+         public boolean authorize(Subject subject, Set<Role> roles, CheckType 
checkType, String address) {
+            return false;
+         }
+
+         @Override
+         public boolean validateUser(String user, String password) {
+            return false;
+         }
+
+         @Override
+         public boolean validateUserAndRole(String user, String password, 
Set<Role> roles, CheckType checkType) {
+            return false;
+         }
+      };
+
+      NotificationService notificationService = 
Mockito.mock(NotificationService.class);
+      SecurityStoreImpl securityStore = new SecurityStoreImpl(new 
HierarchicalObjectRepository<>(), denyingManager, 999, true, "", null, 
notificationService, 10, 10);
+
+      final String user = "deniedUser";
+      RemotingConnection connection = Mockito.mock(RemotingConnection.class);
+      Mockito.when(connection.getSubject()).thenReturn(new Subject());
+      securityStore.authenticate(user, "password", connection);
+
+      SecurityAuth session = new SecurityAuth() {
+         @Override
+         public String getUsername() {
+            return user;
+         }
+
+         @Override
+         public String getPassword() {
+            return "password";
+         }
+
+         @Override
+         public RemotingConnection getRemotingConnection() {
+            return connection;
+         }
+
+         @Override
+         public String getSecurityDomain() {
+            return null;
+         }
+      };
+
+      SimpleString address = SimpleString.of("test.address");
+
+      try {
+         securityStore.check(address, null, CheckType.CONSUME, session);
+         fail("Should throw ActiveMQSecurityException");
+      } catch (ActiveMQSecurityException e) {
+         // Expected
+      }
+
+      // Verify notification was sent with correct check type
+      ArgumentCaptor<Notification> notificationCaptor = 
ArgumentCaptor.forClass(Notification.class);
+      Mockito.verify(notificationService, 
Mockito.times(1)).sendNotification(notificationCaptor.capture());
+
+      Notification notification = notificationCaptor.getValue();
+      assertEquals(CoreNotificationType.SECURITY_PERMISSION_VIOLATION, 
notification.getType());
+      assertEquals(SimpleString.of(CheckType.CONSUME.toString()), 
notification.getProperties().getSimpleStringProperty(ManagementHelper.HDR_CHECK_TYPE));
+   }
+
+   @Test
+   public void testCheckNoNotificationWhenServiceIsNull() throws Exception {
+      ActiveMQSecurityManager5 denyingManager = new ActiveMQSecurityManager5() 
{
+         @Override
+         public Subject authenticate(String user, String password, 
RemotingConnection remotingConnection, String securityDomain) {
+            Subject subject = new Subject();
+            subject.getPrincipals().add(new UserPrincipal(user));
+            return subject;
+         }
+
+         @Override
+         public boolean authorize(Subject subject, Set<Role> roles, CheckType 
checkType, String address) {
+            return false;
+         }
+
+         @Override
+         public boolean validateUser(String user, String password) {
+            return false;
+         }
+
+         @Override
+         public boolean validateUserAndRole(String user, String password, 
Set<Role> roles, CheckType checkType) {
+            return false;
+         }
+      };
+
+      // notificationService is null
+      SecurityStoreImpl securityStore = new SecurityStoreImpl(new 
HierarchicalObjectRepository<>(), denyingManager, 999, true, "", null, null, 
10, 10);
+
+      final String user = "deniedUser";
+      RemotingConnection connection = Mockito.mock(RemotingConnection.class);
+      Mockito.when(connection.getSubject()).thenReturn(new Subject());
+      securityStore.authenticate(user, "password", connection);
+
+      SecurityAuth session = new SecurityAuth() {
+         @Override
+         public String getUsername() {
+            return user;
+         }
+
+         @Override
+         public String getPassword() {
+            return "password";
+         }
+
+         @Override
+         public RemotingConnection getRemotingConnection() {
+            return connection;
+         }
+
+         @Override
+         public String getSecurityDomain() {
+            return null;
+         }
+      };
+
+      try {
+         securityStore.check(SimpleString.of("test.address"), null, 
CheckType.SEND, session);
+         fail("Should throw ActiveMQSecurityException");
+      } catch (ActiveMQSecurityException e) {
+         // Expected - should still throw exception even though no 
notification sent
+         assertTrue(e.getMessage().contains(user));
+      }
+   }
+
+   @Test
+   public void testHasPermissionDoesNotSendNotification() throws Exception {
+      ActiveMQSecurityManager5 denyingManager = new ActiveMQSecurityManager5() 
{
+         @Override
+         public Subject authenticate(String user, String password, 
RemotingConnection remotingConnection, String securityDomain) {
+            Subject subject = new Subject();
+            subject.getPrincipals().add(new UserPrincipal(user));
+            return subject;
+         }
+
+         @Override
+         public boolean authorize(Subject subject, Set<Role> roles, CheckType 
checkType, String address) {
+            return false; // Deny
+         }
+
+         @Override
+         public boolean validateUser(String user, String password) {
+            return false;
+         }
+
+         @Override
+         public boolean validateUserAndRole(String user, String password, 
Set<Role> roles, CheckType checkType) {
+            return false;
+         }
+      };
+
+      NotificationService notificationService = 
Mockito.mock(NotificationService.class);
+      SecurityStoreImpl securityStore = new SecurityStoreImpl(new 
HierarchicalObjectRepository<>(), denyingManager, 999, true, "", null, 
notificationService, 10, 10);
+
+      final String user = "deniedUser";
+      RemotingConnection connection = Mockito.mock(RemotingConnection.class);
+      securityStore.authenticate(user, "password", connection);
+
+      SecurityAuth session = new SecurityAuth() {
+         @Override
+         public String getUsername() {
+            return user;
+         }
+
+         @Override
+         public String getPassword() {
+            return "password";
+         }
+
+         @Override
+         public RemotingConnection getRemotingConnection() {
+            return connection;
+         }
+
+         @Override
+         public String getSecurityDomain() {
+            return null;
+         }
+      };
+
+      // Call hasPermission - should return false
+      assertFalse(securityStore.hasPermission(SimpleString.of("test.address"), 
null, CheckType.SEND, session));
+
+      // Verify NO notification was sent (hasPermission has no side effects)
+      Mockito.verify(notificationService, 
Mockito.never()).sendNotification(ArgumentMatchers.any());
+   }
+
+   @Test
+   public void testCheckNoNotificationOnSuccess() throws Exception {
+      NotificationService notificationService = 
Mockito.mock(NotificationService.class);
+      SecurityStoreImpl securityStore = new SecurityStoreImpl(new 
HierarchicalObjectRepository<>(), securityManager, 999, true, "", null, 
notificationService, 10, 10);
+
+      final String user = "authorizedUser";
+      RemotingConnection connection = Mockito.mock(RemotingConnection.class);
+      securityStore.authenticate(user, "password", connection);
+
+      SecurityAuth session = new SecurityAuth() {
+         @Override
+         public String getUsername() {
+            return user;
+         }
+
+         @Override
+         public String getPassword() {
+            return "password";
+         }
+
+         @Override
+         public RemotingConnection getRemotingConnection() {
+            return connection;
+         }
+
+         @Override
+         public String getSecurityDomain() {
+            return null;
+         }
+      };
+
+      // Successful check
+      securityStore.check(SimpleString.of("test.address"), null, 
CheckType.SEND, session);
+
+      // Verify NO notification was sent on success
+      Mockito.verify(notificationService, 
Mockito.never()).sendNotification(ArgumentMatchers.any());
+   }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to