use of org.eclipse.hono.util.ResourceIdentifier in project hono by eclipse.
the class DelegatingTenantAmqpEndpoint method filterResponse.
/**
* Verifies that a response only contains tenant information that the
* client is authorized to retrieve.
* <p>
* If the response does not contain a tenant ID nor a payload, then the
* returned future will succeed with the response <em>as-is</em>.
* Otherwise the tenant ID is used together with the endpoint and operation
* name to check the client's authority to retrieve the data. If the client
* is authorized, the returned future will succeed with the response as-is,
* otherwise the future will fail with a {@link ClientErrorException} containing a
* <em>403 Forbidden</em> status.
*/
@Override
protected Future<Message> filterResponse(final HonoUser clientPrincipal, final Message request, final Message response) {
Objects.requireNonNull(clientPrincipal);
Objects.requireNonNull(response);
final String tenantId = MessageHelper.getTenantId(response);
final JsonObject payload = MessageHelper.getJsonPayload(response);
if (tenantId == null || payload == null) {
return Future.succeededFuture(response);
} else {
// verify that payload contains tenant that the client is authorized for
final ResourceIdentifier resourceId = ResourceIdentifier.from(TenantConstants.TENANT_ENDPOINT, tenantId, null);
return getAuthorizationService().isAuthorized(clientPrincipal, resourceId, request.getSubject()).map(isAuthorized -> {
if (isAuthorized) {
return response;
} else {
throw new ClientErrorException(HttpURLConnection.HTTP_FORBIDDEN);
}
});
}
}
use of org.eclipse.hono.util.ResourceIdentifier in project hono by eclipse.
the class DelegatingRegistrationAmqpEndpoint method processAssertRequest.
private Future<Message> processAssertRequest(final Message request, final ResourceIdentifier targetAddress, final SpanContext spanContext) {
final String tenantId = targetAddress.getTenantId();
final String deviceId = MessageHelper.getDeviceId(request);
final String gatewayId = MessageHelper.getGatewayId(request);
final Span span = TracingHelper.buildServerChildSpan(tracer, spanContext, SPAN_NAME_ASSERT_DEVICE_REGISTRATION, getClass().getSimpleName()).start();
TracingHelper.setDeviceTags(span, tenantId, deviceId);
final Future<Message> resultFuture;
if (tenantId == null || deviceId == null) {
TracingHelper.logError(span, "missing tenant and/or device");
resultFuture = Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_BAD_REQUEST));
} else {
final Future<RegistrationResult> result;
if (gatewayId == null) {
logger.debug("asserting registration of device [tenant: {}, device-id: {}]", tenantId, deviceId);
result = getService().assertRegistration(tenantId, deviceId, span);
} else {
TracingHelper.TAG_GATEWAY_ID.set(span, gatewayId);
logger.debug("asserting registration of device [tenant: {}, device-id: {}] for gateway [{}]", tenantId, deviceId, gatewayId);
result = getService().assertRegistration(tenantId, deviceId, gatewayId, span);
}
resultFuture = result.map(res -> RegistrationConstants.getAmqpReply(RegistrationConstants.REGISTRATION_ENDPOINT, tenantId, request, res));
}
return finishSpanOnFutureCompletion(span, resultFuture);
}
use of org.eclipse.hono.util.ResourceIdentifier in project hono by eclipse.
the class ProtonBasedCommand method from.
/**
* Creates a command from an AMQP 1.0 message.
* <p>
* The message is required to contain a non-null <em>address</em>, containing non-empty tenant-id and device-id parts
* that identify the command target device. If that is not the case, an {@link IllegalArgumentException} is thrown.
* <p>
* In addition, the message is expected to contain.
* <ul>
* <li>a non-null <em>subject</em></li>
* <li>either a null <em>reply-to</em> address (for a one-way command)
* or a non-null <em>reply-to</em> address that matches the tenant and device IDs and consists
* of four segments</li>
* <li>a String valued <em>correlation-id</em> and/or <em>message-id</em> if the <em>reply-to</em>
* address is not empty</li>
* </ul>
* or otherwise the returned command's {@link #isValid()} method will return {@code false}.
*
* @param message The message containing the command.
* @return The command.
* @throws NullPointerException if any of the parameters is {@code null}.
* @throws IllegalArgumentException if the address of the message is invalid.
*/
public static ProtonBasedCommand from(final Message message) {
Objects.requireNonNull(message);
if (!ResourceIdentifier.isValid(message.getAddress())) {
throw new IllegalArgumentException("address is empty or invalid");
}
final ResourceIdentifier addressIdentifier = ResourceIdentifier.fromString(message.getAddress());
if (Strings.isNullOrEmpty(addressIdentifier.getTenantId())) {
throw new IllegalArgumentException("address is missing tenant-id part");
} else if (Strings.isNullOrEmpty(addressIdentifier.getResourceId())) {
throw new IllegalArgumentException("address is missing device-id part");
}
final String tenantId = addressIdentifier.getTenantId();
final String deviceId = addressIdentifier.getResourceId();
final StringJoiner validationErrorJoiner = new StringJoiner(", ");
if (message.getSubject() == null) {
validationErrorJoiner.add("subject not set");
}
getUnsupportedPayloadReason(message).ifPresent(validationErrorJoiner::add);
String correlationId = null;
final Object correlationIdObj = MessageHelper.getCorrelationId(message);
if (correlationIdObj != null) {
if (correlationIdObj instanceof String) {
correlationId = (String) correlationIdObj;
} else {
validationErrorJoiner.add("message/correlation-id is not of type string, actual type: " + correlationIdObj.getClass().getName());
}
} else if (message.getReplyTo() != null) {
// correlation id is required if a command response is expected
validationErrorJoiner.add("neither message-id nor correlation-id is set");
}
String replyToId = null;
if (message.getReplyTo() != null) {
try {
final ResourceIdentifier replyTo = ResourceIdentifier.fromString(message.getReplyTo());
if (!CommandConstants.isNorthboundCommandResponseEndpoint(replyTo.getEndpoint())) {
validationErrorJoiner.add("reply-to not a command address: " + message.getReplyTo());
} else if (tenantId != null && !tenantId.equals(replyTo.getTenantId())) {
validationErrorJoiner.add("reply-to not targeted at tenant " + tenantId + ": " + message.getReplyTo());
} else {
replyToId = replyTo.getPathWithoutBase();
if (replyToId.isEmpty()) {
validationErrorJoiner.add("reply-to part after tenant not set: " + message.getReplyTo());
}
}
} catch (final IllegalArgumentException e) {
validationErrorJoiner.add("reply-to cannot be parsed: " + message.getReplyTo());
}
}
return new ProtonBasedCommand(validationErrorJoiner.length() > 0 ? Optional.of(validationErrorJoiner.toString()) : Optional.empty(), message, tenantId, deviceId, correlationId, replyToId);
}
use of org.eclipse.hono.util.ResourceIdentifier in project hono by eclipse.
the class CommandAndControlMqttIT method testSendCommandFailsForCommandNotAcknowledgedByDevice.
/**
* Verifies that the adapter forwards the <em>released</em> disposition back to the
* application if the device hasn't sent an acknowledgement for the command message
* published to the device.
*
* @param endpointConfig The endpoints to use for sending/receiving commands.
* @param ctx The vert.x test context.
* @throws InterruptedException if not all commands and responses are exchanged in time.
*/
@ParameterizedTest(name = IntegrationTestSupport.PARAMETERIZED_TEST_NAME_PATTERN)
@MethodSource("allCombinations")
@Timeout(timeUnit = TimeUnit.SECONDS, value = 20)
public void testSendCommandFailsForCommandNotAcknowledgedByDevice(final MqttCommandEndpointConfiguration endpointConfig, final VertxTestContext ctx) throws InterruptedException {
final MqttQoS subscribeQos = MqttQoS.AT_LEAST_ONCE;
final VertxTestContext setup = new VertxTestContext();
final Checkpoint ready = setup.checkpoint(2);
final String commandTargetDeviceId = endpointConfig.isSubscribeAsGateway() ? helper.setupGatewayDeviceBlocking(tenantId, deviceId, 5) : deviceId;
final int totalNoOfCommandsToSend = 2;
final CountDownLatch commandsFailed = new CountDownLatch(totalNoOfCommandsToSend);
final AtomicInteger receivedMessagesCounter = new AtomicInteger(0);
final AtomicInteger counter = new AtomicInteger();
final Handler<MqttPublishMessage> commandConsumer = msg -> {
LOGGER.trace("received command [{}] - no response sent here", msg.topicName());
final ResourceIdentifier topic = ResourceIdentifier.fromString(msg.topicName());
ctx.verify(() -> {
endpointConfig.assertCommandPublishTopicStructure(topic, commandTargetDeviceId, false, "setValue");
});
receivedMessagesCounter.incrementAndGet();
};
final Function<Buffer, Future<Void>> commandSender = payload -> {
counter.incrementAndGet();
return helper.sendCommand(tenantId, commandTargetDeviceId, "setValue", "text/plain", payload, helper.getSendCommandTimeout(counter.get() == 1)).mapEmpty();
};
helper.registry.addDeviceToTenant(tenantId, deviceId, password).compose(ok -> connectToAdapter(IntegrationTestSupport.getUsername(deviceId, tenantId), password)).compose(ok -> injectMqttClientPubAckBlocker(new AtomicBoolean(true))).compose(ok -> createConsumer(tenantId, msg -> {
// expect empty notification with TTD -1
setup.verify(() -> assertThat(msg.getContentType()).isEqualTo(EventConstants.CONTENT_TYPE_EMPTY_NOTIFICATION));
final TimeUntilDisconnectNotification notification = msg.getTimeUntilDisconnectNotification().orElse(null);
LOGGER.info("received notification [{}]", notification);
setup.verify(() -> assertThat(notification).isNotNull());
if (notification.getTtd() == -1) {
ready.flag();
}
})).compose(conAck -> subscribeToCommands(commandTargetDeviceId, commandConsumer, endpointConfig, subscribeQos)).onComplete(setup.succeeding(ok -> ready.flag()));
assertWithMessage("setup of adapter finished within %s seconds", IntegrationTestSupport.getTestSetupTimeout()).that(setup.awaitCompletion(IntegrationTestSupport.getTestSetupTimeout(), TimeUnit.SECONDS)).isTrue();
if (setup.failed()) {
ctx.failNow(setup.causeOfFailure());
return;
}
final AtomicInteger commandsSent = new AtomicInteger(0);
final AtomicLong lastReceivedTimestamp = new AtomicLong(0);
final long start = System.currentTimeMillis();
while (commandsSent.get() < totalNoOfCommandsToSend) {
final CountDownLatch commandSent = new CountDownLatch(1);
context.runOnContext(go -> {
final Buffer msg = Buffer.buffer("value: " + commandsSent.getAndIncrement());
commandSender.apply(msg).onComplete(sendAttempt -> {
if (sendAttempt.succeeded()) {
LOGGER.debug("sending command {} succeeded unexpectedly", commandsSent.get());
} else {
if (sendAttempt.cause() instanceof ServerErrorException && ((ServerErrorException) sendAttempt.cause()).getErrorCode() == HttpURLConnection.HTTP_UNAVAILABLE && !(sendAttempt.cause() instanceof SendMessageTimeoutException)) {
LOGGER.debug("sending command {} failed as expected: {}", commandsSent.get(), sendAttempt.cause().toString());
lastReceivedTimestamp.set(System.currentTimeMillis());
commandsFailed.countDown();
if (commandsFailed.getCount() % 20 == 0) {
LOGGER.info("commands failed as expected: {}", totalNoOfCommandsToSend - commandsFailed.getCount());
}
} else {
LOGGER.debug("sending command {} failed with an unexpected error", commandsSent.get(), sendAttempt.cause());
}
}
commandSent.countDown();
});
});
commandSent.await();
}
// have to wait an extra MqttAdapterProperties.DEFAULT_COMMAND_ACK_TIMEOUT (100ms) for each command message
final long timeToWait = totalNoOfCommandsToSend * 300;
if (!commandsFailed.await(timeToWait, TimeUnit.MILLISECONDS)) {
LOGGER.info("Timeout of {} milliseconds reached, stop waiting for commands", timeToWait);
}
assertThat(receivedMessagesCounter.get()).isEqualTo(totalNoOfCommandsToSend);
final long commandsCompleted = totalNoOfCommandsToSend - commandsFailed.getCount();
LOGGER.info("commands sent: {}, commands failed: {} after {} milliseconds", commandsSent.get(), commandsCompleted, lastReceivedTimestamp.get() - start);
if (commandsCompleted == commandsSent.get()) {
ctx.completeNow();
} else {
ctx.failNow(new java.lang.IllegalStateException("did not complete all commands sent"));
}
}
use of org.eclipse.hono.util.ResourceIdentifier in project hono by eclipse.
the class VertxBasedAmqpProtocolAdapter method createCommandConsumer.
private Future<CommandConsumer> createCommandConsumer(final ProtonSender sender, final ResourceIdentifier sourceAddress, final Device authenticatedDevice, final Span span) {
final Handler<CommandContext> commandHandler = commandContext -> {
final Sample timer = metrics.startTimer();
addMicrometerSample(commandContext, timer);
Tags.COMPONENT.set(commandContext.getTracingSpan(), getTypeName());
final Command command = commandContext.getCommand();
final Future<TenantObject> tenantTracker = getTenantConfiguration(sourceAddress.getTenantId(), commandContext.getTracingContext());
tenantTracker.compose(tenantObject -> {
if (!command.isValid()) {
return Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_BAD_REQUEST, "malformed command message"));
}
if (!HonoProtonHelper.isLinkOpenAndConnected(sender)) {
return Future.failedFuture(new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, "sender link is not open"));
}
return checkMessageLimit(tenantObject, command.getPayloadSize(), commandContext.getTracingContext());
}).compose(success -> {
// check the via-gateways, ensuring that the gateway may act on behalf of the device at this point in time
if (authenticatedDevice != null && !authenticatedDevice.getDeviceId().equals(sourceAddress.getResourceId())) {
return getRegistrationAssertion(authenticatedDevice.getTenantId(), sourceAddress.getResourceId(), authenticatedDevice, commandContext.getTracingContext());
}
return Future.succeededFuture();
}).compose(success -> {
onCommandReceived(tenantTracker.result(), sender, commandContext);
return Future.succeededFuture();
}).otherwise(failure -> {
if (failure instanceof ClientErrorException) {
commandContext.reject(failure);
} else {
commandContext.release(failure);
}
metrics.reportCommand(command.isOneWay() ? Direction.ONE_WAY : Direction.REQUEST, sourceAddress.getTenantId(), tenantTracker.result(), ProcessingOutcome.from(failure), command.getPayloadSize(), timer);
return null;
});
};
final Future<RegistrationAssertion> tokenTracker = Optional.ofNullable(authenticatedDevice).map(v -> getRegistrationAssertion(authenticatedDevice.getTenantId(), sourceAddress.getResourceId(), authenticatedDevice, span.context())).orElseGet(Future::succeededFuture);
if (authenticatedDevice != null && !authenticatedDevice.getDeviceId().equals(sourceAddress.getResourceId())) {
// gateway scenario
return tokenTracker.compose(v -> getCommandConsumerFactory().createCommandConsumer(sourceAddress.getTenantId(), sourceAddress.getResourceId(), authenticatedDevice.getDeviceId(), commandHandler, null, span.context()));
} else {
return tokenTracker.compose(v -> getCommandConsumerFactory().createCommandConsumer(sourceAddress.getTenantId(), sourceAddress.getResourceId(), commandHandler, null, span.context()));
}
}
Aggregations