Skip to content

Commit

Permalink
Make REST API authentication configurable.
Browse files Browse the repository at this point in the history
  • Loading branch information
afalhambra-hivemq committed Feb 14, 2024
1 parent 3ec4548 commit befe182
Show file tree
Hide file tree
Showing 15 changed files with 309 additions and 32 deletions.
4 changes: 2 additions & 2 deletions charts/hivemq-platform/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,12 @@ Usage: {{ include "hivemq-platform.range-service-name" (dict "releaseName" $.Rel
{{- end -}}

{{/*
Checks if a particular service type exists within the services values.
Checks if a particular service type exists and is exposed within the services values.
Params:
- services: The array of services to check.
- expectedType: The expected type to check for.
Returns:
- bool: True if the desired type is found, False otherwise.
- `true` if the desired type is found and the service is marked as `exposed`, empty string otherwise.
*/}}
{{- define "hivemq-platform.has-service-type" -}}
{{- $services := .services }}
Expand Down
2 changes: 1 addition & 1 deletion charts/hivemq-platform/templates/hivemq-configuration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ data:
<rest-api>
<enabled>true</enabled>
<auth>
<enabled>false</enabled>
<enabled>{{ printf "%t" .Values.restApi.authEnabled | default false }}</enabled>
</auth>
{{- range $key, $val := .Values.services }}
{{- if eq $val.type "rest-api" }}
Expand Down
50 changes: 47 additions & 3 deletions charts/hivemq-platform/tests/hivemq_configuration_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -247,11 +247,14 @@ tests:
asserts:
- isNotNull:
path: data["config.xml"]
- matchRegex:
path: data["config.xml"]
pattern: "(?s)<rest-api>.*?<enabled>true</enabled>.*?</rest-api>"
- matchRegex:
path: data["config.xml"]
pattern: "(?s)<rest-api>.*?<port>8890</port>.*?</rest-api>"

- it: with a Rest API service not exposed
- it: with a Rest API service not being exposed
template: hivemq-configuration.yml
set:
services:
Expand All @@ -261,10 +264,51 @@ tests:
asserts:
- isNotNull:
path: data["config.xml"]
- notMatchRegex:
path: data["config.xml"]
pattern: "(?s)<rest-api>.*?<enabled>true</enabled>.*?</rest-api>"
- notMatchRegex:
path: data["config.xml"]
pattern: "(?s)<rest-api>.*?<port>8890</port>.*?</rest-api>"

- it: with REST API authentication enabled
template: hivemq-configuration.yml
set:
restApi.authEnabled: true
services:
- type: rest-api
exposed: true
containerPort: 8890
asserts:
- isNotNull:
path: data["config.xml"]
- matchRegex:
path: data["config.xml"]
pattern: "(?s)<rest-api>.*?<enabled>true</enabled>.*?</rest-api>"
- matchRegex:
path: data["config.xml"]
pattern: "(?s)<rest-api>.*?<auth>.*?<enabled>true</enabled>.*?</auth>.*?</rest-api>"
- matchRegex:
path: data["config.xml"]
pattern: "(?s)<rest-api>.*?<port>8890</port>.*?</rest-api>"

- it: with default REST API authentication
template: hivemq-configuration.yml
set:
services:
- type: rest-api
exposed: true
containerPort: 8890
asserts:
- isNotNull:
path: data["config.xml"]
- matchRegex:
path: data["config.xml"]
pattern: "(?s)<rest-api>.*?<enabled>true</enabled>.*?</rest-api>"
- matchRegex:
path: data["config.xml"]
pattern: "(?s)<rest-api>.*?<auth>.*?<enabled>false</enabled>.*?</auth>.*?</rest-api>"

- it: with a WebSocket service exposed
template: hivemq-configuration.yml
set:
Expand Down Expand Up @@ -309,7 +353,7 @@ tests:
path: data["config.xml"]
- matchRegex:
path: data["config.xml"]
pattern: "(?s)<user>.*?<name>test-username</name>.*?<password>c638833f69bbfb3c267afa0a74434812436b8f08a81fd263c6be6871de4f1265</password>.*?/user>"
pattern: "(?s)<user>.*?<name>test-username</name>.*?<password>c638833f69bbfb3c267afa0a74434812436b8f08a81fd263c6be6871de4f1265</password>.*?</user>"

- it: with default Control Center username and password
template: hivemq-configuration.yml
Expand All @@ -318,7 +362,7 @@ tests:
path: data["config.xml"]
- notMatchRegex:
path: data["config.xml"]
pattern: "(?s)<user>.*?<name>test-username</name>.*?<password>test-password</password>.*?/user>"
pattern: "(?s)<user>.*?<name>test-username</name>.*?<password>test-password</password>.*?</user>"

- it: with Data Hub enabled
template: hivemq-configuration.yml
Expand Down
8 changes: 8 additions & 0 deletions charts/hivemq-platform/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@
}
}
},
"restApi" : {
"type" : "object",
"properties" : {
"authEnabled" : {
"type" : "boolean"
}
}
},
"additionalVolumes" : {
"type" : "array",
"properties" : {
Expand Down
26 changes: 16 additions & 10 deletions charts/hivemq-platform/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ config:
dataValidationEnabled: false
behaviorValidationEnabled: false

# Control Center configuration
controlCenter:
# The name of the user.
# Set both values (username and password) to override the default configuration.
username: ""
# The password of the user as a SHA256 hash. See password generation here:
# https://docs.hivemq.com/hivemq/latest/control-center/configuration.html#generate-password
password: ""

# REST API configuration
restApi:
# Enables or disables authentication and authorization.
authEnabled: false

# Selector name that is used to match the selector of the managing operator.
# This selector assigns the HiveMQ Platform to a specific operator.
operator:
Expand Down Expand Up @@ -131,7 +145,7 @@ services:
- type: control-center
exposed: true
containerPort: 8080
# RestAPI service configuration
# REST API service configuration
- type: rest-api
exposed: false
containerPort: 8888
Expand Down Expand Up @@ -171,14 +185,6 @@ license:
# Overrides the License information via file using --set-file license.lic.
overrideLicense: ""

# Overrides the default ControlCenter username and password.
# Set both values to override the default configuration.
controlCenter:
username: ""
# Password as SHA256 HASH. See password generation here:
# https://docs.hivemq.com/hivemq/latest/control-center/configuration.html#generate-password
password: ""

# HiveMQ Platform extension configuration
extensions:

Expand Down Expand Up @@ -300,7 +306,7 @@ additionalVolumes: []

# type: Choose a type of volume that you want to mount.
# name: Optional name for the secret or the configmap to be mounted
# mountName: The mountName to be used for the StatefulSet Spec.
# mountName: The volume mount name to be used for the StatefulSet Spec.
# path: The path configures the directory to which the volume is mounted in the container
# If the directory already exists then the contents are overwritten!
# subPath: Optional name for the subPath. If a volume is mounted with subPath than the contents
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,92 @@
package com.hivemq.helmcharts.single;

import com.hivemq.helmcharts.AbstractHelmChartIT;
import com.hivemq.helmcharts.testcontainer.HelmChartContainer;
import com.hivemq.helmcharts.util.K8sUtil;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.apache.http.HttpStatus;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;

import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.concurrent.TimeUnit;

import static io.restassured.RestAssured.given;
import static org.assertj.core.api.Assertions.assertThat;

@Tag("Services")
@Tag("Services2")
class HelmRestApiIT extends AbstractHelmChartIT {
class HelmRestApiIT {

private static final @NotNull HelmChartContainer HELM_CHART_CONTAINER = new HelmChartContainer();
private static final @NotNull String NAMESPACE = K8sUtil.getNamespaceName(HelmRestApiIT.class);
private static final @NotNull String OPERATOR_RELEASE_NAME = "test-hivemq-platform-operator";
private static final @NotNull String PLATFORM_RELEASE_NAME = "test-hivemq-platform";
private static final @NotNull String REST_API_SERVICE_NAME = "hivemq-test-hivemq-platform-rest-8890";
private static final int REST_API_SERVICE_PORT = 8890;
@SuppressWarnings("NotNullFieldNotInitialized")
private static @NotNull KubernetesClient client;

@BeforeAll
@Timeout(value = 5, unit = TimeUnit.MINUTES)
static void baseSetup() {
HELM_CHART_CONTAINER.start();
client = HELM_CHART_CONTAINER.getKubernetesClient();
}

@BeforeEach
@Timeout(value = 5, unit = TimeUnit.MINUTES)
void setup() throws Exception {
HELM_CHART_CONTAINER.createNamespace(NAMESPACE);
HELM_CHART_CONTAINER.installOperatorChart(OPERATOR_RELEASE_NAME);
}

@AfterAll
@Timeout(value = 5, unit = TimeUnit.MINUTES)
static void baseTearDown() {
HELM_CHART_CONTAINER.stop();
}

@AfterEach
@Timeout(value = 5, unit = TimeUnit.MINUTES)
void tearDown() throws Exception {
HELM_CHART_CONTAINER.uninstallRelease(PLATFORM_RELEASE_NAME,
"--cascade",
"foreground",
"--namespace",
NAMESPACE);
K8sUtil.waitForNoPodsDeletedInNamespace(client, NAMESPACE);
HELM_CHART_CONTAINER.deleteNamespace(NAMESPACE);

HELM_CHART_CONTAINER.uninstallRelease(OPERATOR_RELEASE_NAME,
"--cascade",
"foreground",
"--namespace",
"default");
K8sUtil.waitForNoPodsDeletedInNamespace(client, "default");
}

@Test
@Timeout(value = 5, unit = TimeUnit.MINUTES)
void platformChart_whenRestApiEnabled_thenCallsEndpoint() throws Exception {
installChartsAndWaitForPlatformRunning("/files/rest-api-test-values.yaml");
HELM_CHART_CONTAINER.installPlatformChart(PLATFORM_RELEASE_NAME,
"-f",
"/files/rest-api-test-values.yaml",
"--namespace",
NAMESPACE);

K8sUtil.waitForHiveMQPlatformStateRunning(client, NAMESPACE, PLATFORM_RELEASE_NAME);

// forward the port from the service
try (final var forwarded = K8sUtil.getPortForward(client,
namespace,
NAMESPACE,
REST_API_SERVICE_NAME,
REST_API_SERVICE_PORT)) {
final var baseRestApiEndpoint = "http://localhost:" + forwarded.getLocalPort();
Expand All @@ -41,4 +100,52 @@ void platformChart_whenRestApiEnabled_thenCallsEndpoint() throws Exception {
assertThat(body.jsonPath().getList("items")).isEmpty();
}
}

@Test
@Timeout(value = 5, unit = TimeUnit.MINUTES)
void platformChart_whenAuthEnabled_thenCallsEndpoint() throws Exception {
K8sUtil.createConfigMap(client, NAMESPACE, "ese-config-map.yml");
K8sUtil.createConfigMap(client, NAMESPACE, "ese-file-realm-config-map.yml");

HELM_CHART_CONTAINER.installPlatformChart(PLATFORM_RELEASE_NAME,
"-f",
"/files/rest-api-test-with-auth-values.yaml",
"--namespace",
NAMESPACE);

K8sUtil.waitForHiveMQPlatformStateRunning(client, NAMESPACE, PLATFORM_RELEASE_NAME);

// forward the port from the service
try (final var forwarded = K8sUtil.getPortForward(client,
NAMESPACE,
REST_API_SERVICE_NAME,
REST_API_SERVICE_PORT)) {
final var baseRestApiEndpoint = "http://localhost:" + forwarded.getLocalPort();

given().header("Authorization", createBasicAuthHeader("test-user", "test-password"))
.when()
.get(new URL(baseRestApiEndpoint + "/api/v1/mqtt/clients"))
.then()
.statusCode(HttpStatus.SC_OK);

given().header("Authorization", createBasicAuthHeader("test-user", "wrong-password"))
.when()
.get(new URL(baseRestApiEndpoint + "/api/v1/mqtt/clients"))
.then()
.statusCode(HttpStatus.SC_UNAUTHORIZED);

given().header("Authorization", createBasicAuthHeader("test-user", "test-password"))
.when()
.get(new URL(baseRestApiEndpoint + "/api/v1/management/backups"))
.then()
.statusCode(HttpStatus.SC_FORBIDDEN);
}
}

@SuppressWarnings("SameParameterValue")
private static @NotNull String createBasicAuthHeader(
final @NotNull String username, final @NotNull String password) {
final String s = username + ":" + password;
return "Basic " + Base64.getEncoder().encodeToString(s.getBytes(StandardCharsets.UTF_8));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand Down Expand Up @@ -215,8 +216,13 @@ public void createNamespace(final @NotNull String name) {

public void deleteNamespace(final @NotNull String name) {
final var namespace = new NamespaceBuilder().withNewMetadata().withName(name).endMetadata().build();
final var response = getKubernetesClient().namespaces().resource(namespace).delete();
final var client = getKubernetesClient();
final var response = client.namespaces().resource(namespace).delete();
assertThat(response).isNotNull();
await().atMost(1, TimeUnit.MINUTES)
.untilAsserted(() -> assertThat(client.namespaces()
.list()
.getItems()).noneMatch(n -> name.equals(n.getMetadata().getName())));
}

public @NotNull LogWaiterUtil getLogWaiter() {
Expand Down
Loading

0 comments on commit befe182

Please sign in to comment.