diff --git a/docs/content/guides/runhawkbit.md b/docs/content/guides/runhawkbit.md index 1a717b4223..2fb508d2f0 100644 --- a/docs/content/guides/runhawkbit.md +++ b/docs/content/guides/runhawkbit.md @@ -66,6 +66,54 @@ spring.rabbitmq.host=localhost spring.rabbitmq.port=5672 ``` +### Set [Eclipse Hono](https://www.eclipse.org/hono/) as hawkBit's device registry + +``` +hawkbit.dmf.hono.enabled=true +hawkbit.dmf.hono.tenant-list-uri=http://HONO_HOST/v1/tenants +hawkbit.dmf.hono.device-list-uri=http://HONO_HOST/v1/devices/$tenantId +hawkbit.dmf.hono.credentials-list-uri=http://HONO_HOST/v1/credentials/$tenantId/$deviceId +``` +`$tenantId` and `$deviceId` are placeholders which will be replaced by hawkBit during the respective requests. + +hawkBit currently supports three different methods to authenticate with hono: +- None (`none`; default) +- BasicAuth (`basic`) +- OpenID Connect (`oidc`) + +If you intend to use any authentication method other than `none` you must provide these additional properties: + +``` +hawkbit.dmf.hono.authentication-method=oidc +hawkbit.dmf.hono.username=USERNAME +hawkbit.dmf.hono.password=PASSWORD + +// Only for authentication-method = oidc +hawkbit.dmf.hono.oidc-token-uri=http://OIDC_HOST/auth/realms/REALM/protocol/openid-connect/token +hawkbit.dmf.hono.oidc-client-id=OIDC_CLIENT_ID +hawkbit.dmf.hono.oidc-client-secret=OIDC_CLIENT_SECRET # You can use a oidc client secret instead of username+password +``` + +hawkBit handles device registry updates through CUD events emitted by Hono over any Spring Cloud Stream supported channel, such as AMQP or Google Cloud Pub/Sub. + +In order to have predictable channel names use the following properties: +``` +spring.cloud.stream.bindings.device-created.destination=device-registry.device-created +spring.cloud.stream.bindings.device-created.group=hawkBit +spring.cloud.stream.bindings.device-updated.destination=device-registry.device-updated +spring.cloud.stream.bindings.device-updated.group=hawkBit +spring.cloud.stream.bindings.device-deleted.destination=device-registry.device-deleted +spring.cloud.stream.bindings.device-deleted.group=hawkBit +``` +For Google Cloud Pub/Sub disable the default Maven profile `hono-amqp` and enable the profile `amqp-gcp-pubsub`. + +Additionally, you can specify a field of the device's extension object which will be used as the corresponding target's name: +``` +hawkbit.dmf.hono.target-name-field=fancyFieldName +``` +If none is specified the device's ID is used as the target's name. + + ### Adapt hostname of example scenario [creation script](https://github.com/eclipse/hawkbit-examples/blob/master/hawkbit-example-mgmt-simulator/src/main/resources/application.properties) Should only be necessary if your system does not run on localhost or uses a different port than the example app. diff --git a/hawkbit-autoconfigure/pom.xml b/hawkbit-autoconfigure/pom.xml index 1cd89aa38f..dfc5c73bf3 100644 --- a/hawkbit-autoconfigure/pom.xml +++ b/hawkbit-autoconfigure/pom.xml @@ -25,12 +25,18 @@ ${project.version} true - + org.eclipse.hawkbit hawkbit-dmf-amqp ${project.version} true + + org.eclipse.hawkbit + hawkbit-dmf-hono + ${project.version} + true + org.eclipse.hawkbit hawkbit-ui diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/dmf/hono/DmfHonoAutoConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/dmf/hono/DmfHonoAutoConfiguration.java new file mode 100644 index 0000000000..0051cf2b55 --- /dev/null +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/dmf/hono/DmfHonoAutoConfiguration.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2019 Kiwigrid GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.autoconfigure.dmf.hono; + +import org.eclipse.hawkbit.dmf.hono.DmfHonoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * The Eclipse Hono based device Management Federation API (DMF) auto configuration. + */ +@Configuration +@ConditionalOnClass(DmfHonoConfiguration.class) +@Import(DmfHonoConfiguration.class) +public class DmfHonoAutoConfiguration { +} \ No newline at end of file diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java index 655a8bc1c2..130cbdc2fd 100644 --- a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java @@ -24,6 +24,7 @@ import org.eclipse.hawkbit.cache.DownloadIdCache; import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants; import org.eclipse.hawkbit.ddi.rest.resource.DdiApiConfiguration; +import org.eclipse.hawkbit.dmf.hono.HonoDeviceSync; import org.eclipse.hawkbit.im.authentication.SpPermission; import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; import org.eclipse.hawkbit.im.authentication.TenantUserPasswordAuthenticationToken; @@ -40,8 +41,10 @@ import org.eclipse.hawkbit.security.HttpControllerPreAuthenticateAnonymousDownloadFilter; import org.eclipse.hawkbit.security.HttpControllerPreAuthenticateSecurityTokenFilter; import org.eclipse.hawkbit.security.HttpControllerPreAuthenticatedGatewaySecurityTokenFilter; +import org.eclipse.hawkbit.security.HttpControllerPreAuthenticatedHonoFilter; import org.eclipse.hawkbit.security.HttpControllerPreAuthenticatedSecurityHeaderFilter; import org.eclipse.hawkbit.security.HttpDownloadAuthenticationFilter; +import org.eclipse.hawkbit.security.PreAuthHonoAuthenticationProvider; import org.eclipse.hawkbit.security.PreAuthTokenSourceTrustAuthenticationProvider; import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.tenancy.TenantAware; @@ -49,6 +52,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -165,6 +169,9 @@ static class ControllerSecurityConfigurationAdapter extends WebSecurityConfigure private final HawkbitSecurityProperties securityProperties; private final SystemSecurityContext systemSecurityContext; + @Autowired(required = false) + private HonoDeviceSync honoDeviceSync; + @Autowired ControllerSecurityConfigurationAdapter(final ControllerManagement controllerManagement, final TenantConfigurationManagement tenantConfigurationManagement, final TenantAware tenantAware, @@ -213,6 +220,17 @@ protected void configure(final HttpSecurity http) throws Exception { securityHeaderFilter.setCheckForPrincipalChanges(true); securityHeaderFilter.setAuthenticationDetailsSource(authenticationDetailsSource); + + HttpControllerPreAuthenticatedHonoFilter honoFilter = null; + if (honoDeviceSync != null) { + honoFilter = new HttpControllerPreAuthenticatedHonoFilter( + tenantConfigurationManagement, tenantAware, controllerManagement, systemSecurityContext, + honoDeviceSync); + honoFilter.setAuthenticationManager(authenticationManager()); + honoFilter.setCheckForPrincipalChanges(true); + honoFilter.setAuthenticationDetailsSource(authenticationDetailsSource); + } + final HttpControllerPreAuthenticateSecurityTokenFilter securityTokenFilter = new HttpControllerPreAuthenticateSecurityTokenFilter( tenantConfigurationManagement, tenantAware, controllerManagement, systemSecurityContext); securityTokenFilter.setAuthenticationManager(authenticationManager()); @@ -244,7 +262,11 @@ protected void configure(final HttpSecurity http) throws Exception { .authenticationFilter(anonymousFilter); } else { - httpSec.addFilter(securityHeaderFilter).addFilter(securityTokenFilter) + httpSec.addFilter(securityHeaderFilter); + if (honoFilter != null) { + httpSec.addFilter(honoFilter); + } + httpSec.addFilter(securityTokenFilter) .addFilter(gatewaySecurityTokenFilter).requestMatchers().antMatchers(DDI_ANT_MATCHERS).and() .anonymous().disable().authorizeRequests().anyRequest().authenticated().and() .exceptionHandling() @@ -257,6 +279,10 @@ protected void configure(final HttpSecurity http) throws Exception { @Override protected void configure(final AuthenticationManagerBuilder auth) throws Exception { + if (honoDeviceSync != null) { + auth.authenticationProvider(new PreAuthHonoAuthenticationProvider(honoDeviceSync, + ddiSecurityConfiguration.getRp().getTrustedIPs())); + } auth.authenticationProvider(new PreAuthTokenSourceTrustAuthenticationProvider( ddiSecurityConfiguration.getRp().getTrustedIPs())); } @@ -281,6 +307,9 @@ static class ControllerDownloadSecurityConfigurationAdapter extends WebSecurityC private final HawkbitSecurityProperties securityProperties; private final SystemSecurityContext systemSecurityContext; + @Autowired(required = false) + private HonoDeviceSync honoDeviceSync; + @Autowired ControllerDownloadSecurityConfigurationAdapter(final ControllerManagement controllerManagement, final TenantConfigurationManagement tenantConfigurationManagement, final TenantAware tenantAware, @@ -329,6 +358,16 @@ protected void configure(final HttpSecurity http) throws Exception { securityHeaderFilter.setCheckForPrincipalChanges(true); securityHeaderFilter.setAuthenticationDetailsSource(authenticationDetailsSource); + HttpControllerPreAuthenticatedHonoFilter honoFilter = null; + if (honoDeviceSync != null) { + honoFilter = new HttpControllerPreAuthenticatedHonoFilter( + tenantConfigurationManagement, tenantAware, controllerManagement, systemSecurityContext, + honoDeviceSync); + honoFilter.setAuthenticationManager(authenticationManager()); + honoFilter.setCheckForPrincipalChanges(true); + honoFilter.setAuthenticationDetailsSource(authenticationDetailsSource); + } + final HttpControllerPreAuthenticateSecurityTokenFilter securityTokenFilter = new HttpControllerPreAuthenticateSecurityTokenFilter( tenantConfigurationManagement, tenantAware, controllerManagement, systemSecurityContext); securityTokenFilter.setAuthenticationManager(authenticationManager()); @@ -366,7 +405,11 @@ protected void configure(final HttpSecurity http) throws Exception { .authenticationFilter(anonymousFilter); } else { - httpSec.addFilter(securityHeaderFilter).addFilter(securityTokenFilter) + httpSec.addFilter(securityHeaderFilter); + if (honoFilter != null) { + httpSec.addFilter(honoFilter); + } + httpSec.addFilter(securityTokenFilter) .addFilter(gatewaySecurityTokenFilter).addFilter(controllerAnonymousDownloadFilter) .requestMatchers().antMatchers(DDI_DL_ANT_MATCHER).and().anonymous().disable() .authorizeRequests().anyRequest().authenticated().and().exceptionHandling() @@ -379,6 +422,10 @@ protected void configure(final HttpSecurity http) throws Exception { @Override protected void configure(final AuthenticationManagerBuilder auth) throws Exception { + if (honoDeviceSync != null) { + auth.authenticationProvider(new PreAuthHonoAuthenticationProvider(honoDeviceSync, + ddiSecurityConfiguration.getRp().getTrustedIPs())); + } auth.authenticationProvider(new PreAuthTokenSourceTrustAuthenticationProvider( ddiSecurityConfiguration.getRp().getTrustedIPs())); } @@ -435,6 +482,9 @@ public static class IdRestSecurityConfigurationAdapter extends WebSecurityConfig @Autowired private DownloadIdCache downloadIdCache; + @Autowired(required = false) + private HonoDeviceSync honoDeviceSync; + @Override protected void configure(final HttpSecurity http) throws Exception { @@ -453,6 +503,10 @@ protected void configure(final HttpSecurity http) throws Exception { @Override protected void configure(final AuthenticationManagerBuilder auth) throws Exception { + if (honoDeviceSync != null) { + auth.authenticationProvider(new PreAuthHonoAuthenticationProvider(honoDeviceSync, + ddiSecurityConfiguration.getRp().getTrustedIPs())); + } auth.authenticationProvider(new PreAuthTokenSourceTrustAuthenticationProvider( ddiSecurityConfiguration.getRp().getTrustedIPs())); } diff --git a/hawkbit-autoconfigure/src/main/resources/META-INF/spring.factories b/hawkbit-autoconfigure/src/main/resources/META-INF/spring.factories index 5716eb54a7..a3c98edc8f 100644 --- a/hawkbit-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/hawkbit-autoconfigure/src/main/resources/META-INF/spring.factories @@ -4,6 +4,7 @@ org.eclipse.hawkbit.autoconfigure.cache.CacheAutoConfiguration,\ org.eclipse.hawkbit.autoconfigure.cache.DownloadIdCacheAutoConfiguration,\ org.eclipse.hawkbit.autoconfigure.ddi.DDiApiAutoConfiguration,\ org.eclipse.hawkbit.autoconfigure.dmf.amqp.DmfApiAutoConfiguration,\ +org.eclipse.hawkbit.autoconfigure.dmf.hono.DmfHonoAutoConfiguration,\ org.eclipse.hawkbit.autoconfigure.mgmt.ui.MgmtUiAutoConfiguration,\ org.eclipse.hawkbit.autoconfigure.mgmt.MgmtApiAutoConfiguration,\ org.eclipse.hawkbit.autoconfigure.repository.event.EventPublisherAutoConfiguration,\ diff --git a/hawkbit-dmf/hawkbit-dmf-hono/pom.xml b/hawkbit-dmf/hawkbit-dmf-hono/pom.xml new file mode 100644 index 0000000000..947976677a --- /dev/null +++ b/hawkbit-dmf/hawkbit-dmf-hono/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + + hawkbit-dmf-parent + org.eclipse.hawkbit + 0.3.0-SNAPSHOT + + hawkbit-dmf-hono + hawkBit :: DMF :: Hono + + + + org.springframework.cloud + spring-cloud-stream + + + org.eclipse.hawkbit + hawkbit-repository-api + 0.3.0-SNAPSHOT + compile + + + org.eclipse.hawkbit + hawkbit-repository-core + 0.3.0-SNAPSHOT + compile + + + org.eclipse.hawkbit + hawkbit-repository-jpa + 0.3.0-SNAPSHOT + compile + + + diff --git a/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/DmfHonoConfiguration.java b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/DmfHonoConfiguration.java new file mode 100644 index 0000000000..9f54372670 --- /dev/null +++ b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/DmfHonoConfiguration.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2019 Kiwigrid GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.hono; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "hawkbit.dmf.hono") +@ConditionalOnProperty(prefix = "hawkbit.dmf.hono", name = "enabled") +@ComponentScan +public class DmfHonoConfiguration { + private String tenantListUri; + private String deviceListUri; + private String credentialsListUri; + private String authenticationMethod = "none"; + private String oidcTokenUri = ""; + private String oidcClientId = ""; + private String oidcClientSecret = ""; + private String username = ""; + private String password = ""; + private String targetNameField = ""; + + @Bean + public HonoDeviceSync honoDeviceSync() { + return new HonoDeviceSync(tenantListUri, deviceListUri, credentialsListUri, authenticationMethod, + oidcTokenUri, oidcClientId, oidcClientSecret, username, password, targetNameField); + } + + public String getTenantListUri() { + return tenantListUri; + } + + public void setTenantListUri(String tenantListUri) { + this.tenantListUri = tenantListUri; + } + + public String getDeviceListUri() { + return deviceListUri; + } + + public void setDeviceListUri(String deviceListUri) { + this.deviceListUri = deviceListUri; + } + + public String getCredentialsListUri() { + return credentialsListUri; + } + + public void setCredentialsListUri(String credentialsListUri) { + this.credentialsListUri = credentialsListUri; + } + + public String getAuthenticationMethod() { + return authenticationMethod; + } + + public void setAuthenticationMethod(String authenticationMethod) { + this.authenticationMethod = authenticationMethod; + } + + public String getOidcTokenUri() { + return oidcTokenUri; + } + + public void setOidcTokenUri(String oidcTokenUri) { + this.oidcTokenUri = oidcTokenUri; + } + + public String getOidcClientId() { + return oidcClientId; + } + + public void setOidcClientId(String oidcClientId) { + this.oidcClientId = oidcClientId; + } + + public String getOidcClientSecret() { + return oidcClientSecret; + } + + public void setOidcClientSecret(String oidcClientSecret) { + this.oidcClientSecret = oidcClientSecret; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getTargetNameField() { + return targetNameField; + } + + public void setTargetNameField(String targetNameField) { + this.targetNameField = targetNameField; + } +} diff --git a/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/HonoDeviceSync.java b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/HonoDeviceSync.java new file mode 100644 index 0000000000..e0e274c773 --- /dev/null +++ b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/HonoDeviceSync.java @@ -0,0 +1,415 @@ +/** + * Copyright (c) 2019 Kiwigrid GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.hono; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.eclipse.hawkbit.dmf.hono.model.*; +import org.eclipse.hawkbit.im.authentication.PermissionService; +import org.eclipse.hawkbit.repository.EntityFactory; +import org.eclipse.hawkbit.repository.SystemManagement; +import org.eclipse.hawkbit.repository.TargetManagement; +import org.eclipse.hawkbit.repository.TargetTagManagement; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetTag; +import org.eclipse.hawkbit.security.SystemSecurityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.cloud.stream.annotation.EnableBinding; +import org.springframework.cloud.stream.annotation.StreamListener; +import org.springframework.context.event.EventListener; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.lang.Nullable; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.Semaphore; + +@EnableBinding(HonoInputSink.class) +public class HonoDeviceSync { + + private static final Logger LOG = LoggerFactory.getLogger(HonoDeviceSync.class); + + @Autowired + private EntityFactory entityFactory; + + @Autowired + private SystemManagement systemManagement; + + @Autowired + private SystemSecurityContext systemSecurityContext; + + @Autowired + private TargetManagement targetManagement; + + @Autowired + private TargetTagManagement targetTagManagement; + + @Autowired + private PermissionService permissionService; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + private final Map mutexes = new HashMap<>(); + private boolean syncedInitially = false; + + private String oidcAccessToken = null; + private Instant oidcAccessTokenExpirationDate; + + private String honoTenantListUri; + private String honoDeviceListUri; + private String honoCredentialsListUri; + private String authenticationMethod; + private String oidcTokenUri; + private String oidcClientId; + private String oidcClientSecret; + private String username; + private String password; + private String targetNameFieldInDeviceExtension; + + HonoDeviceSync(String honoTenantListUri, String honoDevicesEndpoint, String honoCredentialsListUri, + String authenticationMethod, String oidcTokenUri, String oidcClientId, String oidcClientSecret, + String username, String password, String targetNameFieldInDeviceExtension) { + this.honoTenantListUri = honoTenantListUri; + this.honoDeviceListUri = honoDevicesEndpoint; + this.honoCredentialsListUri = honoCredentialsListUri; + this.authenticationMethod = authenticationMethod; + this.oidcTokenUri = oidcTokenUri; + this.oidcClientId = oidcClientId; + this.oidcClientSecret = oidcClientSecret; + this.username = username; + this.password = password; + this.targetNameFieldInDeviceExtension = targetNameFieldInDeviceExtension; + + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + @EventListener(ApplicationReadyEvent.class) + private void init() { + permissionService.setHonoSyncEnabled(true); + + // Since ApplicationReadyEvent is emitted multiple times make sure it is synced at most once during startup. + if (!syncedInitially) { + syncedInitially = true; + synchronize(false); + } + } + + public void synchronize(boolean syncOnlyCurrentTenant) { + try { + String currentTenant = null; + if (syncOnlyCurrentTenant) { + currentTenant = systemManagement.currentTenant(); + } + + List tenants = getAllHonoTenants(); + for (IdentifiableHonoTenant honoTenant : tenants) { + String tenant = honoTenant.getId(); + + if (syncOnlyCurrentTenant && !tenant.equals(currentTenant)) { + continue; + } + + synchronizeTenant(tenant); + } + } catch (IOException e) { + LOG.error("Could not parse hono api response.", e); + } catch (InterruptedException e) { + LOG.warn("Synchronizing hawkbit with Hono has been interrupted.", e); + } + } + + private void synchronizeTenant(String tenant) throws IOException, InterruptedException { + Semaphore semaphore = mutexes.computeIfAbsent(tenant, t -> new Semaphore(1)); + semaphore.acquire(); + + try { + Map honoDevices = getAllHonoDevices(tenant); + Slice targets = systemSecurityContext.runAsSystemAsTenant( + () -> targetManagement.findAll(Pageable.unpaged()), tenant); + + for (Target target : targets) { + String controllerId = target.getControllerId(); + if (honoDevices.containsKey(controllerId)) { + IdentifiableHonoDevice honoDevice = honoDevices.remove(controllerId); + honoDevice.setTenant(tenant); + systemSecurityContext.runAsSystemAsTenant(() -> updateTarget(honoDevice), tenant); + } + else { + systemSecurityContext.runAsSystemAsTenant(() -> { + targetManagement.deleteByControllerID(target.getControllerId()); + return true; + }, tenant); + } + } + + // At this point honoTargets only contains objects which were not found in hawkBit's target repository + for (Map.Entry entry : honoDevices.entrySet()) { + systemSecurityContext.runAsSystemAsTenant(() -> createTarget(entry.getValue()), tenant); + } + } + finally { + semaphore.release(); + } + } + + public void checkDeviceIfAbsentSync(String tenant, String deviceID) { + Optional target = systemSecurityContext.runAsSystemAsTenant( + () -> targetManagement.getByControllerID(deviceID), tenant); + if (!target.isPresent()) { + try { + synchronizeTenant(tenant); + } catch (IOException | InterruptedException e) { + LOG.error("Could not synchronize with hono for tenant {}.", tenant, e); + } + } + } + + private List getAllHonoTenants() throws IOException { + List tenants = new ArrayList<>(); + long offset = 0; + long total = Long.MAX_VALUE; + while (tenants.size() < total) { + HttpURLConnection connection = getHonoData(honoTenantListUri + (honoTenantListUri.contains("?") ? "&" : "?") + "offset=" + offset); + + HonoTenantListPage page = objectMapper.readValue(connection.getInputStream(), HonoTenantListPage.class); + if (page.getItems() != null) { + tenants.addAll(page.getItems()); + offset += page.getItems().size(); + } + total = page.getTotal(); + } + + return tenants; + } + + private Map getAllHonoDevices(String tenant) throws IOException { + Map devices = new HashMap<>(); + long offset = 0; + long total = Long.MAX_VALUE; + while (devices.size() < total) { + HttpURLConnection connection = getHonoData(honoDeviceListUri.replace("$tenantId", tenant) + + (honoDeviceListUri.contains("?") ? "&" : "?") + "offset=" + offset); + + HonoDeviceListPage page = objectMapper.readValue(connection.getInputStream(), HonoDeviceListPage.class); + if (page.getItems() != null) { + for (IdentifiableHonoDevice identifiableDevice : page.getItems()) { + identifiableDevice.setTenant(tenant); + devices.put(identifiableDevice.getId(), identifiableDevice); + } + offset += page.getItems().size(); + } + total = page.getTotal(); + } + + return devices; + } + + public Collection getAllHonoCredentials(String tenant, String deviceId) { + try { + HttpURLConnection connection = getHonoData(honoCredentialsListUri.replace("$tenantId", tenant) + .replace("$deviceId", deviceId)); + return objectMapper.readValue(connection.getInputStream(), new TypeReference>() {}); + } + catch (IOException e) { + LOG.error("Could not read credentials for device '{}/{}'.", tenant, deviceId, e); + return null; + } + } + + private HttpURLConnection getHonoData(String uri) throws IOException { + URL url = new URL(uri); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + + switch (authenticationMethod) { + case "basic": + connection.setRequestProperty("Authorization", "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes())); + break; + + case "oidc": + if (oidcAccessToken == null || + (oidcAccessTokenExpirationDate != null && oidcAccessTokenExpirationDate.isBefore(Instant.now()))) { + + URL oidcTokenUrl = new URL(oidcTokenUri); + HttpURLConnection jwtConnection = (HttpURLConnection) oidcTokenUrl.openConnection(); + jwtConnection.setDoOutput(true); + DataOutputStream outputStream = new DataOutputStream(jwtConnection.getOutputStream()); + if (oidcClientSecret != null && !oidcClientSecret.isEmpty()) { + outputStream.writeBytes("grant_type=client_credentials" + + "&client_id=" + URLEncoder.encode(oidcClientId, "UTF-8") + + "&client_secret=" + URLEncoder.encode(oidcClientSecret, "UTF-8")); + } + else { + outputStream.writeBytes("grant_type=password" + + "&client_id=" + URLEncoder.encode(oidcClientId, "UTF-8") + + "&username=" + URLEncoder.encode(username, "UTF-8") + + "&password=" + URLEncoder.encode(password, "UTF-8")); + } + outputStream.flush(); + outputStream.close(); + + int statusCode = jwtConnection.getResponseCode(); + if (statusCode >= 200 && statusCode < 300) { + JsonNode node = objectMapper.readValue(jwtConnection.getInputStream(), JsonNode.class); + oidcAccessToken = node.get("access_token").asText(); + JsonNode expiresIn = node.get("expires_in"); + if (expiresIn != null) { + oidcAccessTokenExpirationDate = Instant.now().plusSeconds(expiresIn.asLong()); + } + } + else { + throw new IOException("Server returned HTTP response code: " + statusCode + " for URL: " + oidcTokenUrl.toString()); + } + } + connection.setRequestProperty("Authorization", "Bearer " + oidcAccessToken); + break; + } + + return connection; + } + + @StreamListener(HonoInputSink.DEVICE_CREATED) + public void onDeviceCreated(IdentifiableHonoDevice honoDevice) { + final String tenant = honoDevice.getTenant(); + if (tenant == null) { + throw new RuntimeException("The delivered hono device does not contain information about the tenant"); + } + + systemSecurityContext.runAsSystemAsTenant(() -> createTarget(honoDevice), tenant); + } + + @StreamListener(HonoInputSink.DEVICE_UPDATED) + public void onDeviceUpdated(IdentifiableHonoDevice honoDevice) { + final String tenant = honoDevice.getTenant(); + if (tenant == null) { + throw new RuntimeException("The delivered hono device does not contain information about the tenant"); + } + + systemSecurityContext.runAsSystemAsTenant(() -> { + if (targetManagement.getByControllerID(honoDevice.getId()).isPresent()) { + return updateTarget(honoDevice); + } + else { + return createTarget(honoDevice); + } + }, tenant); + } + + @StreamListener(HonoInputSink.DEVICE_DELETED) + public void onDeviceDeleted(IdentifiableHonoDevice honoDevice) { + final String tenant = honoDevice.getTenant(); + if (tenant == null) { + throw new RuntimeException("The delivered hono device does not contain information about the tenant"); + } + + systemSecurityContext.runAsSystemAsTenant(() -> { + try { + targetManagement.deleteByControllerID(honoDevice.getId()); + } + catch (EntityNotFoundException e) { + // Do nothing as it is already deleted + } + return true; + }, tenant); + } + + private Target createTarget(IdentifiableHonoDevice honoDevice) { + systemManagement.getTenantMetadata(honoDevice.getTenant()); + Target target = targetManagement.create(entityFactory.target().create() + .controllerId(honoDevice.getId()) + .name(getDeviceName(honoDevice))); + syncTags(target, getDeviceTags(honoDevice)); + return target; + } + + private Target updateTarget(IdentifiableHonoDevice honoDevice) { + Target target = targetManagement.update(entityFactory.target() + .update(honoDevice.getId()) + .name(getDeviceName(honoDevice))); + syncTags(target, getDeviceTags(honoDevice)); + return target; + } + + private void syncTags(Target target, @Nullable String tags) { + String controllerId = target.getControllerId(); + Collection tagNames; + if (tags != null) { + tagNames = new ArrayList<>(Arrays.asList(tags.split(","))); + } + else { + tagNames = Collections.emptyList(); + } + + Slice assignedTags = targetTagManagement.findByTarget(Pageable.unpaged(), target.getControllerId()); + for (TargetTag tag : assignedTags) { + if (tagNames.contains(tag.getName())) { + tagNames.remove(tag.getName()); + } + else { + targetManagement.unAssignTag(controllerId, tag.getId()); + } + } + + for (String name : tagNames) { + TargetTag tag = targetTagManagement.getByName(name).orElseGet( + () -> targetTagManagement.create(entityFactory.tag().create().name(name))); + + targetManagement.assignTag(Collections.singleton(target.getControllerId()), tag.getId()); + } + } + + private String getDeviceName(IdentifiableHonoDevice honoDevice) { + if (targetNameFieldInDeviceExtension != null) { + Object ext = honoDevice.getDevice().getExt(); + if (ext instanceof JsonNode) { + JsonNode nameValue = ((JsonNode) ext).get(targetNameFieldInDeviceExtension); + if (nameValue != null) { + return nameValue.asText(); + } + else { + LOG.warn("The extension object of device '{}/{}' does not contain the field '{}'.", + honoDevice.getTenant(), honoDevice.getId(), targetNameFieldInDeviceExtension); + } + } + else { + LOG.warn("The extension field of device '{}/{}' is not a valid JSON object.", honoDevice.getTenant(), + honoDevice.getId()); + } + } + + return honoDevice.getId(); + } + + private String getDeviceTags(IdentifiableHonoDevice honoDevice) { + Object ext = honoDevice.getDevice().getExt(); + if (ext instanceof JsonNode) { + JsonNode tagsValue = ((JsonNode) ext).get("TargetTags"); + if (tagsValue != null) { + return tagsValue.asText(); + } + } + else { + LOG.warn("The extension field of device '{}/{}' is not a valid JSON object.", honoDevice.getTenant(), + honoDevice.getId()); + } + + return null; + } +} diff --git a/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/HonoInputSink.java b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/HonoInputSink.java new file mode 100644 index 0000000000..f2af5f4bba --- /dev/null +++ b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/HonoInputSink.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2019 Kiwigrid GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.hono; + +import org.springframework.cloud.stream.annotation.Input; +import org.springframework.messaging.SubscribableChannel; + +public interface HonoInputSink { + String DEVICE_CREATED = "device-created"; + String DEVICE_DELETED = "device-deleted"; + String DEVICE_UPDATED = "device-updated"; + + @Input(DEVICE_CREATED) + SubscribableChannel onDeviceCreated(); + + @Input(DEVICE_UPDATED) + SubscribableChannel onDeviceUpdated(); + + @Input(DEVICE_DELETED) + SubscribableChannel onDeviceDeleted(); +} diff --git a/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoCredentials.java b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoCredentials.java new file mode 100644 index 0000000000..7309b057c8 --- /dev/null +++ b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoCredentials.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2019 Kiwigrid GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.hono.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import java.util.Collection; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = HonoPasswordCredentials.class, name = "hashed-password"), + @JsonSubTypes.Type(value = HonoPSKCredentials.class, name = "psk"), + @JsonSubTypes.Type(value = HonoX509CertificateCredentials.class, name = "x509-cert") +}) +public abstract class HonoCredentials { + private String authId; + private String type; + private boolean enabled = true; + Collection secrets; + + @JsonProperty("type") + public String getType() { + return type; + } + + @JsonProperty("auth-id") + public String getAuthId() { + return authId; + } + + @JsonProperty("enabled") + public boolean isEnabled() { + return enabled; + } + + @JsonProperty("secrets") + public Collection getSecrets() { + return secrets; + } + + @JsonProperty("type") + public void setType(String type) { + this.type = type; + } + + @JsonProperty("auth-id") + public void setAuthId(String authId) { + this.authId = authId; + } + + @JsonProperty("enabled") + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean matches(final String providedSecret) { + if (enabled) { + for (HonoSecret secret : secrets) { + if (secret.isValid() && secret.matches(providedSecret)) { + return true; + } + } + } + + return false; + } +} diff --git a/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoDevice.java b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoDevice.java new file mode 100644 index 0000000000..f30c719d7d --- /dev/null +++ b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoDevice.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2019 Kiwigrid GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.hono.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; + +public class HonoDevice { + private boolean enabled; + private JsonNode ext; + + @JsonProperty("enabled") + public boolean isEnabled() { + return enabled; + } + + @JsonProperty("ext") + public Object getExt() { + return ext; + } + + @JsonProperty("enabled") + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @JsonProperty("ext") + public void setExt(JsonNode ext) { + this.ext = ext; + } +} diff --git a/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoDeviceListPage.java b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoDeviceListPage.java new file mode 100644 index 0000000000..ce811683ce --- /dev/null +++ b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoDeviceListPage.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2019 Kiwigrid GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.hono.model; + +import java.util.List; + +public class HonoDeviceListPage { + private long total; + private List items; + + public long getTotal() { + return total; + } + + public List getItems() { + return items; + } + + public void setTotal(long total) { + this.total = total; + } + + public void setItems(List items) { + this.items = items; + } +} diff --git a/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoPSKCredentials.java b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoPSKCredentials.java new file mode 100644 index 0000000000..90f2e8b42c --- /dev/null +++ b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoPSKCredentials.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2019 Kiwigrid GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.hono.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; + +import java.util.Collection; + +@JsonTypeName("psk") +public class HonoPSKCredentials extends HonoCredentials { + @JsonProperty("secrets") + public void setSecrets(Collection secrets) { + this.secrets = secrets; + } + + public static class Secret extends HonoSecret { + private String key; + + @JsonProperty("key") + public String getKey() { + return key; + } + + @JsonProperty("key") + public void setKey(String key) { + this.key = key; + } + + @Override + public boolean matches(final String key) { + return key != null && key.equals(this.key); + } + } +} diff --git a/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoPasswordCredentials.java b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoPasswordCredentials.java new file mode 100644 index 0000000000..6397d64c42 --- /dev/null +++ b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoPasswordCredentials.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2019 Kiwigrid GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.hono.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.MessageDigestPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.Collection; + +@JsonTypeName("hashed-password") +public class HonoPasswordCredentials extends HonoCredentials { + @JsonProperty("secrets") + public void setSecrets(Collection secrets) { + this.secrets = secrets; + } + + public static class Secret extends HonoSecret { + private String hashFunction; + private String salt; + private String pwdHash; + + @Override + public boolean matches(final String password) { + if ("none".equals(hashFunction)) { + return pwdHash.equals(password); + } + + PasswordEncoder encoder; + if ("bcrypt".equals(hashFunction)) { + encoder = new BCryptPasswordEncoder(); + } + else if("sha-256".equals(hashFunction)) { + encoder = new MessageDigestPasswordEncoder("SHA-256"); + } + else if("SHA-512".equals(hashFunction)) { + encoder = new MessageDigestPasswordEncoder("SHA-512"); + } + else { + return false; + } + + return encoder.matches(password + (salt != null ? salt : ""), pwdHash); + } + + @JsonProperty("hash-function") + public String getHashFunction() { + return hashFunction; + } + + @JsonProperty("salt") + public String getSalt() { + return salt; + } + + @JsonProperty("pwd-hash") + public String getPwdHash() { + return pwdHash; + } + + @JsonProperty("hash-function") + public void setHashFunction(String hashFunction) { + this.hashFunction = hashFunction; + } + + @JsonProperty("salt") + public void setSalt(byte[] salt) { + this.salt = new String(salt); + } + + @JsonProperty("pwd-hash") + public void setPwdHash(byte[] pwdHash) { + this.pwdHash = new String(pwdHash); + } + } +} diff --git a/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoSecret.java b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoSecret.java new file mode 100644 index 0000000000..bacd6f85ad --- /dev/null +++ b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoSecret.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2019 Kiwigrid GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.hono.model; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.time.ZonedDateTime; + +public abstract class HonoSecret { + private String id; + private boolean enabled = true; + private ZonedDateTime notBefore; + private ZonedDateTime notAfter; + + @JsonProperty("id") + public String getId() { + return id; + } + + @JsonProperty("enabled") + public boolean isEnabled() { + return enabled; + } + + @JsonProperty("not-before") + public ZonedDateTime getNotBefore() { + return notBefore; + } + + @JsonProperty("not-after") + public ZonedDateTime getNotAfter() { + return notAfter; + } + + @JsonProperty("id") + public void setId(String id) { + this.id = id; + } + + @JsonProperty("enabled") + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @JsonProperty("not-before") + public void setNotBefore(ZonedDateTime notBefore) { + this.notBefore = notBefore; + } + + @JsonProperty("not-before") + public void setNotBefore(String notBefore) { + this.notBefore = ZonedDateTime.parse(notBefore); + } + + @JsonProperty("not-after") + public void setNotAfter(ZonedDateTime notAfter) { + this.notAfter = notAfter; + } + + @JsonProperty("not-after") + public void setNotAfter(String notAfter) { + this.notAfter = ZonedDateTime.parse(notAfter); + } + + public boolean isValid() { + ZonedDateTime now = ZonedDateTime.now(); + return (notBefore == null || now.compareTo(notBefore) >= 0) && (notAfter == null || now.compareTo(notAfter) <= 0); + } + + public abstract boolean matches(final String password); +} diff --git a/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoTenant.java b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoTenant.java new file mode 100644 index 0000000000..55c1cf382b --- /dev/null +++ b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoTenant.java @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2019 Kiwigrid GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.hono.model; + +public class HonoTenant { + private boolean enabled; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoTenantListPage.java b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoTenantListPage.java new file mode 100644 index 0000000000..309a12a2eb --- /dev/null +++ b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoTenantListPage.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2019 Kiwigrid GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.hono.model; + +import java.util.List; + +public class HonoTenantListPage { + private long total; + private List items; + + public long getTotal() { + return total; + } + + public List getItems() { + return items; + } + + public void setTotal(long total) { + this.total = total; + } + + public void setItems(List items) { + this.items = items; + } +} diff --git a/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoX509CertificateCredentials.java b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoX509CertificateCredentials.java new file mode 100644 index 0000000000..5258e0d669 --- /dev/null +++ b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/HonoX509CertificateCredentials.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2019 Kiwigrid GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.hono.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; + +import java.util.Collection; + +@JsonTypeName("x509-cert") +public class HonoX509CertificateCredentials extends HonoCredentials { + @JsonProperty("secrets") + public void setSecrets(Collection secrets) { + this.secrets = secrets; + } + + public static class Secret extends HonoSecret { + @Override + public boolean matches(String password) { + return false; + } + } +} diff --git a/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/IdentifiableHonoDevice.java b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/IdentifiableHonoDevice.java new file mode 100644 index 0000000000..c714e45895 --- /dev/null +++ b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/IdentifiableHonoDevice.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2019 Kiwigrid GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.hono.model; + +public class IdentifiableHonoDevice { + private String id; + private String tenant; + private HonoDevice device; + + public String getId() { + return id; + } + + public String getTenant() { + return tenant; + } + + public HonoDevice getDevice() { + return device; + } + + public void setId(String id) { + this.id = id; + } + + public void setTenant(String tenant) { + this.tenant = tenant; + } + + public void setDevice(HonoDevice device) { + this.device = device; + } +} diff --git a/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/IdentifiableHonoTenant.java b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/IdentifiableHonoTenant.java new file mode 100644 index 0000000000..6891c78044 --- /dev/null +++ b/hawkbit-dmf/hawkbit-dmf-hono/src/main/java/org/eclipse/hawkbit/dmf/hono/model/IdentifiableHonoTenant.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2019 Kiwigrid GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.dmf.hono.model; + +public class IdentifiableHonoTenant { + private String id; + private HonoTenant tenant; + + public String getId() { + return id; + } + + public HonoTenant getTenant() { + return tenant; + } + + public void setId(String id) { + this.id = id; + } + + public void setTenant(HonoTenant tenant) { + this.tenant = tenant; + } +} diff --git a/hawkbit-dmf/pom.xml b/hawkbit-dmf/pom.xml index d888959d47..d852c11cf8 100644 --- a/hawkbit-dmf/pom.xml +++ b/hawkbit-dmf/pom.xml @@ -23,6 +23,7 @@ hawkbit-dmf-api hawkbit-dmf-amqp + hawkbit-dmf-hono hawkbit-dmf-rabbitmq-test diff --git a/hawkbit-http-security/src/main/java/org/eclipse/hawkbit/security/HttpControllerPreAuthenticatedHonoFilter.java b/hawkbit-http-security/src/main/java/org/eclipse/hawkbit/security/HttpControllerPreAuthenticatedHonoFilter.java new file mode 100644 index 0000000000..e4dff7ea87 --- /dev/null +++ b/hawkbit-http-security/src/main/java/org/eclipse/hawkbit/security/HttpControllerPreAuthenticatedHonoFilter.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2019 Kiwigrid GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +import org.eclipse.hawkbit.dmf.hono.HonoDeviceSync; +import org.eclipse.hawkbit.repository.ControllerManagement; +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; +import org.eclipse.hawkbit.tenancy.TenantAware; + +/** + * An pre-authenticated processing filter which extracts (if enabled through + * configuration) the possibility to authenticate a target based on its target + * security-token with the {@code Authorization} HTTP header. + * {@code Example Header: Authorization: HonoToken + * 5d8fSD54fdsFG98DDsa.} + * + * The {@code Authorization} header is a HTTP standard and reverse proxy or + * other proxies will keep the Authorization headers untouched instead of maybe + * custom headers which have then weird side-effects. Furthermore frameworks are + * aware of the sensitivity of the Authorization header and do not log it and + * store it somewhere. + */ +public class HttpControllerPreAuthenticatedHonoFilter extends AbstractHttpControllerAuthenticationFilter { + + private final ControllerManagement controllerManagement; + private final HonoDeviceSync honoDeviceSync; + + /** + * Constructor. + * + * @param tenantConfigurationManagement + * the system management service to retrieve configuration + * properties + * @param tenantAware + * the tenant aware service to get configuration for the specific + * tenant + * @param controllerManagement + * the controller management to retrieve the specific target + * security token to verify + * @param systemSecurityContext + * the system security context + */ + public HttpControllerPreAuthenticatedHonoFilter( + final TenantConfigurationManagement tenantConfigurationManagement, final TenantAware tenantAware, + final ControllerManagement controllerManagement, final SystemSecurityContext systemSecurityContext, + final HonoDeviceSync honoDeviceSync) { + super(tenantConfigurationManagement, tenantAware, systemSecurityContext); + this.controllerManagement = controllerManagement; + this.honoDeviceSync = honoDeviceSync; + } + + @Override + protected PreAuthenticationFilter createControllerAuthenticationFilter() { + return new ControllerPreAuthenticatedHonoFilter(tenantConfigurationManagement, controllerManagement, + tenantAware, systemSecurityContext, honoDeviceSync); + } + +} diff --git a/hawkbit-http-security/src/test/java/org/eclipse/hawkbit/security/PreAuthHonoProviderTest.java b/hawkbit-http-security/src/test/java/org/eclipse/hawkbit/security/PreAuthHonoProviderTest.java new file mode 100644 index 0000000000..ab06b2c9d2 --- /dev/null +++ b/hawkbit-http-security/src/test/java/org/eclipse/hawkbit/security/PreAuthHonoProviderTest.java @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2019 Kiwigrid GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import io.qameta.allure.Story; +import org.eclipse.hawkbit.dmf.hono.HonoDeviceSync; +import org.eclipse.hawkbit.dmf.hono.model.HonoPasswordCredentials; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.*; + +@Feature("Unit Tests - Security") +@Story("PreAuth Hono Provider Test") +@RunWith(MockitoJUnitRunner.class) +public class PreAuthHonoProviderTest { + + private PreAuthHonoAuthenticationProvider testProvider; + + @Mock + private TenantAwareWebAuthenticationDetails webAuthenticationDetailsMock; + + @Mock + private HonoDeviceSync honoDeviceSyncMock; + + @Before + public void beforeTest() { + this.testProvider = new PreAuthHonoAuthenticationProvider(honoDeviceSyncMock); + } + + @Test + @Description("Testing that the provided credentials are incorrect.") + public void invalidCredentialsThrowsAuthenticationException() { + + HeaderAuthentication principal = new HeaderAuthentication("deviceId", "wrongPassword"); + + HonoPasswordCredentials.Secret secret = new HonoPasswordCredentials.Secret(); + secret.setHashFunction("sha-256"); + secret.setSalt("salt".getBytes()); + secret.setPwdHash("hash".getBytes()); + + HonoPasswordCredentials credentials = new HonoPasswordCredentials(); + credentials.setAuthId("authId"); + credentials.setType("hashed-password"); + credentials.setSecrets(Collections.singletonList(secret)); + + final PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(principal, + Collections.singletonList(credentials)); + token.setDetails(webAuthenticationDetailsMock); + + // test, should throw authentication exception + try { + testProvider.authenticate(token); + fail("Should not work with wrong credentials"); + } catch (final BadCredentialsException e) { + verifyNoMoreInteractions(honoDeviceSyncMock); + } + } + + @Test + @Description("Testing that the provided credentials are correct.") + public void credentialsAreCorrect() { + + String tenant = "tenant"; + String deviceID = "deviceId"; + + HeaderAuthentication principal = new HeaderAuthentication(deviceID, "password"); + + HonoPasswordCredentials.Secret secret = new HonoPasswordCredentials.Secret(); + secret.setHashFunction("sha-256"); + secret.setSalt("salt".getBytes()); + secret.setPwdHash("7a37b85c8918eac19a9089c0fa5a2ab4dce3f90528dcdeec108b23ddf3607b99".getBytes()); + + HonoPasswordCredentials credentials = new HonoPasswordCredentials(); + credentials.setAuthId("authId"); + credentials.setType("hashed-password"); + credentials.setSecrets(Collections.singletonList(secret)); + + final PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(principal, + Collections.singletonList(credentials)); + + final TenantAwareWebAuthenticationDetails details = new TenantAwareWebAuthenticationDetails(tenant, "remoteAddress", true); + token.setDetails(details); + + // test, should throw authentication exception + final Authentication authenticate = testProvider.authenticate(token); + assertThat(authenticate.isAuthenticated()).isTrue(); + + verify(honoDeviceSyncMock, times(1)).checkDeviceIfAbsentSync(tenant, deviceID); + verifyNoMoreInteractions(honoDeviceSyncMock); + } +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryManagement.java index fd5e2fb424..481a291966 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryManagement.java @@ -53,7 +53,7 @@ public interface TargetFilterQueryManagement { * if the maximum number of targets that is addressed by the * given query is exceeded (auto-assignments only) */ - @PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_TARGET) + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_TARGET_FILTER) TargetFilterQuery create(@NotNull @Valid TargetFilterQueryCreate create); /** @@ -65,7 +65,7 @@ public interface TargetFilterQueryManagement { * @throws EntityNotFoundException * if filter with given ID does not exist */ - @PreAuthorize(SpringEvalExpressions.HAS_AUTH_DELETE_TARGET) + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_DELETE_TARGET_FILTER) void delete(long targetFilterQueryId); /** diff --git a/hawkbit-runtime/hawkbit-update-server/pom.xml b/hawkbit-runtime/hawkbit-update-server/pom.xml index e1aaf2ad54..66d2c9ef90 100644 --- a/hawkbit-runtime/hawkbit-update-server/pom.xml +++ b/hawkbit-runtime/hawkbit-update-server/pom.xml @@ -93,4 +93,38 @@ + + + hono-ampq + + true + + + + org.eclipse.hawkbit + hawkbit-dmf-hono + ${project.version} + + + org.springframework.cloud + spring-cloud-stream-binder-rabbit + + + + + hono-gcp-pubsub + + + org.eclipse.hawkbit + hawkbit-dmf-hono + ${project.version} + + + org.springframework.cloud + spring-cloud-gcp-pubsub-stream-binder + + + + + diff --git a/hawkbit-runtime/hawkbit-update-server/src/main/resources/application.properties b/hawkbit-runtime/hawkbit-update-server/src/main/resources/application.properties index 468eb4793b..6a7f9c4ac3 100644 --- a/hawkbit-runtime/hawkbit-update-server/src/main/resources/application.properties +++ b/hawkbit-runtime/hawkbit-update-server/src/main/resources/application.properties @@ -12,6 +12,14 @@ spring.security.user.name=admin spring.security.user.password={noop}admin spring.main.allow-bean-definition-overriding=true +# Configuration of the Spring Cloud Stream endpoints for the optional Eclipse Hono synchronization +spring.cloud.stream.bindings.device-created.destination=device-registry.device-created +spring.cloud.stream.bindings.device-created.group=hawkBit +spring.cloud.stream.bindings.device-updated.destination=device-registry.device-updated +spring.cloud.stream.bindings.device-updated.group=hawkBit +spring.cloud.stream.bindings.device-deleted.destination=device-registry.device-deleted +spring.cloud.stream.bindings.device-deleted.group=hawkBit + # DDI authentication configuration hawkbit.server.ddi.security.authentication.anonymous.enabled=false hawkbit.server.ddi.security.authentication.targettoken.enabled=true diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/PermissionService.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/PermissionService.java index 2cbadd02db..9d799e4730 100644 --- a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/PermissionService.java +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/PermissionService.java @@ -8,8 +8,7 @@ */ package org.eclipse.hawkbit.im.authentication; -import java.util.ArrayList; -import java.util.List; +import java.util.*; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; @@ -22,6 +21,9 @@ */ public class PermissionService { + private boolean honoSyncEnabled = false; + private final List disabledPermissionsByUsingHono = Arrays.asList(SpPermission.CREATE_TARGET, SpPermission.DELETE_TARGET); + /** * Checks if the given {@code permission} contains in the. In case no * {@code context} is available {@code false} will be returned. @@ -42,6 +44,10 @@ public boolean hasPermission(final String permission) { return false; } + if (honoSyncEnabled && disabledPermissionsByUsingHono.contains(permission)) { + return false; + } + for (final GrantedAuthority authority : authentication.getAuthorities()) { if (authority.getAuthority().equals(permission)) { return true; @@ -100,4 +106,7 @@ public boolean hasAtLeastOnePermission(final List permissions) { return false; } + public void setHonoSyncEnabled(boolean honoSyncEnabled) { + this.honoSyncEnabled = honoSyncEnabled; + } } diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java index 10f248c483..f30d9cb3e0 100644 --- a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java @@ -68,7 +68,7 @@ public final class SpPermission { /** * Permission to add new targets to the {@link ProvisioningTargetRepository} * including their meta information and or/relations or - * {@link DistributionSet} assignment.That corresponds in REST API to PUT. + * {@link DistributionSet} assignment. That corresponds in REST API to PUT. */ public static final String CREATE_TARGET = "CREATE_TARGET"; @@ -80,6 +80,24 @@ public final class SpPermission { */ public static final String DELETE_TARGET = "DELETE_TARGET"; + /** + * Permission to add new target filters to the {@link ProvisioningTargetRepository} + * That corresponds in REST API to PUT. + */ + public static final String UPDATE_TARGET_FILTER = "UPDATE_TARGET_FILTER"; + + /** + * Permission to add new target filters to the {@link ProvisioningTargetRepository} + * That corresponds in REST API to POST. + */ + public static final String CREATE_TARGET_FILTER = "CREATE_TARGET_FILTER"; + + /** + * Permission to delete target filters in the {@link ProvisioningTargetRepository}, + * That corresponds in REST API to DELETE. + */ + public static final String DELETE_TARGET_FILTER = "DELETE_TARGET_FILTER"; + /** * Permission to read {@link DistributionSet}s and/or {@link OsPackage}s. * That corresponds in REST API to GET. @@ -290,6 +308,30 @@ public static final class SpringEvalExpressions { public static final String HAS_AUTH_DELETE_TARGET = HAS_AUTH_PREFIX + DELETE_TARGET + HAS_AUTH_SUFFIX + HAS_AUTH_OR + IS_SYSTEM_CODE; + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#CREATE_TARGET_FILTER} or + * {@link #IS_SYSTEM_CODE}. + */ + public static final String HAS_AUTH_CREATE_TARGET_FILTER = HAS_AUTH_PREFIX + CREATE_TARGET_FILTER + + HAS_AUTH_SUFFIX + HAS_AUTH_OR + IS_SYSTEM_CODE; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#UPDATE_TARGET_FILTER} or + * {@link #IS_SYSTEM_CODE}. + */ + public static final String HAS_AUTH_UPDATE_TARGET_FILTER = HAS_AUTH_PREFIX + UPDATE_TARGET_FILTER + + HAS_AUTH_SUFFIX + HAS_AUTH_OR + IS_SYSTEM_CODE; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#DELETE_TARGET_FILTER} or + * {@link #IS_SYSTEM_CODE}. + */ + public static final String HAS_AUTH_DELETE_TARGET_FILTER = HAS_AUTH_PREFIX + DELETE_TARGET_FILTER + + HAS_AUTH_SUFFIX + HAS_AUTH_OR + IS_SYSTEM_CODE; + /** * Spring security eval hasAuthority expression to check if spring * context contains {@link SpPermission#READ_REPOSITORY} and diff --git a/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/im/authentication/PermissionTest.java b/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/im/authentication/PermissionTest.java index bd1f1d661f..c95fd50026 100644 --- a/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/im/authentication/PermissionTest.java +++ b/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/im/authentication/PermissionTest.java @@ -31,7 +31,7 @@ public final class PermissionTest { @Test @Description("Verify the get permission function") public void testGetPermissions() { - final int allPermission = 18; + final int allPermission = 21; final Collection allAuthorities = SpPermission.getAllAuthorities(); final List allAuthoritiesList = PermissionUtils.createAllAuthorityList(); assertThat(allAuthorities).hasSize(allPermission); diff --git a/hawkbit-security-integration/pom.xml b/hawkbit-security-integration/pom.xml index bd9bddf1cf..3ed309a87d 100644 --- a/hawkbit-security-integration/pom.xml +++ b/hawkbit-security-integration/pom.xml @@ -26,6 +26,11 @@ hawkbit-repository-api ${project.version} + + org.eclipse.hawkbit + hawkbit-dmf-hono + ${project.version} + org.springframework.security spring-security-web diff --git a/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/ControllerPreAuthenticatedHonoFilter.java b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/ControllerPreAuthenticatedHonoFilter.java new file mode 100644 index 0000000000..af21325e45 --- /dev/null +++ b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/ControllerPreAuthenticatedHonoFilter.java @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2019 Kiwigrid GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +import java.util.Collection; +import java.util.Optional; + +import org.eclipse.hawkbit.dmf.hono.HonoDeviceSync; +import org.eclipse.hawkbit.dmf.hono.model.HonoCredentials; +import org.eclipse.hawkbit.repository.ControllerManagement; +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An pre-authenticated processing filter which extracts (if enabled through + * configuration) the possibility to authenticate a target based on its target + * security-token with the {@code Authorization} HTTP header. + * {@code Example Header: Authorization: HonoToken + * 5d8fSD54fdsFG98DDsa.} + */ +public class ControllerPreAuthenticatedHonoFilter extends AbstractControllerAuthenticationFilter { + + private static final Logger LOGGER = LoggerFactory.getLogger(ControllerPreAuthenticatedHonoFilter.class); + private static final String TARGET_SECURITY_TOKEN_AUTH_SCHEME = "HonoToken "; + private static final int OFFSET_TARGET_TOKEN = TARGET_SECURITY_TOKEN_AUTH_SCHEME.length(); + + private final ControllerManagement controllerManagement; + private final HonoDeviceSync honoDeviceSync; + + /** + * Constructor. + * + * @param tenantConfigurationManagement + * the tenant management service to retrieve configuration + * properties + * @param controllerManagement + * the controller management to retrieve the specific target + * security token to verify + * @param tenantAware + * the tenant aware service to get configuration for the specific + * tenant + * @param systemSecurityContext + * the system security context to get access to tenant + * configuration + * @param honoDeviceSync + * the hono device sync interface + */ + public ControllerPreAuthenticatedHonoFilter( + final TenantConfigurationManagement tenantConfigurationManagement, + final ControllerManagement controllerManagement, final TenantAware tenantAware, + final SystemSecurityContext systemSecurityContext, final HonoDeviceSync honoDeviceSync) { + super(tenantConfigurationManagement, tenantAware, systemSecurityContext); + this.controllerManagement = controllerManagement; + this.honoDeviceSync = honoDeviceSync; + } + + @Override + public HeaderAuthentication getPreAuthenticatedPrincipal(final DmfTenantSecurityToken secruityToken) { + final String controllerId = resolveControllerId(secruityToken); + final String authHeader = secruityToken.getHeader(DmfTenantSecurityToken.AUTHORIZATION_HEADER); + if ((authHeader != null) && authHeader.startsWith(TARGET_SECURITY_TOKEN_AUTH_SCHEME)) { + LOGGER.debug("found authorization header with scheme {} using target security token for authentication", + TARGET_SECURITY_TOKEN_AUTH_SCHEME); + return new HeaderAuthentication(controllerId, authHeader.substring(OFFSET_TARGET_TOKEN)); + } + LOGGER.debug( + "security token filter is enabled but request does not contain either the necessary path variables {} or the authorization header with scheme {}", + secruityToken, TARGET_SECURITY_TOKEN_AUTH_SCHEME); + return null; + } + + @Override + public Collection getPreAuthenticatedCredentials(final DmfTenantSecurityToken securityToken) { + return honoDeviceSync.getAllHonoCredentials(securityToken.getTenant(), resolveControllerId(securityToken)); + } + + private String resolveControllerId(final DmfTenantSecurityToken securityToken) { + if (securityToken.getControllerId() != null) { + return securityToken.getControllerId(); + } + final Optional foundTarget = systemSecurityContext.runAsSystemAsTenant( + () -> controllerManagement.get(securityToken.getTargetId()), securityToken.getTenant()); + return foundTarget.map(Target::getControllerId).orElse(null); + } + + @Override + protected String getTenantConfigurationKey() { + return TenantConfigurationKey.AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED; + } +} diff --git a/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/HeaderAuthentication.java b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/HeaderAuthentication.java index e7e82f74a1..cfded18a0f 100644 --- a/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/HeaderAuthentication.java +++ b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/HeaderAuthentication.java @@ -63,6 +63,14 @@ public boolean equals(final Object obj) { return true; } + public String getControllerId() { + return controllerId; + } + + public String getHeaderAuth() { + return headerAuth; + } + @Override public String toString() { // only the controller ID because the principal is stored as string for diff --git a/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/PreAuthHonoAuthenticationProvider.java b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/PreAuthHonoAuthenticationProvider.java new file mode 100644 index 0000000000..2fb55cd7f9 --- /dev/null +++ b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/PreAuthHonoAuthenticationProvider.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2019 Kiwigrid GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.security; + +import org.eclipse.hawkbit.dmf.hono.HonoDeviceSync; +import org.eclipse.hawkbit.dmf.hono.model.HonoCredentials; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; + +import java.util.Collection; +import java.util.List; + +public class PreAuthHonoAuthenticationProvider extends PreAuthTokenSourceTrustAuthenticationProvider { + + private HonoDeviceSync honoDeviceSync; + + public PreAuthHonoAuthenticationProvider(HonoDeviceSync honoDeviceSync) { + super(); + this.honoDeviceSync = honoDeviceSync; + } + + public PreAuthHonoAuthenticationProvider(HonoDeviceSync honoDeviceSync, final List authorizedSourceIps) { + super(authorizedSourceIps); + this.honoDeviceSync = honoDeviceSync; + } + + public PreAuthHonoAuthenticationProvider(HonoDeviceSync honoDeviceSync, final String... authorizedSourceIps) { + super(authorizedSourceIps); + this.honoDeviceSync = honoDeviceSync; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + if (!supports(authentication.getClass())) { + return null; + } + + final PreAuthenticatedAuthenticationToken token = (PreAuthenticatedAuthenticationToken) authentication; + final Object credentials = token.getCredentials(); + final Object principal = token.getPrincipal(); + final Object tokenDetails = token.getDetails(); + final Collection authorities = token.getAuthorities(); + + if (!(principal instanceof HeaderAuthentication) || !(credentials instanceof Collection)) { + throw new BadCredentialsException("The provided principal and credentials are not match"); + } + + boolean successAuthentication = false; + for (Object object : (Collection) credentials) { + if (object instanceof HonoCredentials) { + if (((HonoCredentials) object).matches(((HeaderAuthentication) principal).getHeaderAuth())) { + successAuthentication = checkSourceIPAddressIfNeccessary(tokenDetails); + break; + } + } + } + + if (successAuthentication) { + if (tokenDetails instanceof TenantAwareWebAuthenticationDetails) { + TenantAwareWebAuthenticationDetails tenantAwareTokenDetails = (TenantAwareWebAuthenticationDetails) tokenDetails; + honoDeviceSync.checkDeviceIfAbsentSync(tenantAwareTokenDetails.getTenant(), ((HeaderAuthentication) principal).getControllerId()); + } + + final PreAuthenticatedAuthenticationToken successToken = new PreAuthenticatedAuthenticationToken(principal, + credentials, authorities); + successToken.setDetails(tokenDetails); + return successToken; + } + + throw new BadCredentialsException("The provided principal and credentials are not match"); + } +} diff --git a/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/PreAuthTokenSourceTrustAuthenticationProvider.java b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/PreAuthTokenSourceTrustAuthenticationProvider.java index d7dddb4f65..2b02cf6b46 100644 --- a/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/PreAuthTokenSourceTrustAuthenticationProvider.java +++ b/hawkbit-security-integration/src/main/java/org/eclipse/hawkbit/security/PreAuthTokenSourceTrustAuthenticationProvider.java @@ -143,7 +143,7 @@ private boolean calculateAuthenticationSuccess(final Object principal, final Obj return successAuthentication; } - private boolean checkSourceIPAddressIfNeccessary(final Object tokenDetails) { + boolean checkSourceIPAddressIfNeccessary(final Object tokenDetails) { boolean success = authorizedSourceIps == null; String remoteAddress = null; // controllerIds in URL path and request header are the same but is the diff --git a/hawkbit-ui/pom.xml b/hawkbit-ui/pom.xml index b0dbc3ac00..b4c4c244f8 100644 --- a/hawkbit-ui/pom.xml +++ b/hawkbit-ui/pom.xml @@ -168,6 +168,11 @@ hawkbit-repository-api ${project.version} + + org.eclipse.hawkbit + hawkbit-dmf-hono + ${project.version} + commons-io commons-io diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/SpPermissionChecker.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/SpPermissionChecker.java index 15f290a717..080aae64d7 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/SpPermissionChecker.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/SpPermissionChecker.java @@ -46,7 +46,7 @@ public boolean hasTargetReadPermission() { /** * Gets the create Target Permission. - * + * * @return READ_TARGET boolean value */ public boolean hasCreateTargetPermission() { @@ -55,7 +55,7 @@ public boolean hasCreateTargetPermission() { /** * Gets the update Target Permission. - * + * * @return READ_TARGET boolean value */ public boolean hasUpdateTargetPermission() { @@ -64,13 +64,40 @@ public boolean hasUpdateTargetPermission() { /** * Gets the delete Target Permission. - * + * * @return READ_TARGET boolean value */ public boolean hasDeleteTargetPermission() { return hasTargetReadPermission() && permissionService.hasPermission(SpPermission.DELETE_TARGET); } + /** + * Gets the create Target Filter Permission. + * + * @return CREATE_TARGET_FILTER boolean value + */ + public boolean hasCreateTargetFilterPermission() { + return hasTargetReadPermission() && permissionService.hasPermission(SpPermission.CREATE_TARGET_FILTER); + } + + /** + * Gets the delete Target Filter Permission. + * + * @return UPDATE_TARGET_FILTER boolean value + */ + public boolean hasUpdateTargetFilterPermission() { + return hasTargetReadPermission() && permissionService.hasPermission(SpPermission.UPDATE_TARGET_FILTER); + } + + /** + * Gets the delete Target Filter Permission. + * + * @return DELETE_TARGET_FILTER boolean value + */ + public boolean hasDeleteTargetFilterPermission() { + return hasTargetReadPermission() && permissionService.hasPermission(SpPermission.DELETE_TARGET_FILTER); + } + /** * Gets the READ Repository Permission. * diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/table/AbstractDistributionSetTableHeader.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/table/AbstractDistributionSetTableHeader.java index f89acde75d..5c81ccfbdd 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/table/AbstractDistributionSetTableHeader.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/table/AbstractDistributionSetTableHeader.java @@ -31,7 +31,7 @@ public abstract class AbstractDistributionSetTableHeader extends AbstractTableHe protected AbstractDistributionSetTableHeader(final VaadinMessageSource i18n, final SpPermissionChecker permChecker, final UIEventBus eventbus, final ManagementUIState managementUIState, final ManageDistUIState manageDistUIstate, final ArtifactUploadState artifactUploadState) { - super(i18n, permChecker, eventbus, managementUIState, manageDistUIstate, artifactUploadState); + super(i18n, permChecker, eventbus, managementUIState, manageDistUIstate, artifactUploadState, false); } @Override @@ -54,6 +54,11 @@ protected String getAddIconId() { return UIComponentIdProvider.DIST_ADD_ICON; } + @Override + protected String getSyncIconId() { + return null; + } + @Override protected boolean isDropHintRequired() { return true; diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/table/AbstractSoftwareModuleTableHeader.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/table/AbstractSoftwareModuleTableHeader.java index 78f9828186..433e3c9817 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/table/AbstractSoftwareModuleTableHeader.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/table/AbstractSoftwareModuleTableHeader.java @@ -37,7 +37,7 @@ protected AbstractSoftwareModuleTableHeader(final VaadinMessageSource i18n, fina final UIEventBus eventbus, final ManagementUIState managementUIState, final ManageDistUIState manageDistUIstate, final ArtifactUploadState artifactUploadState, final SoftwareModuleAddUpdateWindow softwareModuleAddUpdateWindow) { - super(i18n, permChecker, eventbus, managementUIState, manageDistUIstate, artifactUploadState); + super(i18n, permChecker, eventbus, managementUIState, manageDistUIstate, artifactUploadState, false); this.softwareModuleAddUpdateWindow = softwareModuleAddUpdateWindow; } @@ -61,6 +61,11 @@ protected String getAddIconId() { return UIComponentIdProvider.SW_MODULE_ADD_BUTTON; } + @Override + protected String getSyncIconId() { + return null; + } + @Override protected String getShowFilterButtonLayoutId() { return "show.type.icon"; @@ -76,6 +81,11 @@ protected Boolean isAddNewItemAllowed() { return Boolean.TRUE; } + @Override + protected Boolean isHonoSyncAllowed() { + return Boolean.FALSE; + } + @Override protected void addNewItem(final ClickEvent event) { final Window addSoftwareModule = softwareModuleAddUpdateWindow.createAddSoftwareModuleWindow(); @@ -84,6 +94,11 @@ protected void addNewItem(final ClickEvent event) { addSoftwareModule.setVisible(Boolean.TRUE); } + @Override + protected void syncHono(final ClickEvent event) { + // do nothing + } + @Override protected boolean hasCreatePermission() { return permChecker.hasCreateRepositoryPermission(); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/table/AbstractTableHeader.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/table/AbstractTableHeader.java index ae88a3a0ff..4b9e947eb1 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/table/AbstractTableHeader.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/table/AbstractTableHeader.java @@ -59,6 +59,8 @@ public abstract class AbstractTableHeader extends VerticalLayout { private Button addIcon; + private Button syncHono; + private SPUIButton maxMinIcon; private HorizontalLayout filterDroppedInfo; @@ -71,15 +73,19 @@ public abstract class AbstractTableHeader extends VerticalLayout { private final ArtifactUploadState artifactUploadState; + private final boolean honoSyncEnabled; + protected AbstractTableHeader(final VaadinMessageSource i18n, final SpPermissionChecker permChecker, final UIEventBus eventBus, final ManagementUIState managementUIState, - final ManageDistUIState manageDistUIstate, final ArtifactUploadState artifactUploadState) { + final ManageDistUIState manageDistUIstate, final ArtifactUploadState artifactUploadState, + final boolean honoSyncEnabled) { this.i18n = i18n; this.permChecker = permChecker; this.eventBus = eventBus; this.managementUIState = managementUIState; this.manageDistUIstate = manageDistUIstate; this.artifactUploadState = artifactUploadState; + this.honoSyncEnabled = honoSyncEnabled; createComponents(); buildLayout(); restoreState(); @@ -106,6 +112,8 @@ private void createComponents() { addIcon = createAddIcon(); + syncHono = createSyncHonoIcon(); + bulkUploadIcon = createBulkUploadIcon(); showFilterButtonLayout = createShowFilterButtonLayout(); @@ -154,11 +162,13 @@ private void restoreState() { private void hideAddAndUploadIcon() { addIcon.setVisible(false); + syncHono.setVisible(false); bulkUploadIcon.setVisible(false); } private void showAddAndUploadIcon() { addIcon.setVisible(true); + syncHono.setVisible(true); bulkUploadIcon.setVisible(true); } @@ -170,7 +180,11 @@ private void buildLayout() { titleFilterIconsLayout.setComponentAlignment(searchField, Alignment.TOP_RIGHT); titleFilterIconsLayout.setComponentAlignment(searchResetIcon, Alignment.TOP_RIGHT); titleFilterIconsLayout.setComponentAlignment(showFilterButtonLayout, Alignment.TOP_RIGHT); - if (hasCreatePermission() && isAddNewItemAllowed()) { + if (honoSyncEnabled && isHonoSyncAllowed()) { + titleFilterIconsLayout.addComponent(syncHono); + titleFilterIconsLayout.setComponentAlignment(syncHono, Alignment.TOP_RIGHT); + } + else if (hasCreatePermission() && isAddNewItemAllowed()) { titleFilterIconsLayout.addComponent(addIcon); titleFilterIconsLayout.setComponentAlignment(addIcon, Alignment.TOP_RIGHT); } @@ -237,6 +251,14 @@ private Button createAddIcon() { return button; } + private Button createSyncHonoIcon() { + final Button button = SPUIComponentProvider.getButton(getSyncIconId(), "", + i18n.getMessage(UIMessageIdProvider.TOOLTIP_SYNC_HONO), null, false, FontAwesome.REFRESH, + SPUIButtonStyleNoBorder.class); + button.addClickListener(this::syncHono); + return button; + } + private Button createBulkUploadIcon() { final Button button = SPUIComponentProvider.getButton(getBulkUploadIconId(), "", i18n.getMessage(UIMessageIdProvider.TOOLTIP_BULK_UPLOAD), null, false, FontAwesome.UPLOAD, @@ -434,6 +456,14 @@ protected void reEnableSearch() { */ protected abstract Boolean isAddNewItemAllowed(); + /** + * Checks if the synchronization with Hono is allowed. Default is true. + * + * @return true if the synchronization with hono is allowed, otherwise returns + * false. + */ + protected abstract Boolean isHonoSyncAllowed(); + /** * Get Id of bulk upload Icon. * @@ -486,11 +516,18 @@ protected void reEnableSearch() { /** * Get Id of add Icon. - * + * * @return String of add Icon. */ protected abstract String getAddIconId(); + /** + * Get Id of sync Icon. + * + * @return String of sync Icon. + */ + protected abstract String getSyncIconId(); + /** * Get search box on load text value. * @@ -559,6 +596,8 @@ protected void reEnableSearch() { protected abstract void addNewItem(final Button.ClickEvent event); + protected abstract void syncHono(final Button.ClickEvent event); + protected ManagementUIState getManagementUIState() { return managementUIState; } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/dstable/DistributionSetTableHeader.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/dstable/DistributionSetTableHeader.java index 2bd2adf386..b8fe652e5a 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/dstable/DistributionSetTableHeader.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/dstable/DistributionSetTableHeader.java @@ -102,9 +102,19 @@ protected void addNewItem(final ClickEvent event) { newDistWindow.setVisible(Boolean.TRUE); } + @Override + protected void syncHono(final ClickEvent event) { + // do nothing + } + @Override protected Boolean isAddNewItemAllowed() { return Boolean.TRUE; } + @Override + protected Boolean isHonoSyncAllowed() { + return Boolean.FALSE; + } + } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/CreateOrUpdateFilterHeader.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/CreateOrUpdateFilterHeader.java index cafe563352..d84c3527f3 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/CreateOrUpdateFilterHeader.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/CreateOrUpdateFilterHeader.java @@ -396,9 +396,9 @@ private void updateCustomFilter() { private boolean hasSavePermission() { if (filterManagementUIState.isCreateFilterViewDisplayed()) { - return permissionChecker.hasCreateTargetPermission(); + return permissionChecker.hasCreateTargetFilterPermission(); } else { - return permissionChecker.hasUpdateTargetPermission(); + return permissionChecker.hasUpdateTargetFilterPermission(); } } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TargetFilterHeader.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TargetFilterHeader.java index 8c8a81acec..5093a12782 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TargetFilterHeader.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TargetFilterHeader.java @@ -92,7 +92,7 @@ private Label createHeaderCaption() { private void buildLayout() { final HorizontalLayout titleFilterIconsLayout = createHeaderFilterIconLayout(); titleFilterIconsLayout.addComponents(headerCaption, searchField, searchResetIcon); - if (permissionChecker.hasCreateTargetPermission()) { + if (permissionChecker.hasCreateTargetFilterPermission()) { titleFilterIconsLayout.addComponent(createfilterButton); titleFilterIconsLayout.setComponentAlignment(createfilterButton, Alignment.TOP_LEFT); } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/DeploymentView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/DeploymentView.java index 170c746648..b0c35a3a88 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/DeploymentView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/DeploymentView.java @@ -9,10 +9,12 @@ package org.eclipse.hawkbit.ui.management; import java.util.Map; +import java.util.Optional; import java.util.concurrent.Executor; import javax.annotation.PostConstruct; +import org.eclipse.hawkbit.dmf.hono.HonoDeviceSync; import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.DistributionSetTagManagement; @@ -131,7 +133,8 @@ public class DeploymentView extends AbstractNotificationView implements BrowserW final TargetFilterQueryManagement targetFilterQueryManagement, final SystemManagement systemManagement, final TenantConfigurationManagement configManagement, final SystemSecurityContext systemSecurityContext, final NotificationUnreadButton notificationUnreadButton, - final DeploymentViewMenuItem deploymentViewMenuItem, @Qualifier("uiExecutor") final Executor uiExecutor) { + final DeploymentViewMenuItem deploymentViewMenuItem, @Qualifier("uiExecutor") final Executor uiExecutor, + final Optional honoDeviceSync) { super(eventBus, notificationUnreadButton); this.permChecker = permChecker; this.i18n = i18n; @@ -150,13 +153,14 @@ public class DeploymentView extends AbstractNotificationView implements BrowserW targetFilterQueryManagement, targetTagManagement); final TargetTable targetTable = new TargetTable(eventBus, i18n, uiNotification, targetManagement, managementUIState, permChecker, managementViewClientCriterion, distributionSetManagement, - targetTagManagement, deploymentManagement, configManagement, systemSecurityContext, uiProperties); + targetTagManagement, deploymentManagement, configManagement, systemSecurityContext, uiProperties, + honoDeviceSync.isPresent()); this.countMessageLabel = new CountMessageLabel(eventBus, targetManagement, i18n, managementUIState, targetTable); this.targetTableLayout = new TargetTableLayout(eventBus, targetTable, targetManagement, entityFactory, i18n, uiNotification, managementUIState, managementViewClientCriterion, deploymentManagement, - uiProperties, permChecker, targetTagManagement, distributionSetManagement, uiExecutor); + uiProperties, permChecker, targetTagManagement, distributionSetManagement, uiExecutor, honoDeviceSync.orElse(null)); actionHistoryLayout.registerDetails(((ActionStatusGrid) actionStatusLayout.getGrid()).getDetailsSupport()); actionStatusLayout diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstable/DistributionTableHeader.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstable/DistributionTableHeader.java index 077a20fe12..43a84d1da8 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstable/DistributionTableHeader.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstable/DistributionTableHeader.java @@ -96,9 +96,19 @@ protected void addNewItem(final ClickEvent event) { // is okay and not supported } + @Override + protected void syncHono(final ClickEvent event) { + // is okay and not supported + } + @Override protected Boolean isAddNewItemAllowed() { return Boolean.FALSE; } + @Override + protected Boolean isHonoSyncAllowed() { + return Boolean.FALSE; + } + } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetDetails.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetDetails.java index 02ec3ca531..168fd4f339 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetDetails.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetDetails.java @@ -75,13 +75,16 @@ public class TargetDetails extends AbstractTableDetailsLayout { private VerticalLayout installedDistLayout; + private final boolean honoSyncEnabled; + TargetDetails(final VaadinMessageSource i18n, final UIEventBus eventBus, final SpPermissionChecker permissionChecker, final ManagementUIState managementUIState, final UINotification uiNotification, final TargetTagManagement tagManagement, final TargetManagement targetManagement, final TargetMetadataPopupLayout targetMetadataPopupLayout, final DeploymentManagement deploymentManagement, final EntityFactory entityFactory, - final TargetTable targetTable) { + final TargetTable targetTable, final boolean honoSyncEnabled) { super(i18n, eventBus, permissionChecker, managementUIState); + this.honoSyncEnabled = honoSyncEnabled; this.targetTagToken = new TargetTagToken(permissionChecker, i18n, uiNotification, eventBus, managementUIState, tagManagement, targetManagement); this.targetAddUpdateWindowLayout = new TargetAddUpdateWindowLayout(i18n, targetManagement, eventBus, @@ -194,9 +197,11 @@ private void updateDetailsLayout(final String controllerId, final URI address, f typeLabel.setId(UIComponentIdProvider.TARGET_IP_ADDRESS); detailsTabLayout.addComponent(typeLabel); - final HorizontalLayout securityTokenLayout = getSecurityTokenLayout(securityToken); - controllerLabel.setId(UIComponentIdProvider.TARGET_SECURITY_TOKEN); - detailsTabLayout.addComponent(securityTokenLayout); + if (!honoSyncEnabled) { + final HorizontalLayout securityTokenLayout = getSecurityTokenLayout(securityToken); + controllerLabel.setId(UIComponentIdProvider.TARGET_SECURITY_TOKEN); + detailsTabLayout.addComponent(securityTokenLayout); + } } private HorizontalLayout getSecurityTokenLayout(final String securityToken) { diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTable.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTable.java index 79f0aff308..4dbf79f131 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTable.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTable.java @@ -132,12 +132,15 @@ public class TargetTable extends AbstractTable { private boolean targetPinned; private ConfirmationDialog confirmDialog; + private final boolean honoSyncEnabled; + public TargetTable(final UIEventBus eventBus, final VaadinMessageSource i18n, final UINotification notification, final TargetManagement targetManagement, final ManagementUIState managementUIState, final SpPermissionChecker permChecker, final ManagementViewClientCriterion managementViewClientCriterion, final DistributionSetManagement distributionSetManagement, final TargetTagManagement tagManagement, final DeploymentManagement deploymentManagement, final TenantConfigurationManagement configManagement, - final SystemSecurityContext systemSecurityContext, final UiProperties uiProperties) { + final SystemSecurityContext systemSecurityContext, final UiProperties uiProperties, + final boolean honoSyncEnabled) { super(eventBus, i18n, notification, permChecker); this.targetManagement = targetManagement; this.managementViewClientCriterion = managementViewClientCriterion; @@ -150,6 +153,7 @@ public TargetTable(final UIEventBus eventBus, final VaadinMessageSource i18n, fi this.actionTypeOptionGroupLayout = new ActionTypeOptionGroupAssignmentLayout(i18n); this.maintenanceWindowLayout = new MaintenanceWindowLayout(i18n); this.systemSecurityContext = systemSecurityContext; + this.honoSyncEnabled = honoSyncEnabled; setItemDescriptionGenerator(new AssignInstalledDSTooltipGenerator()); addNewContainerDS(); @@ -971,7 +975,7 @@ protected String getEntityType() { @Override protected boolean hasDeletePermission() { - return getPermChecker().hasDeleteTargetPermission(); + return !honoSyncEnabled && getPermChecker().hasDeleteTargetPermission(); } @Override diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTableHeader.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTableHeader.java index f00a6235b2..591921bc17 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTableHeader.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTableHeader.java @@ -12,6 +12,7 @@ import java.util.Set; import java.util.concurrent.Executor; +import org.eclipse.hawkbit.dmf.hono.HonoDeviceSync; import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.EntityFactory; @@ -78,14 +79,16 @@ public class TargetTableHeader extends AbstractTableHeader { private final transient DistributionSetManagement distributionSetManagement; + private final transient HonoDeviceSync honoDeviceSync; + TargetTableHeader(final VaadinMessageSource i18n, final SpPermissionChecker permChecker, final UIEventBus eventBus, final UINotification notification, final ManagementUIState managementUIState, final ManagementViewClientCriterion managementViewClientCriterion, final TargetManagement targetManagement, final DeploymentManagement deploymentManagement, final UiProperties uiproperties, final EntityFactory entityFactory, final UINotification uiNotification, final TargetTagManagement tagManagement, final DistributionSetManagement distributionSetManagement, - final Executor uiExecutor, final TargetTable targetTable) { - super(i18n, permChecker, eventBus, managementUIState, null, null); + final Executor uiExecutor, final TargetTable targetTable, final HonoDeviceSync honoDeviceSync) { + super(i18n, permChecker, eventBus, managementUIState, null, null, honoDeviceSync != null); this.notification = notification; this.managementViewClientCriterion = managementViewClientCriterion; this.targetAddUpdateWindow = new TargetAddUpdateWindowLayout(i18n, targetManagement, eventBus, uiNotification, @@ -94,6 +97,7 @@ public class TargetTableHeader extends AbstractTableHeader { managementUIState, deploymentManagement, uiproperties, permChecker, uiNotification, tagManagement, distributionSetManagement, entityFactory, uiExecutor); this.distributionSetManagement = distributionSetManagement; + this.honoDeviceSync = honoDeviceSync; onLoadRestoreState(); } @@ -186,6 +190,11 @@ protected String getAddIconId() { return UIComponentIdProvider.TARGET_TBL_ADD_ICON_ID; } + @Override + protected String getSyncIconId() { + return UIComponentIdProvider.TARGET_TBL_SYNC_ICON_ID; + } + @Override protected String getBulkUploadIconId() { return UIComponentIdProvider.TARGET_TBL_BULK_UPLOAD_ICON_ID; @@ -286,6 +295,13 @@ protected void addNewItem(final ClickEvent event) { addTargetWindow.setVisible(Boolean.TRUE); } + @Override + protected void syncHono(final ClickEvent event) { + event.getButton().setEnabled(false); // Make sure there is only one synchronization process at a time. + honoDeviceSync.synchronize(true); + event.getButton().setEnabled(true); + } + @Override protected void bulkUpload(final ClickEvent event) { targetBulkUpdateWindow.resetComponents(); @@ -435,4 +451,9 @@ protected String getFilterIconStyle() { protected Boolean isAddNewItemAllowed() { return Boolean.TRUE; } + + @Override + protected Boolean isHonoSyncAllowed() { + return Boolean.TRUE; + } } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTableLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTableLayout.java index 7545064946..c759c8fcf0 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTableLayout.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTableLayout.java @@ -10,6 +10,7 @@ import java.util.concurrent.Executor; +import org.eclipse.hawkbit.dmf.hono.HonoDeviceSync; import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.EntityFactory; @@ -42,16 +43,17 @@ public TargetTableLayout(final UIEventBus eventBus, final TargetTable targetTabl final ManagementViewClientCriterion managementViewClientCriterion, final DeploymentManagement deploymentManagement, final UiProperties uiProperties, final SpPermissionChecker permissionChecker, final TargetTagManagement tagManagement, - final DistributionSetManagement distributionSetManagement, final Executor uiExecutor) { + final DistributionSetManagement distributionSetManagement, final Executor uiExecutor, + final HonoDeviceSync honoDeviceSync) { final TargetMetadataPopupLayout targetMetadataPopupLayout = new TargetMetadataPopupLayout(i18n, uiNotification, eventBus, targetManagement, entityFactory, permissionChecker); this.eventBus = eventBus; TargetDetails targetDetails = new TargetDetails(i18n, eventBus, permissionChecker, managementUIState, uiNotification, tagManagement, targetManagement, targetMetadataPopupLayout, deploymentManagement, entityFactory, - targetTable); + targetTable, honoDeviceSync != null); TargetTableHeader targetTableHeader = new TargetTableHeader(i18n, permissionChecker, eventBus, uiNotification, managementUIState, managementViewClientCriterion, targetManagement, deploymentManagement, uiProperties, - entityFactory, uiNotification, tagManagement, distributionSetManagement, uiExecutor, targetTable); + entityFactory, uiNotification, tagManagement, distributionSetManagement, uiExecutor, targetTable, honoDeviceSync); super.init(i18n, targetTableHeader, targetTable, targetDetails); } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettag/filter/MultipleTargetFilter.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettag/filter/MultipleTargetFilter.java index ff9d7f35fd..40238fca9a 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettag/filter/MultipleTargetFilter.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettag/filter/MultipleTargetFilter.java @@ -106,9 +106,10 @@ private void buildComponents() { filterByButtons.addStyleName(SPUIStyleDefinitions.NO_TOP_BORDER); targetFilterQueryButtonsTab.init(customTargetTagFilterButtonClick); - menu = new ConfigMenuBar(permChecker.hasCreateTargetPermission(), permChecker.hasUpdateTargetPermission(), - permChecker.hasDeleteRepositoryPermission(), getAddButtonCommand(), getUpdateButtonCommand(), - getDeleteButtonCommand(), UIComponentIdProvider.TARGET_MENU_BAR_ID, i18n); + menu = new ConfigMenuBar(permChecker.hasCreateTargetFilterPermission(), + permChecker.hasUpdateTargetFilterPermission(), permChecker.hasDeleteRepositoryPermission(), + getAddButtonCommand(), getUpdateButtonCommand(), getDeleteButtonCommand(), + UIComponentIdProvider.TARGET_MENU_BAR_ID, i18n); menu.addStyleName("targetTag"); addStyleName(ValoTheme.ACCORDION_BORDERLESS); addTabs(); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java index d8e8ee2e87..82688a337d 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java @@ -643,6 +643,11 @@ public final class UIComponentIdProvider { */ public static final String TARGET_TBL_ADD_ICON_ID = "target.add"; + /** + * Id of the target table sync Icon. + */ + public static final String TARGET_TBL_SYNC_ICON_ID = "target.sync"; + /** * Id of IP address label in target details. */ diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIMessageIdProvider.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIMessageIdProvider.java index 352e52b586..ed4be59345 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIMessageIdProvider.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIMessageIdProvider.java @@ -115,6 +115,8 @@ public final class UIMessageIdProvider { public static final String TOOLTIP_ADD = "tooltip.add"; + public static final String TOOLTIP_SYNC_HONO = "tooltip.sync.hono"; + public static final String TOOLTIP_SHOW_TAGS = "tooltip.showTags"; public static final String TOOLTIP_ASSIGN_TAG = "tooltip.assignTag"; diff --git a/hawkbit-ui/src/main/resources/messages.properties b/hawkbit-ui/src/main/resources/messages.properties index 3a89fdaf1d..ec44068984 100644 --- a/hawkbit-ui/src/main/resources/messages.properties +++ b/hawkbit-ui/src/main/resources/messages.properties @@ -305,6 +305,7 @@ tooltip.target.attributes.update.request = Request attributes update tooltip.target.attributes.update.requested = Update already requested tooltip.documentation.link=Documentation tooltip.artifact.download=Download Artifact +tooltip.sync.hono = Synchronize with Eclipse Hono #rollout action tooltip.rollout.run = Run