org.eclipse.hono.tests.DeviceRegistryHttpClient.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.hono.tests.DeviceRegistryHttpClient.java

Source

/**
 * Copyright (c) 2018 Contributors to the Eclipse Foundation
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 1.0 which is available at
 * https://www.eclipse.org/legal/epl-v10.html
 *
 * SPDX-License-Identifier: EPL-1.0
 */

package org.eclipse.hono.tests;

import java.net.HttpURLConnection;
import java.security.cert.X509Certificate;
import java.util.Objects;
import java.util.Optional;

import javax.security.auth.x500.X500Principal;

import org.eclipse.hono.client.ServiceInvocationException;
import org.eclipse.hono.util.CredentialsConstants;
import org.eclipse.hono.util.CredentialsObject;
import org.eclipse.hono.util.RegistrationConstants;
import org.eclipse.hono.util.TenantConstants;
import org.eclipse.hono.util.TenantObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;

/**
 * A client for accessing the Device Registry's HTTP resources for
 * the Device Registration, Credentials and Tenant API.
 *
 */
public final class DeviceRegistryHttpClient {

    private static final Logger LOG = LoggerFactory.getLogger(DeviceRegistryHttpClient.class);

    private static final String CONTENT_TYPE_APPLICATION_JSON = "application/json";
    private static final String URI_ADD_TENANT = "/" + TenantConstants.TENANT_ENDPOINT;
    private static final String TEMPLATE_URI_TENANT_INSTANCE = String.format("/%s/%%s",
            TenantConstants.TENANT_ENDPOINT);

    private static final String TEMPLATE_URI_REGISTRATION_INSTANCE = String.format("/%s/%%s/%%s",
            RegistrationConstants.REGISTRATION_ENDPOINT);

    private static final String TEMPLATE_URI_CREDENTIALS_INSTANCE = String.format("/%s/%%s/%%s/%%s",
            CredentialsConstants.CREDENTIALS_ENDPOINT);
    private static final String TEMPLATE_URI_CREDENTIALS_BY_DEVICE = String.format("/%s/%%s/%%s",
            CredentialsConstants.CREDENTIALS_ENDPOINT);

    private final CrudHttpClient httpClient;

    /**
     * Creates a new client for a host and port.
     * 
     * @param vertx The vert.x instance to use.
     * @param host The host to invoke the operations on.
     * @param port The port that the service is bound to.
     * @throws NullPointerException if any of the parameters is {@code null}.
     */
    public DeviceRegistryHttpClient(final Vertx vertx, final String host, final int port) {
        this.httpClient = new CrudHttpClient(vertx, host, port);
    }

    // tenant management

    /**
     * Adds configuration information for a tenant.
     * <p>
     * This method simply invokes {@link #addTenant(JsonObject, int)} with
     * {@link HttpURLConnection#HTTP_CREATED} as the expected status code.
     * 
     * @param requestPayload The request payload as specified by the Tenant API.
     * @return A future indicating the outcome of the operation.
     *         The future will succeed if the tenant has been created successfully.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     */
    public Future<Void> addTenant(final JsonObject requestPayload) {
        return addTenant(requestPayload, HttpURLConnection.HTTP_CREATED);
    }

    /**
     * Adds configuration information for a tenant.
     * <p>
     * This method simply invokes {@link #addTenant(JsonObject, String, int)} with
     * <em>application/json</em> as content type and {@link HttpURLConnection#HTTP_CREATED}
     * as the expected status code.
     * 
     * @param requestPayload The request payload as specified by the Tenant API.
     * @param expectedStatusCode The status code indicating a successful outcome.
     * @return A future indicating the outcome of the operation.
     *         The future will succeed if the response contained the expected status code.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     */
    public Future<Void> addTenant(final JsonObject requestPayload, final int expectedStatusCode) {
        return addTenant(requestPayload, CONTENT_TYPE_APPLICATION_JSON, expectedStatusCode);
    }

    /**
     * Adds configuration information for a tenant.
     * 
     * @param requestPayload The request payload as specified by the Tenant API.
     * @param contentType The content type to set in the request.
     * @param expectedStatusCode The status code indicating a successful outcome.
     * @return A future indicating the outcome of the operation.
     *         The future will succeed if the response contained the expected status code.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     */
    public Future<Void> addTenant(final JsonObject requestPayload, final String contentType,
            final int expectedStatusCode) {

        return httpClient.create(URI_ADD_TENANT, requestPayload, contentType,
                statusCode -> statusCode == expectedStatusCode);
    }

    /**
     * Gets configuration information for a tenant.
     * <p>
     * This method simply invokes {@link #getTenant(String, int)} with
     * {@link HttpURLConnection#HTTP_OK} as the expected status code.
     * 
     * @param tenantId The tenant to get information for.
     * @return A future indicating the outcome of the operation.
     *         The future will contain the response payload if the request succeeded.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     */
    public Future<Buffer> getTenant(final String tenantId) {
        return getTenant(tenantId, HttpURLConnection.HTTP_OK);
    }

    /**
     * Gets configuration information for a tenant.
     * 
     * @param tenantId The tenant to get information for.
     * @param expectedStatusCode The status code indicating a successful outcome.
     * @return A future indicating the outcome of the operation.
     *         The future will contain the response payload if the response
     *         contained the expected status code.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     */
    public Future<Buffer> getTenant(final String tenantId, final int expectedStatusCode) {

        final String uri = String.format(TEMPLATE_URI_TENANT_INSTANCE, tenantId);
        return httpClient.get(uri, status -> status == expectedStatusCode);
    }

    /**
     * Updates configuration information for a tenant.
     * 
     * @param tenantId The tenant to update information for.
     * @param requestPayload The configuration information to set.
     * @param expectedStatusCode The status code indicating a successful outcome.
     * @return A future indicating the outcome of the operation.
     *         The future will succeed if the response contained the expected status code.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     */
    public Future<Void> updateTenant(final String tenantId, final JsonObject requestPayload,
            final int expectedStatusCode) {

        final String uri = String.format(TEMPLATE_URI_TENANT_INSTANCE, tenantId);
        return httpClient.update(uri, requestPayload, status -> status == expectedStatusCode);
    }

    /**
     * Removes configuration information for a tenant.
     * <p>
     * This method simply invokes {@link #removeTenant(String, int)} with
     * {@link HttpURLConnection#HTTP_NO_CONTENT} as the expected status code.
     * 
     * @param tenantId The tenant to remove.
     * @return A future indicating the outcome of the operation.
     *         The future will succeed if the tenant has been removed.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     */
    public Future<Void> removeTenant(final String tenantId) {

        return removeTenant(tenantId, HttpURLConnection.HTTP_NO_CONTENT);
    }

    /**
     * Removes configuration information for a tenant.
     * 
     * @param tenantId The tenant to remove.
     * @param expectedStatusCode The status code indicating a successful outcome.
     * @return A future indicating the outcome of the operation.
     *         The future will succeed if the response contained the expected status code.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     */
    public Future<Void> removeTenant(final String tenantId, final int expectedStatusCode) {

        final String uri = String.format(TEMPLATE_URI_TENANT_INSTANCE, tenantId);
        return httpClient.delete(uri, status -> status == expectedStatusCode);
    }

    // device registration

    /**
     * Adds registration information for a device.
     * <p>
     * The device will be enabled by default.
     * <p>
     * This method simply invokes {@link #registerDevice(String, String, JsonObject)}
     * with an empty JSON object as additional data.
     * 
     * @param tenantId The tenant that the device belongs to.
     * @param deviceId The identifier of the device.
     * @return A future indicating the outcome of the operation.
     *         The future will succeed if the registration information has been added successfully.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     * @throws NullPointerException if the tenant is {@code null}.
     */
    public Future<Void> registerDevice(final String tenantId, final String deviceId) {
        return registerDevice(tenantId, deviceId, new JsonObject());
    }

    /**
     * Adds registration information for a device.
     * <p>
     * The device will be enabled by default if not specified otherwise
     * in the additional data.
     * <p>
     * This method simply invokes {@link #registerDevice(String, String, JsonObject, int)}
     * with {@link HttpURLConnection#HTTP_CREATED} as the expected status code.
     * 
     * @param tenantId The tenant that the device belongs to.
     * @param deviceId The identifier of the device.
     * @param data Additional properties to register with the device.
     * @return A future indicating the outcome of the operation.
     *         The future will succeed if the registration information has been added successfully.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     * @throws NullPointerException if the tenant is {@code null}.
     */
    public Future<Void> registerDevice(final String tenantId, final String deviceId, final JsonObject data) {
        return registerDevice(tenantId, deviceId, data, HttpURLConnection.HTTP_CREATED);
    }

    /**
     * Adds registration information for a device.
     * <p>
     * The device will be enabled by default if not specified otherwise
     * in the additional data.
     * <p>
     * This method simply invokes {@link #registerDevice(String, String, JsonObject, String, int)}
     * with <em>application/json</em> as the content type.
     * 
     * @param tenantId The tenant that the device belongs to.
     * @param deviceId The identifier of the device.
     * @param data Additional properties to register with the device.
     * @param expectedStatus The status code indicating a successful outcome.
     * @return A future indicating the outcome of the operation.
     *         The future will succeed if the response contained the expected status code.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     * @throws NullPointerException if the tenant is {@code null}.
     */
    public Future<Void> registerDevice(final String tenantId, final String deviceId, final JsonObject data,
            final int expectedStatus) {
        return registerDevice(tenantId, deviceId, data, CONTENT_TYPE_APPLICATION_JSON, expectedStatus);
    }

    /**
     * Adds registration information for a device.
     * <p>
     * The device will be enabled by default if not specified otherwise
     * in the additional data.
     * 
     * @param tenantId The tenant that the device belongs to.
     * @param deviceId The identifier of the device.
     * @param data Additional properties to register with the device.
     * @param contentType The content type to set on the request.
     * @param expectedStatus The status code indicating a successful outcome.
     * @return A future indicating the outcome of the operation.
     *         The future will succeed if the response contained the expected status code.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     * @throws NullPointerException if the tenant is {@code null}.
     */
    public Future<Void> registerDevice(final String tenantId, final String deviceId, final JsonObject data,
            final String contentType, final int expectedStatus) {

        Objects.requireNonNull(tenantId);
        final JsonObject requestJson = Optional.ofNullable(data).map(json -> json.copy()).orElse(null);
        if (deviceId != null && requestJson != null) {
            requestJson.put(RegistrationConstants.FIELD_PAYLOAD_DEVICE_ID, deviceId);
        }
        final String uri = String.format("/%s/%s", RegistrationConstants.REGISTRATION_ENDPOINT, tenantId);
        return httpClient.create(uri, requestJson, contentType, statusCode -> statusCode == expectedStatus);
    }

    /**
     * Updates registration information for a device.
     * <p>
     * This method simply invokes {@link #updateDevice(String, String, JsonObject, String, int)}
     * with <em>application/json</em> as the content type and
     * {@link HttpURLConnection#HTTP_NO_CONTENT} as the expected status code.
     * 
     * @param tenantId The tenant that the device belongs to.
     * @param deviceId The identifier of the device.
     * @param data Additional properties to register with the device.
     * @return A future indicating the outcome of the operation.
     *         The future will succeed if the registration information has been updated successfully.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     * @throws NullPointerException if the tenant is {@code null}.
     */
    public Future<Void> updateDevice(final String tenantId, final String deviceId, final JsonObject data) {
        return updateDevice(tenantId, deviceId, data, CONTENT_TYPE_APPLICATION_JSON,
                HttpURLConnection.HTTP_NO_CONTENT);
    }

    /**
     * Updates registration information for a device.
     * 
     * @param tenantId The tenant that the device belongs to.
     * @param deviceId The identifier of the device.
     * @param data Additional properties to register with the device.
     * @param contentType The content type to set on the request.
     * @param expectedStatus The status code indicating a successful outcome.
     * @return A future indicating the outcome of the operation.
     *         The future will succeed if the response contained the expected status code.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     * @throws NullPointerException if the tenant is {@code null}.
     */
    public Future<Void> updateDevice(final String tenantId, final String deviceId, final JsonObject data,
            final String contentType, final int expectedStatus) {

        Objects.requireNonNull(tenantId);
        final String requestUri = String.format(TEMPLATE_URI_REGISTRATION_INSTANCE, tenantId, deviceId);
        final JsonObject requestJson = data.copy();
        requestJson.put(RegistrationConstants.FIELD_PAYLOAD_DEVICE_ID, deviceId);
        return httpClient.update(requestUri, requestJson, contentType, status -> status == expectedStatus);
    }

    /**
     * Gets registration information for a device.
     * 
     * @param tenantId The tenant that the device belongs to.
     * @param deviceId The identifier of the device.
     * @return A future indicating the outcome of the operation.
     *         The future will contain the response payload if the request succeeded.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     * @throws NullPointerException if the tenant is {@code null}.
     */
    public Future<Buffer> getRegistrationInfo(final String tenantId, final String deviceId) {

        Objects.requireNonNull(tenantId);
        final String requestUri = String.format(TEMPLATE_URI_REGISTRATION_INSTANCE, tenantId, deviceId);
        return httpClient.get(requestUri, status -> status == HttpURLConnection.HTTP_OK);
    }

    /**
     * Removes registration information for a device.
     * 
     * @param tenantId The tenant that the device belongs to.
     * @param deviceId The identifier of the device.
     * @return A future indicating the outcome of the operation.
     *         The future will succeed if the registration information has been removed.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     * @throws NullPointerException if the tenant is {@code null}.
     */
    public Future<Void> deregisterDevice(final String tenantId, final String deviceId) {

        Objects.requireNonNull(tenantId);
        final String requestUri = String.format(TEMPLATE_URI_REGISTRATION_INSTANCE, tenantId, deviceId);
        return httpClient.delete(requestUri, status -> status == HttpURLConnection.HTTP_NO_CONTENT);
    }

    // credentials management

    /**
     * Adds credentials for a device.
     * <p>
     * This method simply invokes {@link #addCredentials(String, JsonObject, int)}
     * with {@link HttpURLConnection#HTTP_CREATED} as the expected status code.
     * 
     * @param tenantId The tenant that the device belongs to.
     * @param credentialsSpec The JSON object to be sent in the request body.
     * @return A future indicating the outcome of the operation.
     *         The future will succeed if the credentials have been added successfully.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     * @throws NullPointerException if the tenant is {@code null}.
     */
    public Future<Void> addCredentials(final String tenantId, final JsonObject credentialsSpec) {
        return addCredentials(tenantId, credentialsSpec, HttpURLConnection.HTTP_CREATED);
    }

    /**
     * Adds credentials for a device.
     * <p>
     * This method simply invokes {@link #addCredentials(String, JsonObject, String, int)}
     * with <em>application/json</em> as the content type and
     * {@link HttpURLConnection#HTTP_CREATED} as the expected status code.
     * 
     * @param tenantId The tenant that the device belongs to.
     * @param credentialsSpec The JSON object to be sent in the request body.
     * @param expectedStatusCode The status code indicating a successful outcome.
     * @return A future indicating the outcome of the operation.
     *         The future will succeed if the response contained the expected status code.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     * @throws NullPointerException if the tenant is {@code null}.
     */
    public Future<Void> addCredentials(final String tenantId, final JsonObject credentialsSpec,
            final int expectedStatusCode) {
        return addCredentials(tenantId, credentialsSpec, CONTENT_TYPE_APPLICATION_JSON, expectedStatusCode);
    }

    /**
     * Adds credentials for a device.
     * 
     * @param tenantId The tenant that the device belongs to.
     * @param credentialsSpec The JSON object to be sent in the request body.
     * @param contentType The content type to set on the request.
     * @param expectedStatusCode The status code indicating a successful outcome.
     * @return A future indicating the outcome of the operation.
     *         The future will succeed if the response contained the expected status code.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     * @throws NullPointerException if the tenant is {@code null}.
     */
    public Future<Void> addCredentials(final String tenantId, final JsonObject credentialsSpec,
            final String contentType, final int expectedStatusCode) {

        Objects.requireNonNull(tenantId);
        final String uri = String.format("/%s/%s", CredentialsConstants.CREDENTIALS_ENDPOINT, tenantId);
        return httpClient.create(uri, credentialsSpec, contentType, statusCode -> statusCode == expectedStatusCode);
    }

    /**
     * Gets all credentials registered for a device.
     * 
     * @param tenantId The tenant that the device belongs to.
     * @param deviceId The identifier of the device.
     * @return A future indicating the outcome of the operation.
     *         The future will contain the response payload if the request succeeded.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     * @throws NullPointerException if the tenant is {@code null}.
     */
    public Future<Buffer> getCredentials(final String tenantId, final String deviceId) {

        Objects.requireNonNull(tenantId);
        final String uri = String.format(TEMPLATE_URI_CREDENTIALS_BY_DEVICE, tenantId, deviceId);
        return httpClient.get(uri, status -> status == HttpURLConnection.HTTP_OK);
    }

    /**
     * Gets credentials of a specific type for a device.
     * <p>
     * This method simply invokes {@link #getCredentials(String, String, String, int)}
     * {@link HttpURLConnection#HTTP_OK} as the expected status code.
     * 
     * @param tenantId The tenant that the device belongs to.
     * @param authId The authentication identifier of the device.
     * @param type The type of credentials to retrieve.
     * @return A future indicating the outcome of the operation.
     *         The future will contain the response payload if the request succeeded.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     * @throws NullPointerException if the tenant is {@code null}.
     */
    public Future<Buffer> getCredentials(final String tenantId, final String authId, final String type) {
        return getCredentials(tenantId, authId, type, HttpURLConnection.HTTP_OK);
    }

    /**
     * Gets credentials of a specific type for a device.
     * 
     * @param tenantId The tenant that the device belongs to.
     * @param authId The authentication identifier of the device.
     * @param type The type of credentials to retrieve.
     * @param expectedStatusCode The status code indicating a successful outcome.
     * @return A future indicating the outcome of the operation.
     *         The future will contain the response payload if the response contains
     *         the expected status code.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     * @throws NullPointerException if the tenant is {@code null}.
     */
    public Future<Buffer> getCredentials(final String tenantId, final String authId, final String type,
            final int expectedStatusCode) {

        Objects.requireNonNull(tenantId);
        final String uri = String.format(TEMPLATE_URI_CREDENTIALS_INSTANCE, tenantId, authId, type);
        return httpClient.get(uri, status -> status == expectedStatusCode);
    }

    /**
     * Updates credentials of a specific type for a device.
     * <p>
     * This method simply invokes {@link #updateCredentials(String, String, String, JsonObject, int)}
     * with {@link HttpURLConnection#HTTP_NO_CONTENT} as the expected status code.
     * 
     * @param tenantId The tenant that the device belongs to.
     * @param authId The authentication identifier of the device.
     * @param type The type of credentials to update.
     * @param credentialsSpec The JSON object to be sent in the request body.
     * @return A future indicating the outcome of the operation.
     *         The future will succeed if the credentials have been updated successfully.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     * @throws NullPointerException if the tenant is {@code null}.
     */
    public Future<Void> updateCredentials(final String tenantId, final String authId, final String type,
            final JsonObject credentialsSpec) {
        return updateCredentials(tenantId, authId, type, credentialsSpec, HttpURLConnection.HTTP_NO_CONTENT);
    }

    /**
     * Updates credentials of a specific type for a device.
     * 
     * @param tenantId The tenant that the device belongs to.
     * @param authId The authentication identifier of the device.
     * @param type The type of credentials to update.
     * @param credentialsSpec The JSON object to be sent in the request body.
     * @param expectedStatusCode The status code indicating a successful outcome.
     * @return A future indicating the outcome of the operation.
     *         The future will succeed if the response contains the expected status code.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     * @throws NullPointerException if the tenant is {@code null}.
     */
    public Future<Void> updateCredentials(final String tenantId, final String authId, final String type,
            final JsonObject credentialsSpec, final int expectedStatusCode) {

        Objects.requireNonNull(tenantId);
        final String uri = String.format(TEMPLATE_URI_CREDENTIALS_INSTANCE, tenantId, authId, type);
        return httpClient.update(uri, credentialsSpec, status -> status == expectedStatusCode);
    }

    /**
     * Removes credentials of a specific type from a device.
     * 
     * @param tenantId The tenant that the device belongs to.
     * @param authId The authentication identifier of the device.
     * @param type The type of credentials to remove.
     * @return A future indicating the outcome of the operation.
     *         The future will succeed if the credentials have been removed successfully.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     * @throws NullPointerException if the tenant is {@code null}.
     */
    public Future<Void> removeCredentials(final String tenantId, final String authId, final String type) {

        Objects.requireNonNull(tenantId);
        final String uri = String.format(TEMPLATE_URI_CREDENTIALS_INSTANCE, tenantId, authId, type);
        return httpClient.delete(uri, status -> status == HttpURLConnection.HTTP_NO_CONTENT);
    }

    /**
     * Removes all credentials from a device.
     * <p>
     * This method simply invokes {@link #removeAllCredentials(String, String, int)}
     * with {@link HttpURLConnection#HTTP_NO_CONTENT} as the expected status code.
     * 
     * @param tenantId The tenant that the device belongs to.
     * @param deviceId The identifier of the device.
     * @return A future indicating the outcome of the operation.
     *         The future will succeed if the credentials have been removed successfully.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     * @throws NullPointerException if the tenant is {@code null}.
     */
    public Future<Void> removeAllCredentials(final String tenantId, final String deviceId) {

        return removeAllCredentials(tenantId, deviceId, HttpURLConnection.HTTP_NO_CONTENT);
    }

    /**
     * Removes all credentials from a device.
     * 
     * @param tenantId The tenant that the device belongs to.
     * @param deviceId The identifier of the device.
     * @param expectedStatusCode The status code indicating a successful outcome.
     * @return A future indicating the outcome of the operation.
     *         The future will succeed if the response contains the expected status code.
     *         Otherwise the future will fail with a {@link ServiceInvocationException}.
     * @throws NullPointerException if the tenant is {@code null}.
     */
    public Future<Void> removeAllCredentials(final String tenantId, final String deviceId,
            final int expectedStatusCode) {

        Objects.requireNonNull(tenantId);
        final String uri = String.format(TEMPLATE_URI_CREDENTIALS_BY_DEVICE, tenantId, deviceId);
        return httpClient.delete(uri, status -> status == expectedStatusCode);
    }

    // convenience methods

    /**
     * Creates a tenant and adds a device to it with a given password.
     * <p>
     * The password will be added as a <em>sha-512</em> hashed password
     * using the device identifier as the authentication identifier.
     * 
     * @param tenant The tenant to create.
     * @param deviceId The identifier of the device to add to the tenant.
     * @param password The password to use for the device's credentials.
     * @return A future indicating the outcome of the operation.
     */
    public Future<Void> addDeviceForTenant(final TenantObject tenant, final String deviceId,
            final String password) {

        return addTenant(JsonObject.mapFrom(tenant)).compose(ok -> registerDevice(tenant.getTenantId(), deviceId))
                .compose(ok -> {
                    final CredentialsObject credentialsSpec = CredentialsObject.fromHashedPassword(deviceId,
                            deviceId, password, "sha-512", null, null, null);
                    return addCredentials(tenant.getTenantId(), JsonObject.mapFrom(credentialsSpec));
                });
    }

    /**
     * Creates a tenant and adds a device to it with a given client certificate.
     * <p>
     * The device will be registered with a set of <em>x509-cert</em> credentials
     * using the client certificate's subject DN as authentication identifier.
     * 
     * @param tenant The tenant to create.
     * @param deviceId The identifier of the device to add to the tenant.
     * @param deviceCert The device's client certificate.
     * @return A future indicating the outcome of the operation.
     */
    public Future<Void> addDeviceForTenant(final TenantObject tenant, final String deviceId,
            final X509Certificate deviceCert) {

        return addTenant(JsonObject.mapFrom(tenant)).compose(ok -> registerDevice(tenant.getTenantId(), deviceId))
                .compose(ok -> {
                    final CredentialsObject credentialsSpec = CredentialsObject.fromClientCertificate(deviceId,
                            deviceCert, null, null);
                    return addCredentials(tenant.getTenantId(), JsonObject.mapFrom(credentialsSpec));
                }).map(ok -> {
                    LOG.debug(
                            "registered device with client certificate [tenant-id: {}, device-id: {}, auth-id: {}]",
                            tenant.getTenantId(), deviceId,
                            deviceCert.getSubjectX500Principal().getName(X500Principal.RFC2253));
                    return null;
                });
    }

}