Skip to content

Commit a04adbd

Browse files
committed
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
1 parent 9e60cbb commit a04adbd

File tree

4 files changed

+1007
-74
lines changed

4 files changed

+1007
-74
lines changed

artemis-server/src/main/java/org/apache/activemq/artemis/core/security/SecurityStore.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ public interface SecurityStore {
3030

3131
void check(SimpleString address, SimpleString queue, CheckType checkType, SecurityAuth session) throws Exception;
3232

33+
boolean hasPermission(SimpleString address, SimpleString queue, CheckType checkType, SecurityAuth session) throws Exception;
34+
3335
boolean isSecurityEnabled();
3436

3537
void setSecurityEnabled(boolean securityEnabled);

artemis-server/src/main/java/org/apache/activemq/artemis/core/security/impl/SecurityStoreImpl.java

Lines changed: 87 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -297,22 +297,42 @@ public void check(final SimpleString address,
297297
}
298298

299299
@Override
300-
public void check(final SimpleString address,
301-
final SimpleString queue,
302-
final CheckType checkType,
303-
final SecurityAuth session) throws Exception {
304-
if (securityEnabled) {
300+
public boolean hasPermission(final SimpleString address,
301+
final SimpleString queue,
302+
final CheckType checkType,
303+
final SecurityAuth session) throws Exception {
304+
if (!securityEnabled) {
305+
return true;
306+
}
307+
308+
// bypass permission checks for management cluster user
309+
String user = session.getUsername();
310+
if (managementClusterUser.equals(user) && session.getPassword().equals(managementClusterPassword)) {
311+
return true;
312+
}
313+
314+
// Special case: detect authentication failure for ActiveMQSecurityManager5
315+
// This must throw an authentication exception, not an authorization exception
316+
Subject subjectForManager5 = null;
317+
if (securityManager instanceof ActiveMQSecurityManager5 manager5) {
318+
subjectForManager5 = getSubjectForAuthorization(session, manager5);
319+
/*
320+
* A user may authenticate successfully at first, but then later when their Subject is evicted from the
321+
* local cache re-authentication may fail. This could happen, for example, if the user was removed from LDAP
322+
* or the user's token expired.
323+
*
324+
* If the subject is null then authorization will *always* fail.
325+
*/
326+
if (subjectForManager5 == null) {
327+
authenticationFailed(session.getUsername(), session.getRemotingConnection());
328+
}
329+
}
330+
try {
305331
SimpleString bareAddress = CompositeAddress.extractAddressName(address);
306332
SimpleString bareQueue = CompositeAddress.extractQueueName(queue);
307333

308334
logger.trace("checking access permissions to {}", bareAddress);
309335

310-
// bypass permission checks for management cluster user
311-
String user = session.getUsername();
312-
if (managementClusterUser.equals(user) && session.getPassword().equals(managementClusterPassword)) {
313-
AUTHORIZATION_SUCCESS_COUNT_UPDATER.incrementAndGet(this);
314-
return;
315-
}
316336

317337
Set<Role> roles = securityRepository.getMatch(bareAddress.toString());
318338

@@ -325,27 +345,14 @@ public void check(final SimpleString address,
325345
}
326346
}
327347

328-
if (checkAuthorizationCache(fqqn != null ? fqqn : bareAddress, user, checkType)) {
329-
AUTHORIZATION_SUCCESS_COUNT_UPDATER.incrementAndGet(this);
330-
return;
348+
if (checkAuthorizationCache(fqqn != null ? fqqn : bareAddress, user, checkType)) {
349+
return true;
331350
}
332351

333352
final Boolean validated;
334353
if (securityManager instanceof ActiveMQSecurityManager5 manager5) {
335-
Subject subject = getSubjectForAuthorization(session, manager5);
336-
337-
/*
338-
* A user may authenticate successfully at first, but then later when their Subject is evicted from the
339-
* local cache re-authentication may fail. This could happen, for example, if the user was removed from LDAP
340-
* or the user's token expired.
341-
*
342-
* If the subject is null then authorization will *always* fail.
343-
*/
344-
if (subject == null) {
345-
authenticationFailed(user, session.getRemotingConnection());
346-
}
347-
348-
validated = manager5.authorize(subject, roles, checkType, fqqn != null ? fqqn.toString() : bareAddress.toString());
354+
// Reuse subject from earlier authentication check
355+
validated = manager5.authorize(subjectForManager5, roles, checkType, fqqn != null ? fqqn.toString() : bareAddress.toString());
349356
} else if (securityManager instanceof ActiveMQSecurityManager4 manager4) {
350357
validated = manager4.validateUserAndRole(user, session.getPassword(), roles, checkType, bareAddress.toString(), session.getRemotingConnection(), session.getSecurityDomain()) != null;
351358
} else if (securityManager instanceof ActiveMQSecurityManager3 manager3) {
@@ -356,52 +363,67 @@ public void check(final SimpleString address,
356363
validated = securityManager.validateUserAndRole(user, session.getPassword(), roles, checkType);
357364
}
358365

359-
if (!validated) {
360-
if (notificationService != null) {
361-
TypedProperties props = new TypedProperties();
362-
363-
props.putSimpleStringProperty(ManagementHelper.HDR_ADDRESS, bareAddress);
364-
props.putSimpleStringProperty(ManagementHelper.HDR_CHECK_TYPE, SimpleString.of(checkType.toString()));
365-
props.putSimpleStringProperty(ManagementHelper.HDR_USER, SimpleString.of(getCaller(user, session.getRemotingConnection().getSubject())));
366-
367-
Notification notification = new Notification(null, CoreNotificationType.SECURITY_PERMISSION_VIOLATION, props);
368-
369-
notificationService.sendNotification(notification);
370-
}
371-
372-
Exception ex;
373-
if (bareQueue == null) {
374-
ex = ActiveMQMessageBundle.BUNDLE.userNoPermissions(getCaller(user, session.getRemotingConnection().getSubject()), checkType, bareAddress);
366+
if (validated && user != null) {
367+
// if we get here we're granted, add to the cache
368+
ConcurrentHashSet<SimpleString> set;
369+
String key = createAuthorizationCacheKey(user, checkType);
370+
ConcurrentHashSet<SimpleString> act = getAuthorizationCacheEntry(key);
371+
if (act != null) {
372+
set = act;
375373
} else {
376-
ex = ActiveMQMessageBundle.BUNDLE.userNoPermissionsQueue(getCaller(user, session.getRemotingConnection().getSubject()), checkType, bareQueue, bareAddress);
374+
set = new ConcurrentHashSet<>();
375+
putAuthorizationCacheEntry(set, key);
377376
}
378-
AuditLogger.securityFailure(session.getRemotingConnection().getSubject(), session.getRemotingConnection().getRemoteAddress(), ex.getMessage(), ex);
379-
AUTHORIZATION_FAILURE_COUNT_UPDATER.incrementAndGet(this);
380-
throw ex;
377+
set.add(Objects.requireNonNullElse(fqqn, bareAddress));
381378
}
382379

383-
// if we get here we're granted, add to the cache
380+
return validated;
381+
} catch (Exception e) {
382+
logger.debug("Permission check failed", e);
383+
return false;
384+
}
385+
}
386+
387+
@Override
388+
public void check(final SimpleString address,
389+
final SimpleString queue,
390+
final CheckType checkType,
391+
final SecurityAuth session) throws Exception {
392+
if (!securityEnabled) {
393+
return;
394+
}
384395

396+
if (hasPermission(address, queue, checkType, session)) {
385397
AUTHORIZATION_SUCCESS_COUNT_UPDATER.incrementAndGet(this);
398+
return;
399+
}
386400

387-
if (user == null) {
388-
// should get all user/pass into a subject and only cache subjects
389-
// till then when subject is in play, the user may be null and
390-
// we cannot cache as we don't have a unique key
391-
return;
392-
}
401+
// Permission denied - handle side effects
402+
SimpleString bareAddress = CompositeAddress.extractAddressName(address);
403+
SimpleString bareQueue = CompositeAddress.extractQueueName(queue);
404+
String user = session.getUsername();
393405

394-
ConcurrentHashSet<SimpleString> set;
395-
String key = createAuthorizationCacheKey(user, checkType);
396-
ConcurrentHashSet<SimpleString> act = getAuthorizationCacheEntry(key);
397-
if (act != null) {
398-
set = act;
399-
} else {
400-
set = new ConcurrentHashSet<>();
401-
putAuthorizationCacheEntry(set, key);
402-
}
403-
set.add(Objects.requireNonNullElse(fqqn, bareAddress));
406+
if (notificationService != null) {
407+
TypedProperties props = new TypedProperties();
408+
409+
props.putSimpleStringProperty(ManagementHelper.HDR_ADDRESS, bareAddress);
410+
props.putSimpleStringProperty(ManagementHelper.HDR_CHECK_TYPE, SimpleString.of(checkType.toString()));
411+
props.putSimpleStringProperty(ManagementHelper.HDR_USER, SimpleString.of(getCaller(user, session.getRemotingConnection().getSubject())));
412+
413+
Notification notification = new Notification(null, CoreNotificationType.SECURITY_PERMISSION_VIOLATION, props);
414+
415+
notificationService.sendNotification(notification);
416+
}
417+
418+
Exception ex;
419+
if (bareQueue == null) {
420+
ex = ActiveMQMessageBundle.BUNDLE.userNoPermissions(getCaller(user, session.getRemotingConnection().getSubject()), checkType, bareAddress);
421+
} else {
422+
ex = ActiveMQMessageBundle.BUNDLE.userNoPermissionsQueue(getCaller(user, session.getRemotingConnection().getSubject()), checkType, bareQueue, bareAddress);
404423
}
424+
AuditLogger.securityFailure(session.getRemotingConnection().getSubject(), session.getRemotingConnection().getRemoteAddress(), ex.getMessage(), ex);
425+
AUTHORIZATION_FAILURE_COUNT_UPDATER.incrementAndGet(this);
426+
throw ex;
405427
}
406428

407429
@Override

artemis-server/src/main/java/org/apache/activemq/artemis/core/server/management/ArtemisRbacInvocationHandler.java

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -329,24 +329,28 @@ SimpleString addressFrom(ObjectName objectName, String methodName) {
329329
}
330330

331331
private boolean viewPermissionCheckFails(Object candidate) {
332-
boolean failed = false;
333332
ObjectName objectName = candidate instanceof ObjectInstance oi ? oi.getObjectName() : (ObjectName) candidate;
334-
if (!isUncheckedDomain(objectName)) {
335-
try {
336-
final SimpleString rbacAddress = addressFrom(objectName);
337-
securityStoreCheck(rbacAddress, CheckType.VIEW);
338-
} catch (Exception checkFailed) {
339-
failed = true;
340-
}
333+
if (isUncheckedDomain(objectName)) {
334+
return false;
341335
}
342-
return failed;
336+
final SimpleString rbacAddress = addressFrom(objectName);
337+
return !hasPermission(rbacAddress, CheckType.VIEW);
343338
}
344339

345340
private void securityStoreCheck(SimpleString rbacAddress, CheckType checkType) throws Exception {
346341
// use accessor as security store can be updated on config reload
347342
activeMQServer.getSecurityStore().check(rbacAddress, checkType, delegateToAccessController);
348343
}
349344

345+
private boolean hasPermission(SimpleString rbacAddress, CheckType checkType) {
346+
// use accessor as security store can be updated on config reload
347+
try {
348+
return activeMQServer.getSecurityStore().hasPermission(rbacAddress, null, checkType, delegateToAccessController);
349+
} catch (Exception notAuthenticated) {
350+
return false;
351+
}
352+
}
353+
350354
// sufficiently empty to delegate to use of AccessController
351355
// ideally AccessController should be the source of truth
352356
private final SecurityAuth delegateToAccessController = new SecurityAuth() {

0 commit comments

Comments
 (0)