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 4307bbeeb5e..3cd78c687a2 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 c07d7bafef7..0ba198aaa5d 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 void check(final SimpleString address, } @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 roles = securityRepository.getMatch(bareAddress.toString()); @@ -325,27 +345,14 @@ public void check(final SimpleString address, } } - 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 void check(final SimpleString address, 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 set; + String key = createAuthorizationCacheKey(user, checkType); + ConcurrentHashSet 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 set; - String key = createAuthorizationCacheKey(user, checkType); - ConcurrentHashSet 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 9453d90e5bc..40b225ebd4f 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 @@ SimpleString addressFrom(ObjectName objectName, String methodName) { } 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 @@ private void securityStoreCheck(SimpleString rbacAddress, CheckType checkType) t 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 558c4764e8f..9aea05f0448 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 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.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 void testCacheAlgorithm() throws Exception { 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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()); + } }