diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityAutoConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityAutoConfiguration.java index 35395c21c..411969f9d 100644 --- a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityAutoConfiguration.java +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityAutoConfiguration.java @@ -8,16 +8,25 @@ */ package org.eclipse.hawkbit.autoconfigure.security; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.eclipse.hawkbit.autoconfigure.security.MultiUserProperties.User; import org.eclipse.hawkbit.im.authentication.PermissionService; import org.eclipse.hawkbit.security.DdiSecurityProperties; +import org.eclipse.hawkbit.security.InMemoryUserAuthoritiesResolver; import org.eclipse.hawkbit.security.HawkbitSecurityProperties; import org.eclipse.hawkbit.security.SecurityContextTenantAware; import org.eclipse.hawkbit.security.SecurityTokenGenerator; import org.eclipse.hawkbit.security.SpringSecurityAuditorAware; import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.tenancy.TenantAware; +import org.eclipse.hawkbit.tenancy.UserAuthoritiesResolver; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -28,23 +37,57 @@ import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; +import org.springframework.util.CollectionUtils; /** * {@link EnableAutoConfiguration Auto-configuration} for security. */ @Configuration -@EnableConfigurationProperties({ DdiSecurityProperties.class, HawkbitSecurityProperties.class }) +@EnableConfigurationProperties({ SecurityProperties.class, DdiSecurityProperties.class, HawkbitSecurityProperties.class, + MultiUserProperties.class }) public class SecurityAutoConfiguration { /** + * Creates a {@link TenantAware} bean based on the given + * {@link UserAuthoritiesResolver}. + * + * @param authoritiesResolver + * The user authorities/roles resolver + * * @return the {@link TenantAware} singleton bean which holds the current * {@link TenantAware} service and make it accessible in beans which * cannot access the service directly, e.g. JPA entities. */ @Bean @ConditionalOnMissingBean - public TenantAware tenantAware() { - return new SecurityContextTenantAware(); + public TenantAware tenantAware(final UserAuthoritiesResolver authoritiesResolver) { + return new SecurityContextTenantAware(authoritiesResolver); + } + + /** + * Creates a {@link UserAuthoritiesResolver} bean that is responsible for + * resolving user authorities/roles. + * + * @param securityProperties + * The Spring {@link SecurityProperties} for the security user + * @param multiUserProperties + * The {@link MultiUserProperties} for the managed users + * + * @return an {@link InMemoryUserAuthoritiesResolver} bean + */ + @Bean + @ConditionalOnMissingBean + public UserAuthoritiesResolver inMemoryAuthoritiesResolver(final SecurityProperties securityProperties, + final MultiUserProperties multiUserProperties) { + final List multiUsers = multiUserProperties.getUsers(); + final Map> usersToPermissions; + if (!CollectionUtils.isEmpty(multiUsers)) { + usersToPermissions = multiUsers.stream().collect(Collectors.toMap(User::getUsername, User::getPermissions)); + } else { + usersToPermissions = Collections.singletonMap(securityProperties.getUser().getName(), + securityProperties.getUser().getRoles()); + } + return new InMemoryUserAuthoritiesResolver(usersToPermissions); } /** diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/tenancy/TenantAware.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/tenancy/TenantAware.java index 0984fe6fe..6925b33c2 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/tenancy/TenantAware.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/tenancy/TenantAware.java @@ -42,7 +42,27 @@ public interface TenantAware { * @throws any * kind of {@link RuntimeException} */ - T runAsTenant(final String tenant, TenantRunner tenantRunner); + T runAsTenant(String tenant, TenantRunner tenantRunner); + + /** + * Gives the possibility to run a certain code under a specific given + * {@code tenant} and {@code username}. Only the given {@link TenantRunner} is executed under the + * specific tenant and user e.g. under control of an {@link ThreadLocal}. After the + * {@link TenantRunner} it must be ensured that the original tenant before + * this invocation is reset. + * + * @param tenant + * the tenant which the specific code should run with + * @param username + * the username which the specific code should run with + * @param tenantRunner + * the runner which is implemented to run this specific code + * under the given tenant + * @return the return type of the {@link TenantRunner} + * @throws any + * kind of {@link RuntimeException} + */ + T runAsTenantAsUser(String tenant, String username, TenantRunner tenantRunner); /** * An {@link TenantRunner} interface which allows to run specific code under diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/tenancy/UserAuthoritiesResolver.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/tenancy/UserAuthoritiesResolver.java new file mode 100644 index 000000000..e43837953 --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/tenancy/UserAuthoritiesResolver.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2020 Bosch.IO 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.tenancy; + +import java.util.Collection; + +/** + * The service responsible for making the lookup for user authorities/roles + * based on his tenant and username + */ +@FunctionalInterface +public interface UserAuthoritiesResolver { + + /** + * User authorities/roles lookup based on the tenant and the username + * + * @param tenant + * The tenant that this user belongs to + * @param username + * The username of the user + * @return a {@link Collection} of authorities/roles for this user + */ + Collection getUserAuthorities(String tenant, String username); +} diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpControllerAuthenticationTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpControllerAuthenticationTest.java index f3c4748b7..df894849f 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpControllerAuthenticationTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpControllerAuthenticationTest.java @@ -13,6 +13,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -43,6 +44,7 @@ import org.eclipse.hawkbit.security.DmfTenantSecurityToken; import org.eclipse.hawkbit.security.DmfTenantSecurityToken.FileResource; import org.eclipse.hawkbit.security.SecurityContextTenantAware; import org.eclipse.hawkbit.security.SystemSecurityContext; +import org.eclipse.hawkbit.tenancy.UserAuthoritiesResolver; import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -104,10 +106,10 @@ public class AmqpControllerAuthenticationTest { private ArtifactManagement artifactManagementMock; @Mock - private ControllerManagement controllerManagementMock; + private Target targetMock; @Mock - private Target targetMock; + private UserAuthoritiesResolver authoritiesResolver; @Mock private RabbitTemplate rabbitTemplate; @@ -140,7 +142,7 @@ public class AmqpControllerAuthenticationTest { when(tenantConfigurationManagementMock.getConfigurationValue(any(), eq(Boolean.class))) .thenReturn(CONFIG_VALUE_FALSE); - final SecurityContextTenantAware tenantAware = new SecurityContextTenantAware(); + final SecurityContextTenantAware tenantAware = new SecurityContextTenantAware(authoritiesResolver); final SystemSecurityContext systemSecurityContext = new SystemSecurityContext(tenantAware); authenticationManager = new AmqpControllerAuthentication(systemManagement, controllerManagement, @@ -153,18 +155,18 @@ public class AmqpControllerAuthenticationTest { testArtifact.setId(1L); amqpMessageHandlerService = new AmqpMessageHandlerService(rabbitTemplate, - mock(AmqpMessageDispatcherService.class), controllerManagementMock, new JpaEntityFactory(), + mock(AmqpMessageDispatcherService.class), controllerManagement, new JpaEntityFactory(), systemSecurityContext, tenantConfigurationManagementMock); amqpAuthenticationMessageHandlerService = new AmqpAuthenticationMessageHandler(rabbitTemplate, - authenticationManager, artifactManagementMock, cacheMock, hostnameResolverMock, - controllerManagementMock, tenantAware); + authenticationManager, artifactManagementMock, cacheMock, hostnameResolverMock, controllerManagement, + tenantAware); } private void mockAuthenticationWithoutPrincipal() { - when(securityProperties.getAuthentication()).thenReturn(ddiAuthentication); - when(ddiAuthentication.getAnonymous()).thenReturn(anonymous); - when(anonymous.isEnabled()).thenReturn(false); + lenient().when(securityProperties.getAuthentication()).thenReturn(ddiAuthentication); + lenient().when(ddiAuthentication.getAnonymous()).thenReturn(anonymous); + lenient().when(anonymous.isEnabled()).thenReturn(false); } private void mockSuccessfulAuthentication() throws MalformedURLException { @@ -210,13 +212,15 @@ public class AmqpControllerAuthenticationTest { @Test @Description("Tests authentication successful") public void testSuccessfulAuthentication() { + + when(controllerManagement.get(any(Long.class))).thenReturn(Optional.of(targetMock)); + final DmfTenantSecurityToken securityToken = new DmfTenantSecurityToken(TENANT, TENANT_ID, CONTROLLER_ID, TARGET_ID, FileResource.createFileResourceBySha1(SHA1)); when(tenantConfigurationManagementMock.getConfigurationValue( eq(TenantConfigurationKey.AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED), eq(Boolean.class))) .thenReturn(CONFIG_VALUE_TRUE); - when(controllerManagement.get(any(Long.class))).thenReturn(Optional.of(targetMock)); when(targetMock.getSecurityToken()).thenReturn(CONTROLLER_ID); when(targetMock.getControllerId()).thenReturn(CONTROLLER_ID); @@ -256,7 +260,8 @@ public class AmqpControllerAuthenticationTest { when(tenantConfigurationManagementMock.getConfigurationValue( eq(TenantConfigurationKey.AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED), eq(Boolean.class))) - .thenReturn(CONFIG_VALUE_TRUE); + .thenReturn(CONFIG_VALUE_TRUE); + when(rabbitTemplate.getMessageConverter()).thenReturn(messageConverter); securityToken.putHeader(DmfTenantSecurityToken.AUTHORIZATION_HEADER, "TargetToken 12" + CONTROLLER_ID); @@ -275,13 +280,14 @@ public class AmqpControllerAuthenticationTest { @Test @Description("Tests authentication message successful") public void successfulMessageAuthentication() throws Exception { + final MessageProperties messageProperties = createMessageProperties(null); final DmfTenantSecurityToken securityToken = new DmfTenantSecurityToken(TENANT, null, CONTROLLER_ID, null, FileResource.createFileResourceBySha1(SHA1)); mockSuccessfulAuthentication(); when(controllerManagement.getByControllerId(anyString())).thenReturn(Optional.of(targetMock)); - when(controllerManagementMock.hasTargetArtifactAssigned(CONTROLLER_ID, SHA1)).thenReturn(true); + when(controllerManagement.hasTargetArtifactAssigned(CONTROLLER_ID, SHA1)).thenReturn(true); when(artifactManagementMock.findFirstBySHA1(SHA1)).thenReturn(Optional.of(testArtifact)); securityToken.putHeader(DmfTenantSecurityToken.AUTHORIZATION_HEADER, "TargetToken " + CONTROLLER_ID); @@ -312,8 +318,8 @@ public class AmqpControllerAuthenticationTest { mockSuccessfulAuthentication(); when(controllerManagement.get(any(Long.class))).thenReturn(Optional.of(targetMock)); + when(controllerManagement.hasTargetArtifactAssigned(TARGET_ID, SHA1)).thenReturn(true); when(artifactManagementMock.get(ARTIFACT_ID)).thenReturn(Optional.of(testArtifact)); - when(controllerManagementMock.hasTargetArtifactAssigned(TARGET_ID, SHA1)).thenReturn(true); securityToken.putHeader(DmfTenantSecurityToken.AUTHORIZATION_HEADER, "TargetToken " + CONTROLLER_ID); final Message message = amqpMessageHandlerService.getMessageConverter().toMessage(securityToken, @@ -337,6 +343,7 @@ public class AmqpControllerAuthenticationTest { @Test @Description("Tests authentication message successful") public void successfulMessageAuthenticationWithTenantId() throws Exception { + final MessageProperties messageProperties = createMessageProperties(null); final DmfTenantSecurityToken securityToken = new DmfTenantSecurityToken(null, TENANT_ID, CONTROLLER_ID, TARGET_ID, FileResource.createFileResourceBySha1(SHA1)); @@ -344,7 +351,7 @@ public class AmqpControllerAuthenticationTest { mockSuccessfulAuthentication(); when(controllerManagement.get(any(Long.class))).thenReturn(Optional.of(targetMock)); - when(controllerManagementMock.hasTargetArtifactAssigned(CONTROLLER_ID, SHA1)).thenReturn(true); + when(controllerManagement.hasTargetArtifactAssigned(CONTROLLER_ID, SHA1)).thenReturn(true); when(artifactManagementMock.findFirstBySHA1(SHA1)).thenReturn(Optional.of(testArtifact)); when(tenantMetaData.getTenant()).thenReturn(TENANT); when(systemManagement.getTenantMetadata(TENANT_ID)).thenReturn(tenantMetaData); diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java index 4c2802865..b3ec8f82d 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java @@ -60,6 +60,7 @@ import org.eclipse.hawkbit.security.SecurityContextTenantAware; import org.eclipse.hawkbit.security.SecurityTokenGenerator; import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.tenancy.TenantAware; +import org.eclipse.hawkbit.tenancy.UserAuthoritiesResolver; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -133,6 +134,9 @@ public class AmqpMessageHandlerServiceTest { @Mock private TenantAware tenantAwareMock; + @Mock + private UserAuthoritiesResolver authoritiesResolver; + @Captor private ArgumentCaptor> attributesCaptor; @@ -155,7 +159,7 @@ public class AmqpMessageHandlerServiceTest { lenient().when(tenantConfigurationManagement.getConfigurationValue(MULTI_ASSIGNMENTS_ENABLED, Boolean.class)) .thenReturn(multiAssignmentConfig); - final SecurityContextTenantAware tenantAware = new SecurityContextTenantAware(); + final SecurityContextTenantAware tenantAware = new SecurityContextTenantAware(authoritiesResolver); final SystemSecurityContext systemSecurityContext = new SystemSecurityContext(tenantAware); amqpMessageHandlerService = new AmqpMessageHandlerService(rabbitTemplate, amqpMessageDispatcherServiceMock, diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java index 2f319648e..cc50d87d3 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java @@ -14,6 +14,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.locks.Lock; import java.util.stream.Collectors; @@ -62,6 +63,7 @@ import org.eclipse.hawkbit.repository.jpa.utils.WeightValidationHelper; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.BaseEntity; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus; @@ -564,8 +566,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { try { long actionsCreated; do { - actionsCreated = createActionsForTargetsInNewTransaction(rollout.getId(), group.getId(), - TRANSACTION_TARGETS); + actionsCreated = createActionsForTargetsInNewTransaction(rollout.getId(), group.getId(), TRANSACTION_TARGETS); totalActionsCreated += actionsCreated; } while (actionsCreated > 0); @@ -576,7 +577,8 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { return totalActionsCreated; } - private Long createActionsForTargetsInNewTransaction(final long rolloutId, final long groupId, final int limit) { + private Long createActionsForTargetsInNewTransaction(final long rolloutId, final long groupId, + final int limit) { return DeploymentHelper.runInNewTransaction(txManager, "createActionsForTargets", status -> { final PageRequest pageRequest = PageRequest.of(0, limit); final Rollout rollout = rolloutRepository.findById(rolloutId) @@ -828,17 +830,21 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { try { rollouts.forEach(rolloutId -> DeploymentHelper.runInNewTransaction(txManager, handlerId + "-" + rolloutId, - status -> executeFittingHandler(rolloutId))); + status -> handleRollout(rolloutId))); } finally { lock.unlock(); } } - private long executeFittingHandler(final long rolloutId) { - LOGGER.debug("handle rollout {}", rolloutId); + private long handleRollout(final long rolloutId) { final JpaRollout rollout = rolloutRepository.findById(rolloutId) .orElseThrow(() -> new EntityNotFoundException(Rollout.class, rolloutId)); + runInUserContext(rollout, () -> handleRollout(rollout)); + return 0; + } + private void handleRollout(final JpaRollout rollout) { + LOGGER.debug("Handle rollout {}", rollout.getId()); switch (rollout.getStatus()) { case CREATING: handleCreateRollout(rollout); @@ -859,8 +865,6 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { LOGGER.error("Rollout in status {} not supposed to be handled!", rollout.getStatus()); break; } - - return 0; } private void handleStartingRollout(final Rollout rollout) { @@ -1147,4 +1151,9 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { QuotaHelper.assertAssignmentQuota(target.getId(), requested, quota, Action.class, Target.class, actionRepository::countByTargetId); } + + private void runInUserContext(final BaseEntity rollout, final Runnable handler) { + DeploymentHelper.runInNonSystemContext(handler, () -> Objects.requireNonNull(rollout.getCreatedBy()), tenantAware); + } + } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java index fd37edc98..40b538ea5 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java @@ -760,9 +760,9 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { @ConditionalOnMissingBean AutoAssignExecutor autoAssignExecutor(final TargetFilterQueryManagement targetFilterQueryManagement, final TargetManagement targetManagement, final DeploymentManagement deploymentManagement, - final PlatformTransactionManager transactionManager) { + final PlatformTransactionManager transactionManager, final TenantAware tenantAware) { return new AutoAssignChecker(targetFilterQueryManagement, targetManagement, deploymentManagement, - transactionManager); + transactionManager, tenantAware); } /** diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignChecker.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignChecker.java index eb832b13a..9ee5852ad 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignChecker.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignChecker.java @@ -9,6 +9,7 @@ package org.eclipse.hawkbit.repository.jpa.autoassign; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import javax.persistence.PersistenceException; @@ -24,6 +25,7 @@ import org.eclipse.hawkbit.repository.model.DeploymentRequest; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetFilterQuery; +import org.eclipse.hawkbit.tenancy.TenantAware; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; @@ -45,14 +47,6 @@ public class AutoAssignChecker implements AutoAssignExecutor { private static final Logger LOGGER = LoggerFactory.getLogger(AutoAssignChecker.class); - private final TargetFilterQueryManagement targetFilterQueryManagement; - - private final TargetManagement targetManagement; - - private final DeploymentManagement deploymentManagement; - - private final PlatformTransactionManager transactionManager; - /** * Maximum for target filter queries with auto assign DS Maximum for targets * that are fetched in one turn @@ -65,6 +59,16 @@ public class AutoAssignChecker implements AutoAssignExecutor { */ private static final String ACTION_MESSAGE = "Auto assignment by target filter: %s"; + private final TargetFilterQueryManagement targetFilterQueryManagement; + + private final TargetManagement targetManagement; + + private final DeploymentManagement deploymentManagement; + + private final PlatformTransactionManager transactionManager; + + private final TenantAware tenantAware; + /** * Instantiates a new auto assign checker * @@ -76,14 +80,17 @@ public class AutoAssignChecker implements AutoAssignExecutor { * to assign distribution sets to targets * @param transactionManager * to run transactions + * @param tenantAware + * to handle the tenant context */ public AutoAssignChecker(final TargetFilterQueryManagement targetFilterQueryManagement, final TargetManagement targetManagement, final DeploymentManagement deploymentManagement, - final PlatformTransactionManager transactionManager) { + final PlatformTransactionManager transactionManager, final TenantAware tenantAware) { this.targetFilterQueryManagement = targetFilterQueryManagement; this.targetManagement = targetManagement; this.deploymentManagement = deploymentManagement; this.transactionManager = transactionManager; + this.tenantAware = tenantAware; } @Override @@ -95,10 +102,9 @@ public class AutoAssignChecker implements AutoAssignExecutor { final Page filterQueries = targetFilterQueryManagement.findWithAutoAssignDS(pageRequest); - // we should ensure that the filter queries are executed - // in the order of weights + // make sure the filter queries are executed in the order of weights for (final TargetFilterQuery filterQuery : filterQueries) { - checkByTargetFilterQueryAndAssignDS(filterQuery); + runInUserContext(filterQuery, () -> checkByTargetFilterQueryAndAssignDS(filterQuery)); } } @@ -127,7 +133,7 @@ public class AutoAssignChecker implements AutoAssignExecutor { } } - + /** * Runs one page of target assignments within a dedicated transaction * @@ -154,12 +160,6 @@ public class AutoAssignChecker implements AutoAssignExecutor { }); } - private static String getAutoAssignmentInitiatedBy(final TargetFilterQuery targetFilterQuery) { - return StringUtils.isEmpty(targetFilterQuery.getAutoAssignInitiatedBy()) ? - targetFilterQuery.getCreatedBy() : - targetFilterQuery.getAutoAssignInitiatedBy(); - } - /** * Gets all matching targets with the designated action from the target * management @@ -168,8 +168,6 @@ public class AutoAssignChecker implements AutoAssignExecutor { * the query the targets have to match * @param dsId * dsId the targets are not allowed to have in their action history - * @param type - * action type for targets auto assignment * @param count * maximum amount of targets to retrieve * @return list of targets with action type @@ -185,5 +183,16 @@ public class AutoAssignChecker implements AutoAssignExecutor { return targets.getContent().stream().map(t -> DeploymentManagement.deploymentRequest(t.getControllerId(), dsId) .setActionType(autoAssignActionType).setWeight(weight).build()).collect(Collectors.toList()); } + + private void runInUserContext(final TargetFilterQuery targetFilterQuery, final Runnable handler) { + DeploymentHelper.runInNonSystemContext(handler, + () -> Objects.requireNonNull(getAutoAssignmentInitiatedBy(targetFilterQuery)), tenantAware); + } + + private static String getAutoAssignmentInitiatedBy(final TargetFilterQuery targetFilterQuery) { + return StringUtils.isEmpty(targetFilterQuery.getAutoAssignInitiatedBy()) ? + targetFilterQuery.getCreatedBy() : + targetFilterQuery.getAutoAssignInitiatedBy(); + } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/DeploymentHelper.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/DeploymentHelper.java index d2fea0f27..5914b024d 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/DeploymentHelper.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/DeploymentHelper.java @@ -9,6 +9,7 @@ package org.eclipse.hawkbit.repository.jpa.utils; import java.util.List; +import java.util.function.Supplier; import java.util.stream.Collectors; import javax.validation.constraints.NotNull; @@ -20,12 +21,17 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.eclipse.hawkbit.security.SecurityContextTenantAware; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.support.DefaultTransactionDefinition; import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.util.StringUtils; /** * Utility class for deployment related topics. @@ -33,6 +39,8 @@ import org.springframework.transaction.support.TransactionTemplate; */ public final class DeploymentHelper { + private static final Logger LOG = LoggerFactory.getLogger(DeploymentHelper.class); + private DeploymentHelper() { // utility class } @@ -110,4 +118,37 @@ public final class DeploymentHelper { def.setIsolationLevel(isolationLevel); return new TransactionTemplate(txManager, def).execute(action); } + + /** + * Runs the given handler in a non-system user context. Switches to the user + * which is provided by the given callback. + * + * @param handler + * The handler to be invoked in the right user context. + * @param username + * Callback to obtain the real user the user context should be + * established for. + * @param tenantAware + * The {@link TenantAware} bean to determine the current tenant + * context. + */ + public static void runInNonSystemContext(@NotNull final Runnable handler, @NotNull final Supplier username, + @NotNull final TenantAware tenantAware) { + final String currentUser = tenantAware.getCurrentUsername(); + if (isNonSystemUser(currentUser)) { + handler.run(); + return; + } + final String user = username.get(); + LOG.debug("Switching user context from '{}' to '{}'", currentUser, user); + tenantAware.runAsTenantAsUser(tenantAware.getCurrentTenant(), user, () -> { + handler.run(); + return null; + }); + } + + private static boolean isNonSystemUser(final String user) { + return (!(StringUtils.isEmpty(user) || SecurityContextTenantAware.SYSTEM_USER.equals(user))); + } + } diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/TestConfiguration.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/TestConfiguration.java index bd5d2b34b..10131f2ca 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/TestConfiguration.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/TestConfiguration.java @@ -8,6 +8,7 @@ */ package org.eclipse.hawkbit.repository.test; +import java.util.Collections; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -38,11 +39,13 @@ import org.eclipse.hawkbit.security.SecurityTokenGenerator; import org.eclipse.hawkbit.security.SpringSecurityAuditorAware; import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.tenancy.TenantAware; +import org.eclipse.hawkbit.tenancy.UserAuthoritiesResolver; import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties; import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cache.CacheManager; import org.springframework.cache.caffeine.CaffeineCacheManager; import org.springframework.cloud.bus.ConditionalOnBusEnabled; import org.springframework.context.ApplicationEvent; @@ -118,23 +121,30 @@ public class TestConfiguration implements AsyncConfigurer { } @Bean - TenantAware tenantAware() { - return new SecurityContextTenantAware(); + UserAuthoritiesResolver authoritiesResolver() { + return (tenant, username) -> Collections.emptyList(); } @Bean - TenantAwareCacheManager cacheManager() { - return new TenantAwareCacheManager(new CaffeineCacheManager(), tenantAware()); + TenantAware tenantAware(final UserAuthoritiesResolver authoritiesResolver) { + return new SecurityContextTenantAware(authoritiesResolver); + } + + @Bean + TenantAwareCacheManager cacheManager(final TenantAware tenantAware) { + return new TenantAwareCacheManager(new CaffeineCacheManager(), tenantAware); } /** * Bean for the download id cache. * + * @param cacheManager + * The {@link CacheManager} * @return the cache */ @Bean - DownloadIdCache downloadIdCache() { - return new DefaultDownloadIdCache(cacheManager()); + DownloadIdCache downloadIdCache(final CacheManager cacheManager) { + return new DefaultDownloadIdCache(cacheManager); } @Bean(name = AbstractApplicationContext.APPLICATION_EVENT_MULTICASTER_BEAN_NAME) diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/InMemoryUserAuthoritiesResolver.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/InMemoryUserAuthoritiesResolver.java new file mode 100644 index 000000000..5f33f3678 --- /dev/null +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/InMemoryUserAuthoritiesResolver.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2020 Bosch.IO 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.Collections; +import java.util.List; +import java.util.Map; + +import org.eclipse.hawkbit.tenancy.UserAuthoritiesResolver; + +/** + * An implementation of the {@link UserAuthoritiesResolver} that is based on + * in-memory user permissions. + */ +public class InMemoryUserAuthoritiesResolver implements UserAuthoritiesResolver { + + private final Map> usernamesToAuthorities; + + /** + * Constructs the resolver based on the given authority lookup map. + * + * @param usernamesToAuthorities + * The authority map to read from. Must not be null. + */ + public InMemoryUserAuthoritiesResolver(final Map> usernamesToAuthorities) { + this.usernamesToAuthorities = usernamesToAuthorities; + } + + @Override + public Collection getUserAuthorities(final String tenant, final String username) { + // we can ignore the tenant here (no multi-tenancy by default) + final Collection authorities = usernamesToAuthorities.get(username); + if (authorities == null) { + return Collections.emptyList(); + } + return authorities; + } + +} diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SecurityContextTenantAware.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SecurityContextTenantAware.java index e378c2f09..5318af030 100644 --- a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SecurityContextTenantAware.java +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SecurityContextTenantAware.java @@ -8,14 +8,16 @@ */ package org.eclipse.hawkbit.security; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails; import org.eclipse.hawkbit.im.authentication.UserPrincipal; import org.eclipse.hawkbit.tenancy.TenantAware; +import org.eclipse.hawkbit.tenancy.UserAuthoritiesResolver; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -24,14 +26,32 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextImpl; /** - * A {@link TenantAware} implemenation which retrieves the ID of the tenant from - * the {@link SecurityContext#getAuthentication()} + * A {@link TenantAware} implementation which retrieves the ID of the tenant + * from the {@link SecurityContext#getAuthentication()} * {@link Authentication#getDetails()} which holds the * {@link TenantAwareAuthenticationDetails} object. * */ public class SecurityContextTenantAware implements TenantAware { + public static final String SYSTEM_USER = "system"; + private static final Collection SYSTEM_AUTHORITIES = Collections + .singletonList(new SimpleGrantedAuthority(SpringEvalExpressions.SYSTEM_ROLE)); + + private final UserAuthoritiesResolver authoritiesResolver; + + /** + * Creates the {@link SecurityContextTenantAware} based on the given + * {@link UserAuthoritiesResolver}. + * + * @param authoritiesResolver + * Resolver to retrieve the authorities for a given user. Must + * not be null. + */ + public SecurityContextTenantAware(final UserAuthoritiesResolver authoritiesResolver) { + this.authoritiesResolver = authoritiesResolver; + } + @Override public String getCurrentTenant() { final SecurityContext context = SecurityContextHolder.getContext(); @@ -59,44 +79,68 @@ public class SecurityContextTenantAware implements TenantAware { } @Override - public T runAsTenant(final String tenant, final TenantRunner callable) { + public T runAsTenant(final String tenant, final TenantRunner tenantRunner) { + return runInContext(buildSystemSecurityContext(tenant), tenantRunner); + } + + @Override + public T runAsTenantAsUser(final String tenant, final String username, final TenantRunner tenantRunner) { + final List authorities = runAsSystem( + () -> authoritiesResolver.getUserAuthorities(tenant, username).stream().map(SimpleGrantedAuthority::new) + .collect(Collectors.toList())); + return runInContext(buildUserSecurityContext(tenant, username, authorities), tenantRunner); + } + + private static T runInContext(final SecurityContext context, final TenantRunner tenantRunner) { final SecurityContext originalContext = SecurityContextHolder.getContext(); try { - SecurityContextHolder.setContext(buildSecurityContext(tenant)); - return callable.run(); + SecurityContextHolder.setContext(context); + return tenantRunner.run(); } finally { SecurityContextHolder.setContext(originalContext); } } - private static SecurityContext buildSecurityContext(final String tenant) { + private static SecurityContext buildSystemSecurityContext(final String tenant) { + return buildUserSecurityContext(tenant, SYSTEM_USER, SYSTEM_AUTHORITIES); + } + + private static T runAsSystem(final TenantRunner tenantRunner) { + final SecurityContext currentContext = SecurityContextHolder.getContext(); + try { + SystemSecurityContext.setSystemContext(currentContext); + return tenantRunner.run(); + } finally { + SecurityContextHolder.setContext(currentContext); + } + } + + private static SecurityContext buildUserSecurityContext(final String tenant, final String username, + final Collection authorities) { final SecurityContextImpl securityContext = new SecurityContextImpl(); - securityContext.setAuthentication( - new AuthenticationDelegate(SecurityContextHolder.getContext().getAuthentication(), tenant)); + securityContext.setAuthentication(new AuthenticationDelegate( + SecurityContextHolder.getContext().getAuthentication(), tenant, username, authorities)); return securityContext; } /** * An {@link Authentication} implementation to delegate to an existing * {@link Authentication} object except setting the details specifically for - * a specific tenant. + * a specific tenant and user. */ private static final class AuthenticationDelegate implements Authentication { private static final long serialVersionUID = 1L; - private static final String SYSTEM_USER = "system"; - private static final Collection SYSTEM_AUTHORITIES = Arrays - .asList(new SimpleGrantedAuthority(SpringEvalExpressions.SYSTEM_ROLE)); private final Authentication delegate; - private final UserPrincipal systemPrincipal; + private final UserPrincipal principal; private final TenantAwareAuthenticationDetails tenantAwareAuthenticationDetails; - private AuthenticationDelegate(final Authentication delegate, final String tenant) { + private AuthenticationDelegate(final Authentication delegate, final String tenant, final String username, + final Collection authorities) { this.delegate = delegate; - this.systemPrincipal = new UserPrincipal(SYSTEM_USER, SYSTEM_USER, SYSTEM_USER, SYSTEM_USER, SYSTEM_USER, - null, tenant, SYSTEM_AUTHORITIES); + this.principal = new UserPrincipal(username, username, null, null, username, null, tenant, authorities); tenantAwareAuthenticationDetails = new TenantAwareAuthenticationDetails(tenant, false); } @@ -112,27 +156,27 @@ public class SecurityContextTenantAware implements TenantAware { @Override public String toString() { - return (delegate != null) ? delegate.toString() : null; + return delegate != null ? delegate.toString() : null; } @Override public int hashCode() { - return (delegate != null) ? delegate.hashCode() : -1; + return delegate != null ? delegate.hashCode() : -1; } @Override public String getName() { - return (delegate != null) ? delegate.getName() : null; + return delegate != null ? delegate.getName() : null; } @Override public Collection getAuthorities() { - return (delegate != null) ? delegate.getAuthorities() : Collections.emptyList(); + return delegate != null ? delegate.getAuthorities() : Collections.emptyList(); } @Override public Object getCredentials() { - return (delegate != null) ? delegate.getCredentials() : null; + return delegate != null ? delegate.getCredentials() : null; } @Override @@ -142,7 +186,7 @@ public class SecurityContextTenantAware implements TenantAware { @Override public Object getPrincipal() { - return systemPrincipal; + return principal; } @Override diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SystemSecurityContext.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SystemSecurityContext.java index 347dcbce4..53de775c1 100644 --- a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SystemSecurityContext.java +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SystemSecurityContext.java @@ -171,7 +171,7 @@ public class SystemSecurityContext { SecurityContextHolder.setContext(securityContextImpl); } - private static void setSystemContext(final SecurityContext oldContext) { + static void setSystemContext(final SecurityContext oldContext) { final Authentication oldAuthentication = oldContext.getAuthentication(); final SecurityContextImpl securityContextImpl = new SecurityContextImpl(); securityContextImpl.setAuthentication(new SystemCodeAuthentication(oldAuthentication)); diff --git a/hawkbit-security-integration/src/test/java/org/eclipse/hawkbit/security/ControllerPreAuthenticatedSecurityHeaderFilterTest.java b/hawkbit-security-integration/src/test/java/org/eclipse/hawkbit/security/ControllerPreAuthenticatedSecurityHeaderFilterTest.java index e8dcb75d9..8f67edcb2 100644 --- a/hawkbit-security-integration/src/test/java/org/eclipse/hawkbit/security/ControllerPreAuthenticatedSecurityHeaderFilterTest.java +++ b/hawkbit-security-integration/src/test/java/org/eclipse/hawkbit/security/ControllerPreAuthenticatedSecurityHeaderFilterTest.java @@ -17,6 +17,7 @@ import java.util.Collection; import org.eclipse.hawkbit.repository.TenantConfigurationManagement; import org.eclipse.hawkbit.repository.model.TenantConfigurationValue; import org.eclipse.hawkbit.security.DmfTenantSecurityToken.FileResource; +import org.eclipse.hawkbit.tenancy.UserAuthoritiesResolver; import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -33,16 +34,6 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) public class ControllerPreAuthenticatedSecurityHeaderFilterTest { - private ControllerPreAuthenticatedSecurityHeaderFilter underTest; - - @Mock - private TenantConfigurationManagement tenantConfigurationManagementMock; - - @Mock - private DmfTenantSecurityToken tenantSecurityTokenMock; - - private final SecurityContextTenantAware tenantAware = new SecurityContextTenantAware(); - private static final String CA_COMMON_NAME = "ca-cn"; private static final String CA_COMMON_NAME_VALUE = "box1"; @@ -61,8 +52,18 @@ public class ControllerPreAuthenticatedSecurityHeaderFilterTest { private static final TenantConfigurationValue CONFIG_VALUE_MULTI_HASH = TenantConfigurationValue . builder().value(MULTI_HASH).build(); + private ControllerPreAuthenticatedSecurityHeaderFilter underTest; + + @Mock + private TenantConfigurationManagement tenantConfigurationManagementMock; + @Mock + private DmfTenantSecurityToken tenantSecurityTokenMock; + @Mock + private UserAuthoritiesResolver authoritiesResolver; + @BeforeEach public void before() { + final SecurityContextTenantAware tenantAware = new SecurityContextTenantAware(authoritiesResolver); underTest = new ControllerPreAuthenticatedSecurityHeaderFilter(CA_COMMON_NAME, "X-Ssl-Issuer-Hash-%d", tenantConfigurationManagementMock, tenantAware, new SystemSecurityContext(tenantAware)); }