Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ public class ActiveMQConnection implements Connection, TopicConnection, QueueCon
* This strictly rejects non-standard property types such as Character, Map, and List.
*/
private boolean strictCompliance = false;
// set when the client identifier came from the connection factory configuration
private boolean adminConfiguredClientID = false;

private boolean disableTimeStampsByDefault;
private boolean optimizedMessageDispatch = true;
Expand Down Expand Up @@ -463,6 +465,12 @@ public void setClientID(String newClientID) throws JMSException {
throw new IllegalStateException("The clientID has already been set");
}

// The specification forbids overriding an administratively configured client
// identifier; ActiveMQ has always allowed it, so enforce under strict compliance.
if (this.adminConfiguredClientID && this.strictCompliance) {
throw new IllegalStateException("The clientID was administratively configured and cannot be changed");
}

if (this.isConnectionInfoSentToBroker) {
throw new IllegalStateException("Setting clientID on a used Connection is not allowed");
}
Expand All @@ -479,6 +487,7 @@ public void setClientID(String newClientID) throws JMSException {
public void setDefaultClientID(String clientID) throws JMSException {
this.info.setClientId(clientID);
this.userSpecifiedClientID = true;
this.adminConfiguredClientID = true;
}

/**
Expand Down Expand Up @@ -1566,6 +1575,21 @@ protected synchronized void checkClosed() throws JMSException {
}
}

/**
* Validates this connection's credentials with the broker using a throwaway
* ConnectionInfo that is removed again immediately. The connection's own
* ConnectionInfo is left unsent, so setClientID() remains possible afterwards
* as the specification requires. Used by the factory under strictCompliance
* so createConnection fails fast with JMSSecurityException on bad credentials.
*/
protected void authenticate() throws JMSException {
var probe = info.copy();
probe.setConnectionId(new ConnectionId(info.getConnectionId().getValue() + ":auth"));
probe.setClientId(clientIdGenerator.generateId());
syncSendPacket(probe, getConnectResponseTimeout());
asyncSendPacket(probe.createRemoveCommand());
}

/**
* Send the ConnectionInfo to the Broker
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ public JMSContext createContext(String userName, String password) {
*/
@Override
public JMSContext createContext(String userName, String password, int sessionMode) {
ActiveMQSession.validateSessionMode(sessionMode);
try {
return new ActiveMQContext(createActiveMQConnection(userName, password), sessionMode);
} catch (JMSException e) {
Expand All @@ -320,6 +321,7 @@ public JMSContext createContext(String userName, String password, int sessionMod
*/
@Override
public JMSContext createContext(int sessionMode) {
ActiveMQSession.validateSessionMode(sessionMode);
try {
return new ActiveMQContext(createActiveMQConnection(getUserName(), getPassword()), sessionMode);
} catch (JMSException e) {
Expand Down Expand Up @@ -398,6 +400,15 @@ protected ActiveMQConnection createActiveMQConnection(String userName, String pa
connection.setDefaultClientID(clientID);
}

// Jakarta Messaging expects createConnection/createContext to authenticate
// the caller immediately (JMSSecurityException on bad credentials). ActiveMQ
// historically defers the ConnectionInfo exchange until first use, so the
// eager check is only performed under strictCompliance, and it uses a probe
// so the connection's own identity stays unset for a later setClientID().
if (isStrictCompliance()) {
connection.authenticate();
}

return connection;
} catch (JMSException e) {
// Clean up!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ public JMSContext createContext(int sessionMode) {
if(connectionCounter.get() == 0l) {
throw new JMSRuntimeException("Context already closed");
}
ActiveMQSession.validateSessionMode(sessionMode);

connectionCounter.incrementAndGet();
return new ActiveMQContext(activemqConnection, sessionMode, connectionCounter);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import jakarta.jms.InvalidDestinationException;
import jakarta.jms.InvalidSelectorException;
import jakarta.jms.JMSException;
import jakarta.jms.JMSRuntimeException;
import jakarta.jms.MapMessage;
import jakarta.jms.Message;
import jakarta.jms.MessageConsumer;
Expand Down Expand Up @@ -197,6 +198,17 @@ public class ActiveMQSession implements Session, QueueSession, TopicSession, Sta
public static final int INDIVIDUAL_ACKNOWLEDGE = 4;
public static final int MAX_ACK_CONSTANT = INDIVIDUAL_ACKNOWLEDGE;

/**
* Rejects a JMSContext session mode outside the four Jakarta Messaging modes
* and ActiveMQ's INDIVIDUAL_ACKNOWLEDGE extension, as the specification
* requires a JMSRuntimeException for an invalid mode.
*/
static void validateSessionMode(int sessionMode) {
if (sessionMode < Session.SESSION_TRANSACTED || sessionMode > MAX_ACK_CONSTANT) {
throw new JMSRuntimeException("Invalid session mode: " + sessionMode);
}
}

public static interface DeliveryListener {
void beforeDelivery(ActiveMQSession session, Message msg);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -965,7 +965,10 @@ protected void doCompress() throws IOException {
@Override
@SuppressWarnings("unchecked")
public boolean isBodyAssignableTo(Class c) {
return getContent() == null || c.isAssignableFrom(byte[].class);
// Bytes written in write-only mode sit in bytesOut until stored, so a null
// content alone does not mean the message has no body.
var hasBody = getContent() != null || (bytesOut != null && bytesOut.size() > 0);
return !hasBody || c.isAssignableFrom(byte[].class);
}

@SuppressWarnings("unchecked")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -793,8 +793,9 @@ public void setBytes(String name, byte[] value, int offset, int length) throws J
public void setObject(String name, Object value) throws JMSException {
initializeWriting();
if (value != null) {
// byte[] not allowed on properties
if (!(value instanceof byte[])) {
// byte[] and Character are valid MapMessage body types (MapMessage#setBytes,
// #setChar) but not valid property types, so they bypass the property check
if (!(value instanceof byte[]) && !(value instanceof Character)) {
checkValidObject(value);
}
put(name, value);
Expand Down
5 changes: 5 additions & 0 deletions activemq-tooling/activemq-jakarta-messaging-tck/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-kahadb-store</artifactId>
</dependency>
<dependency>
<!-- SimpleAuthenticationPlugin principals, used to reject bad credentials -->
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-jaas</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
import javax.naming.NamingException;
import javax.naming.spi.InitialContextFactory;

import org.apache.activemq.broker.BrokerPlugin;
import org.apache.activemq.security.AuthenticationUser;
import org.apache.activemq.security.SimpleAuthenticationPlugin;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.command.ActiveMQQueue;
Expand Down Expand Up @@ -97,6 +100,11 @@ public Context getInitialContext(final Hashtable<?, ?> environment) throws Namin
private static ActiveMQConnectionFactory createConnectionFactory(final String clientId) {
final ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(BROKER_URL);
factory.setNestedMapAndListEnabled(false);
// The TCK asserts strict Jakarta Messaging semantics that ActiveMQ relaxes by
// default for backwards compatibility: eager authentication on
// createConnection/createContext, the administratively configured client
// identifier, and the strict message property rules.
factory.setStrictCompliance(true);
if (clientId != null) {
factory.setClientID(clientId);
}
Expand All @@ -117,6 +125,14 @@ private static void ensureBrokerStarted() {
bs.setPersistent(false);
bs.setUseJmx(false);
bs.setAdvisorySupport(false);
// The TCK expects invalid credentials to be rejected (JMSSecurityException
// from createConnection, JMSSecurityRuntimeException from createContext).
// Authenticate the ts.jte user and keep anonymous access for the many
// tests that connect without credentials.
var authentication = new SimpleAuthenticationPlugin(
java.util.List.of(new AuthenticationUser("guest", "guest", "users")));
authentication.setAnonymousAccessAllowed(true);
bs.setPlugins(new BrokerPlugin[] {authentication});
bs.start();
bs.waitUntilStarted();
broker = bs;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.junit.Test;

import jakarta.jms.Connection;
import jakarta.jms.MapMessage;
import jakarta.jms.Message;
import jakarta.jms.MessageFormatException;
import jakarta.jms.Session;
Expand Down Expand Up @@ -108,4 +109,49 @@ public void testLegacyModeStillAllowsCharacter() throws Exception {
assertEquals('A', message.getObjectProperty("charProp"));
}
}

@Test
public void testStrictComplianceAllowsCharacterInMapMessageBody() throws Exception {
var factory = new ActiveMQConnectionFactory(connectionUri);
factory.setStrictCompliance(true);

try (var connection = factory.createConnection();
var session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE)) {

// MapMessage bodies support char per the spec; only message properties
// exclude Character under strict compliance
var message = session.createMapMessage();
message.setChar("charEntry", 'A');
message.setObject("charObject", 'B');
assertEquals('A', message.getChar("charEntry"));
assertEquals('B', message.getObject("charObject"));

try {
message.setObjectProperty("charProp", 'C');
fail("Character properties must still be rejected under strict compliance");
} catch (MessageFormatException expected) {
}
}
}

@Test
public void testGetBodyOnWriteOnlyBytesMessageChecksType() throws Exception {
var factory = new ActiveMQConnectionFactory(connectionUri);

try (var connection = factory.createConnection();
var session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE)) {

var message = session.createBytesMessage();
message.writeBytes(new byte[] {1, 2, 3});

// bytes written but not yet stored are still a body: a non-byte[] type
// must be rejected even while the message is in write-only mode
try {
message.getBody(StringBuffer.class);
fail("Expected MessageFormatException for a non byte[] body type");
} catch (MessageFormatException expected) {
}
assertEquals(3, message.getBody(byte[].class).length);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;

import java.util.ArrayList;

import jakarta.jms.JMSException;
import jakarta.jms.JMSSecurityException;
import jakarta.jms.JMSSecurityRuntimeException;

import org.apache.activemq.broker.BrokerPlugin;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.security.AuthenticationUser;
import org.apache.activemq.security.SimpleAuthenticationPlugin;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/**
* Jakarta Messaging expects createConnection and createContext to authenticate
* the caller immediately. ActiveMQ defers the ConnectionInfo exchange until
* first use, so the eager handshake is enabled by strictCompliance.
*/
public class StrictComplianceAuthenticationTest {

private BrokerService broker;
private String connectionUri;

@Before
public void setUp() throws Exception {
broker = new BrokerService();
broker.setPersistent(false);
broker.setUseJmx(false);
broker.setAdvisorySupport(false);
var users = new ArrayList<AuthenticationUser>();
users.add(new AuthenticationUser("system", "manager", "users,admins"));
broker.setPlugins(new BrokerPlugin[] {new SimpleAuthenticationPlugin(users)});
broker.addConnector("tcp://localhost:0");
broker.start();
broker.waitUntilStarted();
connectionUri = broker.getTransportConnectors().get(0).getPublishableConnectString();
}

@After
public void tearDown() throws Exception {
if (broker != null) {
broker.stop();
broker.waitUntilStopped();
}
}

@Test(timeout = 60000)
public void testStrictCreateConnectionRejectsBadCredentialsImmediately() throws Exception {
var factory = new ActiveMQConnectionFactory(connectionUri);
factory.setStrictCompliance(true);

try {
factory.createConnection("invalid", "credentials");
fail("Expected JMSSecurityException from createConnection");
} catch (JMSSecurityException expected) {
}

try (var connection = factory.createConnection("system", "manager")) {
connection.start();
assertNotNull(connection.getClientID());
}
}

@Test(timeout = 60000)
public void testStrictCreateContextRejectsBadCredentialsImmediately() throws Exception {
var factory = new ActiveMQConnectionFactory(connectionUri);
factory.setStrictCompliance(true);

try {
factory.createContext("invalid", "credentials");
fail("Expected JMSSecurityRuntimeException from createContext");
} catch (JMSSecurityRuntimeException expected) {
}

try (var context = factory.createContext("system", "manager")) {
context.start();
assertNotNull(context.getClientID());
}
}

@Test(timeout = 60000)
public void testStrictCreateConnectionStillAllowsSetClientID() throws Exception {
var factory = new ActiveMQConnectionFactory(connectionUri);
factory.setStrictCompliance(true);

// the spec-mandated sequence: set the client identifier immediately after
// creation, before any other action; eager authentication must not
// consume the connection's identity
try (var connection = factory.createConnection("system", "manager")) {
connection.setClientID("strict-client");
connection.start();
assertEquals("strict-client", connection.getClientID());
}
}

@Test(timeout = 60000)
public void testLegacyCreateConnectionDefersAuthenticationToStart() throws Exception {
var factory = new ActiveMQConnectionFactory(connectionUri);

// Default strictCompliance = false: creation succeeds, the failure surfaces
// when the connection first talks to the broker
var connection = factory.createConnection("invalid", "credentials");
try {
connection.start();
fail("Expected JMSSecurityException on start");
} catch (JMSSecurityException expected) {
} catch (JMSException disposed) {
// the failed connection may be torn down asynchronously before the
// security exception reaches start(); either form indicates rejection
} finally {
try { connection.close(); } catch (Exception ignored) {}
}
}
}
Loading
Loading