HiveMQ Enterprise Security Extension Customization SDK
The Enterprise Security Extension Customization SDK allows you to programmatically specify sophisticated custom handling as part of the ESE authentication and authorization processing pipeline.
Features
The Enterprise Security Extension Customization SDK gives you fine-grained control over the content of ESE variables.
Common Preprocessor:
-
The common preprocessor is a custom preprocessor that can be used on any type of pipeline. This preprocessor provides read and write access to the current state of the ESE variables.
MQTT Preprocessor:
-
The MQTT preprocessor is a custom preprocessor that can only be used on MQTT listener pipelines. This preprocessor provides read and write access to the current state of the ESE variables and additional read access to MQTT client-specific input.
Requirements
-
JDK version 11 or higher (depending on the JDK on which the broker runs). JDK 21 is preferred.
-
A Java IDE that supports Gradle. (for example, IntelliJ).
Quick Start with the Customization SDK
The Enterprise Security Extension Customization SDK uses the same Input/Output and async principles as the HiveMQ Extension SDK.
The quickest way to learn about the customization SDK is to check out our Hello World Customization project on GitHub and use it as the basis for your own customization.
The hivemq-enterprise-security-hello-world-customization
project provides examples to get you started with the Enterprise Security Extension Customization SDK:
-
The
IpAllowlistMqttPreprocessor
that adds IP allowlist checking as an additional security layer for MQTT client authentication. -
The
ExternalRolesCommonPreprocessor
that makes roles from an external HTTP server available for authorization.
Deploying A Customization
You can deploy your customization as follows:
-
Add the fully qualified class name of your custom preprocessor to your extension configuration.
-
Run the
./gradlew jar
task from your Gradle project to build your customization.
For example, use the task from the Hello World project. -
Copy the jar file from your local
build/libs
folder to theHIVEMQ_HOME/extensions/hivemq-enterprise-security-extension/customizations
directory of your HiveMQ installation. -
Start the HiveMQ broker.
Customization Metrics
When you deploy a custom preprocessor, the extension adds dedicated metrics to your HiveMQ broker. Monitor these metrics to gain valuable insights into the behavior of your preprocessor over time.
For more information, see Custom Preprocessor Metrics.
In addition to the standard metrics, the Customization SDK metrics registry allows you to create custom metrics to fulfill your individual business needs.
For more information, see Custom Metrics.
Custom Preprocessor Configuration
You can find all of the details regarding configuration in the custom preprocessor section of the Enterprise Security Extension documentation.
Common Preprocessor
The common preprocessor is a custom preprocessor that lets you extend the capabilities of any security pipeline. Implement this preprocessor to programmatically access and process ESE variables, optionally asynchronously, and write them back to be picked up in later stages of the security pipeline.
public class CommonPreprocessorImpl implements CommonPreprocessor {
@Override
public void init(final @NotNull CommonPreprocessorInitInput input) {
// Implement your init logic here. (optional)
}
@Override
public void process(
final @NotNull CommonPreprocessorProcessInput input,
final @NotNull CommonPreprocessorProcessOutput output) {
// Implement your processing logic here.
}
@Override
public void shutdown(final @NotNull CommonPreprocessorShutdownInput input) {
// Implement your shutdown logic here. (optional)
}
}
MQTT Preprocessor
The MQTT preprocessor is a custom preprocessor that lets you extend the capabilities of an MQTT listener pipeline. This preprocessor has the same capabilities as the common preprocessor, plus access to MQTT client-specific information.
public class MqttPreprocessorImpl implements MqttPreprocessor {
@Override
public void init(final @NotNull MqttPreprocessorInitInput input) {
// Implement your init logic here. (optional)
}
@Override
public void process(
final @NotNull MqttPreprocessorProcessInput input,
final @NotNull MqttPreprocessorProcessOutput output) {
// Implement your processing logic here.
}
@Override
public void shutdown(final @NotNull MqttPreprocessorShutdownInput input) {
// Implement your shutdown logic here. (optional)
}
}
Configuring an MQTT preprocessor is only allowed on MQTT listener pipelines. |
MQTT Client-specific Information
The MQTT preprocessor, allows you to access MQTT client-specific information such as the client IP or TLS connection details during processing.
public class MqttClientSpecificInformationPreprocessorImpl implements MqttPreprocessor {
@Override
public void process(
final @NotNull MqttPreprocessorProcessInput input,
final @NotNull MqttPreprocessorProcessOutput output) {
final ConnectionInformation connectionInformation = input.getConnectionInformation();
final ClientInformation clientInformation = input.getClientInformation();
final ConnectPacket connectPacket = input.getConnectPacket();
}
}
Custom Preprocessor Lifecycle
Custom preprocessor implementations referenced in the custom preprocessor configuration are dynamically loaded, instantiated, and initialized when the extension is launched. After these steps, the preprocessor is ready for processing as part of the surrounding pipeline. The preprocessor shuts down when the extension stops.
The preprocessor executes the following steps:
-
Read the fully qualified class name from the custom preprocessor configuration
<implementation>
tag. -
Load the class by scanning all jar files contained in the
HIVEMQ_HOME/extensions/hivemq-enterprise-security-extension/customizations
folder, including subfolders. Your custom preprocessor can access all classes in the jars because they are all part of the same ClassLoader. -
Create the custom preprocessor instance by calling the mandatory public no arg constructor.
-
Call the
init
method once. -
Call the
process
method continuously as part of the security pipeline. -
Call the
shutdown
method once when the extension stops. Theshutdown
method is also called if theinit
method is not successful and the extension stops.
Your compiled implementation (.class files) of the custom preprocessors and all associated dependencies must be placed in java archives (.jar) in the HIVEMQ_HOME/extensions/hivemq-enterprise-security-extension/customizations folder or subfolders.
|
Multiple threads call the process method concurrently. To ensure proper processing, your implementation must be thread-safe.
|
If an error occurs during the initialization of a custom preprocessor, the extension stops. For example, if the class is not found or the init
method throws an Exception
.
If the process
method throws an Exception
the com.hivemq.extensions.ese.custom.preprocessor.process.failed
metric is incremented, an error is logged, and access is denied to the accessing client.
Any Exception
thrown by the shutdown
is logged but not handled further.
We do not encourage throwing exceptions from custom preprocessor methods. All exceptions should be handled inside the methods of your implementation. |
To troubleshoot your custom preprocessor, monitor the Custom Preprocessor Metrics and consult the hivemq.log
file.
ESE Variables
ESE variables are an essential concept of the Enterprise Security Extension and are used to pass information between pipeline steps. The Customization SDK gives you full access to the variables by providing APIs to read and write their content.
public class EseVariableCommonPreprocessorImpl implements CommonPreprocessor {
@Override
public void process(
final @NotNull CommonPreprocessorProcessInput input,
final @NotNull CommonPreprocessorProcessOutput output) {
input.getEseVariablesInput().getAuthenticationKey().ifPresent(authenticationKey -> {
if (!authenticationKey.startsWith("prefix-")) {
output.getEseVariablesOutput().setAuthenticationKey("prefix-" + authenticationKey);
}
});
}
}
-
Read the content of the ESE variable
authentication-key
. -
Check if the ESE variable is present and does not start with
prefix-
-
If
true
, add the missing prefix and write the new value back to the ESE variableauthentication-key
.
The updated output is taken into account as soon as the synchronous or asynchronous processing of the custom preprocessor is finished.
The content of an ESE variable can be removed by setting the value to null
.
Asynchronous Processing
Custom preprocessors are allowed to access external resources in a non-blocking/asynchronous manner only. For example, to make HTTP requests :
public class AsyncCommonPreprocessorImpl implements CommonPreprocessor {
private final @NotNull HttpClient httpClient = HttpClient.newHttpClient();
@Override
public void process(
final @NotNull CommonPreprocessorProcessInput input,
final @NotNull CommonPreprocessorProcessOutput output) {
final Async<CommonPreprocessorProcessOutput> async = output.async();
try {
final HttpRequest request = HttpRequest.newBuilder().build();
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) //
.thenAccept(response -> {
// Implement your response handling here e.g. updating ESE variables.
}).exceptionally(throwable -> {
// Implement your error handling here.
return null;
})
// Always call async.resume() when finished.
.thenRun(async::resume);
} catch (final Exception e) {
// Implement your error handling here.
// Always call async.resume() when finished.
async.resume();
}
}
}
-
Call
output.async()
before starting the asynchronous processing. This will make the rest of the pipeline wait untilasync.resume()
is called. -
Use non-blocking IO to retrieve external data and, if applicable, consider caching.
-
Update the ESE variables for further processing in the pipeline.
-
In any case call
async.resume()
. This can be when anException
was thrown before the asynchronous block even started or when the asynchronous block finished with success or failure.
The output.async() method can only be called once per process call.
|
Once the output.async() method is called, the custom preprocessor must call the async.resume() method when the successful or unsuccessful processing finishes.
|
Custom Settings
The custom preprocessor configuration allows you to externalize your configuration via <custom-settings>
.
These settings can be accessed via SDK:
public class CustomSettingsCommonPreprocessorImpl implements CommonPreprocessor {
private @Nullable String customValue;
@Override
public void init(final @NotNull CommonPreprocessorInitInput input) {
// Accessible in init
final CustomSettings customSettings = input.getCustomSettings();
// Store for later use in process and/or shutdown.
customValue = customSettings.getFirst("configured-setting").orElse(null);
}
@Override
public void process(
final @NotNull CommonPreprocessorProcessInput input,
final @NotNull CommonPreprocessorProcessOutput output) {
// Accessible in process
final CustomSettings customSettings = input.getCustomSettings();
}
@Override
public void shutdown(final @NotNull CommonPreprocessorShutdownInput input) {
// Accessible in shutdown
final CustomSettings customSettings = input.getCustomSettings();
}
}
Custom Metrics
The Customization SDK allows you to create and update custom metrics via the MetricRegistry
.
public class CustomMetricsCommonPreprocessorImpl implements CommonPreprocessor {
private @NotNull Counter counter;
@Override
public void init(final @NotNull CommonPreprocessorInitInput input) {
// Accessible in init
final MetricRegistry metricRegistry = input.getMetricRegistry();
// Store for later use in process and/or shutdown.
counter = metricRegistry.counter("my.custom.Counter");
}
@Override
public void process(
final @NotNull CommonPreprocessorProcessInput input,
final @NotNull CommonPreprocessorProcessOutput output) {
// Accessible in process
final MetricRegistry metricRegistry = input.getMetricRegistry();
}
@Override
public void shutdown(final @NotNull CommonPreprocessorShutdownInput input) {
// Accessible in shutdown
final MetricRegistry metricRegistry = input.getMetricRegistry();
}
}