From 19f9c064fd627d28eb6006f13250bf23cf501673 Mon Sep 17 00:00:00 2001 From: Carsten Lohmann Date: Mon, 30 Oct 2023 18:56:55 +0100 Subject: [PATCH] #1748 Add honoTenantId configuration for HonoConnection. --- .../hono/DefaultHonoConnectionFactory.java | 13 +- .../messaging/hono/HonoConnectionFactory.java | 22 ++ .../DefaultHonoConnectionFactoryTest.java | 55 ++-- .../ConnectionPersistenceActorTest.java | 2 +- ...ion-custom-expected-after-adaptation.json} | 15 +- .../hono-connection-custom-test.json | 1 + .../hono-connection-default-test.json | 3 + ...nant-custom-expected-after-adaptation.json | 268 ++++++++++++++++++ ...onnection-implicit-tenant-custom-test.json | 256 +++++++++++++++++ .../connectivity-protocol-bindings-hono.md | 89 +++--- 10 files changed, 646 insertions(+), 78 deletions(-) rename connectivity/service/src/test/resources/{hono-connection-custom-expected.json => hono-connection-custom-expected-after-adaptation.json} (94%) create mode 100644 connectivity/service/src/test/resources/hono-connection-implicit-tenant-custom-expected-after-adaptation.json create mode 100644 connectivity/service/src/test/resources/hono-connection-implicit-tenant-custom-test.json diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java index f37040c598..6d3acfff39 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java @@ -16,8 +16,6 @@ import java.text.MessageFormat; import java.util.Set; -import org.eclipse.ditto.connectivity.model.Connection; -import org.eclipse.ditto.connectivity.model.ConnectionId; import org.eclipse.ditto.connectivity.model.HonoAddressAlias; import org.eclipse.ditto.connectivity.model.UserPasswordCredentials; import org.eclipse.ditto.connectivity.service.config.DefaultHonoConfig; @@ -35,8 +33,6 @@ public final class DefaultHonoConnectionFactory extends HonoConnectionFactory { private final HonoConfig honoConfig; - private ConnectionId connectionId; - /** * Constructs a {@code DefaultHonoConnectionFactory} for the specified arguments. * @@ -48,11 +44,6 @@ public DefaultHonoConnectionFactory(final ActorSystem actorSystem, final Config honoConfig = new DefaultHonoConfig(actorSystem); } - @Override - protected void preConversion(final Connection honoConnection) { - connectionId = honoConnection.getId(); - } - @Override public URI getBaseUri() { return honoConfig.getBaseUri(); @@ -86,13 +77,13 @@ protected UserPasswordCredentials getCredentials() { @Override protected String resolveSourceAddress(final HonoAddressAlias honoAddressAlias) { return MessageFormat.format("hono.{0}.{1}", - honoAddressAlias.getAliasValue(), connectionId); + honoAddressAlias.getAliasValue(), getHonoTenantId()); } @Override protected String resolveTargetAddress(final HonoAddressAlias honoAddressAlias) { return MessageFormat.format("hono.{0}.{1}/'{{thing:id}}'", - honoAddressAlias.getAliasValue(), connectionId); + honoAddressAlias.getAliasValue(), getHonoTenantId()); } } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java index 9b17143347..2f9d4a99e9 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java @@ -63,6 +63,13 @@ */ public abstract class HonoConnectionFactory implements DittoExtensionPoint { + /** + * The name of the property in the {@code specificConfig} object containing the Hono tenant identifier. + */ + protected static final String SPEC_CONFIG_HONO_TENANT_ID = "honoTenantId"; + + private String honoTenantId; + /** * Constructs a {@code HonoConnectionFactory}. */ @@ -112,6 +119,9 @@ public Connection getHonoConnection(final Connection connection) { connection.getConnectionType()) ); + honoTenantId = connection.getSpecificConfig() + .getOrDefault(SPEC_CONFIG_HONO_TENANT_ID, connection.getId().toString()); + preConversion(connection); return ConnectivityModelFactory.newConnectionBuilder(connection) @@ -134,6 +144,18 @@ protected void preConversion(final Connection honoConnection) { // Do nothing by default. } + /** + * Get the Hono tenant identifier associated with the connection. + * + * @return The Hono tenant identifier. + */ + protected String getHonoTenantId() { + if (honoTenantId == null) { + throw new IllegalStateException("getHonoTenantId invoked before tenant id got determined"); + } + return honoTenantId; + } + protected abstract URI getBaseUri(); protected abstract boolean isValidateCertificates(); diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactoryTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactoryTest.java index ad875b8b87..80c8ca7d00 100644 --- a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactoryTest.java +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactoryTest.java @@ -30,7 +30,6 @@ import org.assertj.core.api.Assertions; import org.eclipse.ditto.connectivity.model.Connection; -import org.eclipse.ditto.connectivity.model.ConnectionId; import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory; import org.eclipse.ditto.connectivity.model.HonoAddressAlias; import org.eclipse.ditto.connectivity.model.ReplyTarget; @@ -58,7 +57,7 @@ public final class DefaultHonoConnectionFactoryTest { private HonoConfig honoConfig; - private static Connection generateConnectionObjectFromJsonFile( String fileName) throws IOException { + private static Connection generateConnectionObjectFromJsonFile(final String fileName) throws IOException { final var testClassLoader = DefaultHonoConnectionFactoryTest.class.getClassLoader(); try (final var connectionJsonFileStreamReader = new InputStreamReader( testClassLoader.getResourceAsStream(fileName) @@ -85,13 +84,26 @@ public void newInstanceWithNullActorSystemThrowsException() { public void getHonoConnectionWithCustomMappingsReturnsExpected() throws IOException { final var userProvidedHonoConnection = generateConnectionObjectFromJsonFile("hono-connection-custom-test.json"); - final var expectedHonoConnection = - generateConnectionObjectFromJsonFile("hono-connection-custom-expected.json"); + final var expectedAdaptedConnection = + generateConnectionObjectFromJsonFile("hono-connection-custom-expected-after-adaptation.json"); final var underTest = new DefaultHonoConnectionFactory(actorSystemResource.getActorSystem(), ConfigFactory.empty()); - assertThat(underTest.getHonoConnection(userProvidedHonoConnection)).isEqualTo(expectedHonoConnection); + assertThat(underTest.getHonoConnection(userProvidedHonoConnection)).isEqualTo(expectedAdaptedConnection); + } + + @Test + public void getHonoConnectionWithImplicitTenantIdAndCustomMappingsReturnsExpected() throws IOException { + final var userProvidedHonoConnection = + generateConnectionObjectFromJsonFile("hono-connection-implicit-tenant-custom-test.json"); + final var expectedAdaptedConnection = + generateConnectionObjectFromJsonFile("hono-connection-implicit-tenant-custom-expected-after-adaptation.json"); + + final var underTest = + new DefaultHonoConnectionFactory(actorSystemResource.getActorSystem(), ConfigFactory.empty()); + + assertThat(underTest.getHonoConnection(userProvidedHonoConnection)).isEqualTo(expectedAdaptedConnection); } @Test @@ -103,11 +115,11 @@ public void getHonoConnectionWithDefaultMappingReturnsExpected() throws IOExcept new DefaultHonoConnectionFactory(actorSystemResource.getActorSystem(), ConfigFactory.empty()); assertThat(underTest.getHonoConnection(userProvidedHonoConnection)) - .isEqualTo(getExpectedHonoConnection(userProvidedHonoConnection)); + .isEqualTo(getExpectedAdaptedHonoConnection(userProvidedHonoConnection)); } @SuppressWarnings("unchecked") - private Connection getExpectedHonoConnection(final Connection originalConnection) { + private Connection getExpectedAdaptedHonoConnection(final Connection originalConnection) { final var sourcesByAddress = getSourcesByAddress(originalConnection.getSources()); final var commandReplyTargetHeaderMapping = ConnectivityModelFactory.newHeaderMapping(Map.of( "correlation-id", "{{ header:correlation-id }}", @@ -122,6 +134,9 @@ private Connection getExpectedHonoConnection(final Connection originalConnection "subject", "{{ header:subject | fn:default(topic:action-subject) }}" ); final var connectionId = originalConnection.getId(); + final String honoTenantId = originalConnection.getSpecificConfig() + .getOrDefault(DefaultHonoConnectionFactory.SPEC_CONFIG_HONO_TENANT_ID, connectionId.toString()); + final String expectedResolvedCommandTargetAddress = getExpectedResolvedCommandTargetAddress(honoTenantId); return ConnectivityModelFactory.newConnectionBuilder(originalConnection) .uri(honoConfig.getBaseUri().toString().replaceFirst("(\\S+://)(\\S+)", "$1" + URLEncoder.encode(honoConfig.getUserPasswordCredentials().getUsername(), StandardCharsets.UTF_8) @@ -135,22 +150,22 @@ private Connection getExpectedHonoConnection(final Connection originalConnection ) .setSources(List.of( ConnectivityModelFactory.newSourceBuilder(sourcesByAddress.get(TELEMETRY.getAliasValue())) - .addresses(Set.of(getExpectedResolvedSourceAddress(TELEMETRY, connectionId))) + .addresses(Set.of(getExpectedResolvedSourceAddress(TELEMETRY, honoTenantId))) .replyTarget(ReplyTarget.newBuilder() - .address(getExpectedResolvedCommandTargetAddress(connectionId)) + .address(expectedResolvedCommandTargetAddress) .headerMapping(commandReplyTargetHeaderMapping) .build()) .build(), ConnectivityModelFactory.newSourceBuilder(sourcesByAddress.get(EVENT.getAliasValue())) - .addresses(Set.of(getExpectedResolvedSourceAddress(EVENT, connectionId))) + .addresses(Set.of(getExpectedResolvedSourceAddress(EVENT, honoTenantId))) .replyTarget(ReplyTarget.newBuilder() - .address(getExpectedResolvedCommandTargetAddress(connectionId)) + .address(expectedResolvedCommandTargetAddress) .headerMapping(commandReplyTargetHeaderMapping) .build()) .build(), ConnectivityModelFactory.newSourceBuilder( sourcesByAddress.get(COMMAND_RESPONSE.getAliasValue())) - .addresses(Set.of(getExpectedResolvedSourceAddress(COMMAND_RESPONSE, connectionId))) + .addresses(Set.of(getExpectedResolvedSourceAddress(COMMAND_RESPONSE, honoTenantId))) .headerMapping(ConnectivityModelFactory.newHeaderMapping(Map.of( "correlation-id", "{{ header:correlation-id }}", "status", "{{ header:status }}" @@ -159,8 +174,8 @@ private Connection getExpectedHonoConnection(final Connection originalConnection )) .setTargets(List.of( ConnectivityModelFactory.newTargetBuilder(targets.get(0)) - .address(getExpectedResolvedCommandTargetAddress(connectionId)) - .originalAddress(getExpectedResolvedCommandTargetAddress(connectionId)) + .address(expectedResolvedCommandTargetAddress) + .originalAddress(expectedResolvedCommandTargetAddress) .headerMapping(ConnectivityModelFactory.newHeaderMapping( Stream.concat( basicAdditionalTargetHeaderMappingEntries.entrySet().stream(), @@ -170,8 +185,8 @@ private Connection getExpectedHonoConnection(final Connection originalConnection )) .build(), ConnectivityModelFactory.newTargetBuilder(targets.get(1)) - .address(getExpectedResolvedCommandTargetAddress(connectionId)) - .originalAddress(getExpectedResolvedCommandTargetAddress(connectionId)) + .address(expectedResolvedCommandTargetAddress) + .originalAddress(expectedResolvedCommandTargetAddress) .headerMapping(ConnectivityModelFactory.newHeaderMapping( basicAdditionalTargetHeaderMappingEntries )) @@ -186,12 +201,12 @@ private static Map getSourcesByAddress(final Iterable so return result; } - private static String getExpectedResolvedSourceAddress(final HonoAddressAlias honoAddressAlias, final ConnectionId connectionId) { - return "hono." + honoAddressAlias.getAliasValue() + "." + connectionId; + private static String getExpectedResolvedSourceAddress(final HonoAddressAlias honoAddressAlias, final String honoTenantId) { + return "hono." + honoAddressAlias.getAliasValue() + "." + honoTenantId; } - private static String getExpectedResolvedCommandTargetAddress(final ConnectionId connectionId) { - return "hono." + HonoAddressAlias.COMMAND.getAliasValue() + "." + connectionId + "/{{thing:id}}"; + private static String getExpectedResolvedCommandTargetAddress(final String honoTenantId) { + return "hono." + HonoAddressAlias.COMMAND.getAliasValue() + "." + honoTenantId + "/{{thing:id}}"; } } diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActorTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActorTest.java index 3a6234d662..80c68364b7 100644 --- a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActorTest.java +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActorTest.java @@ -221,7 +221,7 @@ public void testConnectionTypeHono() throws IOException { .toBuilder() .id(connectionId) .build(); - final var expectedHonoConnection = generateConnectionObjectFromJsonFile("hono-connection-custom-expected.json", connectionId) + final var expectedHonoConnection = generateConnectionObjectFromJsonFile("hono-connection-custom-expected-after-adaptation.json", connectionId) .toBuilder() .id(connectionId) .build(); diff --git a/connectivity/service/src/test/resources/hono-connection-custom-expected.json b/connectivity/service/src/test/resources/hono-connection-custom-expected-after-adaptation.json similarity index 94% rename from connectivity/service/src/test/resources/hono-connection-custom-expected.json rename to connectivity/service/src/test/resources/hono-connection-custom-expected-after-adaptation.json index 6ccab3c12a..3a0f94d6c3 100644 --- a/connectivity/service/src/test/resources/hono-connection-custom-expected.json +++ b/connectivity/service/src/test/resources/hono-connection-custom-expected-after-adaptation.json @@ -7,7 +7,7 @@ "sources": [ { "addresses": [ - "hono.telemetry.test-connection-id" + "hono.telemetry.hono-tenant-id" ], "consumerCount": 1, "qos": 0, @@ -32,7 +32,7 @@ "implicitStandaloneThingCreation" ], "replyTarget": { - "address": "hono.command.test-connection-id/{{thing:id}}", + "address": "hono.command.hono-tenant-id/{{thing:id}}", "headerMapping": { "device_id": "custom_value1", "user_key1": "user_value1", @@ -48,7 +48,7 @@ }, { "addresses": [ - "hono.event.test-connection-id" + "hono.event.hono-tenant-id" ], "consumerCount": 1, "qos": 1, @@ -72,7 +72,7 @@ "implicitStandaloneThingCreation" ], "replyTarget": { - "address": "hono.command.test-connection-id/{{thing:id}}", + "address": "hono.command.hono-tenant-id/{{thing:id}}", "headerMapping": { "device_id": "{{ thing:id }}", "subject": "custom_value2", @@ -88,7 +88,7 @@ }, { "addresses": [ - "hono.command_response.test-connection-id" + "hono.command_response.hono-tenant-id" ], "consumerCount": 1, "qos": 0, @@ -120,7 +120,7 @@ ], "targets": [ { - "address": "hono.command.test-connection-id/{{thing:id}}", + "address": "hono.command.hono-tenant-id/{{thing:id}}", "topics": [ "_/_/things/live/messages", "_/_/things/live/commands" @@ -137,7 +137,7 @@ } }, { - "address": "hono.command.test-connection-id/{{thing:id}}", + "address": "hono.command.hono-tenant-id/{{thing:id}}", "topics": [ "_/_/things/twin/events", "_/_/things/live/events" @@ -158,6 +158,7 @@ "validateCertificates": false, "processorPoolSize": 5, "specificConfig": { + "honoTenantId": "hono-tenant-id", "saslMechanism": "plain", "bootstrapServers": "tcp://server1:port1,tcp://server2:port2,tcp://server3:port3", "groupId": "custom_groupId" diff --git a/connectivity/service/src/test/resources/hono-connection-custom-test.json b/connectivity/service/src/test/resources/hono-connection-custom-test.json index 1c3b288695..e3766625b0 100644 --- a/connectivity/service/src/test/resources/hono-connection-custom-test.json +++ b/connectivity/service/src/test/resources/hono-connection-custom-test.json @@ -148,6 +148,7 @@ "validateCertificates": true, "processorPoolSize": 5, "specificConfig": { + "honoTenantId": "hono-tenant-id", "groupId": "custom_groupId" }, "mappingDefinitions": { diff --git a/connectivity/service/src/test/resources/hono-connection-default-test.json b/connectivity/service/src/test/resources/hono-connection-default-test.json index 7fdb31de0f..a57bbd2411 100644 --- a/connectivity/service/src/test/resources/hono-connection-default-test.json +++ b/connectivity/service/src/test/resources/hono-connection-default-test.json @@ -135,6 +135,9 @@ "failoverEnabled": true, "validateCertificates": true, "processorPoolSize": 5, + "specificConfig": { + "honoTenantId": "hono-tenant-id" + }, "mappingDefinitions": { "implicitEdgeThingCreation": { "mappingEngine": "ImplicitThingCreation", diff --git a/connectivity/service/src/test/resources/hono-connection-implicit-tenant-custom-expected-after-adaptation.json b/connectivity/service/src/test/resources/hono-connection-implicit-tenant-custom-expected-after-adaptation.json new file mode 100644 index 0000000000..9e3dd613e8 --- /dev/null +++ b/connectivity/service/src/test/resources/hono-connection-implicit-tenant-custom-expected-after-adaptation.json @@ -0,0 +1,268 @@ +{ + "id": "hono-tenant-id", + "name": "Things-Hono Test 1", + "connectionType": "hono", + "connectionStatus": "open", + "uri": "tcp://test_username:test_password_w%2Fspecial_char@localhost:9922", + "sources": [ + { + "addresses": [ + "hono.telemetry.hono-tenant-id" + ], + "consumerCount": 1, + "qos": 0, + "authorizationContext": [ + "nginx:ditto" + ], + "enforcement": { + "input": "{{ header:device_id }}", + "filters": [ + "{{ entity:id }}" + ] + }, + "acknowledgementRequests": { + "includes": [], + "filter": "fn:delete()" + }, + "headerMapping": {}, + "payloadMapping": [ + "Ditto", + "status", + "implicitEdgeThingCreation", + "implicitStandaloneThingCreation" + ], + "replyTarget": { + "address": "hono.command.hono-tenant-id/{{thing:id}}", + "headerMapping": { + "device_id": "custom_value1", + "user_key1": "user_value1", + "subject": "{{ header:subject | fn:default(topic:action-subject) | fn:default(topic:criterion) }}-response", + "correlation-id": "{{ header:correlation-id }}" + }, + "expectedResponseTypes": [ + "response", + "error" + ], + "enabled": true + } + }, + { + "addresses": [ + "hono.event.hono-tenant-id" + ], + "consumerCount": 1, + "qos": 1, + "authorizationContext": [ + "nginx:ditto" + ], + "enforcement": { + "input": "{{ header:device_id }}", + "filters": [ + "{{ entity:id }}" + ] + }, + "acknowledgementRequests": { + "includes": [] + }, + "headerMapping": {}, + "payloadMapping": [ + "Ditto", + "status", + "implicitEdgeThingCreation", + "implicitStandaloneThingCreation" + ], + "replyTarget": { + "address": "hono.command.hono-tenant-id/{{thing:id}}", + "headerMapping": { + "device_id": "{{ thing:id }}", + "subject": "custom_value2", + "user_key2": "user_value2", + "correlation-id": "{{ header:correlation-id }}" + }, + "expectedResponseTypes": [ + "response", + "error" + ], + "enabled": true + } + }, + { + "addresses": [ + "hono.command_response.hono-tenant-id" + ], + "consumerCount": 1, + "qos": 0, + "authorizationContext": [ + "nginx:ditto" + ], + "enforcement": { + "input": "{{ header:device_id }}", + "filters": [ + "{{ entity:id }}" + ] + }, + "acknowledgementRequests": { + "includes": [], + "filter": "fn:delete()" + }, + "headerMapping": { + "status": "custom_value3", + "user_key3": "user_value3", + "correlation-id": "{{ header:correlation-id }}" + }, + "payloadMapping": [ + "Ditto" + ], + "replyTarget": { + "enabled": false + } + } + ], + "targets": [ + { + "address": "hono.command.hono-tenant-id/{{thing:id}}", + "topics": [ + "_/_/things/live/messages", + "_/_/things/live/commands" + ], + "authorizationContext": [ + "nginx:ditto" + ], + "headerMapping": { + "user_key4": "user_value4", + "device_id": "{{ thing:id }}", + "response-required": "custom_value4", + "subject": "{{ header:subject | fn:default(topic:action-subject) }}", + "correlation-id": "{{ header:correlation-id }}" + } + }, + { + "address": "hono.command.hono-tenant-id/{{thing:id}}", + "topics": [ + "_/_/things/twin/events", + "_/_/things/live/events" + ], + "authorizationContext": [ + "nginx:ditto" + ], + "headerMapping": { + "user_key5": "user_value5", + "device_id": "{{ thing:id }}", + "subject": "{{ header:subject | fn:default(topic:action-subject) }}", + "correlation-id": "custom_value5" + } + } + ], + "clientCount": 1, + "failoverEnabled": true, + "validateCertificates": false, + "processorPoolSize": 5, + "specificConfig": { + "saslMechanism": "plain", + "bootstrapServers": "tcp://server1:port1,tcp://server2:port2,tcp://server3:port3", + "groupId": "custom_groupId" + }, + "mappingDefinitions": { + "implicitEdgeThingCreation": { + "mappingEngine": "ImplicitThingCreation", + "options": { + "thing": { + "thingId": "{{ header:device_id }}", + "_copyPolicyFrom": "{{ header:gateway_id }}", + "attributes": { + "Info": { + "gatewayId": "{{ header:gateway_id }}" + } + } + }, + "commandHeaders": {} + }, + "incomingConditions": { + "behindGateway": "fn:filter(header:gateway_id, 'exists')", + "honoRegistration": "fn:filter(header:hono_registration_status, 'eq', 'NEW')" + } + }, + "implicitStandaloneThingCreation": { + "mappingEngine": "ImplicitThingCreation", + "options": { + "thing": { + "thingId": "{{ header:device_id }}", + "_policy": { + "entries": { + "DEVICE": { + "subjects": { + "integration:hono": { + "type": "hono-integration" + } + }, + "resources": { + "policy:/": { + "revoke": [], + "grant": [ + "READ" + ] + }, + "thing:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + }, + "message:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + } + } + }, + "DEFAULT": { + "subjects": { + "integration:hono": { + "type": "generated" + } + }, + "resources": { + "policy:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + }, + "thing:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + }, + "message:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + } + } + } + } + } + }, + "commandHeaders": {} + }, + "incomingConditions": { + "honoRegistration": "fn:filter(header:hono_registration_status, 'eq', 'NEW')", + "notBehindGateway": "fn:filter(header:gateway_id, 'exists', 'false')" + } + }, + "status": { + "mappingEngine": "ConnectionStatus", + "options": { + "thingId": "{{ header:device_id }}" + } + } + } +} \ No newline at end of file diff --git a/connectivity/service/src/test/resources/hono-connection-implicit-tenant-custom-test.json b/connectivity/service/src/test/resources/hono-connection-implicit-tenant-custom-test.json new file mode 100644 index 0000000000..02e2b321f9 --- /dev/null +++ b/connectivity/service/src/test/resources/hono-connection-implicit-tenant-custom-test.json @@ -0,0 +1,256 @@ +{ + "id": "hono-tenant-id", + "name": "Things-Hono Test 1", + "connectionType": "hono", + "connectionStatus": "open", + "uri": "ssl://hono-endpoint:1", + "sources": [ + { + "addresses": [ + "telemetry" + ], + "consumerCount": 1, + "qos": 0, + "authorizationContext": [ + "nginx:ditto" + ], + "enforcement": { + "input": "{{ header:device_id }}", + "filters": [ + "{{ entity:id }}" + ] + }, + "acknowledgementRequests": { + "includes": [], + "filter": "fn:delete()" + }, + "headerMapping": {}, + "payloadMapping": [ + "Ditto", + "status", + "implicitEdgeThingCreation", + "implicitStandaloneThingCreation" + ], + "replyTarget": { + "address": "command", + "headerMapping": { + "user_key1": "user_value1", + "device_id": "custom_value1" + }, + "expectedResponseTypes": [ + "response", + "error" + ], + "enabled": true + } + }, + { + "addresses": [ + "event" + ], + "consumerCount": 1, + "qos": 1, + "authorizationContext": [ + "nginx:ditto" + ], + "enforcement": { + "input": "{{ header:device_id }}", + "filters": [ + "{{ entity:id }}" + ] + }, + "acknowledgementRequests": { + "includes": [] + }, + "headerMapping": {}, + "payloadMapping": [ + "Ditto", + "status", + "implicitEdgeThingCreation", + "implicitStandaloneThingCreation" + ], + "replyTarget": { + "address": "command", + "headerMapping": { + "user_key2": "user_value2", + "subject": "custom_value2" + }, + "expectedResponseTypes": [ + "response", + "error" + ], + "enabled": true + } + }, + { + "addresses": [ + "command_response" + ], + "consumerCount": 1, + "qos": 0, + "authorizationContext": [ + "nginx:ditto" + ], + "enforcement": { + "input": "{{ header:device_id }}", + "filters": [ + "{{ entity:id }}" + ] + }, + "acknowledgementRequests": { + "includes": [], + "filter": "fn:delete()" + }, + "headerMapping": { + "user_key3": "user_value3", + "status": "custom_value3" + }, + "payloadMapping": [ + "Ditto" + ], + "replyTarget": { + "enabled": false + } + } + ], + "targets": [ + { + "address": "command", + "topics": [ + "_/_/things/live/messages", + "_/_/things/live/commands" + ], + "authorizationContext": [ + "nginx:ditto" + ], + "headerMapping": { + "user_key4": "user_value4", + "response-required": "custom_value4" + } + }, + { + "address": "command", + "topics": [ + "_/_/things/twin/events", + "_/_/things/live/events" + ], + "authorizationContext": [ + "nginx:ditto" + ], + "headerMapping": { + "user_key5": "user_value5", + "correlation-id": "custom_value5" + } + } + ], + "clientCount": 1, + "failoverEnabled": true, + "validateCertificates": true, + "processorPoolSize": 5, + "specificConfig": { + "groupId": "custom_groupId" + }, + "mappingDefinitions": { + "implicitEdgeThingCreation": { + "mappingEngine": "ImplicitThingCreation", + "options": { + "thing": { + "thingId": "{{ header:device_id }}", + "_copyPolicyFrom": "{{ header:gateway_id }}", + "attributes": { + "Info": { + "gatewayId": "{{ header:gateway_id }}" + } + } + }, + "commandHeaders": {} + }, + "incomingConditions": { + "behindGateway": "fn:filter(header:gateway_id, 'exists')", + "honoRegistration": "fn:filter(header:hono_registration_status, 'eq', 'NEW')" + } + }, + "implicitStandaloneThingCreation": { + "mappingEngine": "ImplicitThingCreation", + "options": { + "thing": { + "thingId": "{{ header:device_id }}", + "_policy": { + "entries": { + "DEVICE": { + "subjects": { + "integration:hono": { + "type": "hono-integration" + } + }, + "resources": { + "policy:/": { + "revoke": [], + "grant": [ + "READ" + ] + }, + "thing:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + }, + "message:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + } + } + }, + "DEFAULT": { + "subjects": { + "integration:hono": { + "type": "generated" + } + }, + "resources": { + "policy:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + }, + "thing:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + }, + "message:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + } + } + } + } + } + }, + "commandHeaders": {} + }, + "incomingConditions": { + "honoRegistration": "fn:filter(header:hono_registration_status, 'eq', 'NEW')", + "notBehindGateway": "fn:filter(header:gateway_id, 'exists', 'false')" + } + }, + "status": { + "mappingEngine": "ConnectionStatus", + "options": { + "thingId": "{{ header:device_id }}" + } + } + } +} \ No newline at end of file diff --git a/documentation/src/main/resources/pages/ditto/connectivity-protocol-bindings-hono.md b/documentation/src/main/resources/pages/ditto/connectivity-protocol-bindings-hono.md index 761e016ede..35dbf47d43 100644 --- a/documentation/src/main/resources/pages/ditto/connectivity-protocol-bindings-hono.md +++ b/documentation/src/main/resources/pages/ditto/connectivity-protocol-bindings-hono.md @@ -6,54 +6,59 @@ permalink: connectivity-protocol-bindings-hono.html --- Consume messages from Eclipse Hono through Apache Kafka brokers and send messages to -Eclipse Hono the same manner as [Kafka connection](connectivity-protocol-bindings-kafka2.html) does. +Eclipse Hono in the same manner as the [Kafka connection](connectivity-protocol-bindings-kafka2.html) does. -This connection type is implemented just for convenience - to avoid the need the user to be aware of the specific -header mappings, address formats and Kafka specificConfig, which are required to connect to Eclipse Hono. +The Hono connection type is implemented just for convenience - to avoid the need for the user to be aware of the specific +header mappings, address formats and Kafka `specificConfig` settings, which are required to connect to Eclipse Hono. These specifics are applied automatically at runtime for the connections of type Hono. -Hono connection is based on Kafka connection and uses it behind the scenes, so most of the -[Kafka connection documentation](connectivity-protocol-bindings-kafka2.html) is valid for Hono connection too, -but with some exceptions, described bellow. +The Hono connection is based on the Kafka connection and uses it behind the scenes, so most of the +[Kafka connection documentation](connectivity-protocol-bindings-kafka2.html) is valid for the Hono connection too, +but with some exceptions, as described below. #### Important note -During the creation of hono connection, the connection ID must be provided to be the same as `Hono-tenantId`. This is needed to match the Kafka topics (aka connection addresses) to the topics on which Hono will send and listen to. See bellow sections [Source addresses](#source-addresses), [Source reply target](#source-reply-target) and [Target Address](#target-address) +A Hono connection is associated with _one_ Hono tenant. That means for each Hono tenant a separate Hono connection needs to be created. +The tenant ID is used in the source and target connection addresses, representing the Kafka topics used by Hono for +sending and receiving messages for this tenant. +See below sections [Source addresses](#source-addresses), [Source reply target](#source-reply-target) and [Target Address](#target-address). +The Hono tenant ID for the connection is defined in the [specific config](#specific-configuration-properties) `"honoTenantId"` property. ## Specific Hono connection configuration ### Connection URI -In Hono connection definition, property `uri` should not be specified (any specified value will be ignored). -The connection URI and credentials are common for all Hono connections and are derived from the configuration of the connectivity service. -`uri` will be automatically generated, based on values of 3 configuration properties of connectivity service - +In the Hono connection definition, the `uri` property should not be specified (any specified value will be ignored). +The connection URI and credentials are common for all Hono connections and are derived from the [configuration](installation-operating.html#ditto-configuration) of the connectivity service. +`uri` will be automatically generated, based on the values of 3 configuration properties of the connectivity service - `ditto.connectivity.hono.base-uri`, `ditto.connectivity.hono.username` and `ditto.connectivity.hono.password`. -Property `base-uri` must specify protocol, host and port number -(see the [example below](#configuration-example)). -In order to connect to Kafka brokers, at runtime `username` and `password` values will be inserted between -protocol identifier and the host name of `base-uri` to form the connection URI like this `tcp://username:password@host.name:port` +The connectivity service property `base-uri` must specify protocol, host and port number (see the [example below](#connectivity-configuration-example)). +In order to connect to Kafka brokers, `username` and `password` values will be inserted at runtime between the +protocol identifier and the host name parts of `base-uri`, resulting in a connection URI of the form `tcp://username:password@host.name:port`. Note: If any of these parameters has to be changed, the service must be restarted to apply the new values. ### Source format #### Source addresses -For a Hono connection source "addresses" are specified as aliases, which are resolved at runtime to Kafka topics to subscribe to. +For a Hono connection, source "addresses" are specified as aliases, which are resolved at runtime to Kafka topics to subscribe to. Valid source addresses (aliases) are `event`, `telemetry` and `command_response`. -Runtime, these are resolved as following: -* `event` -> `{%raw%}hono.event.{{connection:id}}{%endraw%}` -* `telemetry` -> `{%raw%}hono.telemetry.{{connection:id}}{%endraw%}` -* `command_response` -> `{%raw%}hono.command_response.{{connection:id}}{%endraw%}` +At runtime, these are resolved as follows: +* `event` -> `{%raw%}hono.event.{%endraw%}` +* `telemetry` -> `{%raw%}hono.telemetry.{%endraw%}` +* `command_response` -> `{%raw%}hono.command_response.{%endraw%}` -Note: The `{%raw%}{{connection:id}}{%endraw%}` will be replaced by the value of connectionId +`{%raw%}{%endraw%}` will be replaced by the value of `specificConfig.honoTenantId` or, if not set, +by the connection id. #### Source reply target Similar to source addresses, the reply target `address` is an alias as well. The single valid value for it is `command`. It is resolved to Kafka topic/key like this: -* `command` -> `{%raw%}hono.command.{{connection:id}}/{%endraw%}` (<thingId> is substituted by thing ID value). +* `command` -> `{%raw%}hono.command./{%endraw%}` -Note: The `{%raw%}{{connection:id}}{%endraw%}` will be replaced by the value of connectionId +`{%raw%}{%endraw%}` will be replaced by the value of `specificConfig.honoTenantId` or, if not set, +by the connection id. `{%raw%}{%endraw%}` is substituted by the thing ID value. The needed header mappings for the `replyTarget` are also populated automatically at runtime and there is no need to specify them in the connection definition. Any of the following specified value will be substituted (i.e. ignored). -Actually the `headerMapping` subsection is not required and could be omitted at all (in the context of `replyTarget`). +Actually the `headerMapping` subsection is not required and could be omitted completely (in the context of `replyTarget`). For addresses `telemetry` and `event`, the following header mappings will be automatically applied: * `device_id`: `{%raw%}{{ thing:id }}{%endraw%}` @@ -92,7 +97,7 @@ The following example shows a valid Hono-connection source: ``` #### Source header mapping -Hono connection does not need any header mapping for sources. Anyway, the header mappings documented for +The Hono connection does not need any header mapping for sources. Nevertheless, the header mappings documented for [Kafka connection](connectivity-protocol-bindings-kafka2.html) are still available. See [Source header mapping](connectivity-protocol-bindings-kafka2.html#source-header-mapping) in Kafka protocol bindings and [Header mapping for connections](connectivity-header-mapping.html). @@ -101,14 +106,15 @@ and [Header mapping for connections](connectivity-header-mapping.html). #### Target address The target `address` is specified as an alias and the only valid alias is `command`. It is automatically resolved at runtime to the following Kafka topic/key: -* `command` -> `{%raw%}hono.command.{{connection:id}}/{%endraw%}` (<thingId> is substituted by thing ID value). +* `command` -> `{%raw%}hono.command./{%endraw%}` -Note: The `{%raw%}{{connection:id}}{%endraw%}` will be replaced by the value of connectionId +`{%raw%}{%endraw%}` will be replaced by the value of `specificConfig.honoTenantId` or, if not set, +by the connection id. `{%raw%}{%endraw%}` is substituted by the thing ID value. #### Target header mapping The target `headerMapping` section is also populated automatically at runtime and there is -no need to specify it the connection definitionm i.e. could be omitted. -If any of the following keys are specified in the connection will be ignored and automatically substituted as follows: +no need to specify it the connection definition i.e. could be omitted. +If any of the following keys are specified in the connection, they will be ignored and automatically substituted as follows: * `device_id`: `{%raw%}{{ thing:id }}{%endraw%}` * `subject`: `{%raw%}{{ header:subject \| fn:default(topic:action-subject) }}{%endraw%}` * `response-required`: `{%raw%}{{ header:response-required }}{%endraw%}` @@ -130,22 +136,24 @@ The following example shows a valid Hono-connection target: ### Specific configuration properties -The properties needed by Kafka server in section `specificConfig` with the following keys will be automatically added at runtime to the connection. -Any manually specified definition of `bootstrapServers` and `saslMechanism` will be ignored, but `groupId` will not. -* `bootstrapServers` The value will be taken from configuration property `ditto.connectivity.hono.bootstrap-servers` of connectivity service. -It must contain a comma separated list of Kafka bootstrap servers to use for connecting to (in addition to automatically added connection uri). +In the `specificConfig` section, the Hono tenant of the connection is specified in the `honoTenantId` property. +If that property is not set, the connection ID will be taken as Hono tenant ID. + +The following Kafka connection related properties in the `specificConfig` section will be automatically added at runtime +to the connection. Any manually specified definition of `bootstrapServers` and `saslMechanism` will be ignored, but `groupId` will not. +* `bootstrapServers` The value will be taken from the configuration property `ditto.connectivity.hono.bootstrap-servers` of the connectivity service. +It must contain a comma separated list of Kafka bootstrap servers to use for connecting to (in addition to the automatically added connection uri). * `saslMechanism` The value will be taken from configuration property `ditto.connectivity.hono.sasl-mechanism`. The value must be one of `SaslMechanism` enum values to select the SASL mechanisms to use for authentication at Kafka: * `PLAIN` * `SCRAM-SHA-256` * `SCRAM-SHA-512` -* `groupId`: could be specified by the user, but not required. If omitted, the value of the connection ID will be automatically used. +* `groupId`: could be specified by the user, but is not required. If omitted, the value of the connection ID will be automatically used. -Hono connection still allows to manually specify additional properties (like `debugEnabled`), which will be merged with auto-generated ones. -If no additional properties are needed, the whole section `specificConfig` could be omitted. +Hono connection still allows to manually specify additional properties (like `debugEnabled`), which will be merged with the auto-generated ones. ### Certificate validation -The connection property `validateCertificates` is also set automatically. The value is taken from `ditto.connectivity.hono.validate-certificates` property. +The connection property `validateCertificates` is also set automatically. The value is taken from the `ditto.connectivity.hono.validate-certificates` property. For more details see [Connection configuration](connectivity-tls-certificates.html). ## Examples @@ -153,10 +161,13 @@ For more details see [Connection configuration](connectivity-tls-certificates.ht ```json { "connection": { - "id": "hono-example-connection-123", + "id": "connection-for-hono-example-tenant", "connectionType": "hono", "connectionStatus": "open", "failoverEnabled": true, + "specificConfig": { + "honoTenantId": "example-tenant" + }, "sources": [ { "addresses": ["event"], @@ -193,8 +204,8 @@ For more details see [Connection configuration](connectivity-tls-certificates.ht } } ``` -### Configuration example -Here is an example with all the configurations of connectivity, service which are needed by Hono connections: +### Connectivity configuration example +Here is an example with all the configuration options of the connectivity service that are needed by Hono connections: ``` ditto { connection {